Files
assistant-ui--assistant-ui/scripts/generate-api-surface.mjs
T
wehub-resource-sync e30e75b5d4
Code Quality / Oxlint + Oxfmt (push) Waiting to run
Code Quality / Template Sync (push) Waiting to run
Code Quality / Build Changed Packages (push) Waiting to run
Code Quality / Test Changed Packages (push) Waiting to run
Deploy Expo Example / Deploy Production (push) Waiting to run
Deploy Ink Example / Deploy Production (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-stream, 3.12) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Waiting to run
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Waiting to run
Deploy Shadcn Registry / Deploy Production (push) Waiting to run
Template Metrics / LOC + Bundle Size (push) Waiting to run
Changesets / Create Version PR (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

876 lines
25 KiB
JavaScript

#!/usr/bin/env node
import {
existsSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { spawnSync } from "node:child_process";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { optionArgs, optionValues } from "./lib/script-options.mjs";
const repoRoot = process.cwd();
const packagesRoot = path.join(repoRoot, "packages");
const apiSurfaceRoot = path.join(repoRoot, "api-surface");
const tempRoot = path.join(repoRoot, ".api-surface-tmp");
const checkMode = process.argv.includes("--check");
const turboFilters = optionValues(process.argv.slice(2), "--filter");
const requireFromBuildUtils = createRequire(
path.join(repoRoot, "packages/x-buildutils/package.json"),
);
const { build } = await import(requireFromBuildUtils.resolve("tsdown"));
const ts = requireFromBuildUtils("typescript");
function readJson(file) {
return JSON.parse(readFileSync(file, "utf8"));
}
function packageFileName(packageName) {
return `${packageName.replace(/^@/, "").replaceAll("/", "__")}.ts`;
}
function packageEntryName(packageName) {
return packageFileName(packageName).replace(/\.ts$/, "");
}
function posixPath(file) {
return file.replaceAll("\\", "/");
}
function relativeImport(fromDir, toFile) {
const relative = posixPath(path.relative(fromDir, toFile));
return relative.startsWith(".") ? relative : `./${relative}`;
}
function compareStrings(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
function collectTypeTargets(value, conditions = []) {
if (!value || typeof value !== "object") return [];
const targets = [];
if (Object.prototype.hasOwnProperty.call(value, "types")) {
if (typeof value.types === "string") {
targets.push({
conditions,
typePath: value.types,
});
}
return targets;
}
for (const [condition, nested] of Object.entries(value)) {
targets.push(...collectTypeTargets(nested, [...conditions, condition]));
}
return targets;
}
function declarationFilesForTarget(packageDir, typePath) {
if (!typePath.includes("*")) {
const absolute = path.join(packageDir, typePath);
if (!existsSync(absolute)) {
throw new Error(
`Missing declaration file ${path.relative(repoRoot, absolute)}. Run pnpm build before generating API surface files.`,
);
}
return [absolute];
}
if (typePath.split("*").length !== 2) {
throw new Error(
`API surface generation supports a single wildcard in declaration paths, but found ${typePath}.`,
);
}
const [prefix, suffix] = typePath.split("*");
const files = readdirSync(packageDir, {
recursive: true,
withFileTypes: true,
})
.filter((entry) => entry.isFile())
.map((entry) =>
posixPath(path.join(entry.parentPath ?? entry.path, entry.name)),
)
.filter((file) => {
const relative = `./${posixPath(path.relative(packageDir, file))}`;
return relative.startsWith(prefix) && relative.endsWith(suffix);
})
.sort();
if (files.length === 0) {
throw new Error(
`No declaration files matched ${typePath} in ${path.relative(repoRoot, packageDir)}.`,
);
}
return files;
}
function collectPackages() {
const filteredPackageNames = turboFilters.length
? collectTurboFilteredPackageNames(turboFilters)
: undefined;
return readdirSync(packagesRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(packagesRoot, entry.name, "package.json"))
.filter((packageJsonPath) => existsSync(packageJsonPath))
.map((packageJsonPath) => {
const pkg = readJson(packageJsonPath);
return {
packageDir: path.dirname(packageJsonPath),
pkg,
};
})
.filter(({ pkg }) => !pkg.private)
.filter(
({ pkg }) => !filteredPackageNames || filteredPackageNames.has(pkg.name),
)
.sort((a, b) => compareStrings(a.pkg.name, b.pkg.name));
}
function collectTurboFilteredPackageNames(filters) {
const result = spawnSync(
"pnpm",
[
"exec",
"turbo",
"ls",
...optionArgs("--filter", filters),
"--output=json",
],
{
cwd: repoRoot,
encoding: "utf8",
},
);
if (result.status !== 0) {
throw new Error(
`Failed to list packages for API surface filter:\n${result.stdout}${result.stderr}`,
);
}
const jsonStart = result.stdout.indexOf("{");
if (jsonStart === -1) {
throw new Error(`Turbo did not return JSON output:\n${result.stdout}`);
}
const output = JSON.parse(result.stdout.slice(jsonStart));
return new Set(
output.packages.items.map((item) => {
if (typeof item.name !== "string") {
throw new Error("Turbo package list included an item without a name.");
}
return item.name;
}),
);
}
function collectDeclarationEntries(packageDir, pkg) {
const entries = [];
const exportsMap = pkg.exports;
if (exportsMap && typeof exportsMap === "object") {
for (const [exportPath, exportValue] of Object.entries(exportsMap)) {
const targets = collectTypeTargets(exportValue);
for (const target of targets) {
for (const file of declarationFilesForTarget(
packageDir,
target.typePath,
)) {
entries.push({
exportPath,
conditions: target.conditions,
file,
});
}
}
}
}
return entries.sort((a, b) => {
const aKey = `${a.exportPath}:${a.conditions.join("/")}:${a.file}`;
const bKey = `${b.exportPath}:${b.conditions.join("/")}:${b.file}`;
return compareStrings(aKey, bKey);
});
}
function runtimePathForDeclaration(file) {
if (file.endsWith(".d.cts")) return file.replace(/\.d\.cts$/, ".cjs");
if (file.endsWith(".d.mts")) return file.replace(/\.d\.mts$/, ".mjs");
return file.replace(/\.d\.ts$/, ".js");
}
function collectReferenceFiles(file) {
const content = readFileSync(file, "utf8");
const references = [];
for (const match of content.matchAll(
/\/\/\/\s*<reference\s+path=["']([^"']+)["']/g,
)) {
const reference = path.resolve(path.dirname(file), match[1]);
if (existsSync(reference)) references.push(reference);
}
return references;
}
function sanitizeIdentifier(value) {
const sanitized = value.replace(/[^A-Za-z0-9_$]/g, "_");
return /^[A-Za-z_$]/.test(sanitized) ? sanitized : `entry_${sanitized}`;
}
function entryNamespace(entry) {
const conditions =
entry.conditions.length > 0 ? `_${entry.conditions.join("_")}` : "";
const exportPath =
entry.exportPath === "." ? "root" : entry.exportPath.replace(/^\.\//, "");
return sanitizeIdentifier(`entry_${exportPath}${conditions}`);
}
function statementCategory(statement) {
if (
ts.isImportDeclaration(statement) ||
ts.isImportEqualsDeclaration(statement)
) {
return 0;
}
if (ts.isExportDeclaration(statement) || ts.isExportAssignment(statement)) {
return 2;
}
return 1;
}
function statementSortName(statement) {
if (ts.isImportDeclaration(statement)) {
return ts.isStringLiteral(statement.moduleSpecifier)
? statement.moduleSpecifier.text
: "";
}
if (ts.isVariableStatement(statement)) {
const [declaration] = statement.declarationList.declarations;
return declaration && ts.isIdentifier(declaration.name)
? declaration.name.text
: "";
}
if (ts.isModuleDeclaration(statement)) return statement.name.text;
if (statement.name && ts.isIdentifier(statement.name)) {
return statement.name.text;
}
return "";
}
function sortTopLevelStatements(statements) {
return statements
.map((statement, index) => ({
statement,
index,
category: statementCategory(statement),
name: statementSortName(statement),
}))
.toSorted(
(a, b) =>
a.category - b.category ||
compareStrings(a.name, b.name) ||
a.index - b.index,
)
.map(({ statement }) => statement);
}
function printSurfaceFile(sourceFile) {
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
removeComments: true,
});
return sourceFile.statements
.map((statement) =>
printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile),
)
.join("\n\n");
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizeReactImports(content) {
if (/\bReact\./.test(content)) return content;
return content
.replace(
/^import React, \{([^}]+)\} from "react";$/m,
(_, specifiers) => `import {${specifiers}} from "react";`,
)
.replace(/^import React from "react";\n{0,2}/m, "");
}
function normalizeBundlerNamespaceNames(content) {
const exportMatch = content.match(/\nexport \{ ([^}]+) \};?$/);
if (!exportMatch) return content;
const replacements = exportMatch[1]
.split(",")
.map((part) => part.trim().match(/^([A-Za-z_$][\w$]*) as (entry_[\w$]+)$/))
.filter(Boolean)
.map(([, from, to]) => [from, `${to}_exports`])
.sort((a, b) => b[0].length - a[0].length);
let normalized = content;
for (const [from, to] of replacements) {
normalized = normalized.replace(
new RegExp(
`(^|[^A-Za-z0-9_$])${escapeRegExp(from)}(?![A-Za-z0-9_$])`,
"g",
),
`$1${to}`,
);
}
return normalized;
}
function applyTwoSpaceIndent(content) {
return content
.split("\n")
.map((line) =>
line.replace(/^ +/, (spaces) => {
if (spaces.length % 4 !== 0) {
throw new Error(
`Unexpected generated declaration indentation: ${spaces.length} spaces.`,
);
}
return " ".repeat(spaces.length / 2);
}),
)
.join("\n");
}
const KNOWN_DECLARATION_FIXUPS = [
{
pattern:
/declare module "@assistant-ui\/store" {\n\s*interface ScopeRegistry {([\s\S]*?)\n\s*}\n}/g,
replacement: "interface ScopeRegistry {$1\n}",
},
{
pattern: /interface ScopeRegistry {\n}/g,
replacement:
"interface ScopeRegistry {\n [key: string]: { methods: any; meta?: any; events?: any };\n}",
},
{
pattern: /}\[keyof ClientEventMap\]/g,
replacement: "}[Extract<keyof ClientEventMap, string>]",
},
{
pattern: /__ASSISTANT_UI_DEVTOOLS_HOOK__\?: DevToolsHook;/g,
replacement: "__ASSISTANT_UI_DEVTOOLS_HOOK__?: any;",
},
{
pattern:
/(declare const MessagePartPrimitiveText:[\s\S]*?import\("react"\)\.RefAttributes<HTMLSpanElement>, "ref">, )"children" \| "asChild"/g,
replacement: '$1"asChild" | "children"',
},
];
function applyKnownDeclarationFixups(content) {
return KNOWN_DECLARATION_FIXUPS.reduce(
(output, fixup) => output.replace(fixup.pattern, fixup.replacement),
content,
);
}
function typeMemberName(member, sourceFile) {
if (!member.name) return "";
if (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) {
return member.name.text;
}
if (ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)) {
return member.name.text;
}
return member.name.getText(sourceFile);
}
function unwrapParenthesizedType(node) {
let current = node;
while (ts.isParenthesizedTypeNode(current)) current = current.type;
return current;
}
function literalPropertyValue(member) {
if (!member.type) return undefined;
const type = unwrapParenthesizedType(member.type);
if (
ts.isLiteralTypeNode(type) &&
(ts.isStringLiteral(type.literal) || ts.isNumericLiteral(type.literal))
) {
return type.literal.text;
}
if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
return type.typeName.text;
}
return undefined;
}
function collectTypeLiteralProperties(typeLiteral, sourceFile) {
const properties = new Map();
for (const member of typeLiteral.members) {
if (!ts.isPropertySignature(member)) continue;
const name = typeMemberName(member, sourceFile);
if (!name) continue;
properties.set(name, {
optional: Boolean(member.questionToken),
value: literalPropertyValue(member),
});
}
return properties;
}
function collectIntersectionTypeProperties(type, sourceFile) {
const unwrapped = unwrapParenthesizedType(type);
if (!ts.isIntersectionTypeNode(unwrapped)) return undefined;
const properties = new Map();
for (const part of unwrapped.types) {
const partType = unwrapParenthesizedType(part);
if (!ts.isTypeLiteralNode(partType)) continue;
for (const [name, value] of collectTypeLiteralProperties(
partType,
sourceFile,
)) {
properties.set(name, value);
}
}
return properties;
}
function attachmentUnionMemberStatus(properties) {
const source = properties.get("source")?.value;
if (source !== "thread-composer" && source !== "edit-composer") {
return undefined;
}
const status = properties.get("status")?.value;
if (
status !== "CompleteAttachmentStatus" &&
status !== "PendingAttachmentStatus"
) {
return undefined;
}
return status;
}
function attachmentUnionMemberInfo(type, sourceFile) {
const properties = collectIntersectionTypeProperties(type, sourceFile);
if (!properties) return { recognized: false, key: undefined };
const status = attachmentUnionMemberStatus(properties);
if (!status) return { recognized: false, key: undefined };
const statusRank = status === "CompleteAttachmentStatus" ? 0 : 1;
const hasMatchingPayload =
(statusRank === 0 &&
properties.has("content") &&
!properties.get("content").optional) ||
(statusRank === 1 &&
properties.has("file") &&
!properties.get("file").optional);
if (!hasMatchingPayload) return { recognized: true, key: undefined };
return {
recognized: true,
key: `${properties.get("source").value}:${statusRank}`,
};
}
function normalizeAttachmentUnionType(node, sourceFile, factory) {
const keyedTypes = node.types.map((type, index) => {
const info = attachmentUnionMemberInfo(type, sourceFile);
return {
index,
...info,
type,
};
});
const recognizedTypes = keyedTypes.filter(({ recognized }) => recognized);
if (recognizedTypes.length < 2) return node;
if (recognizedTypes.some(({ key }) => key === undefined)) {
throw new Error(
"Found a composer attachment union with an unsupported shape; update normalizeAttachmentUnionType.",
);
}
const sortedRecognizedTypes = recognizedTypes.toSorted(
(a, b) => compareStrings(a.key, b.key) || a.index - b.index,
);
if (
recognizedTypes.every(
({ index }, sortedIndex) =>
index === sortedRecognizedTypes[sortedIndex].index,
)
) {
return node;
}
let sortedIndex = 0;
const types = keyedTypes.map(({ recognized, type }) =>
recognized ? sortedRecognizedTypes[sortedIndex++].type : type,
);
return factory.updateUnionTypeNode(node, types);
}
function stringLiteralUnionMemberValue(type) {
const unwrapped = unwrapParenthesizedType(type);
if (
ts.isLiteralTypeNode(unwrapped) &&
ts.isStringLiteral(unwrapped.literal)
) {
return unwrapped.literal.text;
}
return undefined;
}
function normalizeStringLiteralUnionType(node, factory) {
const memberValues = node.types.map(stringLiteralUnionMemberValue);
if (memberValues.some((value) => value === undefined)) return node;
if (memberValues.length < 2) return node;
const sortedIndices = memberValues
.map((value, index) => ({ value, index }))
.toSorted((a, b) => compareStrings(a.value, b.value) || a.index - b.index);
if (sortedIndices.every(({ index }, sortedIndex) => index === sortedIndex)) {
return node;
}
const types = sortedIndices.map(({ index }) => node.types[index]);
return factory.updateUnionTypeNode(node, types);
}
export function normalizeBundledDeclaration(content) {
const stripped = content
.replaceAll("\r\n", "\n")
.replace(/ ?\/\/# sourceMappingURL=.*$/gm, "")
.replace(/[ \t]+$/gm, "")
.replace(/\n{3,}/g, "\n\n")
.trim();
const sourceFile = ts.createSourceFile(
"api-surface.ts",
stripped,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const sortedSourceFile = ts.factory.updateSourceFile(
sourceFile,
sortTopLevelStatements(sourceFile.statements),
);
const result = ts.transform(sortedSourceFile, [
(context) => {
let bindingParameterIndex = 0;
const visit = (node) => {
if (ts.isUnionTypeNode(node)) {
const attachmentNormalized = normalizeAttachmentUnionType(
ts.visitEachChild(node, visit, context),
sourceFile,
context.factory,
);
return normalizeStringLiteralUnionType(
attachmentNormalized,
context.factory,
);
}
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
/^Primitive(?:\$\d+)?$/.test(node.name.text) &&
node.type &&
ts.isTypeLiteralNode(node.type)
) {
return context.factory.updateVariableDeclaration(
node,
node.name,
node.exclamationToken,
context.factory.updateTypeLiteralNode(
node.type,
[...node.type.members].sort((a, b) =>
compareStrings(
typeMemberName(a, sourceFile),
typeMemberName(b, sourceFile),
),
),
),
node.initializer,
);
}
if (
ts.isParameter(node) &&
(ts.isObjectBindingPattern(node.name) ||
ts.isArrayBindingPattern(node.name))
) {
return context.factory.updateParameterDeclaration(
node,
node.modifiers,
node.dotDotDotToken,
context.factory.createIdentifier(
`_param${bindingParameterIndex++}`,
),
node.questionToken,
node.type,
node.initializer,
);
}
return ts.visitEachChild(node, visit, context);
};
return (node) => ts.visitNode(node, visit);
},
]);
const printed = applyKnownDeclarationFixups(
normalizeBundlerNamespaceNames(
normalizeReactImports(
applyTwoSpaceIndent(printSurfaceFile(result.transformed[0])),
),
),
);
result.dispose();
return `${printed.trim()}\n`;
}
async function bundlePackageSurface(packageInfo) {
const { packageDir, pkg } = packageInfo;
const entries = collectDeclarationEntries(packageDir, pkg);
if (entries.length === 0) return undefined;
const entryName = packageEntryName(pkg.name);
const tempSrc = path.join(tempRoot, "src");
const tempOut = path.join(tempRoot, "out", entryName);
const entryFile = path.join(tempSrc, `${entryName}.ts`);
mkdirSync(tempSrc, { recursive: true });
const source = entries
.flatMap((entry) => {
const namespace = entryNamespace(entry);
const runtimePath = runtimePathForDeclaration(entry.file);
const referenceImports = collectReferenceFiles(entry.file).map(
(reference) =>
`import ${JSON.stringify(relativeImport(tempSrc, runtimePathForDeclaration(reference)))};`,
);
return [
...referenceImports,
`export * as ${namespace} from ${JSON.stringify(relativeImport(tempSrc, runtimePath))};`,
];
})
.join("\n");
writeFileSync(entryFile, `${source}\n`);
await build({
entry: [entryFile],
outDir: tempOut,
cwd: repoRoot,
platform: "neutral",
format: "esm",
dts: true,
sourcemap: false,
clean: true,
logLevel: "silent",
deps: { neverBundle: /^node:/, skipNodeModulesBundle: true },
});
const outputFile = path.join(tempOut, `${entryName}.d.mts`);
if (!existsSync(outputFile)) {
throw new Error(
`tsdown did not emit ${path.relative(repoRoot, outputFile)}`,
);
}
return normalizeBundledDeclaration(readFileSync(outputFile, "utf8"));
}
function cleanDefaultValue(value) {
if (value === undefined) return undefined;
if (value === repoRoot) return "<cwd>";
return value;
}
function cleanObject(value) {
return Object.fromEntries(
Object.entries(value).filter(([, entryValue]) => entryValue !== undefined),
);
}
function formatArgument(argument) {
const name = argument.name();
return cleanObject({
syntax: argument.required
? `<${name}${argument.variadic ? "..." : ""}>`
: `[${name}${argument.variadic ? "..." : ""}]`,
description: argument.description,
required: argument.required,
variadic: argument.variadic || undefined,
defaultValue: cleanDefaultValue(argument.defaultValue),
choices: argument.argChoices,
});
}
function formatOption(option) {
return cleanObject({
flags: option.flags,
description: option.description,
required: option.required || undefined,
optional: option.optional || undefined,
defaultValue: cleanDefaultValue(option.defaultValue),
choices: option.argChoices,
hidden: option.hidden || undefined,
});
}
function formatCommand(command) {
return cleanObject({
name: command.name(),
description: command.description() || undefined,
usage: command.usage() || undefined,
arguments: command.registeredArguments.map(formatArgument),
options: command.options.map(formatOption),
commands: command.commands.map(formatCommand),
});
}
async function buildCliSurface() {
const cliDist = path.join(packagesRoot, "cli", "dist");
const programFile = path.join(cliDist, "program.js");
const requiredFile = path.join(cliDist, "commands", "create.js");
if (!existsSync(requiredFile)) {
throw new Error(
"Missing built CLI files. Run pnpm build before generating API surface files.",
);
}
const { buildProgram } = await import(pathToFileURL(programFile));
const program = buildProgram();
const create = program.commands.find(
(command) => command.name() === "create",
);
if (!create) {
throw new Error("CLI program is missing the create command.");
}
const createSurface = formatCommand(create);
return {
"assistant-ui": formatCommand(program),
"create-assistant-ui": {
name: "create-assistant-ui",
description: "create assistant-ui apps with one command",
forwardsTo: "assistant-ui create",
arguments: createSurface.arguments,
options: createSurface.options,
},
};
}
function renderCliSurface(cliSurface) {
return [
"type CliSurfaceSnapshot = Record<string, unknown>;",
"",
`export const cliSurface: CliSurfaceSnapshot = ${JSON.stringify(cliSurface, null, 2)};`,
"",
].join("\n");
}
function diffContent(file, content) {
const diffFile = path.join(
tempRoot,
"diff",
path.relative(repoRoot, file).replaceAll(path.sep, "__"),
);
mkdirSync(path.dirname(diffFile), { recursive: true });
writeFileSync(diffFile, content);
const diff = spawnSync("git", ["diff", "--no-index", "--", file, diffFile], {
encoding: "utf8",
});
return `${diff.stdout}${diff.stderr}`.split("\n").slice(0, 200).join("\n");
}
function writeOrCheck(file, content, changedFiles) {
const previous = existsSync(file) ? readFileSync(file, "utf8") : undefined;
if (previous === content) return;
changedFiles.push({
file: path.relative(repoRoot, file).replaceAll("\\", "/"),
content,
});
if (!checkMode) writeFileSync(file, content);
}
async function main() {
const packages = collectPackages();
const generatedFiles = new Set();
const changedFiles = [];
rmSync(tempRoot, { recursive: true, force: true });
if (!checkMode) mkdirSync(apiSurfaceRoot, { recursive: true });
try {
const needsCliSurface = packages.some(
({ pkg }) =>
pkg.name === "assistant-ui" || pkg.name === "create-assistant-ui",
);
const cliSurface = needsCliSurface ? await buildCliSurface() : {};
for (const packageInfo of packages) {
const { pkg } = packageInfo;
const bundledSurface = await bundlePackageSurface(packageInfo);
const cliPackageSurface = cliSurface[pkg.name];
if (!bundledSurface && !cliPackageSurface) continue;
const content = bundledSurface ?? renderCliSurface(cliPackageSurface);
const outputFile = path.join(apiSurfaceRoot, packageFileName(pkg.name));
generatedFiles.add(outputFile);
writeOrCheck(outputFile, content, changedFiles);
}
// Filtered checks only know about selected packages; stale cleanup needs the full package set.
if (turboFilters.length === 0 && existsSync(apiSurfaceRoot)) {
for (const entry of readdirSync(apiSurfaceRoot)) {
const file = path.join(apiSurfaceRoot, entry);
if (!entry.endsWith(".ts") || generatedFiles.has(file)) continue;
if (checkMode) {
changedFiles.push({
file: path.relative(repoRoot, file).replaceAll("\\", "/"),
});
} else {
rmSync(file);
}
}
}
if (checkMode && changedFiles.length > 0) {
console.error("API surface files are out of date:");
for (const { file } of changedFiles) console.error(` ${file}`);
for (const { file, content } of changedFiles) {
if (!content) continue;
console.error(`\nDiff for ${file}:`);
console.error(diffContent(path.join(repoRoot, file), content));
}
console.error("Run `pnpm api-surface` and commit the updated files.");
process.exit(1);
}
const action = checkMode ? "Checked" : "Generated";
console.log(`${action} ${generatedFiles.size} API surface file(s).`);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
}
if (import.meta.main) {
await main();
}