mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
1193c9dbaf
dogfood
171 lines
7.1 KiB
TypeScript
171 lines
7.1 KiB
TypeScript
/**
|
|
* Generates the `oxlint-plugin-eslint` package source files.
|
|
*
|
|
* This script produces:
|
|
*
|
|
* 1. `rules/<name>.cjs` - One file for each ESLint core rule, that re-exports the rule's `create` function.
|
|
* 2. `index.ts` - Exports all rules as a `Record<string, CreateRule>`.
|
|
* This is the `rules` property of the `oxlint-plugin-eslint` plugin.
|
|
* 3. `rule_names.ts` - Exports a list of all rule names, which is used in TSDown config.
|
|
*
|
|
* `index.ts` uses a split eager/lazy strategy so that `registerPlugin` can read each rule's `meta`
|
|
* without loading the rule module itself:
|
|
*
|
|
* - `meta` is serialized and inlined at build time.
|
|
* `registerPlugin` needs it at plugin registration time (for `fixable`, `hasSuggestions`, `schema`,
|
|
* `defaultOptions`, `messages`), so it must be available immediately without requiring the rule module.
|
|
*
|
|
* - `create` is deferred via a cached `require` call.
|
|
* The rule module is only loaded the first time `create` is called (i.e. when the rule actually runs at lint time).
|
|
* A top-level variable per rule caches the loaded function so subsequent calls skip the `require` call.
|
|
*
|
|
* Build-time validations:
|
|
* - Each rule object must only have `meta` and `create` properties.
|
|
* - `meta` values are walked to ensure they contain no functions
|
|
* (which would be serialized as executable code by `serialize-javascript`).
|
|
*/
|
|
|
|
import { readdirSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
import { join as pathJoin, basename, relative as pathRelative } from "node:path";
|
|
import { createRequire } from "node:module";
|
|
import { execSync } from "node:child_process";
|
|
import serialize from "serialize-javascript";
|
|
|
|
import type { CreateRule } from "../src-js/plugins/load.ts";
|
|
import type { RuleMeta } from "../src-js/plugins/rule_meta.ts";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
const oxlintDirPath = pathJoin(import.meta.dirname, "..");
|
|
const rootDirPath = pathJoin(oxlintDirPath, "../..");
|
|
const eslintRulesDir = pathJoin(require.resolve("eslint/package.json"), "../lib/rules");
|
|
const generatedDirPath = pathJoin(oxlintDirPath, "src-js/generated/plugin-eslint");
|
|
const generatedRulesDirPath = pathJoin(generatedDirPath, "rules");
|
|
|
|
export default function generatePluginEslint(): void {
|
|
// Get all ESLint rule names (exclude `index.js` which is the registry, not a rule)
|
|
const ruleNames = readdirSync(eslintRulesDir)
|
|
.filter((filename) => filename.endsWith(".js") && filename !== "index.js")
|
|
.map((filename) => basename(filename, ".js"))
|
|
.sort();
|
|
|
|
// oxlint-disable-next-line no-console
|
|
console.log(`Found ${ruleNames.length} ESLint rules`);
|
|
|
|
// Wipe and recreate generated directories
|
|
rmSync(generatedDirPath, { recursive: true, force: true });
|
|
mkdirSync(generatedRulesDirPath, { recursive: true });
|
|
|
|
// Generate a CJS wrapper file for each rule
|
|
for (const ruleName of ruleNames) {
|
|
const relPath = pathRelative(generatedRulesDirPath, pathJoin(eslintRulesDir, `${ruleName}.js`));
|
|
const content = `module.exports = require(${JSON.stringify(relPath)}).create;\n`;
|
|
writeFileSync(pathJoin(generatedRulesDirPath, `${ruleName}.cjs`), content);
|
|
}
|
|
|
|
// Generate the plugin rules index.
|
|
// `meta` is inlined so it's available at registration time without loading the rule module.
|
|
// `create` is deferred via a cached `require` so the rule module is only loaded on first use.
|
|
const indexLines = [
|
|
`
|
|
import { createRequire } from "node:module";
|
|
|
|
import type { CreateRule } from "../../plugins/load.ts";
|
|
|
|
type CreateFn = CreateRule["create"];
|
|
|
|
var require = createRequire(import.meta.url);
|
|
`,
|
|
];
|
|
|
|
// Generate a `let` declaration for each rule's cached `create` function.
|
|
// These are initially `null` and populated on first call.
|
|
for (let i = 0; i < ruleNames.length; i++) {
|
|
indexLines.push(`var create${i}: CreateFn | null = null;`);
|
|
}
|
|
|
|
indexLines.push("", "export default {");
|
|
|
|
for (let i = 0; i < ruleNames.length; i++) {
|
|
const ruleName = ruleNames[i];
|
|
const rulePath = pathJoin(eslintRulesDir, `${ruleName}.js`);
|
|
const rule: CreateRule = require(rulePath);
|
|
|
|
// Validate that the rule only has expected top-level properties.
|
|
// If ESLint adds new properties in a future version, we want to find out at build time.
|
|
const unexpectedKeys = Object.keys(rule).filter((key) => key !== "meta" && key !== "create");
|
|
if (unexpectedKeys.length > 0) {
|
|
throw new Error(
|
|
`Unexpected properties on rule \`${ruleName}\`: ${unexpectedKeys.join(", ")}. `
|
|
+ "Expected only `meta` and `create`.",
|
|
);
|
|
}
|
|
|
|
// Reduce `meta` to only the properties Oxlint uses, with consistent shape and property order.
|
|
// We discard e.g. `deprecated` and `docs` properties. This reduces code size.
|
|
// Default values match what `registerPlugin` assumes when a property is absent.
|
|
const { meta } = rule;
|
|
const reducedMeta: RuleMeta = {
|
|
messages: meta?.messages ?? undefined,
|
|
fixable: meta?.fixable ?? null,
|
|
hasSuggestions: meta?.hasSuggestions ?? false,
|
|
schema: meta?.schema ?? undefined,
|
|
defaultOptions: meta?.defaultOptions ?? undefined,
|
|
};
|
|
|
|
// Check for function values in `reducedMeta`, which would be unexpected and likely a bug.
|
|
// `serialize-javascript` would serialize them as executable code, so catch this at build time.
|
|
assertNoFunctions(reducedMeta, `eslint/lib/rules/${ruleName}.js`, "meta");
|
|
|
|
const metaCode = serialize(reducedMeta, { unsafe: true });
|
|
|
|
indexLines.push(`
|
|
${JSON.stringify(ruleName)}: {
|
|
meta: ${metaCode},
|
|
create(context) {
|
|
if (create${i} === null) create${i} = require("./rules/${ruleName}.cjs") as CreateFn;
|
|
return create${i}(context);
|
|
},
|
|
},
|
|
`);
|
|
}
|
|
indexLines.push("} satisfies Record<string, CreateRule>;\n");
|
|
|
|
const indexFilePath = pathJoin(generatedDirPath, "index.ts");
|
|
writeFileSync(indexFilePath, indexLines.join("\n"));
|
|
|
|
// Format generated index file with oxfmt to clean up unnecessary quotes around property names.
|
|
// This isn't necessary, as it gets minified and bundled anyway, but it makes generated code easier to read
|
|
// when debugging.
|
|
execSync(`pnpm exec oxfmt --write ${JSON.stringify(indexFilePath)}`, { cwd: rootDirPath });
|
|
|
|
// Generate the rule_names.ts file for use in tsdown config
|
|
const ruleNamesCode = [
|
|
"export default [",
|
|
...ruleNames.map((name) => ` ${JSON.stringify(name)},`),
|
|
"] as const;\n",
|
|
].join("\n");
|
|
|
|
writeFileSync(pathJoin(generatedDirPath, "rule_names.ts"), ruleNamesCode);
|
|
|
|
// oxlint-disable-next-line no-console
|
|
console.log("Generated plugin-eslint files.");
|
|
}
|
|
|
|
/**
|
|
* Walk an object tree and throw if any function values are found.
|
|
*/
|
|
function assertNoFunctions(value: unknown, rulePath: string, path: string): void {
|
|
if (typeof value === "function") {
|
|
throw new Error(
|
|
`Unexpected function value in \`${path}\` of rule \`${rulePath}\`. `
|
|
+ "Rule meta objects must be static data.",
|
|
);
|
|
}
|
|
if (typeof value === "object" && value !== null) {
|
|
for (const [key, child] of Object.entries(value)) {
|
|
assertNoFunctions(child, rulePath, `${path}.${key}`);
|
|
}
|
|
}
|
|
}
|