@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 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
- For Deno and Bun, TypeScript configuration files, like `eslint.config.mts`, are natively supported.
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
- ## Contributing
50
-
51
- ### Generate rules
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
- Generates the rules from installed oxlint version
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
- ```shell
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
- ```shell
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 not migrated**
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
- 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.
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 { i as rules_exports, n as preFixForJsPlugins, r as nurseryRules, t as src_default } from "../src-BiX8O61Z.mjs";
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$1) => {
21
+ const getAutodetectedEslintConfigName = (cwd) => {
21
22
  for (const filename of FLAT_CONFIG_FILENAMES) {
22
- const filePath = path.join(cwd$1, filename);
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.42.0";
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$1) => {
215
- return Object.assign(sourceText$1, {
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?.report(`${absoluteFilePath}: failed to parse`);
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?.report(`${absoluteFilePath}, char offset ${comment.start + partialSourceText.offset}: ${error.message}`);
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$1, writeFile$1, options) => {
254
+ const walkAndReplaceProjectFiles = (projectFiles, readFileSync, writeFile, options) => {
262
255
  return Promise.all(projectFiles.map((file) => {
263
- const sourceText = readFileSync$1(file);
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$1(file, newSourceText);
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
- reports = /* @__PURE__ */ new Set();
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
- report(message) {
291
- this.reports.add(message);
292
- }
293
- remove(message) {
294
- this.reports.delete(message);
284
+ addWarning(message) {
285
+ this.warnings.add(message);
295
286
  }
296
- getReports() {
297
- return Array.from(this.reports);
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
- 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) => {
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$1) => getFileContent(filePath$1), (filePath$1, content) => writeFile(filePath$1, content, "utf-8"), options);
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 src_default(eslintConfigs.default, config, options) : await src_default(eslintConfigs, config, options);
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
- for (const report of reporter.getReports()) console.warn(report);
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