UNPKG

@oxlint/migrate

Version:

Generates a `.oxlintrc.json` from a existing eslint flat config

340 lines (329 loc) 12.9 kB
#!/usr/bin/env node import { i as rules_exports, n as preFixForJsPlugins, r as nurseryRules, t as src_default } from "../src-pmJg5CKN.mjs"; import { program } from "commander"; import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { parseSync } from "oxc-parser"; import { glob } from "tinyglobby"; import { writeFile } from "node:fs/promises"; //#region bin/config-loader.ts const FLAT_CONFIG_FILENAMES = [ "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.mts", "eslint.config.cts" ]; const getAutodetectedEslintConfigName = (cwd$1) => { for (const filename of FLAT_CONFIG_FILENAMES) { const filePath = path.join(cwd$1, filename); if (existsSync(filePath)) return filePath; } }; const loadESLintConfig = async (filePath) => { if (filePath.endsWith("json")) throw new Error(`json format is not supported. @oxlint/migrate only supports the eslint flat configuration`); let url = pathToFileURL(filePath).toString(); if (!existsSync(filePath)) throw new Error(`eslint config file not found: ${filePath}`); if ("Bun" in globalThis || "Deno" in globalThis) return import(url); if (filePath.endsWith(".ts") || filePath.endsWith(".mts") || filePath.endsWith(".cts")) { const { createJiti } = await import("jiti"); return createJiti(filePath, { interopDefault: false, moduleCache: false }).import(url); } return import(url); }; //#endregion //#region package.json var version = "1.40.0"; //#endregion //#region src/walker/comments/replaceRuleDirectiveComment.ts const allRules = Object.values(rules_exports).flat(); function replaceRuleDirectiveComment(comment, type, options) { const originalComment = comment; comment = comment.split(" -- ")[0].trimStart(); if (!comment.startsWith("eslint-")) return originalComment; comment = comment.substring(7); if (comment.startsWith("enable")) comment = comment.substring(6); else if (comment.startsWith("disable")) { comment = comment.substring(7); if (type === "Line") { if (comment.startsWith("-next-line")) comment = comment.substring(10); else if (comment.startsWith("-line")) comment = comment.substring(5); } } else return originalComment; if (!comment.startsWith(" ")) return originalComment; comment = comment.trimStart(); if (comment.length === 0) return originalComment; while (comment.length) { let foundRule = false; for (const rule of allRules) if (comment.startsWith(rule)) { if (!options.withNursery && nurseryRules.includes(rule)) continue; foundRule = true; comment = comment.substring(rule.length).trimStart(); break; } if (!foundRule) return originalComment; if (!comment.length) break; if (!comment.startsWith(", ")) return originalComment; comment = comment.substring(1).trimStart(); } return originalComment.replace(/eslint-/, "oxlint-"); } //#endregion //#region src/walker/comments/index.ts function replaceComments(comment, type, options) { const originalComment = comment; comment = comment.trim(); if (comment.startsWith("eslint-")) return replaceRuleDirectiveComment(originalComment, type, options); else if (type === "Block") { if (comment.startsWith("eslint ")) throw new Error("changing eslint rules with inline comment is not supported"); else if (comment.startsWith("global ")) throw new Error("changing globals with inline comment is not supported"); } return originalComment; } //#endregion //#region src/walker/partialSourceTextLoader.ts function extractLangAttribute(source) { const langIndex = source.indexOf("lang"); if (langIndex === -1) return void 0; let cursor = langIndex + 4; while (cursor < source.length && isWhitespace(source[cursor])) cursor++; if (source[cursor] !== "=") return void 0; cursor++; while (cursor < source.length && isWhitespace(source[cursor])) cursor++; const quote = source[cursor]; if (quote !== "\"" && quote !== "'") return void 0; cursor++; let value = ""; while (cursor < source.length && source[cursor] !== quote) value += source[cursor++]; if (value === "js" || value === "jsx" || value === "ts" || value === "tsx") return value; } function extractScriptBlocks(sourceText, offset, maxBlocks, parseLangAttribute) { const results = []; while (offset < sourceText.length) { const idx = sourceText.indexOf("<script", offset); if (idx === -1) break; const nextChar = sourceText[idx + 7]; if (nextChar !== " " && nextChar !== ">" && nextChar !== "\n" && nextChar !== " ") { offset = idx + 7; continue; } let i = idx + 7; let inQuote = null; let genericDepth = 0; let selfClosing = false; while (i < sourceText.length) { const c = sourceText[i]; if (inQuote) { if (c === inQuote) inQuote = null; } else if (c === "\"" || c === "'") inQuote = c; else if (c === "<") genericDepth++; else if (c === ">") if (genericDepth > 0) genericDepth--; else { if (i > idx && sourceText[i - 1] === "/") selfClosing = true; i++; break; } i++; } if (selfClosing) { offset = i; continue; } if (i >= sourceText.length) break; let lang = void 0; if (parseLangAttribute) lang = extractLangAttribute(sourceText.slice(idx, i)); const contentStart = i; const closeIdx = sourceText.indexOf("<\/script>", contentStart); if (closeIdx === -1) break; const content = sourceText.slice(contentStart, closeIdx); results.push({ sourceText: content, offset: contentStart, lang }); if (results.length >= maxBlocks) break; offset = closeIdx + 9; } return results; } function partialSourceTextLoader(absoluteFilePath, fileContent) { if (absoluteFilePath.endsWith(".vue")) return partialVueSourceTextLoader(fileContent); else if (absoluteFilePath.endsWith(".astro")) return partialAstroSourceTextLoader(fileContent); else if (absoluteFilePath.endsWith(".svelte")) return partialSvelteSourceTextLoader(fileContent); return [{ sourceText: fileContent, offset: 0 }]; } function isWhitespace(char) { return char === " " || char === " " || char === "\r"; } function findDelimiter(sourceText, startPos) { let i = startPos; while (i < sourceText.length) { if (i === 0 || sourceText[i - 1] === "\n") { let j = i; while (j < sourceText.length && isWhitespace(sourceText[j])) j++; if (sourceText[j] === "-" && sourceText[j + 1] === "-" && sourceText[j + 2] === "-") { let k = j + 3; while (k < sourceText.length && sourceText[k] !== "\n") { if (!isWhitespace(sourceText[k])) break; k++; } if (k === sourceText.length || sourceText[k] === "\n") return j; } } i++; } return -1; } function partialVueSourceTextLoader(sourceText) { return extractScriptBlocks(sourceText, 0, 2, true); } function partialSvelteSourceTextLoader(sourceText) { return extractScriptBlocks(sourceText, 0, 2, true); } function partialAstroSourceTextLoader(sourceText) { const results = []; let pos = 0; const frontmatterStartDelimiter = findDelimiter(sourceText, pos); if (frontmatterStartDelimiter !== -1) { const frontmatterContentStart = frontmatterStartDelimiter + 3; const frontmatterEndDelimiter = findDelimiter(sourceText, frontmatterContentStart); if (frontmatterEndDelimiter !== -1) { const content = sourceText.slice(frontmatterContentStart, frontmatterEndDelimiter); results.push({ sourceText: content, offset: frontmatterContentStart, lang: "ts", sourceType: "module" }); pos = frontmatterEndDelimiter + 3; } } results.push(...extractScriptBlocks(sourceText, pos, Number.MAX_SAFE_INTEGER, false).map((sourceText$1) => { return Object.assign(sourceText$1, { lang: `ts`, sourceType: `module` }); })); return results; } //#endregion //#region src/walker/replaceCommentsInFile.ts const getComments = (absoluteFilePath, partialSourceText, options) => { const parserResult = parseSync(absoluteFilePath, partialSourceText.sourceText, { lang: partialSourceText.lang, sourceType: partialSourceText.sourceType }); if (parserResult.errors.length > 0) options.reporter?.report(`${absoluteFilePath}: failed to parse`); return parserResult.comments; }; function replaceCommentsInSourceText(absoluteFilePath, partialSourceText, options) { const comments = getComments(absoluteFilePath, partialSourceText, options); let sourceText = partialSourceText.sourceText; for (const comment of comments) try { const replacedStr = replaceComments(comment.value, comment.type, options); if (replacedStr !== comment.value) { const newComment = comment.type === "Line" ? `//${replacedStr}` : `/*${replacedStr}*/`; sourceText = sourceText.slice(0, comment.start) + newComment + sourceText.slice(comment.end); } } catch (error) { if (error instanceof Error) { options.reporter?.report(`${absoluteFilePath}, char offset ${comment.start + partialSourceText.offset}: ${error.message}`); continue; } throw error; } return sourceText; } function replaceCommentsInFile(absoluteFilePath, fileContent, options) { for (const partialSourceText of partialSourceTextLoader(absoluteFilePath, fileContent)) { const newSourceText = replaceCommentsInSourceText(absoluteFilePath, partialSourceText, options); if (newSourceText !== partialSourceText.sourceText) fileContent = fileContent.slice(0, partialSourceText.offset) + newSourceText + fileContent.slice(partialSourceText.offset + partialSourceText.sourceText.length); } return fileContent; } //#endregion //#region src/walker/index.ts const walkAndReplaceProjectFiles = (projectFiles, readFileSync$1, writeFile$1, options) => { return Promise.all(projectFiles.map((file) => { const sourceText = readFileSync$1(file); if (!sourceText) return Promise.resolve(); const newSourceText = replaceCommentsInFile(file, sourceText, options); if (newSourceText === sourceText) return Promise.resolve(); return writeFile$1(file, newSourceText); })); }; //#endregion //#region bin/project-loader.ts const getAllProjectFiles = () => { return glob([ "**/*.{js,cjs,mjs,ts,cts,mts,jsx,tsx,vue,astro,svelte}", "!**/node_modules/**", "!**/dist/**" ], { absolute: true }); }; //#endregion //#region src/reporter.ts var DefaultReporter = class { reports = /* @__PURE__ */ new Set(); report(message) { this.reports.add(message); } remove(message) { this.reports.delete(message); } getReports() { return Array.from(this.reports); } }; //#endregion //#region bin/oxlint-migrate.ts const cwd = process.cwd(); const getFileContent = (absoluteFilePath) => { try { return readFileSync(absoluteFilePath, "utf-8"); } catch { return; } }; 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) => { const cliOptions = program.opts(); const oxlintFilePath = path.join(cwd, cliOptions.outputFile); const reporter = new DefaultReporter(); const options = { reporter, merge: !!cliOptions.merge, withNursery: !!cliOptions.withNursery, typeAware: !!cliOptions.typeAware, jsPlugins: !!cliOptions.jsPlugins }; if (cliOptions.replaceEslintComments) { await walkAndReplaceProjectFiles(await getAllProjectFiles(), (filePath$1) => getFileContent(filePath$1), (filePath$1, content) => writeFile(filePath$1, content, "utf-8"), options); return; } if (filePath === void 0) filePath = getAutodetectedEslintConfigName(cwd); else filePath = path.join(cwd, filePath); if (filePath === void 0) program.error(`could not autodetect eslint config file`); const resetPreFix = await preFixForJsPlugins(); const eslintConfigs = await loadESLintConfig(filePath); resetPreFix(); let config; if (options.merge && existsSync(oxlintFilePath)) config = JSON.parse(readFileSync(oxlintFilePath, { encoding: "utf8", flag: "r" })); const oxlintConfig = "default" in eslintConfigs ? await src_default(eslintConfigs.default, config, options) : await src_default(eslintConfigs, config, options); if (existsSync(oxlintFilePath)) renameSync(oxlintFilePath, `${oxlintFilePath}.bak`); writeFileSync(oxlintFilePath, JSON.stringify(oxlintConfig, null, 2)); for (const report of reporter.getReports()) console.warn(report); }); program.parse(); //#endregion export { };