Files
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

466 lines
14 KiB
TypeScript

import {
Node,
type ExportDeclaration,
type ExportSpecifier,
type Node as TsNode,
} from "ts-morph";
import * as path from "node:path";
import { REACT_GENERATIVE_UI_INDEX, REACT_INDEX, REPO_ROOT } from "./paths.mts";
import {
GENERATIVE_UI_PACKAGE_EXPORTS,
classifyExport,
relatedOrSupportingRole,
} from "./classify.mts";
import {
chooseDeclaration,
extractJsDoc,
extractSignature,
getAllExportedNames,
getProject,
renderJsDocLinks,
type JsDocRenderOptions,
} from "./extract.mts";
export type ApiSection =
| "tools"
| "model-context"
| "transport"
| "external-store"
| "voice"
| "generative-ui"
| "primitives"
| "hooks"
| "adapters"
| "runtimes"
| "context-providers"
| "integrations"
| "utilities";
export type ExportKind =
| "class"
| "component"
| "function"
| "interface"
| "namespace"
| "type"
| "value";
export type ExportInfo = {
name: string;
section: ApiSection;
kind: ExportKind;
page: string;
pageRole: "primary" | "related" | "supporting-type";
sourcePath?: string | undefined;
jsDoc?: string | undefined;
jsDocExamples?: string[] | undefined;
deprecated?: string | undefined;
signature?: string | undefined;
classificationRule: string;
classificationConfidence: "strong" | "medium" | "fallback";
classificationReason: string;
jsDocRenderOptions?: JsDocRenderOptions;
};
export const SECTION_ORDER = [
"tools",
"model-context",
"transport",
"external-store",
"voice",
"generative-ui",
"primitives",
"hooks",
"adapters",
"runtimes",
"context-providers",
"integrations",
"utilities",
] as const satisfies readonly ApiSection[];
export const REACT_API_SECTIONS = SECTION_ORDER.filter(
(section) => section !== "integrations",
);
const IGNORED_EXPORTS = new Set([
"AssistantEventCallback",
"AssistantEventName",
"AssistantEventPayload",
"AssistantEventScope",
"AssistantEventSelector",
"AssistantState",
"DevToolsProviderApi",
"FrameMessageType",
"LanguageModelV1CallSettings",
"Unstable_UseMentionAdapterOptions",
"Unstable_UseSlashCommandAdapterOptions",
]);
type ExportEntry = {
name: string;
exportNode: ExportDeclaration;
specifier?: ExportSpecifier;
};
function getExportEntries(decl: ExportDeclaration): ExportEntry[] {
const namespaceExport = decl.getNamespaceExport();
if (namespaceExport) {
return [{ name: namespaceExport.getName(), exportNode: decl }];
}
return decl.getNamedExports().map((specifier) => ({
name: specifier.getAliasNode()?.getText() ?? specifier.getName(),
exportNode: decl,
specifier,
}));
}
function classifyKind(node: TsNode | undefined, name: string): ExportKind {
if (!node) return "value";
if (Node.isClassDeclaration(node)) return "class";
if (name.endsWith("Provider")) return "component";
if (Node.isInterfaceDeclaration(node)) return "interface";
if (Node.isTypeAliasDeclaration(node)) return "type";
if (Node.isFunctionDeclaration(node)) return "function";
if (Node.isModuleDeclaration(node)) return "namespace";
if (Node.isVariableDeclaration(node)) {
if (/^[A-Z]/.test(name)) return "component";
if (/^(unstable_)?use[A-Z]/.test(name)) return "function";
}
if (Node.isBindingElement(node) && /^(unstable_)?use[A-Z]/.test(name)) {
return "function";
}
return "value";
}
function resolveDeclaration(entry: ExportEntry): TsNode | undefined {
const symbols = [
entry.specifier?.getSymbol()?.getAliasedSymbol(),
entry.specifier?.getSymbol(),
entry.exportNode.getNamespaceExport()?.getSymbol()?.getAliasedSymbol(),
entry.exportNode.getNamespaceExport()?.getSymbol(),
];
for (const symbol of symbols) {
const declaration = chooseDeclaration(symbol?.getDeclarations() ?? []);
if (declaration) return declaration;
}
return undefined;
}
function getLeadingCommentText(node: TsNode): string {
return node
.getLeadingCommentRanges()
.map((range) => range.getText())
.join("\n");
}
function exportEntryDeprecated(entry: ExportEntry): string | undefined {
const comments = [
entry.specifier ? getLeadingCommentText(entry.specifier) : "",
getLeadingCommentText(entry.exportNode),
].join("\n");
if (!comments.includes("@deprecated")) return undefined;
return comments.match(/@deprecated\s+([^*\n]+)/)?.[1]?.trim() || "true";
}
function relativeToRepo(filePath: string | undefined): string | undefined {
if (!filePath) return undefined;
return path.relative(REPO_ROOT, filePath);
}
type DiscoveredExportInput = {
name: string;
resolved: TsNode | undefined;
deprecated?: string | undefined;
};
type ClassifiedExportInput = DiscoveredExportInput & {
sourcePath?: string | undefined;
kind: ExportKind;
placement: ReturnType<typeof classifyExport>;
};
type ApiReferenceLinkItem = Pick<ExportInfo, "name" | "section" | "page">;
const MANUAL_API_REFERENCE_LINKS = new Map([
[
"AISDKToolkit",
"/docs/api-reference/integrations/react-ai-sdk#aisdktoolkit",
],
[
"JSONGenerativeUI.present",
"/docs/api-reference/generative-ui/json-generative-ui#jsongenerativeui",
],
[
"JSONGenerativeUI.promptUser",
"/docs/api-reference/generative-ui/json-generative-ui#jsongenerativeui",
],
[
"AssistantState",
"/docs/api-reference/primitives/assistant-if#assistantstate",
],
]);
function collectExportInputs(entryPath: string): DiscoveredExportInput[] {
const project = getProject();
const sourceFile =
project.getSourceFile(entryPath) ?? project.addSourceFileAtPath(entryPath);
const seen = new Set<string>();
const exports: DiscoveredExportInput[] = [];
const addExport = (input: DiscoveredExportInput) => {
if (seen.has(input.name) || IGNORED_EXPORTS.has(input.name)) return;
seen.add(input.name);
exports.push(input);
};
for (const decl of sourceFile.getExportDeclarations()) {
for (const entry of getExportEntries(decl)) {
addExport({
name: entry.name,
resolved: resolveDeclaration(entry),
deprecated: exportEntryDeprecated(entry),
});
}
}
for (const [name, declarations] of sourceFile.getExportedDeclarations()) {
addExport({ name, resolved: chooseDeclaration(declarations) });
}
return exports.sort((a, b) => a.name.localeCompare(b.name));
}
function headingAnchor(name: string): string {
return name
.trim()
.toLowerCase()
.replace(/[^\p{Letter}\p{Number}\s_-]/gu, "")
.replace(/\s+/g, "-");
}
function apiReferenceHref(item: Pick<ExportInfo, "section" | "page" | "name">) {
return `/docs/api-reference/${item.section}/${item.page}#${headingAnchor(item.name)}`;
}
function cleanLinkTarget(target: string): string {
return target.trim().replace(/^`|`$/g, "").replace(/\(\)$/, "");
}
function createApiReferenceLinkResolver(items: ApiReferenceLinkItem[]) {
const itemByName = new Map(items.map((item) => [item.name, item]));
return (target: string): string | undefined => {
const name = cleanLinkTarget(target);
const manualHref = MANUAL_API_REFERENCE_LINKS.get(name);
if (manualHref) return manualHref;
const item = itemByName.get(name);
return item ? apiReferenceHref(item) : undefined;
};
}
/** Predicate over every discovered export name — including supporting types
* that get no standalone anchor. Lets the JSDoc renderer tell a legitimately
* anchorless reference apart from a broken link (see JsDocRenderOptions).
* Closes over the caller's (already cached) set rather than copying it. */
function createKnownExportPredicate(
known: ReadonlySet<string>,
): (target: string) => boolean {
return (target: string): boolean => known.has(cleanLinkTarget(target));
}
function isBetterDonor(
candidate: ClassifiedExportInput,
current: ClassifiedExportInput,
): boolean {
const primary = (item: ClassifiedExportInput) =>
item.placement.role === "primary" ? 0 : 1;
if (primary(candidate) !== primary(current)) {
return primary(candidate) < primary(current);
}
const strong = (item: ClassifiedExportInput) =>
item.placement.confidence === "strong" ? 0 : 1;
if (strong(candidate) !== strong(current)) {
return strong(candidate) < strong(current);
}
return candidate.name.localeCompare(current.name) < 0;
}
function classifyExportInputs(
inputs: DiscoveredExportInput[],
): ClassifiedExportInput[] {
const classified = inputs.map((input) => {
const sourcePath = relativeToRepo(
input.resolved?.getSourceFile().getFilePath(),
);
const kind = classifyKind(input.resolved, input.name);
const placement = classifyExport({ name: input.name, kind, sourcePath });
return { ...input, sourcePath, kind, placement };
});
// Unplaceable exports inherit a confidently-classified file-mate's location.
const donorBySource = new Map<string, ClassifiedExportInput>();
for (const item of classified) {
if (!item.sourcePath || item.placement.confidence === "fallback") continue;
const current = donorBySource.get(item.sourcePath);
if (!current || isBetterDonor(item, current)) {
donorBySource.set(item.sourcePath, item);
}
}
return classified.map((item) => {
if (item.placement.confidence !== "fallback" || !item.sourcePath) {
return item;
}
const donor = donorBySource.get(item.sourcePath);
if (!donor) return item;
return {
...item,
placement: {
...donor.placement,
role: relatedOrSupportingRole(item.kind),
rule: "fallback:co-location",
confidence: "medium",
reason: `inherited from co-located ${donor.name}`,
},
};
});
}
function linkItemsFor(inputs: ClassifiedExportInput[]): ApiReferenceLinkItem[] {
return inputs
.filter((item) => item.placement.role !== "supporting-type")
.map((item) => ({
name: item.name,
section: item.placement.section,
page: item.placement.page,
}));
}
let reactApiInputs: ClassifiedExportInput[] | undefined;
let reactGenerativeUIApiInputs: ClassifiedExportInput[] | undefined;
let mainApiInputs: ClassifiedExportInput[] | undefined;
let mainApiLinkItems: ApiReferenceLinkItem[] | undefined;
let reactApiRenderOptions: JsDocRenderOptions | undefined;
function getReactApiInputs(): ClassifiedExportInput[] {
reactApiInputs ??= classifyExportInputs(collectExportInputs(REACT_INDEX));
return reactApiInputs;
}
function getReactGenerativeUIApiInputs(): ClassifiedExportInput[] {
reactGenerativeUIApiInputs ??= classifyExportInputs(
collectExportInputs(REACT_GENERATIVE_UI_INDEX),
).filter((item) => GENERATIVE_UI_PACKAGE_EXPORTS.has(item.name));
return reactGenerativeUIApiInputs;
}
function getMainApiInputs(): ClassifiedExportInput[] {
mainApiInputs ??= [
...getReactApiInputs(),
...getReactGenerativeUIApiInputs(),
];
return mainApiInputs;
}
function getMainApiLinkItems(): ApiReferenceLinkItem[] {
mainApiLinkItems ??= linkItemsFor(getMainApiInputs());
return mainApiLinkItems;
}
/** Bundled JSDoc render options (link resolver + known-export predicate) for
* the react public API, lazily built once. Reused by the primitive-docs
* generator so primitive prop JSDoc links resolve the same way the
* api-reference pass resolves them. */
export function getReactApiRenderOptions(): JsDocRenderOptions {
reactApiRenderOptions ??= {
linkResolver: createApiReferenceLinkResolver(getMainApiLinkItems()),
isKnownExport: createKnownExportPredicate(getAllExportedNames()),
};
return reactApiRenderOptions;
}
function buildExportInfo(
{
name,
resolved,
deprecated,
sourcePath,
kind,
placement,
}: ClassifiedExportInput,
renderOptions: JsDocRenderOptions,
overrides: Partial<
Pick<
ExportInfo,
| "section"
| "page"
| "pageRole"
| "classificationRule"
| "classificationConfidence"
| "classificationReason"
>
> = {},
): ExportInfo {
const docs = extractJsDoc(resolved, renderOptions);
const signature = extractSignature(resolved, name);
const resolvedDeprecated = deprecated
? renderJsDocLinks(deprecated, `${name} export @deprecated`, renderOptions)
: docs.deprecated;
return {
name,
section: overrides.section ?? placement.section,
kind,
page: overrides.page ?? placement.page,
pageRole: overrides.pageRole ?? placement.role,
sourcePath,
jsDoc: docs.jsDoc,
jsDocExamples: docs.examples,
deprecated: resolvedDeprecated,
signature,
classificationRule: overrides.classificationRule ?? placement.rule,
classificationConfidence:
overrides.classificationConfidence ?? placement.confidence,
classificationReason: overrides.classificationReason ?? placement.reason,
jsDocRenderOptions: renderOptions,
};
}
export function discoverExports(): ExportInfo[] {
const inputs = getMainApiInputs();
const renderOptions = getReactApiRenderOptions();
return inputs.map((input) => buildExportInfo(input, renderOptions));
}
export function discoverIntegrationExports(
entryPath: string,
page: string,
): ExportInfo[] {
const inputs = classifyExportInputs(collectExportInputs(entryPath)).filter(
({ kind }) => kind !== "interface" && kind !== "type",
);
// Links resolve to where these exports render — their integration page — not
// to the raw placement buildExportInfo overrides below.
const renderOptions: JsDocRenderOptions = {
linkResolver: createApiReferenceLinkResolver([
...getMainApiLinkItems(),
// inputs already excludes interface/type kinds (the only ones that ever
// get a "supporting-type" role), so every entry here renders on the page.
...inputs.map((item) => ({
name: item.name,
section: "integrations" as const,
page,
})),
]),
isKnownExport: createKnownExportPredicate(getAllExportedNames()),
};
return inputs.map((input) =>
buildExportInfo(input, renderOptions, {
section: "integrations",
page,
pageRole: "primary",
classificationRule: "integration:package",
classificationConfidence: "strong",
classificationReason: "package-level integration export",
}),
);
}