chore: import upstream snapshot with attribution
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,875 @@
|
||||
#!/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();
|
||||
}
|
||||
Reference in New Issue
Block a user