mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
58f7ab9c1d
Follow-on after #25585. Pure style change. I hate the way LLMs put line breaks in weird places like after "the" - it makes comments hard to read. And what's with all the colons and semicolons? No substantive changes, just reformatting comments and docs to avoid this unreadable formatting.
182 lines
6.9 KiB
TypeScript
182 lines
6.9 KiB
TypeScript
import { parseSync, Visitor } from "oxc-parser";
|
|
|
|
import type { Plugin } from "rolldown";
|
|
|
|
/**
|
|
* Plugin to turn every mapped write back into a plain one, for builds without source maps.
|
|
*
|
|
* ```ts
|
|
* // Original code
|
|
* writeWithMap(state, "declare ", CAT_OTHER, node);
|
|
*
|
|
* // After transform
|
|
* write(state, "declare ", CAT_OTHER);
|
|
* ```
|
|
*
|
|
* The mapped writes and `markWithMap*` exist to record a source mapping for the node they are given.
|
|
* A build without source map support has nothing to record, so the call becomes the plain write
|
|
* it would otherwise be, and the node argument goes with it - it would still be evaluated,
|
|
* and held live across the call, for a function which ignores it.
|
|
*
|
|
* The import is rewritten to match, which both keeps the plain name in scope and leaves the two
|
|
* mapped functions unreferenced for the minifier to remove.
|
|
*
|
|
* Only valid where `SOURCEMAPS` is `false`, which is where the config includes it.
|
|
*
|
|
* The checks afterwards fail the build rather than let a mismatch through. A rewrite which dropped
|
|
* the wrong argument, or left a call to a name nothing imports, produces a printer that is broken
|
|
* everywhere or subtly wrong everywhere, and nothing downstream names the plugin as the cause.
|
|
*/
|
|
const REWRITES = {
|
|
// `write` takes the `last` category between the code and the node, `writeNoLast` does not
|
|
writeWithMap: { arity: 4, plain: "write" },
|
|
writeWithMapNoLast: { arity: 3, plain: "writeNoLast" },
|
|
writeWithMapEnd: { arity: 4, plain: "write" },
|
|
// A standalone mark has no non-sourcemap equivalent. `void 0` is removed by the minifier.
|
|
markWithMap: { arity: 2, plain: null },
|
|
markWithMapNoName: { arity: 2, plain: null },
|
|
markWithMapAfter: { arity: 2, plain: null },
|
|
markWithMapAtStartOffset: { arity: 3, plain: null },
|
|
} as const;
|
|
|
|
const WRITE_MODULE = "./write.ts";
|
|
|
|
type MappedName = keyof typeof REWRITES;
|
|
|
|
const isMapped = (name: string): name is MappedName => name in REWRITES;
|
|
|
|
const plugin: Plugin = {
|
|
name: "unmap-writes",
|
|
transform: {
|
|
// Only process TS files in `src-js/print` directory
|
|
filter: { id: /\/src-js\/print\/.+\.ts$/ },
|
|
|
|
handler(code, path, meta) {
|
|
const magicString = meta.magicString!;
|
|
const { program, errors } = parseSync(path, code);
|
|
if (errors.length !== 0) throw new Error(`Failed to parse ${path}: ${errors[0].message}`);
|
|
|
|
// Plain names this file will need in scope once its calls are rewritten
|
|
const needed = new Set<string>();
|
|
let rewrote = false;
|
|
|
|
new Visitor({
|
|
CallExpression(node) {
|
|
const { callee } = node;
|
|
if (callee.type !== "Identifier" || !isMapped(callee.name)) return;
|
|
|
|
const { arity, plain } = REWRITES[callee.name];
|
|
const args = node.arguments;
|
|
if (args.length !== arity) {
|
|
throw new Error(
|
|
`\`${callee.name}\` takes ${arity} arguments, found ${args.length}: ` +
|
|
`${path}:${node.start}`,
|
|
);
|
|
}
|
|
|
|
rewrote = true;
|
|
if (plain === null) {
|
|
magicString.overwrite(node.start, node.end, "void 0");
|
|
} else {
|
|
magicString.overwrite(callee.start, callee.end, plain);
|
|
// Remove `, node`, from the end of the argument before it to the end of it
|
|
magicString.remove(args[arity - 2].end, args[arity - 1].end);
|
|
needed.add(plain);
|
|
}
|
|
},
|
|
}).visit(program);
|
|
|
|
if (rewrote) {
|
|
// Rewrite the import to drop the mapped names and carry whatever the rewrites now need.
|
|
// `write.ts` declares them itself and imports nothing, so it never reaches this.
|
|
// A file may import from `write.ts` more than once - the types separately from the values.
|
|
// The one to rewrite is whichever brought the mapped names in.
|
|
const imports = program.body.filter(
|
|
(statement) =>
|
|
statement.type === "ImportDeclaration" &&
|
|
statement.source.value === WRITE_MODULE &&
|
|
statement.specifiers.some(
|
|
(specifier) =>
|
|
specifier.type === "ImportSpecifier" &&
|
|
specifier.imported.type === "Identifier" &&
|
|
isMapped(specifier.imported.name),
|
|
),
|
|
);
|
|
|
|
if (imports.length !== 1) {
|
|
throw new Error(
|
|
`Expected 1 \`${WRITE_MODULE}\` import bringing in mapped writes, found ` +
|
|
`${imports.length}: ${path}`,
|
|
);
|
|
}
|
|
|
|
const importNode = imports[0];
|
|
if (importNode.type !== "ImportDeclaration") throw new Error(`Unreachable: ${path}`);
|
|
|
|
const names = new Set(needed);
|
|
for (const specifier of importNode.specifiers) {
|
|
if (specifier.type !== "ImportSpecifier" || specifier.imported.type !== "Identifier") {
|
|
throw new Error(`Unexpected \`${WRITE_MODULE}\` import specifier: ${path}`);
|
|
}
|
|
if (!isMapped(specifier.imported.name)) names.add(specifier.local.name);
|
|
}
|
|
|
|
const sorted = [...names].sort();
|
|
magicString.overwrite(
|
|
importNode.start,
|
|
importNode.end,
|
|
`import { ${sorted.join(", ")} } from "${WRITE_MODULE}";`,
|
|
);
|
|
}
|
|
|
|
const transformed = magicString.toString();
|
|
const { program: after, errors: afterErrors } = parseSync(path, transformed);
|
|
if (afterErrors.length !== 0) {
|
|
throw new Error(`Transform produced invalid code: ${path}: ${afterErrors[0].message}`);
|
|
}
|
|
|
|
// Nothing may still reach a mapped write.
|
|
// Their declarations stay, for the minifier to remove once nothing imports them.
|
|
const bound = new Set<string>();
|
|
const problems: string[] = [];
|
|
|
|
new Visitor({
|
|
ImportSpecifier(node) {
|
|
if (node.imported.type !== "Identifier") return;
|
|
if (isMapped(node.imported.name)) {
|
|
problems.push(`import of ${node.imported.name} at ${node.start}`);
|
|
}
|
|
bound.add(node.local.name);
|
|
},
|
|
FunctionDeclaration(node) {
|
|
if (node.id !== null) bound.add(node.id.name);
|
|
},
|
|
}).visit(after);
|
|
|
|
const called = new Set<string>();
|
|
new Visitor({
|
|
CallExpression(node) {
|
|
const { callee } = node;
|
|
if (callee.type !== "Identifier") return;
|
|
if (isMapped(callee.name)) problems.push(`call to ${callee.name} at ${node.start}`);
|
|
if (callee.name === "write" || callee.name === "writeNoLast") called.add(callee.name);
|
|
},
|
|
}).visit(after);
|
|
|
|
// A rewritten call to a name this file does not have in scope would be a `ReferenceError`
|
|
// on the first print, so check rather than assume the import rewrite above got it right
|
|
for (const name of called) {
|
|
if (!bound.has(name)) problems.push(`\`${name}\` is called but not in scope`);
|
|
}
|
|
|
|
if (problems.length !== 0) {
|
|
throw new Error(`unmap-writes left ${path} broken: ${problems.join(", ")}`);
|
|
}
|
|
|
|
return { code: magicString };
|
|
},
|
|
},
|
|
};
|
|
|
|
export default plugin;
|