@oxlint/migrate
Version:
Generates a `.oxlintrc.json` from a existing eslint flat config
340 lines (329 loc) • 12.9 kB
JavaScript
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 { };