mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
- Generate native AST views, syntax guards, and token spellings from pinned TypeScript 7. - Connect frontend analysis directly to owned AST, checker, and session objects. - Preserve node identity and handle absent parents across lowering.
154 lines
8.7 KiB
JavaScript
154 lines
8.7 KiB
JavaScript
import { createRequire } from "node:module";
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
/** Generate class views rather than structural copies of native AST nodes.
|
|
* The build-time parser reads declarations only; no parser joins the client. */
|
|
export function generateAstViews(root, packageRoot, version, check) {
|
|
const require = createRequire(join(root, "package.json"));
|
|
const ts = require("typescript");
|
|
const read = (name) => readFileSync(join(packageRoot, "dist/ast", name), "utf8");
|
|
const parse = (name, source = read(name)) => ts.createSourceFile(name, source, ts.ScriptTarget.Latest, true);
|
|
const generated = parse("ast.generated.d.ts");
|
|
const header = [
|
|
`// Generated from typescript@${version} by scripts/generate-ts7-ast-schema.mjs.`,
|
|
"// TypeScript is Copyright Microsoft Corporation, licensed under Apache-2.0.",
|
|
"// Regenerate when changing the TypeScript pin; do not edit by hand.",
|
|
];
|
|
const write = (name, lines) => {
|
|
const path = join(root, "packages/compiler/src/frontend/ts7", name);
|
|
const output = lines.join("\n") + "\n";
|
|
if (check) {
|
|
if (readFileSync(path, "utf8") !== output) throw new Error(`${name} is stale; run node scripts/generate-ts7-ast-schema.mjs`);
|
|
} else writeFileSync(path, output);
|
|
};
|
|
const interfaces = new Map(generated.statements.filter(ts.isInterfaceDeclaration).map((node) => [node.name.text, node]));
|
|
const replace = (text, bindings) => text.replace(/\b[A-Za-z_$][\w$]*\b/g, (word) => bindings.get(word) ?? word);
|
|
const members = (name, bindings = new Map()) => {
|
|
if (name === "Node") return new Map();
|
|
const declaration = interfaces.get(name);
|
|
if (!declaration) throw new Error(`Unknown AST base ${name}`);
|
|
const fields = new Map();
|
|
for (const clause of declaration.heritageClauses ?? []) for (const base of clause.types) {
|
|
const baseName = base.expression.getText(generated);
|
|
const params = interfaces.get(baseName)?.typeParameters ?? [];
|
|
const args = base.typeArguments ?? [];
|
|
const inherited = new Map();
|
|
params.forEach((param, index) => {
|
|
const argument = args[index] ?? param.default;
|
|
if (!argument) throw new Error(`Missing AST type argument ${baseName}.${param.name.text}`);
|
|
inherited.set(param.name.text, replace(argument.getText(generated), bindings));
|
|
});
|
|
for (const [key, value] of members(baseName, inherited)) fields.set(key, value);
|
|
}
|
|
for (const member of declaration.members) {
|
|
if (!ts.isPropertySignature(member) || !member.type || !ts.isIdentifier(member.name)) throw new Error(`Unexpected AST member in ${name}`);
|
|
const key = member.name.text;
|
|
// Brands and synthetic-factory fields do not exist on remote nodes.
|
|
if (/^_.*Brand$/.test(key) ||
|
|
(name === "FunctionLikeBase" && key === "fullSignature") ||
|
|
(name === "SyntheticExpression" && (key === "isSpread" || key === "type"))) continue;
|
|
let type = replace(member.type.getText(generated), bindings);
|
|
// The pinned native wire and RemoteNode getter encode one tag, despite
|
|
// the SDK factory-oriented interface declaring an array of tags.
|
|
if (key === "jsdocPropertyTags") type = "JSDocPropertyTag";
|
|
if (member.questionToken) type += " | undefined";
|
|
fields.set(key, ` readonly ${key}: ${type};`);
|
|
}
|
|
return fields;
|
|
};
|
|
const types = [...header, "",
|
|
'import type { AstNode } from "./ast-node.js";',
|
|
'import type { JsxTagNamePropertyAccess, NodeArray } from "./ast-types.js";',
|
|
'import { SyntaxKind, NodeFlags, ModifierFlags, TokenFlags } from "./enums.js";',
|
|
"type Node = AstNode;",
|
|
];
|
|
// Brand-only bases otherwise become identical to Node, making the false
|
|
// side of a predicate narrow every node to never. Use actual descendant
|
|
// kind discriminants instead of inventing fields in native storage.
|
|
const derivesFrom = (name, base) => name === base || (interfaces.get(name)?.heritageClauses ?? []).some((clause) => clause.types.some((type) => derivesFrom(type.expression.getText(generated), base)));
|
|
for (const node of generated.statements) {
|
|
if (ts.isTypeAliasDeclaration(node)) types.push("", node.name.text === "TypeNode"
|
|
? "export type TypeNode = TypeNodeBase | ExpressionWithTypeArguments;"
|
|
: node.getText(generated));
|
|
if (!ts.isInterfaceDeclaration(node)) continue;
|
|
const params = node.typeParameters?.length ? `<${node.typeParameters.map((param) => param.getText(generated)).join(", ")}>` : "";
|
|
const fields = members(node.name.text);
|
|
if (!fields.has("kind")) {
|
|
const kinds = new Set();
|
|
for (const [name, child] of interfaces) {
|
|
if (!derivesFrom(name, node.name.text)) continue;
|
|
let kind = child.members.find((member) => member.name?.getText(generated) === "kind")?.type?.getText(generated);
|
|
if (kind === undefined) continue;
|
|
for (const parameter of child.typeParameters ?? []) kind = replace(kind, new Map([[parameter.name.text, parameter.constraint?.getText(generated) ?? "never"]]));
|
|
kinds.add(kind);
|
|
}
|
|
if (kinds.size > 0) fields.set("kind", ` readonly kind: ${[...kinds].join(" | ")};`);
|
|
}
|
|
types.push("", `export interface ${node.name.text}${params} extends Node {`, ...fields.values(), "}");
|
|
}
|
|
write("ast-types.generated.ts", types);
|
|
|
|
// Pair upstream executable bodies with their declared predicates. This
|
|
// preserves the pinned kind/flag semantics without loading SDK JavaScript.
|
|
const guards = [...header, "", 'import { SyntaxKind, NodeFlags, ScriptKind, OuterExpressionKinds } from "./enums.js";'];
|
|
for (const stem of ["is.generated", "is"]) {
|
|
const declarations = parse(`${stem}.d.ts`);
|
|
const signatures = new Map();
|
|
for (const node of declarations.statements) {
|
|
if (ts.isImportDeclaration(node) && node.importClause?.isTypeOnly) {
|
|
guards.push(node.getText(declarations).replace('"./ast.ts"', '"./ast-types.js"'));
|
|
}
|
|
if (ts.isTypeAliasDeclaration(node)) guards.push(node.getText(declarations)
|
|
.replace("WrappedExpression<T extends Expression>", "WrappedExpression")
|
|
.replace("WrappedExpression<Expression>", "WrappedExpression"));
|
|
if (ts.isFunctionDeclaration(node)) signatures.set(node.name.text, node);
|
|
}
|
|
const source = parse(`${stem}.js`);
|
|
for (const fn of source.statements) {
|
|
if (!ts.isFunctionDeclaration(fn) || !fn.body || !fn.name) continue;
|
|
const name = fn.name.text;
|
|
const signature = signatures.get(name);
|
|
let head;
|
|
if (signature) {
|
|
head = signature.getText(declarations).replace("declare ", "").replace(/;$/, "");
|
|
if (fn.parameters.some((param) => param.initializer)) {
|
|
head = head.replace("kinds?: OuterExpressionKinds", "kinds: OuterExpressionKinds = OuterExpressionKinds.All");
|
|
}
|
|
} else {
|
|
if (name === "isJSDocTypeAssertion") head = "function isJSDocTypeAssertion(node: Node): boolean";
|
|
else {
|
|
if (fn.parameters.length !== 1 || fn.parameters[0].name.getText(source) !== "kind") throw new Error(`Unrecognized private guard ${name}`);
|
|
head = `function ${name}(kind: SyntaxKind): boolean`;
|
|
}
|
|
}
|
|
let body = fn.body.getText(source);
|
|
if (name === "isJSDocTypeAssertion") body = body.replace("expression.kind !==", "expression === undefined || expression.kind !==");
|
|
if (head.includes("node: Node") && name.startsWith("is")) {
|
|
head = head.replace("node: Node", "node: Node | undefined");
|
|
body = body.replace("{", "{\n if (node === undefined) return false;");
|
|
}
|
|
guards.push("", head + " " + body);
|
|
}
|
|
}
|
|
// Merge duplicate type-only imports before writing the generated module.
|
|
const imported = new Set();
|
|
const content = guards.filter((line) => {
|
|
if (!line.startsWith("import type {")) return true;
|
|
for (const name of line.slice(line.indexOf("{") + 1, line.indexOf("}")).split(",")) imported.add(name.trim());
|
|
return false;
|
|
});
|
|
content.splice(header.length + 1, 0, `import type { ${[...imported].sort().join(", ")} } from "./ast-types.js";`);
|
|
write("ast-guards.generated.ts", content);
|
|
|
|
const { tokenToString } = require(join(packageRoot, "dist/ast/scanner.js"));
|
|
const { SyntaxKind } = require(join(packageRoot, "dist/enums/syntaxKind.js"));
|
|
const tokens = [...header, "", 'import { SyntaxKind } from "./enums.js";', "", "export function tokenToString(kind: SyntaxKind): string | undefined {", " switch (kind) {"];
|
|
for (let kind = 0; kind <= SyntaxKind.Count; kind++) {
|
|
const text = tokenToString(kind);
|
|
if (text !== undefined) tokens.push(` case ${kind}: return ${JSON.stringify(text)};`);
|
|
}
|
|
tokens.push(" default: return undefined;", " }", "}");
|
|
write("ast-tokens.generated.ts", tokens);
|
|
}
|