Skip to content

Commit fa8f77e

Browse files
committed
feat(eslint-plugin-runner): new package for ESLint plugin compatibility runtime
Adds @rslint/eslint-plugin-runner as a self-contained npm package. The runner runs unmodified ESLint plugin rules inside Node worker_threads spawned by a future host (CLI / LSP server). No upstream caller in this PR — package can be imported and used independently. Surface - WorkerPool: queued task model, per-task SharedArrayBuffer cancel flag, worker_threads lifecycle (spawn / respawn-on-crash / shutdown drain), per-task timeout + terminate path - LintWorker entry: imports user rslint configs directly, extracts live plugin instances per configDirectory, dispatches lint tasks by configKey - ESLint v10 surface fills: Rule.Context, SourceCode (lazy tokens + getDisableDirectives), scope-manager / visitor-keys integration, oxc-parser preserveParens:false to match espree - Tokenizer (tokenizer.ts + tokenizer-jsx.ts): JS / TS lexer with paren/brace context tracking, Unicode-escape identifiers, regex / template / private-identifier scans. JSX is opt-in via TokenizeOptions.jsx — separate state machine in tokenizer-jsx.ts emits JSXIdentifier / JSXText / Punctuator per espree's contract; TSX disambiguation rejects generic shapes (`<T,>`, `<T extends X>`, `<T>(…)`) from JSX entry - IPC client: framed protocol for the future host's reverse-RPC - compat-task-builder: pure helpers projecting LSP/CLI wire shapes to internal lint tasks Tests - 336 tests across 18 files, all pass standalone: - worker-pool-e2e: queue model, cancel, timeout/respawn, shutdown drain, postMessage failure modes, multi-worker mid-respawn race - context-api / source-code / scope-factory / normalize-ast / listener-merge / plugin-loader / cancel-flag / ipc-client / language-options / lazy-cache / compat-task-builder - regression-coverage: behavior-pinning tests across the v10 compatibility surface — disable-directive line/block carrier contract, drain semantics under multi-worker respawn (including mid-init crash), fix() result validation (array/iterable/string), tokenizer regex-vs-division context tracking, Unicode-escape identifiers, BOM stripping, unified bare+esquery specificity dispatch, TS scope globalReturn + commonjs parity, .mts/.cts JSX promotion, interpolate.js message template parity, getInlineConfigNodes / getDisableDirectives line-vs-block + label-boundary rules, boundary token getters returning null, options deep-clone, init failure-on-any-rejection, getIndexFromLoc throwing on invalid / out-of-range loc - jsx-tokenizer: 72 espree-oracle differential tests covering basic elements, attributes (name-only / string / expression / spread), JSXText, dotted / namespaced / hyphenated names, nesting, fragments, expression containers, template-literal interaction, `<` disambiguation (comparison / shift / TSX generics), and non-JSX regression Build - Standalone: \`pnpm --filter @rslint/eslint-plugin-runner build && test\` - ESM-only ("type": "module"), runtime deps: @typescript-eslint/{scope-manager, visitor-keys}, oxc-parser; devDep: espree (test oracle); peerDep: jiti
1 parent d67ae07 commit fa8f77e

66 files changed

Lines changed: 20966 additions & 91 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ jobs:
155155
pnpm --filter @rslint/core run build:js
156156
pnpm --filter @typescript-eslint/rule-tester build
157157
pnpm --filter rslint build
158+
pnpm --filter @rslint/eslint-plugin-runner build
158159
159160
- name: VSCode Test Cache
160161
uses: lynx-infra/cache@5c6160a6a4c7fca80a2f3057bb9dfc9513fcb732

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"license": "MIT",
1515
"scripts": {
16-
"build": "pnpm -r --filter=@rslint/test-tools... --filter=rslint... build",
16+
"build": "pnpm -r --filter=@rslint/test-tools... --filter=rslint... --filter=@rslint/eslint-plugin-runner build",
1717
"build:npm": "zx scripts/build-npm.mjs",
1818
"build:website": "pnpm --filter @rslint/core run build:js && pnpm --filter=!@rslint/core --filter @rslint/website... -r build",
1919
"bench:cli": "pnpm --filter rslint-bench-cli run bench",
@@ -25,7 +25,7 @@
2525
"publish:ovsx": "zx scripts/publish-marketplace.mjs --marketplace=ovsx",
2626
"format": "prettier --write .",
2727
"format:check": "prettier --check .",
28-
"test": "pnpm -r --filter=@rslint/test-tools... --filter=rslint... test",
28+
"test": "pnpm -r --filter=@rslint/test-tools... --filter=rslint... --filter=@rslint/eslint-plugin-runner test",
2929
"test:go": "go test ./internal/...",
3030
"typecheck": "pnpm tsgo -b tsconfig.json",
3131
"lint": "rslint --config rslint.config.ts",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# @rslint/eslint-plugin-runner
2+
3+
Internal ESLint-plugin compatibility runtime for [rslint](https://github.com/web-infra-dev/rslint).
4+
5+
This package is **not** intended for direct end-user consumption. It is embedded
6+
by `@rslint/core` (CLI) and the VS Code extension to execute ESLint plugin rules
7+
on a Node `worker_threads` pool, while the rslint Go binary drives the lint
8+
pipeline.
9+
10+
## Why a separate package
11+
12+
`@rslint/core` is a thin CLI / API wrapper around the Go binary. The pieces that
13+
must run inside Node — plugin loading, oxc-parser → ESTree normalization, scope
14+
analysis, fixer / suggestion plumbing, IPC framing — live here so:
15+
16+
- `@rslint/core` keeps a small surface and can be loaded fast even when no
17+
ESLint plugin is configured.
18+
- The same runtime is reusable by the VS Code extension's in-process
19+
`WorkerPool`, where Go is the LSP server and Node is the LSP client.
20+
21+
## Stability
22+
23+
The exports are an implementation detail of rslint's plugin compatibility
24+
layer. They may change in any minor release without a deprecation cycle. If you
25+
need to embed rslint in your own tool, use `@rslint/core` instead.
26+
27+
## License
28+
29+
MIT
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"name": "@rslint/eslint-plugin-runner",
3+
"version": "0.5.2",
4+
"description": "ESLint-plugin compatibility runtime for rslint — Worker pool, per-file lint pipeline, scope manager. Embedded by the CLI (@rslint/core) and by the VS Code extension; not invoked as a standalone process.",
5+
"type": "module",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/web-infra-dev/rslint.git",
9+
"directory": "packages/rslint-eslint-plugin-runner"
10+
},
11+
"license": "MIT",
12+
"publisher": "rslint",
13+
"keywords": [
14+
"rslint",
15+
"eslint",
16+
"eslint-plugin",
17+
"linter",
18+
"internal"
19+
],
20+
"exports": {
21+
".": {
22+
"@typescript/source": "./src/index.ts",
23+
"default": "./dist/index.js"
24+
},
25+
"./ipc-client": {
26+
"@typescript/source": "./src/ipc-client.ts",
27+
"default": "./dist/ipc-client.js"
28+
},
29+
"./types": {
30+
"@typescript/source": "./src/types.ts",
31+
"default": "./dist/types.js"
32+
}
33+
},
34+
"files": [
35+
"dist",
36+
"README.md"
37+
],
38+
"publishConfig": {
39+
"access": "public"
40+
},
41+
"engines": {
42+
"node": ">=20"
43+
},
44+
"scripts": {
45+
"build": "rslib build",
46+
"test": "rstest run"
47+
},
48+
"devDependencies": {
49+
"@eslint/plugin-kit": "0.3.5",
50+
"@rslib/core": "0.21.2",
51+
"@rstest/core": "0.9.2",
52+
"@types/node": "24.0.14",
53+
"espree": "11.2.0"
54+
},
55+
"dependencies": {
56+
"@typescript-eslint/scope-manager": "8.59.4",
57+
"eslint-scope": "9.1.2",
58+
"esquery": "1.7.0",
59+
"globals": "17.6.0",
60+
"oxc-parser": "0.132.0"
61+
},
62+
"peerDependencies": {
63+
"jiti": "^2.0.0"
64+
},
65+
"peerDependenciesMeta": {
66+
"jiti": {
67+
"optional": true
68+
}
69+
}
70+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { defineConfig } from '@rslib/core';
2+
3+
/**
4+
* Build configuration for `@rslint/eslint-plugin-runner`.
5+
*
6+
* Each library is its own `lib` block so Rspack treats them as
7+
* independent builds — that means each output file gets the entire
8+
* reachable module graph inlined and there are no `dist/<hash>.js`
9+
* shared chunks between entries. Crucial for the worker entry, which
10+
* is loaded via `new Worker(workerEntryPath)`: the worker spawns a
11+
* fresh V8 isolate and pays a filesystem-open + parse cost for every
12+
* additional chunk it has to load. Splitting `lint-worker.js` into a
13+
* thin 10 KB stub plus a 158 KB shared chunk doubled the worker's
14+
* boot-time disk reads for no shared-cache benefit (each worker is
15+
* its own isolate; modules can't be reused across them).
16+
*
17+
* `oxc-parser` is the only runtime dep that MUST stay external — it
18+
* uses a NAPI loader (`src-js/bindings.js`) that resolves its
19+
* platform-specific `.node` binary relative to `import.meta.url`;
20+
* bundling its loader would repoint that URL at our bundle file and
21+
* the native binary lookup would fail.
22+
*
23+
* Five outputs map 1:1 to the package.json `exports` field:
24+
*
25+
* - `index.js` — main entry, host integration
26+
* - `lint-worker.js` — worker_threads entry (resolved by sibling
27+
* lookup inside `worker-pool.ts`, so the file
28+
* MUST land at `dist/lint-worker.js`)
29+
* - `ipc-client.js` — Go-side stdio framing helpers
30+
* - `types.js` — shared type re-exports
31+
*/
32+
const baseLib = {
33+
format: 'esm' as const,
34+
bundle: true,
35+
// All three categories external — runtime deps come from the user's
36+
// node_modules (correct version resolution + tree-shareable), peerDeps
37+
// are explicitly host-supplied. Only devDependencies bundle into dist.
38+
//
39+
// Two source-level idioms tacitly rely on this:
40+
// - `@typescript-eslint/scope-manager` / `visitor-keys` are loaded
41+
// via `createRequire(import.meta.url)` (CJS packages on an ESM
42+
// entry). Static `import` would tempt a future contributor and
43+
// `autoExternal: { dependencies: false }` would silently bundle
44+
// them.
45+
// - `oxc-parser` is ESM with a NAPI loader that resolves its
46+
// platform `.node` next to `import.meta.url`; bundling repoints
47+
// that URL at our dist file and breaks the native binary lookup.
48+
autoExternal: true,
49+
output: {
50+
target: 'node' as const,
51+
},
52+
source: {
53+
tsconfigPath: './tsconfig.build.json',
54+
},
55+
};
56+
57+
export default defineConfig({
58+
lib: [
59+
{
60+
...baseLib,
61+
source: { ...baseLib.source, entry: { index: './src/index.ts' } },
62+
// Bundle dts only on the main entry — the other entries either
63+
// re-export from `index` or are tiny standalone modules; emitting
64+
// independent dts for each would duplicate the same types five
65+
// times. The main `dist/index.d.ts` covers all public surface.
66+
dts: { bundle: true },
67+
},
68+
{
69+
...baseLib,
70+
source: {
71+
...baseLib.source,
72+
entry: { 'lint-worker': './src/lint-worker.ts' },
73+
},
74+
dts: false,
75+
},
76+
{
77+
...baseLib,
78+
source: {
79+
...baseLib.source,
80+
entry: { 'ipc-client': './src/ipc-client.ts' },
81+
},
82+
dts: { bundle: true },
83+
},
84+
{
85+
...baseLib,
86+
source: { ...baseLib.source, entry: { types: './src/types.ts' } },
87+
dts: { bundle: true },
88+
},
89+
],
90+
});

0 commit comments

Comments
 (0)