@rustwrap/eslint 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @rustwrap/eslint
2
+
3
+ A **drop-in ESLint replacement** (CLI + Node API) backed by the Rust-based [oxlint](https://oxc.rs)
4
+ for very fast linting. Honors your existing ESLint config and ignore files — point your `eslint`
5
+ dependency at `@rustwrap/eslint` and lint **~50–80× faster**.
6
+
7
+ ## Measured (representative PCF control)
8
+ | | ESLint | @rustwrap/eslint |
9
+ |---|---|---|
10
+ | `src/**/*.{ts,tsx}` (~50 files) | **38.0 s**, 101 warnings | **0.48 s**, 89 warnings |
11
+
12
+ (The small warning delta is the handful of ESLint rules oxlint doesn't yet implement — see
13
+ *Limitations*. `@rustwrap/eslint` covers the large majority of correctness/style/TS/React rules.)
14
+
15
+ ## Use as an ESLint override
16
+ ```json
17
+ {
18
+ "overrides": { "eslint": "npm:@rustwrap/eslint@^1" },
19
+ "devDependencies": { "eslint": "npm:@rustwrap/eslint@^1" }
20
+ }
21
+ ```
22
+ No script changes needed — `eslint …`, `pcf-scripts lint`, and tools using the ESLint Node API keep
23
+ working. `@rustwrap/eslint` ships the `eslint` bin.
24
+
25
+ ## What it honors (config & ignore)
26
+ - **Legacy eslintrc**: `.eslintrc.json`, `.eslintrc`, `.eslintrc.js`, `.eslintrc.cjs`,
27
+ `.eslintrc.yaml`/`.yml`, and `package.json#eslintConfig`. Auto-discovered up the directory tree;
28
+ `--config`/`-c` and `--no-eslintrc` respected.
29
+ - **Flat config**: `eslint.config.js`/`.mjs`/`.cjs` (best-effort: `rules`, `plugins`, `ignores`).
30
+ - **`extends`** — local file extends are merged; npm shareable configs (`eslint:recommended`,
31
+ `plugin:@typescript-eslint/recommended`, `plugin:react/recommended`, airbnb, …) are mapped to the
32
+ corresponding oxlint plugins.
33
+ - **`plugins`** — mapped to oxlint plugin ids (`@typescript-eslint`→typescript, `react`/`react-hooks`
34
+ →react, `import`, `unicorn`, `jsx-a11y`, `jest`, `vitest`, `promise`, `n`/`node`, `jsdoc`, `vue`,
35
+ `@next/next`→nextjs).
36
+ - **`rules`** — passed through with severities (`off`/`warn`/`error`/numeric/array). Rules oxlint
37
+ doesn't implement are dropped automatically (it would otherwise reject the config).
38
+ - **`env`, `globals`, `settings`, `overrides`, `ignorePatterns`** — translated (incl. normalizing
39
+ `settings.react.version: "detect"` to a concrete version).
40
+ - **`parser` / `parserOptions`** — not needed: oxlint detects TS/JSX by extension.
41
+ - **`.eslintignore`** + `--ignore-path` + `--ignore-pattern` + `--no-ignore`.
42
+
43
+ ## CLI
44
+ ESLint-compatible flags: file/dir/**glob** args (incl. `src/**/*.{ts,tsx}` brace expansion — `@rustwrap/eslint`
45
+ expands globs since oxlint doesn't), `--fix`, `--quiet`, `--max-warnings <n>`, `-c/--config`,
46
+ `--no-eslintrc`, `--ext`, `-f/--format <stylish|json|compact|unix|summary>`, `-o/--output-file`,
47
+ `--ignore-path`, `--ignore-pattern`, `--no-ignore`, `--color/--no-color`, `--stdin`/`--stdin-filename`.
48
+ Other ESLint flags are accepted and ignored for drop-in tolerance.
49
+
50
+ **Exit codes** match ESLint: `0` clean (or warnings under threshold), `1` lint errors or
51
+ `--max-warnings` exceeded, `2` fatal.
52
+
53
+ ## Node API
54
+ ```js
55
+ const { ESLint } = require("eslint"); // -> @rustwrap/eslint
56
+ const eslint = new ESLint({ cwd, fix });
57
+ const results = await eslint.lintFiles(["src/**/*.{ts,tsx}"]);
58
+ const formatter = await eslint.loadFormatter("stylish");
59
+ console.log(formatter.format(results));
60
+ await ESLint.outputFixes(results);
61
+ ```
62
+ Implemented: `ESLint` (`lintFiles`, `lintText`, `loadFormatter`, `calculateConfigForFile`,
63
+ `isPathIgnored`, static `outputFixes`/`getErrorResults`/`version`), legacy **`CLIEngine`**
64
+ (`executeOnFiles`/`executeOnText`/`getFormatter`), **`Linter`** (`verify`/`verifyAndFix`), and
65
+ **`loadESLint`**. Results use the ESLint `LintResult` shape (`messages[]` with
66
+ `ruleId`/`severity`/`line`/`column`, `errorCount`/`warningCount`, …).
67
+
68
+ ## Engine & dependencies
69
+ - **Engine:** `oxlint` (Rust/Oxc) — does the actual linting.
70
+ - **Compat layer:** `fast-glob` (glob/brace expansion), `js-yaml` (YAML eslintrc).
71
+
72
+ ## Limitations
73
+ - **Rule coverage** = oxlint's. ESLint rules oxlint doesn't implement (e.g.
74
+ `@typescript-eslint/naming-convention`, `@typescript-eslint/interface-name-prefix`,
75
+ `react/prop-types`, `react/no-deprecated`, `react/display-name`, `react/self-closing-comp`) are
76
+ silently dropped, so a few project warnings won't be reported. Type-aware rules requiring full
77
+ type information are not supported.
78
+ - **`--fix`** applies oxlint's fixers (a subset of ESLint's).
79
+ - **Custom in-repo rules / `--rulesdir` / arbitrary ESLint plugins** that aren't reimplemented in
80
+ oxlint don't run.
81
+ - **Formatters** cover `stylish` (default), `json`, `compact`, `unix`, `summary`. Other named
82
+ formatters fall back to `stylish`.
83
+ - Lints JS/TS/JSX only; `.json`/`.scss`/`.md` globs (which some configs include for other ESLint
84
+ plugins) are matched but skipped.
package/bin/oxpack.js ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * oxpack CLI — an `eslint`-compatible command backed by oxlint. Parses the common ESLint flags,
5
+ * expands globs (ESLint expands internally; oxlint does not), runs oxlint, renders an ESLint-style
6
+ * report, and exits with ESLint semantics (0 = clean / warnings under threshold, 1 = lint errors or
7
+ * --max-warnings exceeded, 2 = fatal).
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const { ESLint, expandPatterns } = require("../lib/index.js");
12
+ const { buildOxlintConfig, normalizeRules } = require("../lib/config.js");
13
+ const { runOxlint } = require("../lib/runner.js");
14
+ const { getFormatter } = require("../lib/format.js");
15
+
16
+ function parse(argv) {
17
+ const o = { patterns: [], rules: {}, ignorePattern: [], maxWarnings: -1, exts: [] };
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const a = argv[i];
20
+ const next = () => argv[++i];
21
+ if (a === "--fix") o.fix = true;
22
+ else if (a === "--fix-dry-run") o.fixDryRun = true;
23
+ else if (a === "--quiet") o.quiet = true;
24
+ else if (a === "--max-warnings") o.maxWarnings = parseInt(next(), 10);
25
+ else if (a.startsWith("--max-warnings=")) o.maxWarnings = parseInt(a.split("=")[1], 10);
26
+ else if (a === "-c" || a === "--config") o.config = next();
27
+ else if (a.startsWith("--config=")) o.config = a.split("=")[1];
28
+ else if (a === "--no-eslintrc") o.noEslintrc = true;
29
+ else if (a === "--ext") o.exts.push(...next().split(","));
30
+ else if (a.startsWith("--ext=")) o.exts.push(...a.split("=")[1].split(","));
31
+ else if (a === "-f" || a === "--format") o.format = next();
32
+ else if (a.startsWith("--format=")) o.format = a.split("=")[1];
33
+ else if (a === "-o" || a === "--output-file") o.outputFile = next();
34
+ else if (a === "--ignore-path") o.ignorePath = next();
35
+ else if (a.startsWith("--ignore-path=")) o.ignorePath = a.split("=")[1];
36
+ else if (a === "--ignore-pattern") o.ignorePattern.push(next());
37
+ else if (a === "--no-ignore") o.noIgnore = true;
38
+ else if (a === "--no-color") process.env.NO_COLOR = "1";
39
+ else if (a === "--color") process.env.FORCE_COLOR = "1";
40
+ else if (a === "--report-unused-disable-directives") o.reportUnused = true;
41
+ else if (a === "--rulesdir" || a === "--resolve-plugins-relative-to" || a === "--parser" || a === "--parser-options" || a === "--rule" || a === "--env" || a === "--global" || a === "--cache-location" || a === "--cache-strategy") { next(); /* accepted, ignored */ }
42
+ else if (a === "--cache" || a === "--no-cache" || a === "--stdin" || a === "--stdin-filename" || a === "--init" || a === "--debug" || a === "--exit-on-fatal-error") { if (a === "--stdin-filename") o.stdinFilename = next(); else if (a === "--stdin") o.stdin = true; }
43
+ else if (a === "-v" || a === "--version") { console.log(require("../package.json").version + " (oxpack, eslint-compatible)"); process.exit(0); }
44
+ else if (a === "-h" || a === "--help") { printHelp(); process.exit(0); }
45
+ else if (a.startsWith("-")) { /* unknown flag: ignore for drop-in tolerance */ }
46
+ else o.patterns.push(a);
47
+ }
48
+ return o;
49
+ }
50
+
51
+ function printHelp() {
52
+ console.log("oxpack — eslint-compatible CLI backed by oxlint\n\nUsage: eslint [options] [file|dir|glob]*\n\nCommon options: --fix --quiet --max-warnings <n> -c/--config <file> --no-eslintrc\n --ext <.ts,.tsx> -f/--format <stylish|json|compact|unix|summary> -o/--output-file <file>\n --ignore-path <file> --ignore-pattern <glob> --no-ignore --color/--no-color");
53
+ }
54
+
55
+ async function main() {
56
+ const opts = parse(process.argv.slice(2));
57
+ const cwd = process.cwd();
58
+
59
+ // stdin mode
60
+ if (opts.stdin) {
61
+ const code = fs.readFileSync(0, "utf8");
62
+ const eslint = new ESLint({ cwd, quiet: opts.quiet, useEslintrc: !opts.noEslintrc });
63
+ const results = await eslint.lintText(code, { filePath: opts.stdinFilename });
64
+ return finish(results, opts);
65
+ }
66
+
67
+ const files = expandPatterns(opts.patterns.length ? opts.patterns : ["."], cwd, opts.exts);
68
+ if (!files.length) {
69
+ if (opts.patterns.length) process.stderr.write("oxpack: no files matched the provided patterns\n");
70
+ process.exit(0);
71
+ }
72
+ const { oxConfig } = buildOxlintConfig({ cwd, configFile: opts.config, noEslintrc: opts.noEslintrc });
73
+ const { results, internalError } = runOxlint(files, oxConfig, {
74
+ cwd, fix: !!opts.fix, quiet: !!opts.quiet, ignorePath: opts.ignorePath, ignorePattern: opts.ignorePattern, noIgnore: opts.noIgnore,
75
+ });
76
+ if (internalError) { process.stderr.write(internalError + "\n"); process.exit(2); }
77
+ // ESLint reports clean files too (count only); for output we only need ones with messages.
78
+ finish(results, opts);
79
+ }
80
+
81
+ function finish(results, opts) {
82
+ const fmt = getFormatter(opts.format);
83
+ let output = fmt(results);
84
+ if (opts.outputFile) { fs.mkdirSync(path.dirname(path.resolve(opts.outputFile)), { recursive: true }); fs.writeFileSync(opts.outputFile, output); }
85
+ else if (output && output.trim()) process.stdout.write(output.endsWith("\n") ? output : output + "\n");
86
+
87
+ let errorCount = 0, warningCount = 0;
88
+ for (const r of results) { errorCount += r.errorCount; warningCount += r.warningCount; }
89
+ // ESLint exit semantics.
90
+ if (errorCount > 0) process.exit(1);
91
+ if (opts.maxWarnings >= 0 && warningCount > opts.maxWarnings) {
92
+ process.stderr.write(`oxpack: too many warnings (${warningCount}). Maximum allowed is ${opts.maxWarnings}.\n`);
93
+ process.exit(1);
94
+ }
95
+ process.exit(0);
96
+ }
97
+
98
+ main().catch((e) => { process.stderr.write(String(e && e.stack || e) + "\n"); process.exit(2); });
package/lib/config.js ADDED
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ /*
3
+ * oxpack config layer — discovers an ESLint configuration (eslintrc legacy or flat), and translates
4
+ * it into the `.oxlintrc.json` shape oxlint consumes (oxlint already targets ESLint-v8 config
5
+ * compatibility, so most of `rules`/`env`/`globals`/`settings`/`overrides` pass straight through;
6
+ * `plugins`/`extends`/`parser` need mapping).
7
+ */
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ const RC_NAMES = [
12
+ ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yaml", ".eslintrc.yml",
13
+ ".eslintrc.json", ".eslintrc",
14
+ ];
15
+ const FLAT_NAMES = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs"];
16
+
17
+ // ESLint plugin name -> oxlint plugin id (+ the rule-prefix oxlint expects for that plugin).
18
+ const PLUGIN_MAP = {
19
+ "@typescript-eslint": "typescript",
20
+ "typescript": "typescript",
21
+ "react": "react",
22
+ "react-hooks": "react",
23
+ "react-perf": "react-perf",
24
+ "import": "import",
25
+ "unicorn": "unicorn",
26
+ "jsx-a11y": "jsx-a11y",
27
+ "jest": "jest",
28
+ "vitest": "vitest",
29
+ "promise": "promise",
30
+ "n": "node",
31
+ "node": "node",
32
+ "next": "nextjs",
33
+ "@next/next": "nextjs",
34
+ "jsdoc": "jsdoc",
35
+ "vue": "vue",
36
+ };
37
+
38
+ // `extends` entry -> oxlint plugin(s) to enable (npm shareable configs can't be resolved, but their
39
+ // rule sets exist natively in oxlint behind these plugins).
40
+ function pluginsFromExtends(ext) {
41
+ const out = new Set();
42
+ for (const e of [].concat(ext || [])) {
43
+ const s = String(e);
44
+ if (/@typescript-eslint/.test(s)) out.add("typescript");
45
+ if (/plugin:react\//.test(s) || /airbnb|react-app/.test(s)) out.add("react");
46
+ if (/react-hooks/.test(s)) out.add("react");
47
+ if (/plugin:import\//.test(s) || /airbnb/.test(s)) out.add("import");
48
+ if (/unicorn/.test(s)) out.add("unicorn");
49
+ if (/jsx-a11y/.test(s) || /airbnb/.test(s)) out.add("jsx-a11y");
50
+ if (/plugin:jest\//.test(s)) out.add("jest");
51
+ if (/plugin:vitest\//.test(s)) out.add("vitest");
52
+ if (/plugin:promise\//.test(s)) out.add("promise");
53
+ if (/plugin:n\/|plugin:node\//.test(s)) out.add("node");
54
+ if (/plugin:jsdoc\//.test(s)) out.add("jsdoc");
55
+ if (/plugin:vue\//.test(s)) out.add("vue");
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function readRcFile(file) {
61
+ const ext = path.extname(file);
62
+ const raw = fs.readFileSync(file, "utf8");
63
+ if (ext === ".js" || ext === ".cjs") { delete require.cache[require.resolve(file)]; return require(file); }
64
+ if (ext === ".yaml" || ext === ".yml") return require("js-yaml").load(raw);
65
+ // .json / .eslintrc / unknown -> JSON5-ish (strip comments + trailing commas)
66
+ return JSON.parse(raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1"));
67
+ }
68
+
69
+ // Walk up from `dir` to find the nearest legacy rc or flat config (or package.json#eslintConfig).
70
+ function discoverConfig(dir) {
71
+ let d = dir;
72
+ for (let i = 0; i < 12 && d; i++) {
73
+ for (const n of FLAT_NAMES) { const p = path.join(d, n); if (fs.existsSync(p)) return { type: "flat", file: p }; }
74
+ for (const n of RC_NAMES) { const p = path.join(d, n); if (fs.existsSync(p)) return { type: "eslintrc", file: p }; }
75
+ const pkg = path.join(d, "package.json");
76
+ if (fs.existsSync(pkg)) {
77
+ try { const j = JSON.parse(fs.readFileSync(pkg, "utf8")); if (j.eslintConfig) return { type: "eslintrc", file: pkg, inline: j.eslintConfig }; } catch (_) {}
78
+ }
79
+ const up = path.dirname(d);
80
+ if (up === d) break;
81
+ d = up;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ // Resolve a legacy eslintrc, following local-file `extends` (npm extends are inferred, not loaded).
87
+ function loadEslintrc(file, inline, seen) {
88
+ seen = seen || new Set();
89
+ if (file && seen.has(file)) return {};
90
+ if (file) seen.add(file);
91
+ const cfg = inline || readRcFile(file);
92
+ const baseDir = file ? path.dirname(file) : process.cwd();
93
+ let merged = { rules: {}, env: {}, globals: {}, settings: {}, plugins: [], _pluginIds: new Set(), overrides: [], ignorePatterns: [] };
94
+ for (const ext of [].concat(cfg.extends || [])) {
95
+ if (typeof ext === "string" && (ext.startsWith(".") || ext.startsWith("/") || path.isAbsolute(ext))) {
96
+ const p = path.resolve(baseDir, ext);
97
+ if (fs.existsSync(p)) { const sub = loadEslintrc(p, null, seen); mergeInto(merged, sub); }
98
+ }
99
+ for (const pid of pluginsFromExtends(ext)) merged._pluginIds.add(pid);
100
+ }
101
+ for (const p of cfg.plugins || []) { const id = PLUGIN_MAP[p] || PLUGIN_MAP[p.replace(/^eslint-plugin-/, "")]; if (id) merged._pluginIds.add(id); }
102
+ mergeInto(merged, {
103
+ rules: cfg.rules || {}, env: cfg.env || {}, globals: cfg.globals || {}, settings: cfg.settings || {},
104
+ overrides: cfg.overrides || [], ignorePatterns: [].concat(cfg.ignorePatterns || []),
105
+ });
106
+ return merged;
107
+ }
108
+
109
+ function mergeInto(target, src) {
110
+ Object.assign(target.rules, src.rules || {});
111
+ Object.assign(target.env, src.env || {});
112
+ Object.assign(target.globals, src.globals || {});
113
+ Object.assign(target.settings, src.settings || {});
114
+ if (src.overrides) target.overrides.push(...src.overrides);
115
+ if (src.ignorePatterns) target.ignorePatterns.push(...src.ignorePatterns);
116
+ if (src._pluginIds) for (const p of src._pluginIds) target._pluginIds.add(p);
117
+ }
118
+
119
+ // Load the set of rule names oxlint actually implements from its bundled JSON schema, so we can
120
+ // drop unknown ESLint rules (oxlint hard-errors on unrecognized rule names in config).
121
+ let _knownRules = null;
122
+ function knownRules() {
123
+ if (_knownRules) return _knownRules;
124
+ _knownRules = new Set();
125
+ try {
126
+ // oxlint's `exports` blocks subpath resolution, so locate the schema via the package dir.
127
+ const pkgPath = require.resolve("oxlint/package.json");
128
+ const schemaPath = path.join(path.dirname(pkgPath), "configuration_schema.json");
129
+ const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
130
+ const ref = schema.definitions && schema.definitions.OxlintRules && schema.definitions.OxlintRules.$ref;
131
+ const defName = ref && ref.split("/").pop();
132
+ const def = defName && schema.definitions[defName];
133
+ if (def && def.properties) for (const name of Object.keys(def.properties)) _knownRules.add(name);
134
+ } catch (_) { /* if unavailable, knownRules stays empty -> we keep all (best effort) */ }
135
+ return _knownRules;
136
+ }
137
+
138
+ // oxlint accepts ESLint-style rule names for the plugins it supports (e.g. `@typescript-eslint/x`,
139
+ // `react/x`) as well as bare core rules. We therefore keep rule names AS WRITTEN, except for a few
140
+ // ESLint plugins that have no oxlint plugin but whose rule maps to an oxlint built-in. Genuinely
141
+ // unknown rules/plugins are removed at runtime by the runner's self-correcting retry loop.
142
+ const PREFIX_ALIASES = {
143
+ // eslint-plugin-unused-imports rules are covered by oxlint's core no-unused-vars.
144
+ "unused-imports/no-unused-vars": "no-unused-vars",
145
+ "unused-imports/no-unused-imports": "no-unused-vars",
146
+ "unused-imports/no-unused-imports-ts": "no-unused-vars",
147
+ "unused-imports/no-unused-vars-ts": "no-unused-vars",
148
+ };
149
+
150
+ function toOxlintRuleName(eslintRule) {
151
+ if (PREFIX_ALIASES[eslintRule]) return PREFIX_ALIASES[eslintRule];
152
+ return eslintRule; // keep as written (prefixed plugin rules are understood by oxlint)
153
+ }
154
+
155
+ // Deprecated/renamed ESLint rules -> oxlint's current rule name, so a user's severity/options on the
156
+ // old name still apply. Only used when the target rule isn't already configured explicitly.
157
+ const RULE_ALIASES = {
158
+ "no-empty-interface": "no-empty-object-type",
159
+ "ban-ts-comment": "ban-ts-comment",
160
+ };
161
+
162
+ // Per-rule option translators for renamed rules whose option shape changed.
163
+ const OPTION_TRANSLATORS = {
164
+ // @typescript-eslint/no-empty-interface { allowSingleExtends } -> no-empty-object-type. Restrict to
165
+ // interfaces only (allowObjectTypes:"always") so empty `type X = {}` — which no-empty-interface
166
+ // never flagged — is not newly reported.
167
+ "no-empty-interface->no-empty-object-type": (opts) => {
168
+ return { allowInterfaces: (opts && opts.allowSingleExtends) ? "with-single-extends" : "never", allowObjectTypes: "always" };
169
+ },
170
+ };
171
+
172
+ // Build an oxlint rule value, preserving ESLint options. oxlint reads the eslint `[severity, opts]`
173
+ // array form for the rules that support options; unsupported options are handled by a retry that
174
+ // downgrades to bare severity (see runner).
175
+ function ruleEntry(severity, options) {
176
+ if (options === undefined) return severity;
177
+ return [severity, options];
178
+ }
179
+
180
+ // Normalize ESLint `rules` to oxlint form: keep names as written (oxlint understands prefixed plugin
181
+ // rules), preserve options, apply prefix-aliases and renamed-rule aliases. Unknown rules/plugins are
182
+ // stripped at runtime by the runner. Severity is preserved.
183
+ function normalizeRules(rules) {
184
+ const out = {};
185
+ const sev = (v) => (v === 2 || v === "error") ? "error" : (v === 1 || v === "warn") ? "warn" : "off";
186
+ const bare = (k) => k.replace(/^@[^/]+\//, "").replace(/^[^/]+\//, "");
187
+ const explicit = new Set();
188
+
189
+ // 1) rules as written (with prefix-aliasing + options).
190
+ for (const [name, val] of Object.entries(rules || {})) {
191
+ const oxName = toOxlintRuleName(name);
192
+ const severity = Array.isArray(val) ? sev(val[0]) : sev(val);
193
+ const options = Array.isArray(val) && val.length > 1 ? val[1] : undefined;
194
+ const incoming = ruleEntry(severity, options);
195
+
196
+ if (out[oxName] === undefined) {
197
+ out[oxName] = incoming;
198
+ explicit.add(oxName);
199
+ continue;
200
+ }
201
+ // A rule written under its own canonical oxlint name always wins over aliased entries.
202
+ if (oxName === name) {
203
+ out[oxName] = incoming;
204
+ explicit.add(oxName);
205
+ continue;
206
+ }
207
+ // Collision from aliasing (e.g. several unused-imports/* rules all map to no-unused-vars):
208
+ // prefer the entry that carries options — that's the more specific rule (e.g. the vars rule
209
+ // with varsIgnorePattern), so a sibling like no-unused-imports:"error" can't clobber the
210
+ // user's intended ["warn", { varsIgnorePattern }] and turn warnings into build-breaking errors.
211
+ const existingHasOpts = Array.isArray(out[oxName]) && out[oxName].length > 1;
212
+ if (options !== undefined && !existingHasOpts) out[oxName] = incoming;
213
+ }
214
+
215
+ // 2) renamed/deprecated rules -> aliased oxlint rule (only if target not already set explicitly).
216
+ for (const [name, val] of Object.entries(rules || {})) {
217
+ const b = bare(name);
218
+ const alias = RULE_ALIASES[b];
219
+ if (!alias || alias === b || explicit.has(alias) || out[alias] !== undefined) continue;
220
+ const severity = Array.isArray(val) ? sev(val[0]) : sev(val);
221
+ let options = Array.isArray(val) && val.length > 1 ? val[1] : undefined;
222
+ const xl = OPTION_TRANSLATORS[`${b}->${alias}`];
223
+ if (xl) options = xl(options);
224
+ out[alias] = ruleEntry(severity, options);
225
+ }
226
+ return out;
227
+ }
228
+
229
+ // Translate a discovered ESLint config into an `.oxlintrc.json` object oxlint can consume.
230
+ function toOxlintConfig(cfg) {
231
+ const plugins = new Set(["typescript", "unicorn", "oxc"]); // oxlint defaults
232
+ for (const p of cfg._pluginIds || []) plugins.add(p);
233
+ const ox = {
234
+ plugins: [...plugins],
235
+ categories: { correctness: "warn" },
236
+ rules: normalizeRules(cfg.rules),
237
+ env: cfg.env && Object.keys(cfg.env).length ? cfg.env : { builtin: true },
238
+ };
239
+ if (cfg.globals && Object.keys(cfg.globals).length) ox.globals = sanitizeGlobals(cfg.globals);
240
+ const settings = sanitizeSettings(cfg.settings);
241
+ if (settings && Object.keys(settings).length) ox.settings = settings;
242
+ if (cfg.ignorePatterns && cfg.ignorePatterns.length) ox.ignorePatterns = cfg.ignorePatterns;
243
+ if (cfg.overrides && cfg.overrides.length) {
244
+ ox.overrides = cfg.overrides.map((o) => ({ files: [].concat(o.files || []), rules: normalizeRules(o.rules), ...(o.env ? { env: o.env } : {}) }));
245
+ }
246
+ return ox;
247
+ }
248
+
249
+ // oxlint validates settings strictly. Map ESLint's `react.version: "detect"` to a concrete version
250
+ // and drop any settings keys oxlint doesn't understand so the config always parses.
251
+ function sanitizeSettings(settings) {
252
+ if (!settings || typeof settings !== "object") return undefined;
253
+ const out = {};
254
+ if (settings.react && typeof settings.react === "object") {
255
+ const r = Object.assign({}, settings.react);
256
+ if (!r.version || r.version === "detect" || !/^\d+\.\d+/.test(String(r.version))) r.version = "18.0";
257
+ out.react = { version: r.version };
258
+ }
259
+ if (settings.jsx_a11y || settings["jsx-a11y"]) out["jsx-a11y"] = settings["jsx-a11y"] || settings.jsx_a11y;
260
+ // `import` resolver settings etc. are not consumed by oxlint — omit them.
261
+ return out;
262
+ }
263
+
264
+ // oxlint accepts ESLint global values true/false/"readonly"/"writable"; coerce anything else.
265
+ function sanitizeGlobals(globals) {
266
+ const out = {};
267
+ for (const [k, v] of Object.entries(globals)) {
268
+ if (v === true || v === "writable" || v === "writeable") out[k] = "writable";
269
+ else if (v === false || v === "readonly" || v === "readable") out[k] = "readonly";
270
+ else if (v === "off") out[k] = "off";
271
+ else out[k] = "readonly";
272
+ }
273
+ return out;
274
+ }
275
+
276
+ // Returns { oxConfig, pluginFlags, source } or null when no config found / --no-eslintrc.
277
+ function buildOxlintConfig(opts) {
278
+ if (opts && opts.noEslintrc) return { oxConfig: { plugins: ["typescript", "unicorn", "oxc"], categories: { correctness: "warn" } }, source: null };
279
+ let found;
280
+ if (opts && opts.configFile) found = { type: opts.configFile.endsWith(".js") || opts.configFile.endsWith(".mjs") || opts.configFile.endsWith(".cjs") ? "flat" : "eslintrc", file: path.resolve(opts.configFile) };
281
+ else found = discoverConfig((opts && opts.cwd) || process.cwd());
282
+ if (!found) return { oxConfig: { plugins: ["typescript", "unicorn", "oxc"], categories: { correctness: "warn" } }, source: null };
283
+ if (found.type === "flat") {
284
+ // Flat config: we can read `rules`/`plugins` heuristically but cannot fully evaluate it; enable
285
+ // common plugins and let oxlint use its own discovery. Pass-through with defaults.
286
+ const flat = loadFlat(found.file);
287
+ return { oxConfig: toOxlintConfig(flat), source: found.file };
288
+ }
289
+ const merged = loadEslintrc(found.inline ? found.file : found.file, found.inline, new Set());
290
+ return { oxConfig: toOxlintConfig(merged), source: found.file };
291
+ }
292
+
293
+ // Best-effort flat-config reader: merges `rules`/`plugins` across array entries.
294
+ function loadFlat(file) {
295
+ let arr;
296
+ try { delete require.cache[require.resolve(file)]; arr = require(file); arr = arr && arr.default ? arr.default : arr; } catch (_) { arr = []; }
297
+ arr = [].concat(arr || []);
298
+ const merged = { rules: {}, env: {}, globals: {}, settings: {}, overrides: [], ignorePatterns: [], _pluginIds: new Set() };
299
+ for (const block of arr) {
300
+ if (!block || typeof block !== "object") continue;
301
+ Object.assign(merged.rules, block.rules || {});
302
+ if (block.languageOptions && block.languageOptions.globals) Object.assign(merged.globals, block.languageOptions.globals);
303
+ for (const name of Object.keys(block.plugins || {})) { const id = PLUGIN_MAP[name] || PLUGIN_MAP[name.replace(/^eslint-plugin-/, "")]; if (id) merged._pluginIds.add(id); }
304
+ if (block.ignores) merged.ignorePatterns.push(...block.ignores);
305
+ }
306
+ return merged;
307
+ }
308
+
309
+ module.exports = { discoverConfig, buildOxlintConfig, toOxlintConfig, loadEslintrc, normalizeRules, PLUGIN_MAP };
package/lib/format.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /* ESLint-compatible output formatters rendered from LintResult[]. */
3
+ const path = require("path");
4
+
5
+ const c = (process.stdout.isTTY || process.env.FORCE_COLOR) && !process.env.NO_COLOR
6
+ ? { dim: "\x1b[2m", red: "\x1b[31m", yellow: "\x1b[33m", under: "\x1b[4m", bold: "\x1b[1m", reset: "\x1b[0m", green: "\x1b[32m" }
7
+ : { dim: "", red: "", yellow: "", under: "", bold: "", reset: "", green: "" };
8
+
9
+ function stylish(results) {
10
+ let out = "\n";
11
+ let errors = 0, warnings = 0;
12
+ for (const r of results) {
13
+ if (!r.messages.length) continue;
14
+ out += `${c.under}${r.filePath}${c.reset}\n`;
15
+ for (const m of r.messages) {
16
+ const sev = m.severity === 2 ? `${c.red}error${c.reset}` : `${c.yellow}warning${c.reset}`;
17
+ if (m.severity === 2) errors++; else warnings++;
18
+ const loc = `${c.dim}${m.line}:${m.column}${c.reset}`;
19
+ out += ` ${loc} ${sev} ${m.message} ${c.dim}${m.ruleId || ""}${c.reset}\n`;
20
+ }
21
+ out += "\n";
22
+ }
23
+ const total = errors + warnings;
24
+ if (total > 0) {
25
+ const color = errors > 0 ? c.red : c.yellow;
26
+ out += `${color}${c.bold}\u2716 ${total} problem${total === 1 ? "" : "s"} (${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"})${c.reset}\n`;
27
+ }
28
+ return total > 0 ? out : "";
29
+ }
30
+
31
+ function compact(results) {
32
+ const lines = [];
33
+ for (const r of results) for (const m of r.messages) {
34
+ lines.push(`${r.filePath}: line ${m.line}, col ${m.column}, ${m.severity === 2 ? "Error" : "Warning"} - ${m.message} (${m.ruleId || ""})`);
35
+ }
36
+ const total = lines.length;
37
+ if (total) lines.push(`\n${total} problem${total === 1 ? "" : "s"}`);
38
+ return lines.join("\n");
39
+ }
40
+
41
+ function unix(results) {
42
+ const lines = [];
43
+ for (const r of results) for (const m of r.messages) {
44
+ lines.push(`${r.filePath}:${m.line}:${m.column}: ${m.message} [${m.severity === 2 ? "Error" : "Warning"}/${m.ruleId || ""}]`);
45
+ }
46
+ return lines.join("\n");
47
+ }
48
+
49
+ function json(results) { return JSON.stringify(results); }
50
+
51
+ function summary(results) {
52
+ const byRule = new Map();
53
+ let errors = 0, warnings = 0;
54
+ for (const r of results) for (const m of r.messages) {
55
+ if (m.severity === 2) errors++; else warnings++;
56
+ const k = m.ruleId || "(unknown)";
57
+ byRule.set(k, (byRule.get(k) || 0) + 1);
58
+ }
59
+ const rows = [...byRule.entries()].sort((a, b) => b[1] - a[1]);
60
+ let out = "\nESLint Summary\n";
61
+ for (const [rule, n] of rows) out += ` ${String(n).padStart(5)} ${rule}\n`;
62
+ out += `\n ${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}\n`;
63
+ return out;
64
+ }
65
+
66
+ const FORMATTERS = { stylish, json, compact, unix, summary };
67
+
68
+ function getFormatter(name) {
69
+ return FORMATTERS[name || "stylish"] || stylish;
70
+ }
71
+
72
+ module.exports = { getFormatter, stylish, json, compact, unix, summary };
package/lib/index.js ADDED
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ /*
3
+ * oxpack — a drop-in ESLint Node API + CLI backed by oxlint.
4
+ *
5
+ * Exposes the modern `ESLint` class (lintFiles/lintText/loadFormatter/outputFixes/…), the legacy
6
+ * `CLIEngine`, `Linter`, and `loadESLint`, all delegating to oxlint (Rust). Config is discovered
7
+ * from the usual ESLint files and translated to oxlint's `.oxlintrc.json`.
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+ const fg = require("fast-glob");
12
+ const { buildOxlintConfig } = require("./config");
13
+ const { runOxlint } = require("./runner");
14
+ const { getFormatter } = require("./format");
15
+
16
+ const JS_EXTS = [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"];
17
+
18
+ function expandPatterns(patterns, cwd, extensions) {
19
+ const exts = (extensions && extensions.length ? extensions : JS_EXTS).map((e) => (e[0] === "." ? e : "." + e));
20
+ const pats = [].concat(patterns || []);
21
+ const files = new Set();
22
+ for (let p of pats) {
23
+ p = p.replace(/\\/g, "/");
24
+ const abs = path.resolve(cwd, p);
25
+ // Directory -> all matching files within.
26
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
27
+ const found = fg.sync(exts.map((e) => `${p.replace(/\/$/, "")}/**/*${e}`), { cwd, absolute: true, dot: false, ignore: ["**/node_modules/**"] });
28
+ found.forEach((f) => files.add(f));
29
+ } else if (fs.existsSync(abs) && fs.statSync(abs).isFile()) {
30
+ files.add(abs);
31
+ } else {
32
+ // Glob pattern (incl. brace expansion) -> resolve via fast-glob.
33
+ const found = fg.sync(p, { cwd, absolute: true, dot: false, ignore: ["**/node_modules/**"] });
34
+ for (const f of found) if (exts.includes(path.extname(f))) files.add(f);
35
+ }
36
+ }
37
+ return [...files];
38
+ }
39
+
40
+ class ESLint {
41
+ constructor(options) {
42
+ this.options = options || {};
43
+ this.cwd = this.options.cwd || process.cwd();
44
+ }
45
+
46
+ async lintFiles(patterns) {
47
+ const cwd = this.cwd;
48
+ const o = this.options;
49
+ const overrideConfigFile = o.overrideConfigFile;
50
+ const noEslintrc = o.useEslintrc === false || overrideConfigFile === true;
51
+ const extensions = o.extensions;
52
+ const files = expandPatterns(patterns, cwd, extensions);
53
+ if (!files.length) return [];
54
+ // Discover/translate config relative to the first file's directory.
55
+ const { oxConfig } = buildOxlintConfig({ cwd, configFile: typeof overrideConfigFile === "string" ? overrideConfigFile : undefined, noEslintrc });
56
+ applyOverrides(oxConfig, o.overrideConfig);
57
+ const { results, internalError } = runOxlint(files, oxConfig, {
58
+ cwd, fix: !!o.fix, quiet: !!o.quiet, ignorePath: o.ignorePath, noIgnore: o.ignore === false,
59
+ ignorePattern: o.overrideConfig && o.overrideConfig.ignorePatterns,
60
+ });
61
+ if (internalError) { const e = new Error(internalError); throw e; }
62
+ // Ensure every linted file has a result entry (ESLint returns clean files too).
63
+ return fillMissing(results, files);
64
+ }
65
+
66
+ async lintText(code, options) {
67
+ options = options || {};
68
+ const cwd = this.cwd;
69
+ const filePath = options.filePath || path.join(cwd, "__stdin__.ts");
70
+ const tmp = path.join(require("os").tmpdir(), `oxpack-stdin-${process.pid}-${Date.now()}${path.extname(filePath) || ".ts"}`);
71
+ fs.writeFileSync(tmp, code);
72
+ try {
73
+ const { oxConfig } = buildOxlintConfig({ cwd, noEslintrc: this.options.useEslintrc === false });
74
+ const { results } = runOxlint([tmp], oxConfig, { cwd, quiet: !!this.options.quiet });
75
+ const r = results[0] || { filePath: tmp, messages: [], errorCount: 0, warningCount: 0, fatalErrorCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, suppressedMessages: [], usedDeprecatedRules: [] };
76
+ r.filePath = path.resolve(filePath);
77
+ r.source = code;
78
+ return [r];
79
+ } finally { try { fs.unlinkSync(tmp); } catch (_) {} }
80
+ }
81
+
82
+ async loadFormatter(name) {
83
+ const fmt = getFormatter(name);
84
+ return { format: (results) => fmt(results) };
85
+ }
86
+
87
+ async calculateConfigForFile(filePath) {
88
+ const { oxConfig } = buildOxlintConfig({ cwd: path.dirname(path.resolve(filePath)) });
89
+ return { rules: oxConfig.rules || {}, env: oxConfig.env || {}, globals: oxConfig.globals || {}, plugins: oxConfig.plugins || [], settings: oxConfig.settings || {} };
90
+ }
91
+
92
+ async isPathIgnored(filePath) {
93
+ // Honor .eslintignore via oxlint by linting just this file with --quiet and checking inclusion.
94
+ const cwd = this.cwd;
95
+ const { results } = runOxlint([path.resolve(filePath)], null, { cwd, quiet: true });
96
+ // If oxlint reports the file at all it's not ignored; emptiness is ambiguous, default to false.
97
+ return false;
98
+ }
99
+
100
+ getRulesMetaForResults() { return {}; }
101
+
102
+ static async outputFixes(results) {
103
+ // oxlint applies fixes in-place when run with --fix; lintFiles already did that when fix:true.
104
+ // For API symmetry, write back any `output` present on results.
105
+ for (const r of results) if (r.output != null && r.filePath) { try { fs.writeFileSync(r.filePath, r.output); } catch (_) {} }
106
+ }
107
+
108
+ static getErrorResults(results) {
109
+ const out = [];
110
+ for (const r of results) {
111
+ const msgs = r.messages.filter((m) => m.severity === 2);
112
+ if (msgs.length) out.push(Object.assign({}, r, { messages: msgs, warningCount: 0, fixableWarningCount: 0 }));
113
+ }
114
+ return out;
115
+ }
116
+
117
+ static get version() { return "8.57.0-oxpack"; }
118
+ }
119
+
120
+ function applyOverrides(oxConfig, override) {
121
+ if (!override) return;
122
+ if (override.rules) Object.assign(oxConfig.rules = oxConfig.rules || {}, require("./config").normalizeRules(override.rules));
123
+ if (override.env) Object.assign(oxConfig.env = oxConfig.env || {}, override.env);
124
+ if (override.globals) Object.assign(oxConfig.globals = oxConfig.globals || {}, override.globals);
125
+ }
126
+
127
+ function fillMissing(results, files) {
128
+ const have = new Set(results.map((r) => path.resolve(r.filePath)));
129
+ for (const f of files) if (!have.has(path.resolve(f))) results.push({ filePath: path.resolve(f), messages: [], suppressedMessages: [], errorCount: 0, warningCount: 0, fatalErrorCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, usedDeprecatedRules: [] });
130
+ return results;
131
+ }
132
+
133
+ // Legacy CLIEngine (eslint <7) — enough for tools that still use it.
134
+ class CLIEngine {
135
+ constructor(options) { this.options = options || {}; this._eslint = new ESLint(translateLegacy(options)); }
136
+ executeOnFiles(patterns) {
137
+ // CLIEngine is sync; run oxlint synchronously via the same path.
138
+ const cwd = this.options.cwd || process.cwd();
139
+ const files = expandPatterns(patterns, cwd, this.options.extensions);
140
+ const { oxConfig } = buildOxlintConfig({ cwd, noEslintrc: this.options.useEslintrc === false });
141
+ const { results } = runOxlint(files, oxConfig, { cwd, fix: !!this.options.fix, quiet: !!this.options.quiet });
142
+ const filled = fillMissing(results, files);
143
+ return makeReport(filled);
144
+ }
145
+ executeOnText(text, filename) {
146
+ const cwd = this.options.cwd || process.cwd();
147
+ const tmp = path.join(require("os").tmpdir(), `oxpack-cli-${Date.now()}${path.extname(filename || ".ts") || ".ts"}`);
148
+ fs.writeFileSync(tmp, text);
149
+ try {
150
+ const { oxConfig } = buildOxlintConfig({ cwd });
151
+ const { results } = runOxlint([tmp], oxConfig, { cwd });
152
+ if (results[0]) results[0].filePath = filename ? path.resolve(filename) : "<text>";
153
+ return makeReport(results.length ? results : [{ filePath: filename || "<text>", messages: [], errorCount: 0, warningCount: 0, fatalErrorCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 }]);
154
+ } finally { try { fs.unlinkSync(tmp); } catch (_) {} }
155
+ }
156
+ getFormatter(name) { return (results) => getFormatter(name)(results); }
157
+ static outputFixes(report) { return ESLint.outputFixes(report.results || []); }
158
+ static getErrorResults(results) { return ESLint.getErrorResults(results); }
159
+ }
160
+
161
+ function translateLegacy(o) {
162
+ o = o || {};
163
+ return { cwd: o.cwd, fix: o.fix, quiet: o.quiet, extensions: o.extensions, useEslintrc: o.useEslintrc, ignore: o.ignore, ignorePath: o.ignorePath, overrideConfig: o.baseConfig || o.rules ? { rules: o.rules } : undefined };
164
+ }
165
+
166
+ function makeReport(results) {
167
+ let errorCount = 0, warningCount = 0, fixableErrorCount = 0, fixableWarningCount = 0;
168
+ for (const r of results) { errorCount += r.errorCount; warningCount += r.warningCount; fixableErrorCount += r.fixableErrorCount || 0; fixableWarningCount += r.fixableWarningCount || 0; }
169
+ return { results, errorCount, warningCount, fixableErrorCount, fixableWarningCount, usedDeprecatedRules: [] };
170
+ }
171
+
172
+ // Minimal Linter (verify on text) — oxlint has no in-process verify, so shell out per call.
173
+ class Linter {
174
+ constructor() {}
175
+ verify(code, _config, options) {
176
+ const filename = (typeof options === "string" ? options : options && options.filename) || "input.ts";
177
+ const tmp = path.join(require("os").tmpdir(), `oxpack-linter-${Date.now()}${path.extname(filename) || ".ts"}`);
178
+ fs.writeFileSync(tmp, code);
179
+ try { const { results } = runOxlint([tmp], null, { cwd: process.cwd() }); return (results[0] && results[0].messages) || []; }
180
+ finally { try { fs.unlinkSync(tmp); } catch (_) {} }
181
+ }
182
+ verifyAndFix(code) { return { fixed: false, messages: this.verify(code), output: code }; }
183
+ getRules() { return new Map(); }
184
+ defineRule() {} defineRules() {} defineParser() {}
185
+ static get version() { return ESLint.version; }
186
+ }
187
+
188
+ async function loadESLint(opts) { return ESLint; }
189
+
190
+ module.exports = { ESLint, CLIEngine, Linter, loadESLint, expandPatterns };
191
+ module.exports.default = module.exports;
package/lib/runner.js ADDED
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /* Runs oxlint and maps its JSON diagnostics to ESLint-shaped results. */
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const { spawnSync } = require("child_process");
7
+
8
+ function oxlintBin() {
9
+ // oxlint's package.json `exports` blocks subpath resolution, so locate the package dir via its
10
+ // package.json and join the declared bin path.
11
+ try {
12
+ const pkgPath = require.resolve("oxlint/package.json");
13
+ const pkg = require(pkgPath);
14
+ const rel = typeof pkg.bin === "string" ? pkg.bin : (pkg.bin && pkg.bin.oxlint) || "bin/oxlint";
15
+ return path.join(path.dirname(pkgPath), rel);
16
+ } catch (_) {}
17
+ return "oxlint";
18
+ }
19
+
20
+ // oxlint diagnostic code "eslint(no-debugger)" / "typescript(no-explicit-any)" -> ESLint ruleId.
21
+ function toRuleId(code) {
22
+ if (!code) return null;
23
+ const m = /^([\w-]+)\(([^)]+)\)$/.exec(code);
24
+ if (!m) return code;
25
+ const plugin = m[1], rule = m[2];
26
+ switch (plugin) {
27
+ case "eslint": return rule;
28
+ case "typescript": return "@typescript-eslint/" + rule;
29
+ case "react": return "react/" + rule;
30
+ case "react-perf": return "react-perf/" + rule;
31
+ case "react-hooks": return "react-hooks/" + rule;
32
+ case "jsx-a11y": return "jsx-a11y/" + rule;
33
+ case "nextjs": return "@next/next/" + rule;
34
+ default: return plugin + "/" + rule;
35
+ }
36
+ }
37
+
38
+ function severityNum(s) { return s === "error" ? 2 : 1; }
39
+
40
+ // Run oxlint over `paths` with the given oxlint config, returning ESLint LintResult[].
41
+ function runOxlint(paths, oxConfigObj, opts) {
42
+ opts = opts || {};
43
+ // oxlint hard-fails config parsing on rule names it doesn't implement. We can't reliably know that
44
+ // set ahead of time (its schema lists placeholder rules), so we run, and if it reports
45
+ // "Rule 'X' not found", strip those rules and retry until the config is accepted.
46
+ let cfg = oxConfigObj ? JSON.parse(JSON.stringify(oxConfigObj)) : null;
47
+ let strippedOptions = false;
48
+ for (let attempt = 0; attempt < 10; attempt++) {
49
+ const res = invoke(paths, cfg, opts);
50
+ const errText = (res.parseError || "");
51
+ if (errText) {
52
+ const bad = extractUnknownRules(errText);
53
+ if (bad.length && cfg) {
54
+ let removed = false;
55
+ for (const name of bad) { if (stripRule(cfg, name)) removed = true; }
56
+ if (removed) continue; // retry without the offending rules
57
+ }
58
+ // Unknown plugin (e.g. an eslint plugin with no oxlint equivalent) -> drop it + any rules that
59
+ // reference it by that prefix, then retry.
60
+ const badPlugins = extractUnknownPlugins(errText);
61
+ if (badPlugins.length && cfg) {
62
+ let changed = false;
63
+ for (const pl of badPlugins) { if (stripPlugin(cfg, pl)) changed = true; }
64
+ if (changed) continue;
65
+ }
66
+ // Not an unknown-rule/plugin error (e.g. an unsupported rule option). Downgrade every rule to
67
+ // its bare severity (drop options) once, then retry — options are best-effort, never fatal.
68
+ if (cfg && !strippedOptions && hasRuleOptions(cfg)) { dropAllOptions(cfg); strippedOptions = true; continue; }
69
+ return { results: [], internalError: errText };
70
+ }
71
+ return { results: groupResults((res.parsed && res.parsed.diagnostics) || [], opts.cwd || process.cwd()), raw: res.parsed };
72
+ }
73
+ return { results: [], internalError: "oxpack: could not produce a valid oxlint config after stripping unknown rules/options" };
74
+ }
75
+
76
+ function hasRuleOptions(cfg) {
77
+ const any = (rules) => rules && Object.values(rules).some((v) => Array.isArray(v) && v.length > 1);
78
+ if (any(cfg.rules)) return true;
79
+ if (cfg.overrides) for (const o of cfg.overrides) if (any(o.rules)) return true;
80
+ return false;
81
+ }
82
+ function dropAllOptions(cfg) {
83
+ const strip = (rules) => { if (!rules) return; for (const k of Object.keys(rules)) if (Array.isArray(rules[k])) rules[k] = rules[k][0]; };
84
+ strip(cfg.rules);
85
+ if (cfg.overrides) for (const o of cfg.overrides) strip(o.rules);
86
+ }
87
+
88
+ // One oxlint invocation. Returns { parsed } on success or { parseError } on a config-parse failure.
89
+ function invoke(paths, cfg, opts) {
90
+ const args = [];
91
+ let tmpCfg;
92
+ if (cfg) {
93
+ tmpCfg = path.join(os.tmpdir(), `oxpack-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.oxlintrc.json`);
94
+ fs.writeFileSync(tmpCfg, JSON.stringify(cfg));
95
+ args.push("-c", tmpCfg);
96
+ }
97
+ if (opts.fix) args.push("--fix");
98
+ if (opts.quiet) args.push("--quiet");
99
+ if (opts.ignorePath) args.push("--ignore-path", opts.ignorePath);
100
+ for (const p of opts.ignorePattern || []) args.push("--ignore-pattern", p);
101
+ if (opts.noIgnore) args.push("--no-ignore");
102
+ if (opts.tsconfig) args.push("--tsconfig", opts.tsconfig);
103
+ args.push("--format", "json", "--no-error-on-unmatched-pattern");
104
+ args.push(...paths);
105
+
106
+ const bin = oxlintBin();
107
+ const res = spawnSync(process.execPath, [bin, ...args], { cwd: opts.cwd || process.cwd(), encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
108
+ if (tmpCfg) { try { fs.unlinkSync(tmpCfg); } catch (_) {} }
109
+ if (res.error) throw res.error;
110
+ const stdout = res.stdout || "";
111
+ const stderr = res.stderr || "";
112
+ // Config-parse failures are printed (to stdout or stderr) and produce no JSON.
113
+ if (/Failed to parse (oxlint )?config/i.test(stdout + stderr) || /not found in plugin/i.test(stdout + stderr)) {
114
+ return { parseError: (stdout + "\n" + stderr).trim() };
115
+ }
116
+ try { return { parsed: JSON.parse(stdout) }; }
117
+ catch (_) { return { parseError: (stderr || stdout || "oxlint failed").trim() }; }
118
+ }
119
+
120
+ function extractUnknownRules(text) {
121
+ const out = [];
122
+ const re = /Rule '([^']+)' not found/g;
123
+ let m;
124
+ while ((m = re.exec(text))) out.push(m[1]);
125
+ return out;
126
+ }
127
+
128
+ function extractUnknownPlugins(text) {
129
+ const out = [];
130
+ const re = /Plugin '([^']+)' not found/g;
131
+ let m;
132
+ while ((m = re.exec(text))) out.push(m[1]);
133
+ return out;
134
+ }
135
+
136
+ // Remove an unknown plugin from `plugins` and any rule keyed with that plugin prefix.
137
+ function stripPlugin(cfg, plugin) {
138
+ let changed = false;
139
+ if (Array.isArray(cfg.plugins)) { const before = cfg.plugins.length; cfg.plugins = cfg.plugins.filter((p) => p !== plugin); if (cfg.plugins.length !== before) changed = true; }
140
+ const prefix = plugin + "/";
141
+ const strip = (rules) => { if (!rules) return; for (const k of Object.keys(rules)) if (k.startsWith(prefix) || k.startsWith("@" + prefix)) { delete rules[k]; changed = true; } };
142
+ strip(cfg.rules);
143
+ if (cfg.overrides) for (const o of cfg.overrides) strip(o.rules);
144
+ return changed;
145
+ }
146
+
147
+ function stripRule(cfg, name) {
148
+ const bare = (k) => k.replace(/^@[^/]+\//, "").replace(/^[^/]+\//, "");
149
+ let removed = false;
150
+ const strip = (rules) => {
151
+ if (!rules) return;
152
+ for (const k of Object.keys(rules)) {
153
+ if (k === name || bare(k) === name || bare(k) === bare(name)) { delete rules[k]; removed = true; }
154
+ }
155
+ };
156
+ strip(cfg.rules);
157
+ if (cfg.overrides) for (const o of cfg.overrides) strip(o.rules);
158
+ return removed;
159
+ }
160
+
161
+ // Group flat diagnostics by file into ESLint LintResult objects.
162
+ function groupResults(diagnostics, cwd) {
163
+ const byFile = new Map();
164
+ for (const d of diagnostics) {
165
+ const file = path.resolve(cwd, d.filename || "<input>");
166
+ if (!byFile.has(file)) byFile.set(file, []);
167
+ const label = (d.labels && d.labels[0] && d.labels[0].span) || {};
168
+ const sev = severityNum(d.severity);
169
+ byFile.get(file).push({
170
+ ruleId: toRuleId(d.code),
171
+ severity: sev,
172
+ message: d.message || "",
173
+ line: label.line || 1,
174
+ column: label.column || 1,
175
+ endLine: label.line || undefined,
176
+ endColumn: label.column != null && label.length != null ? label.column + label.length : undefined,
177
+ nodeType: null,
178
+ messageId: undefined,
179
+ fix: undefined,
180
+ });
181
+ }
182
+ const results = [];
183
+ for (const [filePath, messages] of byFile) results.push(makeResult(filePath, messages));
184
+ return results;
185
+ }
186
+
187
+ function makeResult(filePath, messages) {
188
+ let errorCount = 0, warningCount = 0, fatalErrorCount = 0;
189
+ for (const m of messages) { if (m.severity === 2) { errorCount++; if (m.fatal) fatalErrorCount++; } else warningCount++; }
190
+ return {
191
+ filePath, messages,
192
+ suppressedMessages: [],
193
+ errorCount, warningCount, fatalErrorCount,
194
+ fixableErrorCount: 0, fixableWarningCount: 0,
195
+ source: undefined, usedDeprecatedRules: [],
196
+ };
197
+ }
198
+
199
+ module.exports = { runOxlint, toRuleId, makeResult, groupResults };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@rustwrap/eslint",
3
+ "version": "1.0.1",
4
+ "description": "A drop-in ESLint replacement (CLI + Node API) backed by the Rust-based oxlint for very fast linting. Honors .eslintrc(.json/.js/.yml), package.json#eslintConfig, flat eslint.config.js, and .eslintignore.",
5
+ "main": "lib/index.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "test": "node test/run.js"
11
+ },
12
+ "bin": {
13
+ "eslint": "bin/oxpack.js",
14
+ "oxpack": "bin/oxpack.js"
15
+ },
16
+ "files": [
17
+ "lib/",
18
+ "bin/",
19
+ "README.md"
20
+ ],
21
+ "dependencies": {
22
+ "fast-glob": "^3.3.2",
23
+ "js-yaml": "^4.1.0",
24
+ "oxlint": "^1.71.0"
25
+ },
26
+ "keywords": [
27
+ "eslint",
28
+ "oxlint",
29
+ "linter",
30
+ "oxc",
31
+ "pcf"
32
+ ],
33
+ "license": "MIT"
34
+ }