@oxlint/migrate 1.42.0 → 1.46.0
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 +13 -23
- package/dist/bin/oxlint-migrate.mjs +137 -32
- package/dist/{src-BiX8O61Z.mjs → settings-C8UlaScv.mjs} +628 -559
- package/dist/src/index.d.mts +2673 -6
- package/dist/src/index.mjs +58 -2
- package/package.json +10 -13
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ When no config file is provided, the script searches for the default ESLint conf
|
|
|
24
24
|
| `--type-aware` | Include type aware rules, which are supported with `oxlint --type-aware` and [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) |
|
|
25
25
|
| `--with-nursery` | Include oxlint rules which are currently under development |
|
|
26
26
|
| `--js-plugins` | \*\* Include ESLint plugins via `jsPlugins` key. |
|
|
27
|
+
| `--details` | List rules that could not be migrated to oxlint |
|
|
27
28
|
| `--output-file <file>` | The oxlint configuration file where ESLint v9 rules will be written to, default: `.oxlintrc.json` |
|
|
28
29
|
| `--replace-eslint-comments` | Search in the project files for ESLint comments and replaces them with oxlint. Some ESLint comments are not supported and will be reported. |
|
|
29
30
|
|
|
@@ -43,31 +44,18 @@ Tested ESLint Plugins with `oxlint` can be found in this [Oxc Discussion](https:
|
|
|
43
44
|
|
|
44
45
|
### TypeScript ESLint Configuration Files
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
For Node.js, you must install [jiti](https://www.npmjs.com/package/jiti) as a dev dependency.
|
|
47
|
+
TypeScript configuration files, like `eslint.config.mts`, are supported in the following environments:
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
- **Deno and Bun**: TypeScript configuration files are natively supported.
|
|
50
|
+
- **Node.js >=22.18.0**: TypeScript configuration files are supported natively with built-in type-stripping enabled by default.
|
|
51
|
+
- **Node.js >=22.6.0**: TypeScript configuration files can be used by setting `NODE_OPTIONS=--experimental-strip-types`.
|
|
52
|
+
- **Node.js <22.6.0**: TypeScript configuration files can be used by setting `NODE_OPTIONS=--import @oxc-node/core/register` and installing [@oxc-node/core](https://www.npmjs.com/package/@oxc-node/core) as a dev dependency.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
If you attempt to use a TypeScript configuration file without the proper setup for your Node.js version, Node.js will throw an error when trying to import the file.
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
pnpm generate
|
|
57
|
-
pnpm format
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Unit + Integration Test
|
|
61
|
-
|
|
62
|
-
```shell
|
|
63
|
-
pnpm vitest
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Manual Testing
|
|
56
|
+
## Contributing
|
|
67
57
|
|
|
68
|
-
|
|
69
|
-
pnpm manual-test
|
|
70
|
-
```
|
|
58
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for details on how to contribute to this project.
|
|
71
59
|
|
|
72
60
|
## Caveats
|
|
73
61
|
|
|
@@ -75,9 +63,11 @@ The migration tool has been tested to work quite well for simple ESLint flat con
|
|
|
75
63
|
|
|
76
64
|
Here are some known caveats to be aware of:
|
|
77
65
|
|
|
78
|
-
**`settings` field
|
|
66
|
+
**`settings` field migration**
|
|
67
|
+
|
|
68
|
+
The `settings` field (e.g. for setting the React version) is migrated for known oxlint-supported plugins: `jsx-a11y`, `next`, `react`, `jsdoc`, and `vitest`. By default, other settings keys are skipped since they aren't supported by oxlint. If using the `--js-plugins` flag, other settings keys will also be migrated in order to support JS Plugins.
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
Note: Oxlint does not support `settings` in override configs. If your ESLint config has settings in configs with `files` patterns, those settings will be skipped and a warning will be shown.
|
|
81
71
|
|
|
82
72
|
Not all `settings` options are supported by oxlint, and so rule behavior in certain edge-cases may differ. See [the Settings docs](https://oxc.rs/docs/guide/usage/linter/config-file-reference.html#settings) for more info.
|
|
83
73
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as preFixForJsPlugins, f as nurseryRules, p as rules_exports, u as isOffValue } from "../settings-C8UlaScv.mjs";
|
|
3
|
+
import main from "../src/index.mjs";
|
|
3
4
|
import { program } from "commander";
|
|
4
5
|
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
@@ -17,9 +18,9 @@ const FLAT_CONFIG_FILENAMES = [
|
|
|
17
18
|
"eslint.config.mts",
|
|
18
19
|
"eslint.config.cts"
|
|
19
20
|
];
|
|
20
|
-
const getAutodetectedEslintConfigName = (cwd
|
|
21
|
+
const getAutodetectedEslintConfigName = (cwd) => {
|
|
21
22
|
for (const filename of FLAT_CONFIG_FILENAMES) {
|
|
22
|
-
const filePath = path.join(cwd
|
|
23
|
+
const filePath = path.join(cwd, filename);
|
|
23
24
|
if (existsSync(filePath)) return filePath;
|
|
24
25
|
}
|
|
25
26
|
};
|
|
@@ -27,20 +28,12 @@ const loadESLintConfig = async (filePath) => {
|
|
|
27
28
|
if (filePath.endsWith("json")) throw new Error(`json format is not supported. @oxlint/migrate only supports the eslint flat configuration`);
|
|
28
29
|
let url = pathToFileURL(filePath).toString();
|
|
29
30
|
if (!existsSync(filePath)) throw new Error(`eslint config file not found: ${filePath}`);
|
|
30
|
-
if ("Bun" in globalThis || "Deno" in globalThis) return import(url);
|
|
31
|
-
if (filePath.endsWith(".ts") || filePath.endsWith(".mts") || filePath.endsWith(".cts")) {
|
|
32
|
-
const { createJiti } = await import("jiti");
|
|
33
|
-
return createJiti(filePath, {
|
|
34
|
-
interopDefault: false,
|
|
35
|
-
moduleCache: false
|
|
36
|
-
}).import(url);
|
|
37
|
-
}
|
|
38
31
|
return import(url);
|
|
39
32
|
};
|
|
40
33
|
|
|
41
34
|
//#endregion
|
|
42
35
|
//#region package.json
|
|
43
|
-
var version = "1.
|
|
36
|
+
var version = "1.46.0";
|
|
44
37
|
|
|
45
38
|
//#endregion
|
|
46
39
|
//#region src/walker/comments/replaceRuleDirectiveComment.ts
|
|
@@ -211,8 +204,8 @@ function partialAstroSourceTextLoader(sourceText) {
|
|
|
211
204
|
pos = frontmatterEndDelimiter + 3;
|
|
212
205
|
}
|
|
213
206
|
}
|
|
214
|
-
results.push(...extractScriptBlocks(sourceText, pos, Number.MAX_SAFE_INTEGER, false).map((sourceText
|
|
215
|
-
return Object.assign(sourceText
|
|
207
|
+
results.push(...extractScriptBlocks(sourceText, pos, Number.MAX_SAFE_INTEGER, false).map((sourceText) => {
|
|
208
|
+
return Object.assign(sourceText, {
|
|
216
209
|
lang: `ts`,
|
|
217
210
|
sourceType: `module`
|
|
218
211
|
});
|
|
@@ -227,7 +220,7 @@ const getComments = (absoluteFilePath, partialSourceText, options) => {
|
|
|
227
220
|
lang: partialSourceText.lang,
|
|
228
221
|
sourceType: partialSourceText.sourceType
|
|
229
222
|
});
|
|
230
|
-
if (parserResult.errors.length > 0) options.reporter?.
|
|
223
|
+
if (parserResult.errors.length > 0) options.reporter?.addWarning(`${absoluteFilePath}: failed to parse`);
|
|
231
224
|
return parserResult.comments;
|
|
232
225
|
};
|
|
233
226
|
function replaceCommentsInSourceText(absoluteFilePath, partialSourceText, options) {
|
|
@@ -241,7 +234,7 @@ function replaceCommentsInSourceText(absoluteFilePath, partialSourceText, option
|
|
|
241
234
|
}
|
|
242
235
|
} catch (error) {
|
|
243
236
|
if (error instanceof Error) {
|
|
244
|
-
options.reporter?.
|
|
237
|
+
options.reporter?.addWarning(`${absoluteFilePath}, char offset ${comment.start + partialSourceText.offset}: ${error.message}`);
|
|
245
238
|
continue;
|
|
246
239
|
}
|
|
247
240
|
throw error;
|
|
@@ -258,13 +251,13 @@ function replaceCommentsInFile(absoluteFilePath, fileContent, options) {
|
|
|
258
251
|
|
|
259
252
|
//#endregion
|
|
260
253
|
//#region src/walker/index.ts
|
|
261
|
-
const walkAndReplaceProjectFiles = (projectFiles, readFileSync
|
|
254
|
+
const walkAndReplaceProjectFiles = (projectFiles, readFileSync, writeFile, options) => {
|
|
262
255
|
return Promise.all(projectFiles.map((file) => {
|
|
263
|
-
const sourceText = readFileSync
|
|
256
|
+
const sourceText = readFileSync(file);
|
|
264
257
|
if (!sourceText) return Promise.resolve();
|
|
265
258
|
const newSourceText = replaceCommentsInFile(file, sourceText, options);
|
|
266
259
|
if (newSourceText === sourceText) return Promise.resolve();
|
|
267
|
-
return writeFile
|
|
260
|
+
return writeFile(file, newSourceText);
|
|
268
261
|
}));
|
|
269
262
|
};
|
|
270
263
|
|
|
@@ -281,20 +274,18 @@ const getAllProjectFiles = () => {
|
|
|
281
274
|
//#endregion
|
|
282
275
|
//#region src/reporter.ts
|
|
283
276
|
var DefaultReporter = class {
|
|
284
|
-
|
|
277
|
+
warnings = /* @__PURE__ */ new Set();
|
|
285
278
|
skippedRules = new Map([
|
|
286
279
|
["nursery", /* @__PURE__ */ new Set()],
|
|
287
280
|
["type-aware", /* @__PURE__ */ new Set()],
|
|
288
|
-
["unsupported", /* @__PURE__ */ new Set()]
|
|
281
|
+
["unsupported", /* @__PURE__ */ new Set()],
|
|
282
|
+
["js-plugins", /* @__PURE__ */ new Set()]
|
|
289
283
|
]);
|
|
290
|
-
|
|
291
|
-
this.
|
|
292
|
-
}
|
|
293
|
-
remove(message) {
|
|
294
|
-
this.reports.delete(message);
|
|
284
|
+
addWarning(message) {
|
|
285
|
+
this.warnings.add(message);
|
|
295
286
|
}
|
|
296
|
-
|
|
297
|
-
return Array.from(this.
|
|
287
|
+
getWarnings() {
|
|
288
|
+
return Array.from(this.warnings);
|
|
298
289
|
}
|
|
299
290
|
markSkipped(rule, category) {
|
|
300
291
|
this.skippedRules.get(category)?.add(rule);
|
|
@@ -306,6 +297,7 @@ var DefaultReporter = class {
|
|
|
306
297
|
const result = {
|
|
307
298
|
nursery: [],
|
|
308
299
|
"type-aware": [],
|
|
300
|
+
"js-plugins": [],
|
|
309
301
|
unsupported: []
|
|
310
302
|
};
|
|
311
303
|
for (const [category, rules] of this.skippedRules) result[category] = Array.from(rules);
|
|
@@ -313,6 +305,92 @@ var DefaultReporter = class {
|
|
|
313
305
|
}
|
|
314
306
|
};
|
|
315
307
|
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region bin/output-formatter.ts
|
|
310
|
+
const CATEGORY_METADATA = {
|
|
311
|
+
nursery: {
|
|
312
|
+
label: "Nursery",
|
|
313
|
+
description: "Experimental:"
|
|
314
|
+
},
|
|
315
|
+
"type-aware": {
|
|
316
|
+
label: "Type-aware",
|
|
317
|
+
description: "Requires TS info:"
|
|
318
|
+
},
|
|
319
|
+
"js-plugins": {
|
|
320
|
+
label: "JS Plugins",
|
|
321
|
+
description: "Requires JS plugins:"
|
|
322
|
+
},
|
|
323
|
+
unsupported: { label: "Unsupported" }
|
|
324
|
+
};
|
|
325
|
+
const MAX_LABEL_LENGTH = Math.max(...Object.values(CATEGORY_METADATA).map((meta) => meta.label.length));
|
|
326
|
+
/**
|
|
327
|
+
* Formats a category summary as either inline (with example) or vertical list
|
|
328
|
+
*/
|
|
329
|
+
function formatCategorySummary(count, category, rules, showAll) {
|
|
330
|
+
const meta = CATEGORY_METADATA[category];
|
|
331
|
+
if (!showAll) {
|
|
332
|
+
const maxRules = 3;
|
|
333
|
+
const exampleList = rules.slice(0, maxRules).join(", ");
|
|
334
|
+
const suffix = count > maxRules ? ", and more" : "";
|
|
335
|
+
const prefix = meta.description ? `${meta.description} ` : "";
|
|
336
|
+
return ` - ${String(count).padStart(3)} ${meta.label.padEnd(MAX_LABEL_LENGTH)} (${prefix}${exampleList}${suffix})\n`;
|
|
337
|
+
}
|
|
338
|
+
let output = ` - ${count} ${meta.label}\n`;
|
|
339
|
+
for (const rule of rules) output += ` - ${rule}\n`;
|
|
340
|
+
return output;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Detects which CLI flags are missing and could enable more rules
|
|
344
|
+
*/
|
|
345
|
+
function detectMissingFlags(byCategory, cliOptions) {
|
|
346
|
+
const missingFlags = [];
|
|
347
|
+
if (byCategory.nursery.length > 0 && !cliOptions.withNursery) missingFlags.push("--with-nursery");
|
|
348
|
+
if (byCategory["type-aware"].length > 0 && !cliOptions.typeAware) missingFlags.push("--type-aware");
|
|
349
|
+
if (byCategory["js-plugins"].length > 0 && !cliOptions.jsPlugins) missingFlags.push("--js-plugins");
|
|
350
|
+
return missingFlags;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Formats the complete migration output message
|
|
354
|
+
*/
|
|
355
|
+
function formatMigrationOutput(data) {
|
|
356
|
+
let output = "";
|
|
357
|
+
const showAll = data.cliOptions.details || false;
|
|
358
|
+
if (data.enabledRulesCount === 0) output += `\n⚠️ ${data.outputFileName} created with no rules enabled.\n`;
|
|
359
|
+
else output += `\n✨ ${data.outputFileName} created with ${data.enabledRulesCount} rules.\n`;
|
|
360
|
+
const byCategory = data.skippedRulesByCategory;
|
|
361
|
+
const nurseryCount = byCategory.nursery.length;
|
|
362
|
+
const typeAwareCount = byCategory["type-aware"].length;
|
|
363
|
+
const unsupportedCount = byCategory.unsupported.length;
|
|
364
|
+
const jsPluginsCount = byCategory["js-plugins"].length;
|
|
365
|
+
const totalSkipped = nurseryCount + typeAwareCount + unsupportedCount + jsPluginsCount;
|
|
366
|
+
if (totalSkipped > 0) {
|
|
367
|
+
output += `\n Skipped ${totalSkipped} rules:\n`;
|
|
368
|
+
if (nurseryCount > 0) output += formatCategorySummary(nurseryCount, "nursery", byCategory.nursery, showAll);
|
|
369
|
+
if (typeAwareCount > 0) output += formatCategorySummary(typeAwareCount, "type-aware", byCategory["type-aware"], showAll);
|
|
370
|
+
if (jsPluginsCount > 0) output += formatCategorySummary(jsPluginsCount, "js-plugins", byCategory["js-plugins"], showAll);
|
|
371
|
+
if (unsupportedCount > 0) output += formatCategorySummary(unsupportedCount, "unsupported", byCategory.unsupported, showAll);
|
|
372
|
+
if (!showAll) {
|
|
373
|
+
const maxExamples = 3;
|
|
374
|
+
if (nurseryCount > maxExamples || typeAwareCount > maxExamples || unsupportedCount > maxExamples || jsPluginsCount > maxExamples) output += `\n Tip: Use --details to see the full list.\n`;
|
|
375
|
+
}
|
|
376
|
+
const missingFlags = detectMissingFlags(byCategory, data.cliOptions);
|
|
377
|
+
if (missingFlags.length > 0) {
|
|
378
|
+
const eslintConfigArg = data.eslintConfigPath ? ` ${path.basename(data.eslintConfigPath)}` : "";
|
|
379
|
+
output += `\n👉 Re-run with flags to include more:\n`;
|
|
380
|
+
output += ` npx @oxlint/migrate${eslintConfigArg} ${missingFlags.join(" ")}\n`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (data.enabledRulesCount > 0) {
|
|
384
|
+
output += `\n🚀 Next:\n`;
|
|
385
|
+
output += ` npx oxlint .\n`;
|
|
386
|
+
}
|
|
387
|
+
return output;
|
|
388
|
+
}
|
|
389
|
+
function displayMigrationResult(outputMessage, warnings) {
|
|
390
|
+
console.log(outputMessage);
|
|
391
|
+
for (const warning of warnings) console.warn(warning);
|
|
392
|
+
}
|
|
393
|
+
|
|
316
394
|
//#endregion
|
|
317
395
|
//#region bin/oxlint-migrate.ts
|
|
318
396
|
const cwd = process.cwd();
|
|
@@ -323,7 +401,22 @@ const getFileContent = (absoluteFilePath) => {
|
|
|
323
401
|
return;
|
|
324
402
|
}
|
|
325
403
|
};
|
|
326
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Count enabled rules (excluding "off" rules) from both rules and overrides
|
|
406
|
+
*/
|
|
407
|
+
const countEnabledRules = (config) => {
|
|
408
|
+
const enabledRules = /* @__PURE__ */ new Set();
|
|
409
|
+
if (config.rules) {
|
|
410
|
+
for (const [ruleName, ruleValue] of Object.entries(config.rules)) if (!isOffValue(ruleValue)) enabledRules.add(ruleName);
|
|
411
|
+
}
|
|
412
|
+
if (config.overrides && Array.isArray(config.overrides)) {
|
|
413
|
+
for (const override of config.overrides) if (override.rules) {
|
|
414
|
+
for (const [ruleName, ruleValue] of Object.entries(override.rules)) if (!isOffValue(ruleValue)) enabledRules.add(ruleName);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return enabledRules.size;
|
|
418
|
+
};
|
|
419
|
+
program.name("oxlint-migrate").version(version).argument("[eslint-config]", "The path to the eslint v9 config file").option("--output-file <file>", "The oxlint configuration file where to eslint v9 rules will be written to", ".oxlintrc.json").option("--merge", "Merge eslint configuration with an existing .oxlintrc.json configuration", false).option("--with-nursery", "Include oxlint rules which are currently under development", false).option("--replace-eslint-comments", "Search in the project files for eslint comments and replaces them with oxlint. Some eslint comments are not supported and will be reported.").option("--type-aware", "Includes supported type-aware rules. Needs the same flag in `oxlint` to enable it.").option("--js-plugins", "Tries to convert unsupported oxlint plugins with `jsPlugins`.").option("--details", "List rules that could not be migrated to oxlint.", false).action(async (filePath) => {
|
|
327
420
|
const cliOptions = program.opts();
|
|
328
421
|
const oxlintFilePath = path.join(cwd, cliOptions.outputFile);
|
|
329
422
|
const reporter = new DefaultReporter();
|
|
@@ -335,7 +428,7 @@ program.name("oxlint-migrate").version(version).argument("[eslint-config]", "The
|
|
|
335
428
|
jsPlugins: !!cliOptions.jsPlugins
|
|
336
429
|
};
|
|
337
430
|
if (cliOptions.replaceEslintComments) {
|
|
338
|
-
await walkAndReplaceProjectFiles(await getAllProjectFiles(), (filePath
|
|
431
|
+
await walkAndReplaceProjectFiles(await getAllProjectFiles(), (filePath) => getFileContent(filePath), (filePath, content) => writeFile(filePath, content, "utf-8"), options);
|
|
339
432
|
return;
|
|
340
433
|
}
|
|
341
434
|
if (filePath === void 0) filePath = getAutodetectedEslintConfigName(cwd);
|
|
@@ -349,10 +442,22 @@ program.name("oxlint-migrate").version(version).argument("[eslint-config]", "The
|
|
|
349
442
|
encoding: "utf8",
|
|
350
443
|
flag: "r"
|
|
351
444
|
}));
|
|
352
|
-
const oxlintConfig = "default" in eslintConfigs ? await
|
|
445
|
+
const oxlintConfig = "default" in eslintConfigs ? await main(eslintConfigs.default, config, options) : await main(eslintConfigs, config, options);
|
|
353
446
|
if (existsSync(oxlintFilePath)) renameSync(oxlintFilePath, `${oxlintFilePath}.bak`);
|
|
354
447
|
writeFileSync(oxlintFilePath, JSON.stringify(oxlintConfig, null, 2));
|
|
355
|
-
|
|
448
|
+
const enabledRulesCount = countEnabledRules(oxlintConfig);
|
|
449
|
+
displayMigrationResult(formatMigrationOutput({
|
|
450
|
+
outputFileName: cliOptions.outputFile,
|
|
451
|
+
enabledRulesCount,
|
|
452
|
+
skippedRulesByCategory: reporter.getSkippedRulesByCategory(),
|
|
453
|
+
cliOptions: {
|
|
454
|
+
withNursery: !!cliOptions.withNursery,
|
|
455
|
+
typeAware: !!cliOptions.typeAware,
|
|
456
|
+
details: !!cliOptions.details,
|
|
457
|
+
jsPlugins: !!cliOptions.jsPlugins
|
|
458
|
+
},
|
|
459
|
+
eslintConfigPath: filePath
|
|
460
|
+
}), reporter.getWarnings());
|
|
356
461
|
});
|
|
357
462
|
program.parse();
|
|
358
463
|
|