@oxlint/migrate 1.41.0 → 1.43.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 CHANGED
@@ -4,7 +4,9 @@
4
4
  [![NPM Version](https://img.shields.io/npm/v/%40oxlint%2Fmigrate)](https://www.npmjs.com/package/@oxlint/migrate)
5
5
  [![NPM Downloads](https://img.shields.io/npm/dm/%40oxlint%2Fmigrate)](https://www.npmjs.com/package/@oxlint/migrate)
6
6
 
7
- Generates a `.oxlintrc.json` from an existing eslint flat config.
7
+ Generates a `.oxlintrc.json` from an existing ESLint flat config.
8
+
9
+ See [the Migration Guide in the Oxlint docs](https://oxc.rs/docs/guide/usage/linter/migrate-from-eslint.html) for more information on migrating from ESLint to Oxlint.
8
10
 
9
11
  ## Usage
10
12
 
@@ -12,32 +14,33 @@ Generates a `.oxlintrc.json` from an existing eslint flat config.
12
14
  npx @oxlint/migrate <optional-eslint-flat-config-path>
13
15
  ```
14
16
 
15
- When no config file provided, the script searches for the default eslint config filenames in the current directory.
17
+ When no config file is provided, the script searches for the default ESLint config filenames in the current directory.
16
18
 
17
19
  ### Options
18
20
 
19
21
  | Options | Description |
20
22
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | `--merge` | \* merge eslint configuration with an existing .oxlintrc.json configuration |
22
- | `--type-aware` | Include type aware rules, which are supported with `oxlint --type-aware` |
23
+ | `--merge` | \* merge ESLint configuration with an existing .oxlintrc.json configuration |
24
+ | `--type-aware` | Include type aware rules, which are supported with `oxlint --type-aware` and [oxlint-tsgolint](https://github.com/oxc-project/tsgolint) |
23
25
  | `--with-nursery` | Include oxlint rules which are currently under development |
24
26
  | `--js-plugins` | \*\* Include ESLint plugins via `jsPlugins` key. |
25
- | `--output-file <file>` | The oxlint configuration file where to eslint v9 rules will be written to, default: `.oxlintrc.json` |
26
- | `--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. |
27
+ | `--details` | List rules that could not be migrated to oxlint |
28
+ | `--output-file <file>` | The oxlint configuration file where ESLint v9 rules will be written to, default: `.oxlintrc.json` |
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. |
27
30
 
28
31
  \* WARNING: When some `categories` are enabled, this tools will enable more rules with the combination of `plugins`.
29
- Else we need to disable each rule `plugin/categories` combination, which is not covered by your eslint configuration.
32
+ Else we need to disable each rule `plugin/categories` combination, which is not covered by your ESLint configuration.
30
33
  This behavior can change in the future.
31
34
 
32
- \*\* WARNING: Tries to guess the plugin name. Should work with most of the plugin names.
35
+ \*\* WARNING: Tries to guess the plugin name. Should work fine with most plugin names, but is not perfect.
33
36
  Not every ESLint API is integrated with `oxlint`.
34
- Tested ESLint Plugins with `oxlint` can be found in this [Oxc Discussion](https://github.com/oxc-project/oxc/discussions/14862).
37
+ Tested ESLint Plugins with `oxlint` can be found in this [Oxc Discussion](https://github.com/oxc-project/oxc/discussions/14862). See the caveats section for more details.
35
38
 
36
39
  ### User Flow
37
40
 
38
41
  - Upgrade `oxlint` and `@oxlint/migrate` to the same version.
39
42
  - Execute `npx @oxlint/migrate`
40
- - (Optional): Disable supported rules via [eslint-plugin-oxlint](https://github.com/oxc-project/eslint-plugin-oxlint)
43
+ - (Optional): Disable supported rules via [eslint-plugin-oxlint](https://github.com/oxc-project/eslint-plugin-oxlint), if you have any rules you need that aren't in Oxlint yet.
41
44
 
42
45
  ### TypeScript ESLint Configuration Files
43
46
 
@@ -66,3 +69,64 @@ pnpm vitest
66
69
  ```shell
67
70
  pnpm manual-test
68
71
  ```
72
+
73
+ ## Caveats
74
+
75
+ The migration tool has been tested to work quite well for simple ESLint flat config files. It has also been tested to work correctly for the large majority of complex flat config files. However, there may be some edge cases where the migration is not perfect, or the behavior of Oxlint itself differs from ESLint.
76
+
77
+ Here are some known caveats to be aware of:
78
+
79
+ **`settings` field not migrated**
80
+
81
+ The `settings` field (e.g. for setting the React version) is not migrated to the oxlint config yet. You may need to copy it over manually if you have any settings specified.
82
+
83
+ 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.
84
+
85
+ **Local ESLint Plugins imported via path are not migrated**
86
+
87
+ The `--js-plugins` flag cannot migrate ESLint plugins from file paths in the same repo currently (e.g. if you have `../eslint-plugin-myplugin` in your `eslint.config.mjs`). You will need to copy them over into the `jsPlugins` manually. See [the JS Plugins docs]() for more info.
88
+
89
+ **`globals` field with large number of values**
90
+
91
+ If you end up with a very large list of values for the `globals` field, it's likely because your version of the `globals` npm package is older (or newer!) than the one used in `@oxlint/migrate`.
92
+
93
+ You can generally fix this by updating the `globals` package to the latest version so we can recognize the relevant globals and handle it as a simple `env` field.
94
+
95
+ For example, this is a good `.oxlintrc.json` and means the globals used by your ESLint config were recognized:
96
+
97
+ ```jsonc
98
+ {
99
+ "env": {
100
+ "browser": true,
101
+ },
102
+ }
103
+ ```
104
+
105
+ And this is bad:
106
+
107
+ ```jsonc
108
+ {
109
+ "globals": {
110
+ "window": "readonly",
111
+ "document": "readonly",
112
+ "navigator": "readonly",
113
+ // ...and a few hundred more
114
+ },
115
+ }
116
+ ```
117
+
118
+ **Oxlint can potentially lint more files by default**
119
+
120
+ If you extend certain ESLint configs (e.g. the airbnb config), they can disable many - or even all - rules for specific files or file types. And this is not always obvious to the end-user.
121
+
122
+ Depending on how this is implemented by the given config, these behaviors may not migrate to your Oxlint config. If you see certain files that you do not want to run Oxlint on which the migrator did not handle, you can add the relevant patterns to the `ignorePatterns` field in `.oxlintrc.json`.
123
+
124
+ **Not all ESLint plugins will work with JS Plugins**
125
+
126
+ The JS Plugins API supports almost all ESLint v9+ plugins for linting JS/TS/JSX/TSX files, but there are still some minor holes in support. See the [JS Plugins documentation](https://oxc.rs/docs/guide/usage/linter/js-plugins.html) for specifics.
127
+
128
+ For example, if you currently use `eslint-plugin-prettier`, it will not work as a JS Plugin, as we do not support custom parsers for JS Plugins. To replace the functionality of `eslint-plugin-prettier`, you can use `prettier --check` in CI and/or your git pre-commit hooks to ensure code formatting is enforced.
129
+
130
+ You could also consider [replacing Prettier with Oxfmt](https://oxc.rs/docs/guide/usage/formatter/migrate-from-prettier.html).
131
+
132
+ Note that `eslint-config-prettier` is different from the prettier plugin, and _will_ be migrated fine, as it's just a config to disable formatting-related ESLint rules, not an actual plugin.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as rules_exports, n as preFixForJsPlugins, r as nurseryRules, t as src_default } from "../src-BNblTDrB.mjs";
2
+ import { a as rules_exports, i as nurseryRules, n as preFixForJsPlugins, r as isOffValue, t as src_default } from "../src-BMWXAQvA.mjs";
3
3
  import { program } from "commander";
4
4
  import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
@@ -17,9 +17,9 @@ const FLAT_CONFIG_FILENAMES = [
17
17
  "eslint.config.mts",
18
18
  "eslint.config.cts"
19
19
  ];
20
- const getAutodetectedEslintConfigName = (cwd$1) => {
20
+ const getAutodetectedEslintConfigName = (cwd) => {
21
21
  for (const filename of FLAT_CONFIG_FILENAMES) {
22
- const filePath = path.join(cwd$1, filename);
22
+ const filePath = path.join(cwd, filename);
23
23
  if (existsSync(filePath)) return filePath;
24
24
  }
25
25
  };
@@ -40,7 +40,7 @@ const loadESLintConfig = async (filePath) => {
40
40
 
41
41
  //#endregion
42
42
  //#region package.json
43
- var version = "1.41.0";
43
+ var version = "1.43.0";
44
44
 
45
45
  //#endregion
46
46
  //#region src/walker/comments/replaceRuleDirectiveComment.ts
@@ -211,8 +211,8 @@ function partialAstroSourceTextLoader(sourceText) {
211
211
  pos = frontmatterEndDelimiter + 3;
212
212
  }
213
213
  }
214
- results.push(...extractScriptBlocks(sourceText, pos, Number.MAX_SAFE_INTEGER, false).map((sourceText$1) => {
215
- return Object.assign(sourceText$1, {
214
+ results.push(...extractScriptBlocks(sourceText, pos, Number.MAX_SAFE_INTEGER, false).map((sourceText) => {
215
+ return Object.assign(sourceText, {
216
216
  lang: `ts`,
217
217
  sourceType: `module`
218
218
  });
@@ -227,7 +227,7 @@ const getComments = (absoluteFilePath, partialSourceText, options) => {
227
227
  lang: partialSourceText.lang,
228
228
  sourceType: partialSourceText.sourceType
229
229
  });
230
- if (parserResult.errors.length > 0) options.reporter?.report(`${absoluteFilePath}: failed to parse`);
230
+ if (parserResult.errors.length > 0) options.reporter?.addWarning(`${absoluteFilePath}: failed to parse`);
231
231
  return parserResult.comments;
232
232
  };
233
233
  function replaceCommentsInSourceText(absoluteFilePath, partialSourceText, options) {
@@ -241,7 +241,7 @@ function replaceCommentsInSourceText(absoluteFilePath, partialSourceText, option
241
241
  }
242
242
  } catch (error) {
243
243
  if (error instanceof Error) {
244
- options.reporter?.report(`${absoluteFilePath}, char offset ${comment.start + partialSourceText.offset}: ${error.message}`);
244
+ options.reporter?.addWarning(`${absoluteFilePath}, char offset ${comment.start + partialSourceText.offset}: ${error.message}`);
245
245
  continue;
246
246
  }
247
247
  throw error;
@@ -258,13 +258,13 @@ function replaceCommentsInFile(absoluteFilePath, fileContent, options) {
258
258
 
259
259
  //#endregion
260
260
  //#region src/walker/index.ts
261
- const walkAndReplaceProjectFiles = (projectFiles, readFileSync$1, writeFile$1, options) => {
261
+ const walkAndReplaceProjectFiles = (projectFiles, readFileSync, writeFile, options) => {
262
262
  return Promise.all(projectFiles.map((file) => {
263
- const sourceText = readFileSync$1(file);
263
+ const sourceText = readFileSync(file);
264
264
  if (!sourceText) return Promise.resolve();
265
265
  const newSourceText = replaceCommentsInFile(file, sourceText, options);
266
266
  if (newSourceText === sourceText) return Promise.resolve();
267
- return writeFile$1(file, newSourceText);
267
+ return writeFile(file, newSourceText);
268
268
  }));
269
269
  };
270
270
 
@@ -281,20 +281,18 @@ const getAllProjectFiles = () => {
281
281
  //#endregion
282
282
  //#region src/reporter.ts
283
283
  var DefaultReporter = class {
284
- reports = /* @__PURE__ */ new Set();
284
+ warnings = /* @__PURE__ */ new Set();
285
285
  skippedRules = new Map([
286
286
  ["nursery", /* @__PURE__ */ new Set()],
287
287
  ["type-aware", /* @__PURE__ */ new Set()],
288
- ["unsupported", /* @__PURE__ */ new Set()]
288
+ ["unsupported", /* @__PURE__ */ new Set()],
289
+ ["js-plugins", /* @__PURE__ */ new Set()]
289
290
  ]);
290
- report(message) {
291
- this.reports.add(message);
291
+ addWarning(message) {
292
+ this.warnings.add(message);
292
293
  }
293
- remove(message) {
294
- this.reports.delete(message);
295
- }
296
- getReports() {
297
- return Array.from(this.reports);
294
+ getWarnings() {
295
+ return Array.from(this.warnings);
298
296
  }
299
297
  markSkipped(rule, category) {
300
298
  this.skippedRules.get(category)?.add(rule);
@@ -306,6 +304,7 @@ var DefaultReporter = class {
306
304
  const result = {
307
305
  nursery: [],
308
306
  "type-aware": [],
307
+ "js-plugins": [],
309
308
  unsupported: []
310
309
  };
311
310
  for (const [category, rules] of this.skippedRules) result[category] = Array.from(rules);
@@ -313,6 +312,92 @@ var DefaultReporter = class {
313
312
  }
314
313
  };
315
314
 
315
+ //#endregion
316
+ //#region bin/output-formatter.ts
317
+ const CATEGORY_METADATA = {
318
+ nursery: {
319
+ label: "Nursery",
320
+ description: "Experimental:"
321
+ },
322
+ "type-aware": {
323
+ label: "Type-aware",
324
+ description: "Requires TS info:"
325
+ },
326
+ "js-plugins": {
327
+ label: "JS Plugins",
328
+ description: "Requires JS plugins:"
329
+ },
330
+ unsupported: { label: "Unsupported" }
331
+ };
332
+ const MAX_LABEL_LENGTH = Math.max(...Object.values(CATEGORY_METADATA).map((meta) => meta.label.length));
333
+ /**
334
+ * Formats a category summary as either inline (with example) or vertical list
335
+ */
336
+ function formatCategorySummary(count, category, rules, showAll) {
337
+ const meta = CATEGORY_METADATA[category];
338
+ if (!showAll) {
339
+ const maxRules = 3;
340
+ const exampleList = rules.slice(0, maxRules).join(", ");
341
+ const suffix = count > maxRules ? ", and more" : "";
342
+ const prefix = meta.description ? `${meta.description} ` : "";
343
+ return ` - ${String(count).padStart(3)} ${meta.label.padEnd(MAX_LABEL_LENGTH)} (${prefix}${exampleList}${suffix})\n`;
344
+ }
345
+ let output = ` - ${count} ${meta.label}\n`;
346
+ for (const rule of rules) output += ` - ${rule}\n`;
347
+ return output;
348
+ }
349
+ /**
350
+ * Detects which CLI flags are missing and could enable more rules
351
+ */
352
+ function detectMissingFlags(byCategory, cliOptions) {
353
+ const missingFlags = [];
354
+ if (byCategory.nursery.length > 0 && !cliOptions.withNursery) missingFlags.push("--with-nursery");
355
+ if (byCategory["type-aware"].length > 0 && !cliOptions.typeAware) missingFlags.push("--type-aware");
356
+ if (byCategory["js-plugins"].length > 0 && !cliOptions.jsPlugins) missingFlags.push("--js-plugins");
357
+ return missingFlags;
358
+ }
359
+ /**
360
+ * Formats the complete migration output message
361
+ */
362
+ function formatMigrationOutput(data) {
363
+ let output = "";
364
+ const showAll = data.cliOptions.details || false;
365
+ if (data.enabledRulesCount === 0) output += `\n⚠️ ${data.outputFileName} created with no rules enabled.\n`;
366
+ else output += `\n✨ ${data.outputFileName} created with ${data.enabledRulesCount} rules.\n`;
367
+ const byCategory = data.skippedRulesByCategory;
368
+ const nurseryCount = byCategory.nursery.length;
369
+ const typeAwareCount = byCategory["type-aware"].length;
370
+ const unsupportedCount = byCategory.unsupported.length;
371
+ const jsPluginsCount = byCategory["js-plugins"].length;
372
+ const totalSkipped = nurseryCount + typeAwareCount + unsupportedCount + jsPluginsCount;
373
+ if (totalSkipped > 0) {
374
+ output += `\n Skipped ${totalSkipped} rules:\n`;
375
+ if (nurseryCount > 0) output += formatCategorySummary(nurseryCount, "nursery", byCategory.nursery, showAll);
376
+ if (typeAwareCount > 0) output += formatCategorySummary(typeAwareCount, "type-aware", byCategory["type-aware"], showAll);
377
+ if (jsPluginsCount > 0) output += formatCategorySummary(jsPluginsCount, "js-plugins", byCategory["js-plugins"], showAll);
378
+ if (unsupportedCount > 0) output += formatCategorySummary(unsupportedCount, "unsupported", byCategory.unsupported, showAll);
379
+ if (!showAll) {
380
+ const maxExamples = 3;
381
+ if (nurseryCount > maxExamples || typeAwareCount > maxExamples || unsupportedCount > maxExamples || jsPluginsCount > maxExamples) output += `\n Tip: Use --details to see the full list.\n`;
382
+ }
383
+ const missingFlags = detectMissingFlags(byCategory, data.cliOptions);
384
+ if (missingFlags.length > 0) {
385
+ const eslintConfigArg = data.eslintConfigPath ? ` ${path.basename(data.eslintConfigPath)}` : "";
386
+ output += `\n👉 Re-run with flags to include more:\n`;
387
+ output += ` npx @oxlint/migrate${eslintConfigArg} ${missingFlags.join(" ")}\n`;
388
+ }
389
+ }
390
+ if (data.enabledRulesCount > 0) {
391
+ output += `\n🚀 Next:\n`;
392
+ output += ` npx oxlint .\n`;
393
+ }
394
+ return output;
395
+ }
396
+ function displayMigrationResult(outputMessage, warnings) {
397
+ console.log(outputMessage);
398
+ for (const warning of warnings) console.warn(warning);
399
+ }
400
+
316
401
  //#endregion
317
402
  //#region bin/oxlint-migrate.ts
318
403
  const cwd = process.cwd();
@@ -323,7 +408,22 @@ const getFileContent = (absoluteFilePath) => {
323
408
  return;
324
409
  }
325
410
  };
326
- 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`.").action(async (filePath) => {
411
+ /**
412
+ * Count enabled rules (excluding "off" rules) from both rules and overrides
413
+ */
414
+ const countEnabledRules = (config) => {
415
+ const enabledRules = /* @__PURE__ */ new Set();
416
+ if (config.rules) {
417
+ for (const [ruleName, ruleValue] of Object.entries(config.rules)) if (!isOffValue(ruleValue)) enabledRules.add(ruleName);
418
+ }
419
+ if (config.overrides && Array.isArray(config.overrides)) {
420
+ for (const override of config.overrides) if (override.rules) {
421
+ for (const [ruleName, ruleValue] of Object.entries(override.rules)) if (!isOffValue(ruleValue)) enabledRules.add(ruleName);
422
+ }
423
+ }
424
+ return enabledRules.size;
425
+ };
426
+ 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
427
  const cliOptions = program.opts();
328
428
  const oxlintFilePath = path.join(cwd, cliOptions.outputFile);
329
429
  const reporter = new DefaultReporter();
@@ -335,7 +435,7 @@ program.name("oxlint-migrate").version(version).argument("[eslint-config]", "The
335
435
  jsPlugins: !!cliOptions.jsPlugins
336
436
  };
337
437
  if (cliOptions.replaceEslintComments) {
338
- await walkAndReplaceProjectFiles(await getAllProjectFiles(), (filePath$1) => getFileContent(filePath$1), (filePath$1, content) => writeFile(filePath$1, content, "utf-8"), options);
438
+ await walkAndReplaceProjectFiles(await getAllProjectFiles(), (filePath) => getFileContent(filePath), (filePath, content) => writeFile(filePath, content, "utf-8"), options);
339
439
  return;
340
440
  }
341
441
  if (filePath === void 0) filePath = getAutodetectedEslintConfigName(cwd);
@@ -352,7 +452,19 @@ program.name("oxlint-migrate").version(version).argument("[eslint-config]", "The
352
452
  const oxlintConfig = "default" in eslintConfigs ? await src_default(eslintConfigs.default, config, options) : await src_default(eslintConfigs, config, options);
353
453
  if (existsSync(oxlintFilePath)) renameSync(oxlintFilePath, `${oxlintFilePath}.bak`);
354
454
  writeFileSync(oxlintFilePath, JSON.stringify(oxlintConfig, null, 2));
355
- for (const report of reporter.getReports()) console.warn(report);
455
+ const enabledRulesCount = countEnabledRules(oxlintConfig);
456
+ displayMigrationResult(formatMigrationOutput({
457
+ outputFileName: cliOptions.outputFile,
458
+ enabledRulesCount,
459
+ skippedRulesByCategory: reporter.getSkippedRulesByCategory(),
460
+ cliOptions: {
461
+ withNursery: !!cliOptions.withNursery,
462
+ typeAware: !!cliOptions.typeAware,
463
+ details: !!cliOptions.details,
464
+ jsPlugins: !!cliOptions.jsPlugins
465
+ },
466
+ eslintConfigPath: filePath
467
+ }), reporter.getWarnings());
356
468
  });
357
469
  program.parse();
358
470