@oxlint/migrate 1.7.0 → 1.8.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
@@ -16,11 +16,12 @@ When no config file provided, the script searches for the default eslint config
16
16
 
17
17
  ### Options
18
18
 
19
- | Options | Description |
20
- | ---------------------- | ---------------------------------------------------------------------------------------------------- |
21
- | `--merge` | \* merge eslint configuration with an existing .oxlintrc.json configuration |
22
- | `--with-nursery` | Include oxlint rules which are currently under development |
23
- | `--output-file <file>` | The oxlint configuration file where to eslint v9 rules will be written to, default: `.oxlintrc.json` |
19
+ | Options | Description |
20
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `--merge` | \* merge eslint configuration with an existing .oxlintrc.json configuration |
22
+ | `--with-nursery` | Include oxlint rules which are currently under development |
23
+ | `--output-file <file>` | The oxlint configuration file where to eslint v9 rules will be written to, default: `.oxlintrc.json` |
24
+ | `--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. |
24
25
 
25
26
  \* WARNING: When some `categories` are enabled, this tools will enable more rules with the combination of `plugins`.
26
27
  Else we need to disable each rule `plugin/categories` combination, which is not covered by your eslint configuration.
@@ -5,7 +5,17 @@ import path from "node:path";
5
5
  import { getAutodetectedEslintConfigName, loadESLintConfig } from "./config-loader.mjs";
6
6
  import main from "../src/index.mjs";
7
7
  import packageJson from "../package.json.mjs";
8
+ import { walkAndReplaceProjectFiles } from "../src/walker/index.mjs";
9
+ import { getAllProjectFiles } from "./project-loader.mjs";
10
+ import { writeFile } from "node:fs/promises";
8
11
  const cwd = process.cwd();
12
+ const getFileContent = (absoluteFilePath) => {
13
+ try {
14
+ return readFileSync(absoluteFilePath, "utf-8");
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ };
9
19
  program.name("oxlint-migrate").version(packageJson.version).argument("[eslint-config]", "The path to the eslint v9 config file").option(
10
20
  "--output-file <file>",
11
21
  "The oxlint configuration file where to eslint v9 rules will be written to",
@@ -18,9 +28,26 @@ program.name("oxlint-migrate").version(packageJson.version).argument("[eslint-co
18
28
  "--with-nursery",
19
29
  "Include oxlint rules which are currently under development",
20
30
  false
31
+ ).option(
32
+ "--replace-eslint-comments",
33
+ "Search in the project files for eslint comments and replaces them with oxlint. Some eslint comments are not supported and will be reported."
21
34
  ).action(async (filePath) => {
22
35
  const cliOptions = program.opts();
23
36
  const oxlintFilePath = path.join(cwd, cliOptions.outputFile);
37
+ const options = {
38
+ reporter: console.warn,
39
+ merge: !!cliOptions.merge,
40
+ withNursery: !!cliOptions.withNursery
41
+ };
42
+ if (cliOptions.replaceEslintComments) {
43
+ await walkAndReplaceProjectFiles(
44
+ await getAllProjectFiles(),
45
+ (filePath2) => getFileContent(filePath2),
46
+ (filePath2, content) => writeFile(filePath2, content, "utf-8"),
47
+ options
48
+ );
49
+ return;
50
+ }
24
51
  if (filePath === void 0) {
25
52
  filePath = getAutodetectedEslintConfigName(cwd);
26
53
  } else {
@@ -30,11 +57,6 @@ program.name("oxlint-migrate").version(packageJson.version).argument("[eslint-co
30
57
  program.error(`could not autodetect eslint config file`);
31
58
  }
32
59
  const eslintConfigs = await loadESLintConfig(filePath);
33
- const options = {
34
- reporter: console.warn,
35
- merge: !!cliOptions.merge,
36
- withNursery: !!cliOptions.withNursery
37
- };
38
60
  let config;
39
61
  if (options.merge && existsSync(oxlintFilePath)) {
40
62
  config = JSON.parse(
@@ -0,0 +1,16 @@
1
+ import { glob } from "tinyglobby";
2
+ const getAllProjectFiles = () => {
3
+ return glob(
4
+ [
5
+ "**/*.{js,cjs,mjs,ts,cts,mts,vue,astro,svelte}",
6
+ "!**/node_modules/**",
7
+ "!**/dist/**"
8
+ ],
9
+ {
10
+ absolute: true
11
+ }
12
+ );
13
+ };
14
+ export {
15
+ getAllProjectFiles
16
+ };
@@ -1,4 +1,4 @@
1
- const version = "1.7.0";
1
+ const version = "1.8.0";
2
2
  const packageJson = {
3
3
  version
4
4
  };
@@ -9,7 +9,8 @@ const rulesPrefixesForPlugins = {
9
9
  n: "node",
10
10
  promise: "promise",
11
11
  react: "react",
12
- "react-perf": "react",
12
+ "react-perf": "react-perf",
13
+ "react-hooks": "react",
13
14
  "@typescript-eslint": "typescript",
14
15
  unicorn: "unicorn",
15
16
  vitest: "vitest"
@@ -218,7 +218,6 @@ const styleRules = [
218
218
  "unicorn/prefer-object-from-entries",
219
219
  "unicorn/prefer-array-index-of",
220
220
  "unicorn/prefer-spread",
221
- "unicorn/prefer-array-flat-map",
222
221
  "unicorn/prefer-dom-node-text-content",
223
222
  "unicorn/prefer-includes",
224
223
  "unicorn/prefer-logical-operator-over-ternary",
@@ -588,6 +587,7 @@ const perfRules = [
588
587
  "react-perf/jsx-no-new-function-as-prop",
589
588
  "react-perf/jsx-no-new-object-as-prop",
590
589
  "unicorn/prefer-array-find",
590
+ "unicorn/prefer-array-flat-map",
591
591
  "unicorn/prefer-set-has"
592
592
  ];
593
593
  export {
@@ -0,0 +1,2 @@
1
+ import { Options } from '../../types.js';
2
+ export default function replaceComments(comment: string, type: 'Line' | 'Block', options: Options): string;
@@ -0,0 +1,20 @@
1
+ import replaceRuleDirectiveComment from "./replaceRuleDirectiveComment.mjs";
2
+ function replaceComments(comment, type, options) {
3
+ const originalComment = comment;
4
+ comment = comment.trim();
5
+ if (comment.startsWith("eslint-")) {
6
+ return replaceRuleDirectiveComment(originalComment, type, options);
7
+ } else if (type === "Block") {
8
+ if (comment.startsWith("eslint ")) {
9
+ throw new Error(
10
+ "changing eslint rules with inline comment is not supported"
11
+ );
12
+ } else if (comment.startsWith("global ")) {
13
+ throw new Error("changing globals with inline comment is not supported");
14
+ }
15
+ }
16
+ return originalComment;
17
+ }
18
+ export {
19
+ replaceComments as default
20
+ };
@@ -0,0 +1,2 @@
1
+ import { Options } from '../../types.js';
2
+ export default function replaceRuleDirectiveComment(comment: string, type: 'Line' | 'Block', options: Options): string;
@@ -0,0 +1,59 @@
1
+ import * as rules from "../../generated/rules.mjs";
2
+ import { nurseryRules } from "../../generated/rules.mjs";
3
+ const allRules = Object.values(rules).flat();
4
+ function replaceRuleDirectiveComment(comment, type, options) {
5
+ const originalComment = comment;
6
+ comment = comment.split(" -- ")[0].trimStart();
7
+ if (!comment.startsWith("eslint-")) {
8
+ return originalComment;
9
+ }
10
+ comment = comment.substring(7);
11
+ if (comment.startsWith("enable")) {
12
+ comment = comment.substring(6);
13
+ } else if (comment.startsWith("disable")) {
14
+ comment = comment.substring(7);
15
+ if (type === "Line") {
16
+ if (comment.startsWith("-next-line")) {
17
+ comment = comment.substring(10);
18
+ } else if (comment.startsWith("-line")) {
19
+ comment = comment.substring(5);
20
+ }
21
+ }
22
+ } else {
23
+ return originalComment;
24
+ }
25
+ if (!comment.startsWith(" ")) {
26
+ return originalComment;
27
+ }
28
+ comment = comment.trimStart();
29
+ if (comment.length === 0) {
30
+ return originalComment;
31
+ }
32
+ while (comment.length) {
33
+ let foundRule = false;
34
+ for (const rule of allRules) {
35
+ if (comment.startsWith(rule)) {
36
+ if (!options.withNursery && nurseryRules.includes(rule)) {
37
+ continue;
38
+ }
39
+ foundRule = true;
40
+ comment = comment.substring(rule.length).trimStart();
41
+ break;
42
+ }
43
+ }
44
+ if (!foundRule) {
45
+ return originalComment;
46
+ }
47
+ if (!comment.length) {
48
+ break;
49
+ }
50
+ if (!comment.startsWith(", ")) {
51
+ return originalComment;
52
+ }
53
+ comment = comment.substring(1).trimStart();
54
+ }
55
+ return originalComment.replace(/eslint-/, "oxlint-");
56
+ }
57
+ export {
58
+ replaceRuleDirectiveComment as default
59
+ };
@@ -0,0 +1,10 @@
1
+ import { Options } from '../types.js';
2
+ export declare const walkAndReplaceProjectFiles: (
3
+ /** all projects files to check */
4
+ projectFiles: string[],
5
+ /** function for reading the file */
6
+ readFileSync: (filePath: string) => string | undefined,
7
+ /** function for writing the file */
8
+ writeFile: (filePath: string, content: string) => Promise<void>,
9
+ /** options for the walker, for `reporter` and `withNurseryRules` */
10
+ options: Options) => Promise<void[]>;
@@ -0,0 +1,19 @@
1
+ import replaceCommentsInFile from "./replaceCommentsInFile.mjs";
2
+ const walkAndReplaceProjectFiles = (projectFiles, readFileSync, writeFile, options) => {
3
+ return Promise.all(
4
+ projectFiles.map((file) => {
5
+ const sourceText = readFileSync(file);
6
+ if (!sourceText) {
7
+ return Promise.resolve();
8
+ }
9
+ const newSourceText = replaceCommentsInFile(file, sourceText, options);
10
+ if (newSourceText === sourceText) {
11
+ return Promise.resolve();
12
+ }
13
+ return writeFile(file, newSourceText);
14
+ })
15
+ );
16
+ };
17
+ export {
18
+ walkAndReplaceProjectFiles
19
+ };
@@ -0,0 +1,10 @@
1
+ export type PartialSourceText = {
2
+ sourceText: string;
3
+ offset: number;
4
+ lang?: 'js' | 'jsx' | 'ts' | 'tsx';
5
+ sourceType?: 'script' | 'module';
6
+ };
7
+ export default function partialSourceTextLoader(absoluteFilePath: string, fileContent: string): PartialSourceText[];
8
+ export declare function partialVueSourceTextLoader(sourceText: string): PartialSourceText[];
9
+ export declare function partialSvelteSourceTextLoader(sourceText: string): PartialSourceText[];
10
+ export declare function partialAstroSourceTextLoader(sourceText: string): PartialSourceText[];
@@ -0,0 +1,167 @@
1
+ function extractLangAttribute(source) {
2
+ const langIndex = source.indexOf("lang");
3
+ if (langIndex === -1) return void 0;
4
+ let cursor = langIndex + 4;
5
+ while (cursor < source.length && isWhitespace(source[cursor])) {
6
+ cursor++;
7
+ }
8
+ if (source[cursor] !== "=") return void 0;
9
+ cursor++;
10
+ while (cursor < source.length && isWhitespace(source[cursor])) {
11
+ cursor++;
12
+ }
13
+ const quote = source[cursor];
14
+ if (quote !== '"' && quote !== "'") return void 0;
15
+ cursor++;
16
+ let value = "";
17
+ while (cursor < source.length && source[cursor] !== quote) {
18
+ value += source[cursor++];
19
+ }
20
+ if (value === "js" || value === "jsx" || value === "ts" || value === "tsx") {
21
+ return value;
22
+ }
23
+ return void 0;
24
+ }
25
+ function extractScriptBlocks(sourceText, offset, maxBlocks, parseLangAttribute) {
26
+ const results = [];
27
+ while (offset < sourceText.length) {
28
+ const idx = sourceText.indexOf("<script", offset);
29
+ if (idx === -1) break;
30
+ const nextChar = sourceText[idx + 7];
31
+ if (nextChar !== " " && nextChar !== ">" && nextChar !== "\n" && nextChar !== " ") {
32
+ offset = idx + 7;
33
+ continue;
34
+ }
35
+ let i = idx + 7;
36
+ let inQuote = null;
37
+ let genericDepth = 0;
38
+ let selfClosing = false;
39
+ while (i < sourceText.length) {
40
+ const c = sourceText[i];
41
+ if (inQuote) {
42
+ if (c === inQuote) inQuote = null;
43
+ } else if (c === '"' || c === "'") {
44
+ inQuote = c;
45
+ } else if (c === "<") {
46
+ genericDepth++;
47
+ } else if (c === ">") {
48
+ if (genericDepth > 0) {
49
+ genericDepth--;
50
+ } else {
51
+ if (i > idx && sourceText[i - 1] === "/") {
52
+ selfClosing = true;
53
+ }
54
+ i++;
55
+ break;
56
+ }
57
+ }
58
+ i++;
59
+ }
60
+ if (selfClosing) {
61
+ offset = i;
62
+ continue;
63
+ }
64
+ if (i >= sourceText.length) break;
65
+ let lang = void 0;
66
+ if (parseLangAttribute) {
67
+ lang = extractLangAttribute(sourceText.slice(idx, i));
68
+ }
69
+ const contentStart = i;
70
+ const closeTag = "<\/script>";
71
+ const closeIdx = sourceText.indexOf(closeTag, contentStart);
72
+ if (closeIdx === -1) break;
73
+ const content = sourceText.slice(contentStart, closeIdx);
74
+ results.push({ sourceText: content, offset: contentStart, lang });
75
+ if (results.length >= maxBlocks) {
76
+ break;
77
+ }
78
+ offset = closeIdx + closeTag.length;
79
+ }
80
+ return results;
81
+ }
82
+ function partialSourceTextLoader(absoluteFilePath, fileContent) {
83
+ if (absoluteFilePath.endsWith(".vue")) {
84
+ return partialVueSourceTextLoader(fileContent);
85
+ } else if (absoluteFilePath.endsWith(".astro")) {
86
+ return partialAstroSourceTextLoader(fileContent);
87
+ } else if (absoluteFilePath.endsWith(".svelte")) {
88
+ return partialSvelteSourceTextLoader(fileContent);
89
+ }
90
+ return [
91
+ {
92
+ sourceText: fileContent,
93
+ offset: 0
94
+ }
95
+ ];
96
+ }
97
+ function isWhitespace(char) {
98
+ return char === " " || char === " " || char === "\r";
99
+ }
100
+ function findDelimiter(sourceText, startPos) {
101
+ let i = startPos;
102
+ while (i < sourceText.length) {
103
+ if (i === 0 || sourceText[i - 1] === "\n") {
104
+ let j = i;
105
+ while (j < sourceText.length && isWhitespace(sourceText[j])) j++;
106
+ if (sourceText[j] === "-" && sourceText[j + 1] === "-" && sourceText[j + 2] === "-") {
107
+ let k = j + 3;
108
+ while (k < sourceText.length && sourceText[k] !== "\n") {
109
+ if (!isWhitespace(sourceText[k])) break;
110
+ k++;
111
+ }
112
+ if (k === sourceText.length || sourceText[k] === "\n") {
113
+ return j;
114
+ }
115
+ }
116
+ }
117
+ i++;
118
+ }
119
+ return -1;
120
+ }
121
+ function partialVueSourceTextLoader(sourceText) {
122
+ return extractScriptBlocks(sourceText, 0, 2, true);
123
+ }
124
+ function partialSvelteSourceTextLoader(sourceText) {
125
+ return extractScriptBlocks(sourceText, 0, 2, true);
126
+ }
127
+ function partialAstroSourceTextLoader(sourceText) {
128
+ const results = [];
129
+ let pos = 0;
130
+ const frontmatterStartDelimiter = findDelimiter(sourceText, pos);
131
+ if (frontmatterStartDelimiter !== -1) {
132
+ const frontmatterContentStart = frontmatterStartDelimiter + 3;
133
+ const frontmatterEndDelimiter = findDelimiter(
134
+ sourceText,
135
+ frontmatterContentStart
136
+ );
137
+ if (frontmatterEndDelimiter !== -1) {
138
+ const content = sourceText.slice(
139
+ frontmatterContentStart,
140
+ frontmatterEndDelimiter
141
+ );
142
+ results.push({
143
+ sourceText: content,
144
+ offset: frontmatterContentStart,
145
+ lang: "ts",
146
+ sourceType: "module"
147
+ });
148
+ pos = frontmatterEndDelimiter + 3;
149
+ }
150
+ }
151
+ results.push(
152
+ ...extractScriptBlocks(sourceText, pos, Number.MAX_SAFE_INTEGER, false).map(
153
+ (sourceText2) => ({
154
+ ...sourceText2,
155
+ lang: "ts",
156
+ sourceType: "module"
157
+ })
158
+ )
159
+ );
160
+ return results;
161
+ }
162
+ export {
163
+ partialSourceTextLoader as default,
164
+ partialAstroSourceTextLoader,
165
+ partialSvelteSourceTextLoader,
166
+ partialVueSourceTextLoader
167
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { Options } from '../types.js';
2
+ export default function replaceCommentsInFile(absoluteFilePath: string, fileContent: string, options: Options): string;
@@ -0,0 +1,60 @@
1
+ import { parseSync } from "oxc-parser";
2
+ import replaceComments from "./comments/index.mjs";
3
+ import partialSourceTextLoader from "./partialSourceTextLoader.mjs";
4
+ const getComments = (absoluteFilePath, partialSourceText, options) => {
5
+ const parserResult = parseSync(
6
+ absoluteFilePath,
7
+ partialSourceText.sourceText,
8
+ {
9
+ lang: partialSourceText.lang,
10
+ sourceType: partialSourceText.sourceType
11
+ }
12
+ );
13
+ if (parserResult.errors.length > 0 && options.reporter) {
14
+ options.reporter(`${absoluteFilePath}: failed to parse`);
15
+ }
16
+ return parserResult.comments;
17
+ };
18
+ function replaceCommentsInSourceText(absoluteFilePath, partialSourceText, options) {
19
+ const comments = getComments(absoluteFilePath, partialSourceText, options);
20
+ let sourceText = partialSourceText.sourceText;
21
+ for (const comment of comments) {
22
+ try {
23
+ const replacedStr = replaceComments(comment.value, comment.type, options);
24
+ if (replacedStr !== comment.value) {
25
+ const newComment = comment.type === "Line" ? `//${replacedStr}` : `/*${replacedStr}*/`;
26
+ sourceText = sourceText.slice(0, comment.start) + newComment + sourceText.slice(comment.end);
27
+ }
28
+ } catch (error) {
29
+ if (error instanceof Error && options.reporter) {
30
+ options.reporter(
31
+ `${absoluteFilePath}, char offset ${comment.start + partialSourceText.offset}: ${error.message}`
32
+ );
33
+ continue;
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+ return sourceText;
39
+ }
40
+ function replaceCommentsInFile(absoluteFilePath, fileContent, options) {
41
+ for (const partialSourceText of partialSourceTextLoader(
42
+ absoluteFilePath,
43
+ fileContent
44
+ )) {
45
+ const newSourceText = replaceCommentsInSourceText(
46
+ absoluteFilePath,
47
+ partialSourceText,
48
+ options
49
+ );
50
+ if (newSourceText !== partialSourceText.sourceText) {
51
+ fileContent = fileContent.slice(0, partialSourceText.offset) + newSourceText + fileContent.slice(
52
+ partialSourceText.offset + partialSourceText.sourceText.length
53
+ );
54
+ }
55
+ }
56
+ return fileContent;
57
+ }
58
+ export {
59
+ replaceCommentsInFile as default
60
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxlint/migrate",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Generates a `.oxlintrc.json` from a existing eslint flat config",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,12 +57,15 @@
57
57
  "eslint-plugin-jsdoc": "^51.2.3",
58
58
  "eslint-plugin-local": "^6.0.0",
59
59
  "eslint-plugin-oxlint": "^1.3.0",
60
+ "eslint-plugin-react": "^7.37.5",
61
+ "eslint-plugin-react-hooks": "^5.2.0",
62
+ "eslint-plugin-react-perf": "^3.3.3",
60
63
  "eslint-plugin-regexp": "^2.9.0",
61
64
  "eslint-plugin-unicorn": "^59.0.1",
62
65
  "husky": "^9.1.7",
63
66
  "jiti": "^2.4.2",
64
67
  "lint-staged": "^16.1.2",
65
- "oxlint": "^1.7.0",
68
+ "oxlint": "^1.8.0",
66
69
  "prettier": "^3.6.1",
67
70
  "typescript": "^5.8.3",
68
71
  "typescript-eslint": "^8.35.0",
@@ -74,7 +77,9 @@
74
77
  "*": "prettier --ignore-unknown --write"
75
78
  },
76
79
  "dependencies": {
77
- "commander": "^14.0.0"
80
+ "commander": "^14.0.0",
81
+ "oxc-parser": "^0.77.0",
82
+ "tinyglobby": "^0.2.14"
78
83
  },
79
84
  "peerDependencies": {
80
85
  "globals": "^14.0.0 || ^15.0.0 || ^16.0.0",