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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:40:13 +08:00
commit e30e75b5d4
3893 changed files with 533074 additions and 0 deletions
@@ -0,0 +1,676 @@
import type { ApiSection, ExportInfo, ExportKind } from "./discover.mts";
export type Classification = {
section: ApiSection;
page: string;
role: ExportInfo["pageRole"];
rule: string;
confidence: ExportInfo["classificationConfidence"];
reason: string;
};
export type ClassificationInput = {
name: string;
kind: ExportKind;
sourcePath?: string | undefined;
};
type ClassificationRule = (
input: ClassificationInput,
) => Classification | undefined;
type DocPlacement = { page: string; role: ExportInfo["pageRole"] };
const PRIMARY_FEATURE_TYPES = new Set([
"RealtimeVoiceAdapter",
"VoiceSessionState",
"VoiceSessionControls",
"VoiceSessionHelpers",
"SpeechSynthesisAdapter",
"DictationAdapter",
"DictationState",
"ExternalStoreAdapter",
"ExternalThreadQueueAdapter",
"ExternalThreadProps",
]);
const RUNTIME_CREATION_HOOKS = new Set([
"useAssistantTransportRuntime",
"useCloudThreadListRuntime",
"useExternalStoreRuntime",
"useLocalRuntime",
"useLocalThreadRuntime",
"useRemoteThreadListRuntime",
"unstable_useRemoteThreadListRuntime",
]);
const STATE_HOOKS = new Set([
"useAui",
"useAuiState",
"useAuiEvent",
"useAssistantApi",
"useAssistantState",
"useAssistantEvent",
]);
const COMPOSER_TRIGGER_HOOKS = new Set([
"unstable_useMentionAdapter",
"unstable_useSlashCommandAdapter",
"unstable_useLiveCompletionAdapter",
"unstable_useTriggerPopoverRootContext",
"unstable_useTriggerPopoverRootContextOptional",
"unstable_useTriggerPopoverScopeContext",
"unstable_useTriggerPopoverScopeContextOptional",
"unstable_useTriggerPopoverTriggers",
"unstable_useTriggerPopoverTriggersOptional",
]);
function kebabCase(value: string): string {
return value
.replace(/^unstable_/, "unstable-")
.replace(/Primitive$/, "")
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
.toLowerCase();
}
export function supportingTypeRole(kind: ExportKind): ExportInfo["pageRole"] {
return kind === "interface" || kind === "type"
? "supporting-type"
: "primary";
}
// Like supportingTypeRole, but values render as "related" rather than "primary"
// so a confidently-classified page's core APIs sort above them. Used for
// loosely-placed exports (suffix fallback, co-location) that earned a spot on a
// page but should not lead it.
export function relatedOrSupportingRole(
kind: ExportKind,
): ExportInfo["pageRole"] {
return kind === "interface" || kind === "type"
? "supporting-type"
: "related";
}
export function pageForToolExport(name: string): string {
if (
name === "makeAssistantTool" ||
name === "useAssistantTool" ||
name === "AssistantTool" ||
name === "AssistantToolProps"
) {
return "component-tools";
}
if (name === "DataRenderers") {
return "rendering";
}
if (
name.includes("ToolArgs") ||
name.includes("ToolExecution") ||
name.includes("ToolCall")
) {
return "status";
}
if (
name.includes("ToolUI") ||
name.includes("DataUI") ||
name === "makeAssistantDataUI" ||
name === "useAssistantDataUI"
) {
return "rendering";
}
return "toolkits";
}
function classification(
section: ApiSection,
page: string,
role: ExportInfo["pageRole"],
rule: string,
confidence: Classification["confidence"],
reason: string,
): Classification {
return { section, page, role, rule, confidence, reason };
}
function toolsRule(input: ClassificationInput): Classification | undefined {
const { name, kind } = input;
if (
name === "tool" ||
name === "Tool" ||
name === "Toolkit" ||
name === "ToolDefinition" ||
name === "ToolsConfig" ||
name === "McpAppResourceOutput" ||
name === "useToolArgsStatus" ||
name === "Tools" ||
name === "DataRenderers" ||
name.includes("AssistantTool") ||
name.includes("AssistantDataUI") ||
name.includes("ToolArgs") ||
name.includes("ToolExecution") ||
name.includes("ToolCall")
) {
return classification(
"tools",
pageForToolExport(name),
name === "Toolkit" || name === "ToolDefinition"
? "primary"
: supportingTypeRole(kind),
"feature:tools",
"strong",
"tool definition, toolkit, component registration, rendering, or status export",
);
}
return undefined;
}
function transportRule(input: ClassificationInput): Classification | undefined {
const { name } = input;
if (
name.includes("AssistantTransport") ||
name.includes("SendCommands") ||
name.includes("Frame") ||
name.startsWith("Serialized") ||
name === "FRAME_MESSAGE_CHANNEL"
) {
// Transport frame/protocol helpers intentionally render as primary entries:
// the page split already groups them, and demoting them hides important protocol docs.
return classification(
"transport",
name.includes("Frame") ||
name.startsWith("Serialized") ||
name === "FRAME_MESSAGE_CHANNEL"
? "frame"
: "assistant-transport",
"primary",
"feature:transport",
"strong",
"assistant transport, frame, or protocol export",
);
}
return undefined;
}
function externalStoreRule(
input: ClassificationInput,
): Classification | undefined {
const { name, kind } = input;
if (
name.includes("ExternalStore") ||
name.includes("ExternalThread") ||
name.includes("ExternalMessage") ||
name.includes("MessageConverter") ||
name === "getExternalStoreMessages" ||
name === "bindExternalStoreMessage" ||
name === "unstable_convertExternalMessages" ||
name === "unstable_createMessageConverter"
) {
return classification(
"external-store",
name.includes("Message") || name.includes("Converter")
? "message-conversion"
: "runtime",
PRIMARY_FEATURE_TYPES.has(name) ? "primary" : supportingTypeRole(kind),
"feature:external-store",
"strong",
"external store runtime or message conversion export",
);
}
return undefined;
}
function modelContextRule(
input: ClassificationInput,
): Classification | undefined {
const { name, kind } = input;
if (
name.includes("ModelContext") ||
name.includes("AssistantContext") ||
name.includes("AssistantInstructions") ||
name.includes("InlineRender") ||
name === "mergeModelContexts" ||
name === "ModelContextRegistry"
) {
return classification(
"model-context",
name.includes("Registry") ? "registry" : "context",
supportingTypeRole(kind),
"feature:model-context",
"strong",
"model context, instructions, context, or registry export",
);
}
return undefined;
}
function voiceRule(input: ClassificationInput): Classification | undefined {
const { name, kind } = input;
if (
name.includes("Voice") ||
name.includes("Speech") ||
name.includes("Dictation") ||
name === "createVoiceSession"
) {
return classification(
"voice",
name.includes("Speech") || name.includes("Dictation")
? "speech-dictation"
: "session",
PRIMARY_FEATURE_TYPES.has(name) ? "primary" : supportingTypeRole(kind),
"feature:voice",
"strong",
"voice, speech, or dictation export",
);
}
return undefined;
}
const GENERATIVE_UI_SPEC_TYPES = new Set([
"GenerativeUISpec",
"GenerativeUINode",
"GenerativeUIMessagePart",
]);
export const GENERATIVE_UI_PACKAGE_EXPORTS = new Map<
string,
{ page: string; role: ExportInfo["pageRole"] }
>([
["JSONGenerativeUI", { page: "json-generative-ui", role: "primary" }],
["JSONGenerativeUIOptions", { page: "json-generative-ui", role: "primary" }],
["PresentToolOptions", { page: "json-generative-ui", role: "primary" }],
["PresentTool", { page: "json-generative-ui", role: "supporting-type" }],
["PromptUserTool", { page: "json-generative-ui", role: "supporting-type" }],
["defineGenerativeComponents", { page: "components", role: "primary" }],
["defaultGenerativeUILibrary", { page: "components", role: "primary" }],
["GenerativeUILibrary", { page: "components", role: "primary" }],
["GenerativeUIComponent", { page: "components", role: "primary" }],
["createActionRegistry", { page: "actions", role: "primary" }],
["emptyActionRegistry", { page: "actions", role: "primary" }],
["ActionRegistry", { page: "actions", role: "primary" }],
["ActionHandler", { page: "actions", role: "primary" }],
["ActionDispatchContext", { page: "actions", role: "primary" }],
["renderGenerativeUI", { page: "rendering", role: "primary" }],
["generativeUIToJSX", { page: "rendering", role: "primary" }],
["buildPresentParameters", { page: "rendering", role: "primary" }],
["GenerativeUIRenderContext", { page: "rendering", role: "primary" }],
["TEXT_SIZES", { page: "tokens", role: "primary" }],
["IMAGE_SIZE_TOKENS", { page: "tokens", role: "primary" }],
["WEIGHTS", { page: "tokens", role: "primary" }],
["COLORS", { page: "tokens", role: "primary" }],
["ALIGNS", { page: "tokens", role: "primary" }],
["JUSTIFIES", { page: "tokens", role: "primary" }],
["BUTTON_STYLES", { page: "tokens", role: "primary" }],
["ALERT_TONES", { page: "tokens", role: "primary" }],
["TextSize", { page: "tokens", role: "supporting-type" }],
["ImageSize", { page: "tokens", role: "supporting-type" }],
["Weight", { page: "tokens", role: "supporting-type" }],
["Color", { page: "tokens", role: "supporting-type" }],
["Align", { page: "tokens", role: "supporting-type" }],
["Justify", { page: "tokens", role: "supporting-type" }],
["ButtonStyle", { page: "tokens", role: "supporting-type" }],
["AlertTone", { page: "tokens", role: "supporting-type" }],
]);
function generativeUIPackageRule(
input: ClassificationInput,
): Classification | undefined {
const placement = GENERATIVE_UI_PACKAGE_EXPORTS.get(input.name);
if (!placement) return undefined;
return classification(
"generative-ui",
placement.page,
placement.role,
"feature:react-generative-ui",
"strong",
"@assistant-ui/react-generative-ui package export",
);
}
function generativeUIRule(
input: ClassificationInput,
): Classification | undefined {
const { name, kind } = input;
if (!name.startsWith("GenerativeUI")) return undefined;
// The spec format types are the cross-reference targets
// ({@link GenerativeUISpec}, {@link GenerativeUIMessagePart}); they must be
// primary to earn an anchor. The renderer, error, and registry/prop helpers
// live on the rendering page.
const isSpecType = GENERATIVE_UI_SPEC_TYPES.has(name);
return classification(
"generative-ui",
isSpecType ? "spec" : "rendering",
isSpecType ? "primary" : supportingTypeRole(kind),
"feature:generative-ui",
"strong",
"generative UI spec, renderer, or component registry export",
);
}
const LEGACY_INTERACTABLE_EXPORTS = new Set([
"useAssistantInteractable",
"AssistantInteractableProps",
"useInteractableState",
"Interactables",
"InteractableStateSchema",
"InteractablesState",
"InteractableDefinition",
"InteractableRegistration",
"InteractablesMethods",
"InteractablePersistedState",
"InteractablePersistenceAdapter",
"InteractablePersistenceStatus",
"InteractablesClientSchema",
]);
function interactablesRule(
input: ClassificationInput,
): Classification | undefined {
const { name, kind } = input;
const isUnstableInteractable =
name.startsWith("unstable_") && name.toLowerCase().includes("interactable");
const isUnstableInteractableType =
name.startsWith("Unstable_") && name.includes("Interactable");
if (isUnstableInteractable || isUnstableInteractableType) {
return classification(
"tools",
"interactables",
supportingTypeRole(kind),
"feature:interactables",
"strong",
"unstable interactables API export",
);
}
if (LEGACY_INTERACTABLE_EXPORTS.has(name)) {
return classification(
"tools",
"interactables-legacy",
supportingTypeRole(kind),
"feature:interactables-legacy",
"strong",
"legacy interactables API export",
);
}
return undefined;
}
function kindRule(input: ClassificationInput): Classification | undefined {
const { name, sourcePath } = input;
let section: ApiSection | undefined;
let forcedPrimary = false;
if (
name === "AuiIf" ||
name === "AssistantIf" ||
name.endsWith("Primitive")
) {
section = "primitives";
} else if (/^(unstable_)?use[A-Z]/.test(name)) {
section = "hooks";
} else if (name.endsWith("Provider")) {
section = "context-providers";
} else if (/Adapter(s)?$/.test(name)) {
section = "adapters";
} else if (/(Runtime|State)$/.test(name)) {
section = "runtimes";
} else if (
[
"ChatModelRunOptions",
"ChatModelRunResult",
"ChatModelRunUpdate",
"CreateStartRunConfig",
"CreateResumeRunConfig",
"LanguageModelConfig",
].includes(name)
) {
section = "adapters";
forcedPrimary = true;
}
if (!section) return undefined;
const placement = inferKindDocPlacement(name, section, sourcePath);
if (!placement) return undefined;
return classification(
section,
placement.page,
forcedPrimary ? "primary" : placement.role,
`kind:${section}`,
"medium",
`${section} export matched by public API shape`,
);
}
function fallbackRule(input: ClassificationInput): Classification {
// Suffix match routes *Tool/*Toolkit helpers the exact-name list misses.
// Value helpers render as "related" so the page's core tool APIs (tool,
// Toolkit, ToolDefinition) sort above them; types stay supporting.
if (/Tool(kit)?$/.test(input.name)) {
return classification(
"tools",
pageForToolExport(input.name),
relatedOrSupportingRole(input.kind),
"fallback:name",
"medium",
"name ends in Tool/Toolkit",
);
}
return classification(
"utilities",
"miscellaneous",
supportingTypeRole(input.kind),
"fallback:utilities",
"fallback",
"no feature or kind rule matched",
);
}
const CLASSIFICATION_RULES: ClassificationRule[] = [
toolsRule,
transportRule,
externalStoreRule,
modelContextRule,
voiceRule,
generativeUIPackageRule,
generativeUIRule,
interactablesRule,
kindRule,
];
/** Exact-name overrides consulted before any heuristic. The escape hatch for
* exports the rules can't place from name/source shape alone (mirrors
* MANUAL_API_REFERENCE_LINKS in discover.mts): pinning one is a single entry
* here instead of contorting a regex rule. */
const MANUAL_CLASSIFICATIONS = new Map<
string,
{ section: ApiSection; page: string; role: ExportInfo["pageRole"] }
>([
// Constructor for the `groupBy` prop of <MessagePrimitive.GroupedParts>; its
// home is the message primitive page, beside the part it configures.
[
"groupPartByType",
{ section: "primitives", page: "message", role: "related" },
],
// MCP app embedding/rendering surface.
["McpAppRenderer", { section: "tools", page: "rendering", role: "primary" }],
[
"McpAppsRemoteHost",
{ section: "tools", page: "rendering", role: "primary" },
],
[
"getMcpAppFromToolPart",
{ section: "tools", page: "rendering", role: "primary" },
],
// Marks a component subtree visible to the assistant's model context.
[
"makeAssistantVisible",
{ section: "model-context", page: "context", role: "primary" },
],
// Serializable thread/message snapshot used for persistence.
[
"ExportedMessageRepository",
{ section: "adapters", page: "persistence", role: "primary" },
],
]);
export function classifyExport(input: ClassificationInput): Classification {
const manual = MANUAL_CLASSIFICATIONS.get(input.name);
if (manual) {
return classification(
manual.section,
manual.page,
manual.role,
"manual",
"strong",
"manually pinned classification",
);
}
for (const rule of CLASSIFICATION_RULES) {
const result = rule(input);
if (result) return result;
}
return fallbackRule(input);
}
export function inferKindDocPlacement(
name: string,
section: ApiSection,
sourcePath?: string,
): DocPlacement | undefined {
const source = (sourcePath ?? "").replaceAll("\\", "/");
if (section === "primitives") {
if (name === "AuiIf") return { page: "assistant-if", role: "primary" };
if (name === "AssistantIf")
return { page: "assistant-if", role: "related" };
return { page: kebabCase(name), role: "primary" };
}
if (section === "hooks") {
if (STATE_HOOKS.has(name)) {
return { page: "state", role: "primary" };
}
if (RUNTIME_CREATION_HOOKS.has(name)) {
return { page: "runtimes", role: "primary" };
}
if (COMPOSER_TRIGGER_HOOKS.has(name)) {
return { page: "composer-triggers", role: "primary" };
}
if (source.includes("assistant-transport")) {
return { page: "assistant-transport", role: "primary" };
}
if (source.includes("voice") || name.includes("Voice")) {
return { page: "voice", role: "primary" };
}
if (
name.includes("AssistantTool") ||
name.includes("AssistantData") ||
name.includes("AssistantInstructions") ||
name.includes("AssistantContext") ||
name.includes("InlineRender") ||
name.includes("Interactable") ||
name.includes("ToolArgs") ||
source.includes("model-context")
) {
return { page: "model-context", role: "primary" };
}
if (
source.includes("/primitives/") ||
source.includes("/legacy-runtime/hooks/") ||
source.includes("/runtimes/cloud/") ||
name.includes("Attachment") ||
name.includes("Composer") ||
name.includes("Message") ||
name.includes("Quote") ||
name.includes("RuntimeAdapters") ||
name.includes("Scroll") ||
name.includes("Thread") ||
name.includes("Viewport")
) {
return { page: "primitives", role: "primary" };
}
return undefined;
}
if (section === "adapters") {
if (
name === "ChatModelAdapter" ||
name.includes("ChatModel") ||
name.includes("LanguageModel") ||
name.includes("RunConfig")
) {
return { page: "model", role: "primary" };
}
if (name === "FeedbackAdapter") {
return { page: "feedback", role: "primary" };
}
if (name.includes("Attachment"))
return { page: "attachments", role: "primary" };
if (
name.includes("Thread") ||
name.includes("History") ||
name.includes("ExternalStore") ||
name.includes("MessageFormat")
) {
return { page: "persistence", role: "primary" };
}
if (name.includes("Voice")) return { page: "voice", role: "primary" };
if (name.includes("Suggestion"))
return { page: "suggestions", role: "primary" };
if (name.includes("RuntimeAdapter"))
return { page: "runtime", role: "primary" };
return undefined;
}
if (section === "context-providers") {
if (name === "AssistantRuntimeProvider") {
return { page: "assistant-runtime-provider", role: "primary" };
}
if (name === "AuiProvider") {
return { page: "assistant-runtime-provider", role: "related" };
}
if (name.includes("Frame"))
return { page: "assistant-frame-provider", role: "primary" };
if (name.includes("ModelContext")) {
return { page: "model-context-provider", role: "primary" };
}
return { page: "scoped-providers", role: "primary" };
}
if (section === "runtimes") {
if (name === "PartState" || name === "EnrichedPartState") {
return { page: "message-part-runtime", role: "primary" };
}
if (name.includes("Assistant"))
return { page: "assistant-runtime", role: "primary" };
// Order matters: ThreadListItem includes ThreadList, and ThreadList includes Thread.
if (name.includes("ThreadListItem")) {
return { page: "thread-list-item-runtime", role: "primary" };
}
if (name.includes("ThreadList")) {
return { page: "thread-list-runtime", role: "primary" };
}
if (name.includes("Thread"))
return { page: "thread-runtime", role: "primary" };
if (name.includes("Composer"))
return { page: "composer-runtime", role: "primary" };
// Order matters: MessagePart includes Message.
if (name.includes("MessagePart")) {
return { page: "message-part-runtime", role: "primary" };
}
if (name.includes("Message"))
return { page: "message-runtime", role: "primary" };
if (name.includes("Attachment")) {
return { page: "attachment-runtime", role: "primary" };
}
if (name.includes("Voice")) return { page: "voice-state", role: "primary" };
if (name.includes("Dictation"))
return { page: "dictation-state", role: "primary" };
if (name.includes("Queue")) return { page: "queue-state", role: "primary" };
}
return undefined;
}
@@ -0,0 +1,465 @@
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",
}),
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
import * as path from "node:path";
import { fileURLToPath } from "node:url";
export const REPO_ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../..",
);
export const DOCS_ROOT = path.join(REPO_ROOT, "apps/docs");
export const REACT_PKG = path.join(REPO_ROOT, "packages/react/src");
export const REACT_GENERATIVE_UI_PKG = path.join(
REPO_ROOT,
"packages/react-generative-ui/src",
);
export const CORE_PKG = path.join(REPO_ROOT, "packages/core/src");
export const PRIMITIVES_DIR = path.join(REACT_PKG, "primitives");
export const REACT_INDEX = path.join(REACT_PKG, "index.ts");
export const REACT_GENERATIVE_UI_INDEX = path.join(
REACT_GENERATIVE_UI_PKG,
"index.ts",
);
export const TYPE_DOCS_INPUT = path.join(
DOCS_ROOT,
"content/types-to-generate/typeDocs.ts",
);
export const TYPE_DOCS_OUTPUT = path.join(DOCS_ROOT, "generated/typeDocs.ts");
export const INTEGRATION_TYPE_DOCS_OUTPUT = path.join(
DOCS_ROOT,
"generated/integrationTypeDocs.ts",
);
export const PRIMITIVE_DOCS_OUTPUT = path.join(
DOCS_ROOT,
"generated/primitiveDocs.ts",
);
export const API_REFERENCE_DIR = path.join(
DOCS_ROOT,
"content/docs/(reference)/api-reference",
);
export const INTEGRATION_PACKAGES = [
{
slug: "react-ai-sdk",
packageName: "@assistant-ui/react-ai-sdk",
entry: path.join(REPO_ROOT, "packages/react-ai-sdk/src/index.ts"),
},
{
slug: "react-data-stream",
packageName: "@assistant-ui/react-data-stream",
entry: path.join(REPO_ROOT, "packages/react-data-stream/src/index.ts"),
},
{
slug: "cloud-ai-sdk",
packageName: "@assistant-ui/cloud-ai-sdk",
entry: path.join(REPO_ROOT, "packages/cloud-ai-sdk/src/index.ts"),
},
{
slug: "eve",
packageName: "@assistant-ui/eve",
entry: path.join(REPO_ROOT, "packages/eve/src/index.ts"),
},
] as const;
@@ -0,0 +1,211 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { PRIMITIVE_DOCS_OUTPUT } from "./paths.mts";
import { getReactApiRenderOptions } from "./discover.mts";
import type { InheritedFrom, PropModel } from "./extract.mts";
import {
extractPrimitiveParts,
type PrimitivePartModel,
} from "./primitive-extract.mts";
// ── Projection: PrimitivePartModel[] \u2192 grouped doc shape ──────────────────
type RenderedProp = {
name: string;
type?: string;
description?: string;
default?: string;
required?: boolean;
deprecated?: string;
children?: Array<{ type?: string; parameters: RenderedProp[] }>;
};
type RenderedPart = {
element?: string;
description?: string;
deprecated?: string;
props: RenderedProp[];
};
type GroupedPrimitives = Record<string, Record<string, RenderedPart>>;
const PRIMITIVE_FILTER: ReadonlySet<InheritedFrom> = new Set([
"react",
"csstype",
"react-textarea-autosize",
"radix",
]);
function shouldDropForPrimitive(prop: PropModel): boolean {
if (prop.name.startsWith("__")) return true;
// `tw` is a polluting global JSX prop from `@vercel/og` types pulled in by
// the Next.js tsconfig. Legacy primitive-docs ran with
// `skipAddingFilesFromTsConfig: true` so it never saw this prop; we drop
// it by name here to preserve that behaviour.
if (prop.name === "tw") return true;
return (
prop.inheritedFrom !== undefined && PRIMITIVE_FILTER.has(prop.inheritedFrom)
);
}
/** Truncate very long inline object types at a token boundary. Mirrors the
* legacy primitive-docs cleanTypeText behaviour exactly. */
function presentPrimitiveType(rawType: string | undefined): string | undefined {
if (!rawType) return undefined;
if (rawType.length <= 120) return rawType;
return rawType.replace(/\{[^{}]{100,}\}/g, (match) => {
let cutoff = 80;
const semicolonIdx = match.lastIndexOf(";", cutoff);
const commaIdx = match.lastIndexOf(",", cutoff);
const breakIdx = Math.max(semicolonIdx, commaIdx);
if (breakIdx > 20) cutoff = breakIdx + 1;
return `${match.substring(0, cutoff)} ... }`;
});
}
/** Project one level of children. Legacy primitive-docs only expanded
* children one level deep (no recursion), and emitted children for the
* `components` prop without a wrapping `type` field. */
function projectChildrenShallow(
children: PropModel["children"],
parentPropName: string,
): RenderedProp["children"] | undefined {
if (!children) return undefined;
const omitType = parentPropName === "components";
const out = children
.map(({ typeName, props }) => {
const parameters = props
.filter((child) => !shouldDropForPrimitive(child))
.map((child) => projectProp(child, false));
if (parameters.length === 0) return undefined;
return omitType ? { parameters } : { type: typeName, parameters };
})
.filter((entry): entry is { type?: string; parameters: RenderedProp[] } =>
Boolean(entry),
);
return out.length > 0 ? out : undefined;
}
function isInlineObjectTypeText(typeText: string | undefined): boolean {
if (!typeText) return false;
const text = typeText.trim();
return text.startsWith("{") || text.startsWith("| {");
}
function projectProp(prop: PropModel, allowChildren = true): RenderedProp {
// Legacy primitive-docs only expanded children when:
// - prop is named `components` AND its type text contained `{`, OR
// - the prop's type-node was an inline object literal.
// Use rawType as a proxy for both checks.
const rawText = prop.rawType ?? "";
const shouldExpand =
allowChildren &&
((prop.name === "components" && rawText.includes("{")) ||
isInlineObjectTypeText(prop.rawType));
const children = shouldExpand
? projectChildrenShallow(prop.children, prop.name)
: undefined;
const type = presentPrimitiveType(prop.rawType);
const out: RenderedProp = { name: prop.name };
if (type !== undefined) out.type = type;
if (prop.description) out.description = prop.description;
if (prop.default) out.default = prop.default;
if (prop.required) out.required = true;
if (prop.deprecated) out.deprecated = prop.deprecated;
if (children) out.children = children;
return out;
}
function projectPart(part: PrimitivePartModel): RenderedPart {
let props: RenderedProp[];
if (part.isActionButton) {
// Legacy primitive-docs only documented hook-specific params for
// ActionButton parts. Both `render` (locally declared in WithRenderProp)
// and the rest of PrimitiveButtonProps are excluded here; `asChild` is
// re-injected below as a bare row. Also strip trailing `| undefined`
// from hook param types to match legacy ActionButton output.
props = part.props
.filter((p) => !shouldDropForPrimitive(p))
.filter((p) => p.name !== "render" && p.name !== "asChild")
// Legacy parity: this projection historically gated child expansion on
// the array index, so the first prop never expands its children.
.map((p, i) => projectProp(p, i !== 0))
.map((p) =>
p.type ? { ...p, type: p.type.replace(/\s*\|\s*undefined$/, "") } : p,
);
props = [{ name: "asChild" }, ...props];
} else {
props = part.props
.filter((p) => !shouldDropForPrimitive(p))
// Legacy parity: child expansion was historically gated on the array
// index, so the first prop never expands its children.
.map((p, i) => projectProp(p, i !== 0));
// Inject a bare asChild when the part supports it and the projection
// didn't already carry one.
if (part.supportsAsChild && !props.some((p) => p.name === "asChild")) {
props = [{ name: "asChild" }, ...props];
}
}
const out: RenderedPart = { props };
if (part.element) out.element = part.element;
if (part.description) out.description = part.description;
if (part.deprecated) out.deprecated = part.deprecated;
return out;
}
function projectToPrimitiveDocs(
parts: PrimitivePartModel[],
): GroupedPrimitives {
const result: GroupedPrimitives = {};
// First-wins per (primitiveName, localName) so that `Content` aliased to
// `Parts` doesn't emit a duplicate entry. Order matches barrel order from
// the underlying source file.
const seen = new Map<string, Set<string>>();
for (const part of parts) {
const seenLocals = seen.get(part.primitiveName) ?? new Set();
if (seenLocals.has(part.localName)) continue;
seenLocals.add(part.localName);
seen.set(part.primitiveName, seenLocals);
let primitive = result[part.primitiveName];
if (!primitive) {
primitive = {};
result[part.primitiveName] = primitive;
}
primitive[part.partName] = projectPart(part);
}
return result;
}
function serialize(grouped: GroupedPrimitives): string {
const lines: string[] = [
"// AUTO-GENERATED by scripts/generate-type-docs.mts",
"// Do not edit manually.",
"",
];
for (const [name, parts] of Object.entries(grouped)) {
lines.push(`export const ${name} = ${JSON.stringify(parts, null, 2)};\n`);
}
return lines.join("\n");
}
// ── Main ───────────────────────────────────────────────────────────────────
export function generatePrimitiveDocs() {
console.log("Generating primitive docs...");
const parts = extractPrimitiveParts(getReactApiRenderOptions());
const grouped = projectToPrimitiveDocs(parts);
const output = serialize(grouped);
fs.mkdirSync(path.dirname(PRIMITIVE_DOCS_OUTPUT), { recursive: true });
fs.writeFileSync(PRIMITIVE_DOCS_OUTPUT, output);
const totalParts = Object.values(grouped).reduce(
(sum, p) => sum + Object.keys(p).length,
0,
);
console.log(
`Generated docs for ${Object.keys(grouped).length} primitives with ${totalParts} parts \u2192 ${PRIMITIVE_DOCS_OUTPUT}`,
);
}
@@ -0,0 +1,496 @@
import {
Node,
type ExportedDeclarations,
type InterfaceDeclaration,
type ModuleDeclaration,
type SourceFile,
type TypeAliasDeclaration,
} from "ts-morph";
import * as fs from "node:fs";
import * as path from "node:path";
import { REACT_INDEX, REACT_PKG, REPO_ROOT } from "./paths.mts";
import {
getProject,
getJsDocCommentText,
jsDocTag,
jsDocTags,
processComponentDeclaration,
processTypeOrInterface,
type JsDocRenderOptions,
type PropModel,
} from "./extract.mts";
export type PrimitivePartModel = {
primitiveName: string; // "ComposerPrimitive"
partName: string; // "Input"
propsTypeName: string; // "ComposerPrimitiveInputProps"
/** The local name of the underlying React component declaration. Two
* exports that re-export the same component (e.g. `Parts` aliased as
* `Content`) share a localName, so the primitive-docs projection can
* dedupe to one entry while api-ref still emits a typeDocs entry per
* exported part name. */
localName: string;
element?: string;
description?: string;
deprecated?: string;
examples?: string[];
supportsAsChild: boolean;
isActionButton: boolean;
isRequireAtLeastOne: boolean;
props: PropModel[];
sourcePath: string;
};
const primitiveSourceFiles = new Map<string, SourceFile>();
const primitiveParts = new Map<string, string[]>();
let primitiveBarrelExports: Map<string, string> | undefined;
export function primitivePartTypeDocName(
primitiveName: string,
part: string,
): string {
return `${primitiveName}${part}Props`;
}
export function primitiveModuleSourceFile(
primitiveName: string,
): SourceFile | undefined {
const cached = primitiveSourceFiles.get(primitiveName);
if (cached) return cached;
const project = getProject();
const index =
project.getSourceFile(REACT_INDEX) ??
project.addSourceFileAtPath(REACT_INDEX);
const exportDecl = index
.getExportDeclarations()
.find((decl) => decl.getNamespaceExport()?.getName() === primitiveName);
const moduleSpec = exportDecl?.getModuleSpecifierValue();
if (!moduleSpec) return undefined;
const modulePath = path.join(REACT_PKG, moduleSpec.replace(/^\.\//, ""));
const sourcePath = [
`${modulePath}.ts`,
`${modulePath}.tsx`,
path.join(modulePath, "index.ts"),
path.join(modulePath, "index.tsx"),
].find((file) => fs.existsSync(file));
if (!sourcePath) return undefined;
const sourceFile =
project.getSourceFile(sourcePath) ??
project.addSourceFileAtPath(sourcePath);
primitiveSourceFiles.set(primitiveName, sourceFile);
return sourceFile;
}
export function readPrimitiveParts(primitiveName: string): string[] {
const cached = primitiveParts.get(primitiveName);
if (cached) return cached;
const sourceFile = primitiveModuleSourceFile(primitiveName);
if (!sourceFile) {
primitiveParts.set(primitiveName, []);
return [];
}
const isPrimitivePart = (name: string) =>
(/^[A-Z]/.test(name) || /^Unstable_[A-Z]/.test(name)) &&
!name.includes("Primitive");
const orderedParts = sourceFile
.getExportDeclarations()
.flatMap((decl) =>
decl.getNamedExports().map((specifier) => {
return specifier.getAliasNode()?.getText() ?? specifier.getName();
}),
)
.filter(isPrimitivePart);
const orderedPartNames = new Set(orderedParts);
const fallbackParts = [...sourceFile.getExportedDeclarations().keys()]
.filter(isPrimitivePart)
.filter((name) => !orderedPartNames.has(name))
.sort((a, b) => a.localeCompare(b));
const parts = [
...new Set([
...orderedParts.filter((part) => part === "Root"),
...orderedParts.filter((part) => part !== "Root"),
...fallbackParts,
]),
];
primitiveParts.set(primitiveName, parts);
return parts;
}
function findNamespace(
sourceFile: SourceFile,
localName: string,
): ModuleDeclaration | undefined {
for (const ns of sourceFile.getModules()) {
if (ns.getName() === localName) return ns;
}
return undefined;
}
function extractElementType(ns: ModuleDeclaration): string | undefined {
for (const typeAlias of ns.getTypeAliases()) {
if (typeAlias.getName() !== "Element") continue;
const typeText = typeAlias.getType().getText();
if (typeText.includes("HTMLTextAreaElement")) return "textarea";
if (typeText.includes("HTMLButtonElement")) return "button";
if (typeText.includes("HTMLInputElement")) return "input";
if (typeText.includes("HTMLDivElement")) return "div";
if (typeText.includes("HTMLSpanElement")) return "span";
if (typeText.includes("HTMLFormElement")) return "form";
if (typeText.includes("ActionButtonElement")) return "button";
}
return undefined;
}
function getPrimitiveComponentMeta(
sourceFile: SourceFile,
localName: string,
options?: JsDocRenderOptions,
): {
description?: string | undefined;
deprecated?: string | undefined;
examples?: string[] | undefined;
} {
for (const varDecl of sourceFile.getVariableDeclarations()) {
if (varDecl.getName() !== localName) continue;
const statement = varDecl.getVariableStatement();
if (!statement) continue;
const jsDocs = statement.getJsDocs();
if (jsDocs.length === 0) continue;
const doc = jsDocs[0]!;
const description = getJsDocCommentText(
doc,
`${localName} primitive`,
options,
);
const deprecated = jsDocTag(
doc,
"deprecated",
`${localName} primitive`,
options,
);
const examples = jsDocTags(
doc,
"example",
`${localName} primitive`,
options,
);
return { description, deprecated, examples };
}
return {};
}
function typeSupportsAsChild(
typeAlias: TypeAliasDeclaration | InterfaceDeclaration,
): boolean {
return typeAlias
.getType()
.getProperties()
.some((prop) => prop.getName() === "asChild");
}
function referencesRequireAtLeastOne(
decl: TypeAliasDeclaration | InterfaceDeclaration,
sourceFile: SourceFile,
visited = new Set<string>(),
): boolean {
const key = decl.getName();
if (visited.has(key)) return false;
visited.add(key);
const text = decl.getText();
if (text.includes("RequireAtLeastOne")) return true;
const typeNodeText =
"getTypeNode" in decl ? (decl.getTypeNode?.()?.getText() ?? text) : text;
const referencedNames = new Set(
Array.from(typeNodeText.matchAll(/\b[A-Z][A-Za-z0-9_]*\b/g)).map(
(match) => match[0],
),
);
for (const name of referencedNames) {
if (name === key || name === "PropsWithChildren") continue;
const referencedType = sourceFile.getTypeAlias(name);
if (
referencedType &&
referencesRequireAtLeastOne(referencedType, sourceFile, visited)
) {
return true;
}
const referencedInterface = sourceFile.getInterface(name);
if (
referencedInterface &&
referencesRequireAtLeastOne(referencedInterface, sourceFile, visited)
) {
return true;
}
}
return false;
}
function extractPropsFromComponentDeclaration(
sourceFile: SourceFile,
localName: string,
ownerTypeName: string,
options?: JsDocRenderOptions,
): PropModel[] | undefined {
const propsTypeNames = new Set<string>();
const variableDecl = sourceFile
.getVariableDeclarations()
.find((decl) => decl.getName() === localName);
const typeNodeText = variableDecl?.getTypeNode()?.getText();
if (typeNodeText) {
for (const match of typeNodeText.matchAll(
/<\s*([A-Za-z0-9_]+Props)\s*>/g,
)) {
propsTypeNames.add(match[1]!);
}
}
if (propsTypeNames.size === 0) {
const suffix = localName.replace(/^[A-Za-z]+Primitive/, "");
propsTypeNames.add(`${suffix}Props`);
}
for (const propsTypeName of propsTypeNames) {
const typeAlias = sourceFile.getTypeAlias(propsTypeName);
if (typeAlias) {
return processTypeOrInterface(typeAlias, ownerTypeName, options);
}
const iface = sourceFile.getInterface(propsTypeName);
if (iface) return processTypeOrInterface(iface, ownerTypeName, options);
}
return undefined;
}
function discoverPrimitiveBarrelExports(): Map<string, string> {
if (primitiveBarrelExports) return primitiveBarrelExports;
const project = getProject();
const result = new Map<string, string>();
const indexFile =
project.getSourceFile(REACT_INDEX) ??
project.addSourceFileAtPath(REACT_INDEX);
for (const decl of indexFile.getExportDeclarations()) {
const namespaceExport = decl.getNamespaceExport();
if (!namespaceExport) continue;
const name = namespaceExport.getName();
const moduleSpec = decl.getModuleSpecifierValue();
if (name.endsWith("Primitive") && moduleSpec) {
result.set(name, moduleSpec);
}
}
primitiveBarrelExports = result;
return result;
}
type SubComponent = {
exportedName: string;
declaration: ExportedDeclarations;
};
function discoverSubComponents(primitiveModulePath: string): SubComponent[] {
const candidatePaths = [
`${primitiveModulePath}.ts`,
`${primitiveModulePath}.tsx`,
path.join(primitiveModulePath, "index.ts"),
path.join(primitiveModulePath, "index.tsx"),
];
const indexPath = candidatePaths.find((candidate) =>
fs.existsSync(candidate),
);
if (!indexPath) return [];
const project = getProject();
let sourceFile: SourceFile;
try {
sourceFile =
project.getSourceFile(indexPath) ??
project.addSourceFileAtPath(indexPath);
} catch {
return [];
}
const components: SubComponent[] = [];
for (const [
exportedName,
declarations,
] of sourceFile.getExportedDeclarations()) {
// Accept capital and lowercase `unstable_` prefixes — the lowercase form
// matches legacy primitive-docs (e.g. AttachmentPrimitive.unstable_Thumb).
// The api-ref typeDocs projection filters lowercase `unstable_` parts
// back out to match legacy api-surface behaviour.
const isPart =
(/^[A-Z]/.test(exportedName) ||
/^Unstable_[A-Z]/.test(exportedName) ||
/^unstable_[A-Z]/.test(exportedName)) &&
!exportedName.includes("Primitive");
if (!isPart) continue;
const declaration = declarations.find((decl) => {
if (Node.isVariableDeclaration(decl)) return true;
if (Node.isFunctionDeclaration(decl)) return true;
if (Node.isClassDeclaration(decl)) return true;
return false;
});
if (!declaration) continue;
components.push({ exportedName, declaration });
}
return components;
}
function localNameFor(declaration: ExportedDeclarations): string | undefined {
const symbolName = declaration.getSymbol()?.getName();
if (symbolName) return symbolName;
if (
Node.isVariableDeclaration(declaration) ||
Node.isFunctionDeclaration(declaration) ||
Node.isClassDeclaration(declaration) ||
Node.isInterfaceDeclaration(declaration) ||
Node.isTypeAliasDeclaration(declaration) ||
Node.isModuleDeclaration(declaration)
) {
return declaration.getName();
}
return undefined;
}
function extractPrimitivePart(
primitiveName: string,
sub: SubComponent,
options?: JsDocRenderOptions,
): PrimitivePartModel | undefined {
const sourceFile = sub.declaration.getSourceFile();
const localName = localNameFor(sub.declaration);
if (!localName) return undefined;
const ns = findNamespace(sourceFile, localName);
const propsAlias = ns?.getTypeAliases().find((t) => t.getName() === "Props");
const element = ns ? extractElementType(ns) : undefined;
const { description, deprecated, examples } = getPrimitiveComponentMeta(
sourceFile,
localName,
options,
);
let props: PropModel[] | undefined;
let isActionButton = false;
let supportsAsChild = false;
let isRequireAtLeastOne = false;
const propsTypeName = primitivePartTypeDocName(
primitiveName,
sub.exportedName,
);
if (propsAlias) {
const propsText = propsAlias.getText();
isActionButton = propsText.includes("ActionButtonProps");
supportsAsChild = typeSupportsAsChild(propsAlias);
isRequireAtLeastOne = referencesRequireAtLeastOne(propsAlias, sourceFile);
// Process the Props alias uniformly — including ActionButton-style parts.
// The intersection naturally surfaces both PrimitiveButtonProps members
// (asChild, render, … tagged inheritedFrom: "radix") and the hook-specific
// params (tagged inheritedFrom: undefined). Each projection then filters
// according to its policy.
props = processTypeOrInterface(propsAlias, propsTypeName, options) ?? [];
// RequireAtLeastOne: every prop is technically optional individually,
// but at least one must be supplied. Surface this by marking all props
// optional in the model so projections render them consistently.
if (isRequireAtLeastOne) {
props = props.map((p) => ({ ...p, required: false }));
}
} else {
props = extractPropsFromComponentDeclaration(
sourceFile,
localName,
propsTypeName,
options,
);
// Fallback: when neither a Props alias nor a *Props type by name is
// available (e.g. wrappers built with `Object.assign(Base, ...)` lose
// the namespace), fall back to inspecting the component's call signature
// parameter directly. Mirrors legacy api-surface's `primitivePartTypeDoc`
// behaviour so e.g. `Unstable_TriggerPopover` still gets typed props.
if (!props) {
props = processComponentDeclaration(
sub.declaration,
propsTypeName,
options,
);
}
// Detect asChild from the resolved component props (no namespace alias
// available, so check the projected props instead).
if (props?.some((p) => p.name === "asChild")) {
supportsAsChild = true;
}
}
if (!props) return undefined;
const sourcePath = path
.relative(REPO_ROOT, sourceFile.getFilePath())
.replaceAll("\\", "/");
const model: PrimitivePartModel = {
primitiveName,
partName: sub.exportedName,
propsTypeName,
localName,
supportsAsChild,
isActionButton,
isRequireAtLeastOne,
props,
sourcePath,
};
if (element) model.element = element;
if (description) model.description = description;
if (deprecated) model.deprecated = deprecated;
if (examples) model.examples = examples;
return model;
}
const _partsCache = new Map<string, PrimitivePartModel[]>();
export function extractPrimitivePartsFor(
primitiveName: string,
options?: JsDocRenderOptions,
): PrimitivePartModel[] {
const cacheKey = options?.linkResolver
? `${primitiveName}:links`
: primitiveName;
const cached = _partsCache.get(cacheKey);
if (cached) return cached;
const primitives = discoverPrimitiveBarrelExports();
const moduleSpec = primitives.get(primitiveName);
if (!moduleSpec) {
_partsCache.set(cacheKey, []);
return [];
}
const primitiveModulePath = path.join(
REACT_PKG,
moduleSpec.replace(/^\.\//, ""),
);
const subs = discoverSubComponents(primitiveModulePath);
const result: PrimitivePartModel[] = [];
// Emit one model per exported part name, even when several exports share
// the same local declaration. The primitive-docs projection dedupes by
// localName so it still produces one entry per component, while api-ref
// gets a typeDocs entry per exported name (matching legacy api-surface).
for (const sub of subs) {
try {
const part = extractPrimitivePart(primitiveName, sub, options);
if (part) result.push(part);
} catch (e) {
console.warn(
` Warning: Failed to extract ${primitiveName}.${sub.exportedName}:`,
(e as Error).message,
);
}
}
_partsCache.set(cacheKey, result);
return result;
}
/** Extract every primitive part across every primitive. Used by
* generate-type-docs.mts. Pass the react api render options so {@link}
* references in primitive prop JSDoc resolve to anchors (and only genuinely
* broken links warn), matching the api-reference pass. */
export function extractPrimitiveParts(
options?: JsDocRenderOptions,
): PrimitivePartModel[] {
const result: PrimitivePartModel[] = [];
for (const primitiveName of discoverPrimitiveBarrelExports().keys()) {
result.push(...extractPrimitivePartsFor(primitiveName, options));
}
return result;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,305 @@
import * as fs from "node:fs";
import * as path from "node:path";
import {
INTEGRATION_PACKAGES,
INTEGRATION_TYPE_DOCS_OUTPUT,
REPO_ROOT,
TYPE_DOCS_INPUT,
TYPE_DOCS_OUTPUT,
} from "./paths.mts";
import { type ExportInfo, discoverIntegrationExports } from "./discover.mts";
import {
extractExportShape,
extractSupportingTypeShapes,
isPrimitiveOnlyType,
type ExtractedShape,
type InheritedFrom,
type PropModel,
} from "./extract.mts";
import {
extractPrimitivePartsFor,
type PrimitivePartModel,
} from "./primitive-extract.mts";
// ── TypeDoc presentation shape ─────────────────────────────────────────────
export type TypeDocParameter = {
name: string;
type?: string;
description?: string;
required?: boolean;
default?: string;
deprecated?: string;
children?: { type?: string; parameters: TypeDocParameter[] }[];
};
export type TypeDoc = {
type?: string;
parameters: TypeDocParameter[];
};
export type TypeDocBindings = Map<string, string>;
// ── Projection: PropModel → TypeDocParameter (API reference) ───────────────
// Match legacy api-surface filter exactly. Radix is intentionally NOT
// filtered here — PrimitiveButtonProps members like `asChild`/`render` come
// from @radix-ui/react-primitive and need to surface in api-ref typeDocs for
// ActionButton-style primitive parts. The primitive-docs projection still
// filters radix (and inserts a bare `asChild` row).
const API_REF_FILTER: ReadonlySet<InheritedFrom> = new Set([
"react",
"csstype",
"react-textarea-autosize",
"tw",
]);
function shouldDropForApiRef(prop: PropModel): boolean {
// Always show asChild even if its declaration site is third-party.
if (prop.name === "asChild") return false;
// Drop the legacy "tw" pseudo-prop by name (used to live on every component
// before tailwind-variants was internalized).
if (prop.name === "tw") return true;
return (
prop.inheritedFrom !== undefined && API_REF_FILTER.has(prop.inheritedFrom)
);
}
function isInlineObjectTypeText(typeText: string): boolean {
const text = typeText.trim();
return text.startsWith("{") || text.startsWith("| {");
}
function projectChildren(
children: PropModel["children"],
): TypeDocParameter["children"] | undefined {
if (!children || children.length === 0) return undefined;
const out = children
.map(({ typeName, props }) => {
const parameters = props
.filter((child) => !shouldDropForApiRef(child))
.map(projectPropToTypeDoc);
return parameters.length > 0 ? { type: typeName, parameters } : undefined;
})
.filter(
(entry): entry is { type: string; parameters: TypeDocParameter[] } =>
Boolean(entry),
);
return out.length > 0 ? out : undefined;
}
function projectPropToTypeDoc(prop: PropModel): TypeDocParameter {
const children = projectChildren(prop.children);
// Api-ref consumes `declaredType` (authored type-node where available,
// falls back to resolved type minus `| undefined`). When children are
// present and the type text is an inline object literal, the literal is
// redundant — replace with the child type's name.
const baseType = prop.declaredType ?? prop.rawType;
const typeText =
children && baseType && isInlineObjectTypeText(baseType)
? (children[0]?.type ?? baseType)
: baseType;
const out: TypeDocParameter = { name: prop.name };
if (typeText) out.type = typeText;
if (prop.required !== undefined) out.required = prop.required;
// Preserve empty descriptions (class members are emitted with `""`).
if (prop.description !== undefined) out.description = prop.description;
if (prop.default) out.default = prop.default;
if (prop.deprecated) out.deprecated = prop.deprecated;
if (children) out.children = children;
return out;
}
function shapeToTypeDoc(shape: ExtractedShape): TypeDoc | undefined {
const parameters = shape.parameters
.filter((prop) => !shouldDropForApiRef(prop))
.map(projectPropToTypeDoc);
// Match legacy: a shape whose every property is filtered as inherited
// produces no typeDocs entry. Class shapes are exempt — they intentionally
// emit empty member lists for completeness.
if (parameters.length === 0 && shape.kind !== "class") return undefined;
// For class shapes the legacy renderer used the bare type name (no " props"
// suffix) — keep that. For component shapes it appended " props".
const typeName =
shape.kind === "component" ? `${shape.name} props` : shape.name;
return { type: typeName, parameters };
}
function primitivePartToTypeDoc(part: PrimitivePartModel): TypeDoc | undefined {
let parameters = part.props
.filter((prop) => !shouldDropForApiRef(prop))
.map(projectPropToTypeDoc);
// Inject a bare asChild row when the type supports it and the projection
// didn't already carry one. Mirrors primitive-docs behaviour.
const hasAsChild = parameters.some((p) => p.name === "asChild");
if (!hasAsChild && (part.supportsAsChild || part.isActionButton)) {
parameters = [{ name: "asChild" }, ...parameters];
}
// Match legacy: parts whose Props are empty (e.g. `Record<string, never>`)
// produced no typeDocs entry. The MDX renderer falls back to the
// primitiveDocs entry for these.
if (parameters.length === 0) return undefined;
return { type: part.propsTypeName, parameters };
}
// ── Building the typeDocs maps ─────────────────────────────────────────────
export function buildTypeDocs(exports: ExportInfo[]): {
typeDocs: Map<string, TypeDoc>;
integrationTypeDocs: Map<string, TypeDoc>;
integrationsByPackage: Array<{
slug: string;
items: ExportInfo[];
typeDocBindings: TypeDocBindings;
typeDocNames: Set<string>;
}>;
} {
const typeDocs = new Map<string, TypeDoc>();
// 1. Static input file: types declared in content/types-to-generate/typeDocs.ts.
for (const [name, shape] of extractSupportingTypeShapes(TYPE_DOCS_INPUT)) {
const td = shapeToTypeDoc(shape);
if (td) typeDocs.set(name, td);
}
// 2. For each export, lazily extract primitive parts (for primitive
// sections) interleaved with per-export shape extraction. This load
// order matters: ts-morph's intersection property iteration depends on
// when source files are first added to the project, and legacy
// api-surface interleaved primitive-part loading inside the export
// loop. Mirroring that here keeps property order byte-for-byte stable.
for (const item of exports) {
if (item.section === "primitives") {
for (const part of extractPrimitivePartsFor(
item.name,
item.jsDocRenderOptions,
)) {
// Skip lowercase `unstable_*` parts in api-ref typeDocs to match
// legacy api-surface (which only matched capital `Unstable_`).
// The primitive-docs projection still emits these.
if (/^unstable_[A-Z]/.test(part.partName)) continue;
const name = part.propsTypeName;
if (typeDocs.has(name)) continue;
const td = primitivePartToTypeDoc(part);
if (td) typeDocs.set(name, td);
}
}
}
// 3. Each export → its own shape (and supporting types from the same file
// when the primary shape can't be projected).
for (const item of exports) {
if (!item.sourcePath) continue;
if (typeDocs.has(item.name)) continue;
const shape = extractExportShape(item);
const td = shape ? shapeToTypeDoc(shape) : undefined;
if (td) {
typeDocs.set(item.name, td);
} else {
// Either the shape couldn't be projected, or every property was
// filtered as inherited. In both cases, fall back to absorbing other
// supporting type aliases declared in the same file.
const filePath = path.join(REPO_ROOT, item.sourcePath);
if (!fs.existsSync(filePath)) continue;
for (const [name, supportingShape] of extractSupportingTypeShapes(
filePath,
item.jsDocRenderOptions,
)) {
if (typeDocs.has(name)) continue;
const supportingTd = shapeToTypeDoc(supportingShape);
if (supportingTd) typeDocs.set(name, supportingTd);
}
}
}
// 4. Integration packages: discover, project shapes (skipping primitive
// type aliases), namespace bindings.
const integrationTypeDocs = new Map<string, TypeDoc>();
const integrationsByPackage: Array<{
slug: string;
items: ExportInfo[];
typeDocBindings: TypeDocBindings;
typeDocNames: Set<string>;
}> = [];
for (const integration of INTEGRATION_PACKAGES) {
const items = discoverIntegrationExports(
integration.entry,
integration.slug,
);
const localTypeDocs = new Map<string, TypeDoc>();
for (const item of items) {
if (!item.sourcePath) continue;
if (localTypeDocs.has(item.name)) continue;
const filePath = path.join(REPO_ROOT, item.sourcePath);
if (!fs.existsSync(filePath)) continue;
if (isPrimitiveOnlyType(filePath, item.name)) continue;
const shape = extractExportShape(item);
const td = shape ? shapeToTypeDoc(shape) : undefined;
if (td) localTypeDocs.set(item.name, td);
}
const typeDocBindings: TypeDocBindings = new Map(
[...localTypeDocs.keys()].map((name) => [
name,
typeDocBindingForIntegration(integration.slug, name),
]),
);
for (const [name, typeDoc] of localTypeDocs) {
integrationTypeDocs.set(
typeDocBindingForIntegration(integration.slug, name),
typeDoc,
);
}
integrationsByPackage.push({
slug: integration.slug,
items,
typeDocBindings,
typeDocNames: new Set(localTypeDocs.keys()),
});
}
return { typeDocs, integrationTypeDocs, integrationsByPackage };
}
export function typeDocBindingForIntegration(
slug: string,
name: string,
): string {
return `${slug.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase())}_${name}`;
}
function writeTypeDocsFile(
outputPath: string,
typeDocs: Map<string, TypeDoc>,
bindingForName: (name: string) => string,
): void {
const output = [
"// AUTO-GENERATED by scripts/generate-type-docs.mts",
"// Do not edit manually.",
"",
...[...typeDocs.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(
([name, type]) =>
`export const ${bindingForName(name)} = ${JSON.stringify(type, null, 2)};\n`,
),
].join("\n");
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, output);
}
export function writeTypeDocs(typeDocs: Map<string, TypeDoc>): void {
writeTypeDocsFile(TYPE_DOCS_OUTPUT, typeDocs, (name) => name);
}
export function writeIntegrationTypeDocs(
integrationTypeDocs: Map<string, TypeDoc>,
): void {
writeTypeDocsFile(
INTEGRATION_TYPE_DOCS_OUTPUT,
integrationTypeDocs,
(name) => name,
);
}