import * as fs from "node:fs"; import * as path from "node:path"; import { API_REFERENCE_DIR, INTEGRATION_PACKAGES, REPO_ROOT, } from "./paths.mts"; import { REACT_API_SECTIONS, SECTION_ORDER, type ApiSection, type ExportInfo, } from "./discover.mts"; import { extractPrimitivePartsFor, primitivePartTypeDocName, readPrimitiveParts, type PrimitivePartModel, } from "./primitive-extract.mts"; import type { TypeDoc, TypeDocBindings } from "./type-docs.mts"; // ── MDX rendering ────────────────────────────────────────────────────────── const GENERATED_PAGE_MARKER = "{/* AUTO-GENERATED PAGE by scripts/generate-api-reference.mts */}"; const SKIP_AUTO_GENERATION_MARKER = "{/* api-reference:skip-auto-generation */}"; const API_REFERENCE_START = "{/* api-reference:start */}"; const API_REFERENCE_END = "{/* api-reference:end */}"; type AuthoredSlot = { kind: "api-manual" | "api-example"; name?: string; content: string; }; type PageSlots = { manual?: string | undefined; namedManual: Map; examples: Map; explicit: AuthoredSlot[]; }; type AuthoredFrontmatter = { title?: string; description?: string; }; type AuthoredPageParts = { frontmatter: AuthoredFrontmatter; slots: PageSlots; skipAutoGeneration: boolean; }; type PageSummary = { slug: string; title: string; description: string }; function frontmatter(title: string, description: string): string { return [ "---", `title: ${yamlScalar(title)}`, `description: ${yamlScalar(description)}`, "---", "", ].join("\n"); } function yamlScalar(value: string): string { return /^[A-Za-z0-9<][A-Za-z0-9 .,@'()&+<>/-]*$/.test(value) ? value : JSON.stringify(value); } const META_PRINT_WIDTH = 80; /** Serialize a meta.json object the way oxfmt would: scalars stay on one line * and an array collapses onto a single line when it fits within the print * width, breaking to one element per line only when it doesn't. Matching the * formatter here keeps generated meta.json byte-identical across runs — * `JSON.stringify(value, null, 2)` always explodes arrays multi-line, so the * files would otherwise re-collapse on the next `oxfmt` pass and churn. */ function formatMetaJson( meta: Record, ): string { const indent = " "; const entries = Object.entries(meta); const lines = entries.map(([key, value], index) => { const tail = index < entries.length - 1 ? "," : ""; const prefix = `${indent}${JSON.stringify(key)}: `; if (!Array.isArray(value)) { return `${prefix}${JSON.stringify(value)}${tail}`; } const inline = `[${value.map((item) => JSON.stringify(item)).join(", ")}]`; if (prefix.length + inline.length + tail.length <= META_PRINT_WIDTH) { return `${prefix}${inline}${tail}`; } const items = value .map((item) => `${indent}${indent}${JSON.stringify(item)}`) .join(",\n"); return `${prefix}[\n${items}\n${indent}]${tail}`; }); return `{\n${lines.join("\n")}\n}\n`; } function titleForPage(page: string, exports: ExportInfo[]): string { const primary = exports.filter((item) => item.pageRole === "primary"); if (primary.length === 1) return primary[0]!.name; return titleCaseSlug(page); } function titleCaseSlug(slug: string): string { return slug .split("-") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } const SECTION_TITLE_OVERRIDES: Partial> = { "generative-ui": "Generative UI", }; function sectionTitle(section: ApiSection): string { return SECTION_TITLE_OVERRIDES[section] ?? titleCaseSlug(section); } function mdxEscape(value: string): string { return value .split(/(```[\s\S]*?```|`[^`]*`)/g) .map((part) => part.startsWith("`") ? part : part .replaceAll("{", "\\{") .replaceAll("}", "\\}") .replaceAll("<", "<") .replaceAll(">", ">"), ) .join(""); } function renderJsDocExample(value: string): string { const trimmed = value.trim(); const example = trimmed.includes("```") ? trimmed : ["```tsx", trimmed, "```"].join("\n"); return mdxEscape(example); } type ApiStatus = "stable" | "experimental" | "deprecated"; function isExperimentalDeprecation(deprecated?: string): boolean { return /^(unstable \/ experimental|experimental\b|this (api|feature) is experimental\b|this api is (still )?under active development\b|under active development\b)/i.test( deprecated ?? "", ); } function apiStatusForDeprecatedTag(deprecated?: string): ApiStatus { if (!deprecated) return "stable"; return isExperimentalDeprecation(deprecated) ? "experimental" : "deprecated"; } function apiStatusCallout(deprecated?: string): string[] { if (!deprecated) return []; if (apiStatusForDeprecatedTag(deprecated) === "experimental") { return [ ``, `Experimental. ${mdxEscape(deprecated)}`, ``, "", ]; } return [ ``, `Deprecated. ${mdxEscape(deprecated)}`, ``, "", ]; } function mdxCommentMarker(name: string, boundary: "start" | "end"): string { return `{/* ${name}:${boundary} */}`; } function extractManualSlot(source: string): string | undefined { const regex = /\{\/\*\s*api-manual:start\s*\*\/\}([\s\S]*?)\{\/\*\s*api-manual:end\s*\*\/\}/; const match = source.match(regex); return match ? [ mdxCommentMarker("api-manual", "start"), (match[1] ?? "").trim(), mdxCommentMarker("api-manual", "end"), ].join("\n") : undefined; } function extractNamedManualSlots(source: string): Map { const slots = new Map(); const regex = /\{\/\*\s*api-manual:([^:\s]+):start\s*\*\/\}([\s\S]*?)\{\/\*\s*api-manual:\1:end\s*\*\/\}/g; for (const match of source.matchAll(regex)) { const name = match[1]!; slots.set( name, [ mdxCommentMarker(`api-manual:${name}`, "start"), (match[2] ?? "").trim(), mdxCommentMarker(`api-manual:${name}`, "end"), ].join("\n"), ); } return slots; } function apiReferencePagePath(section: ApiSection, slug: string): string { return path.join(API_REFERENCE_DIR, section, `${slug}.mdx`); } function exampleSlot(name: string, content: string): string { return [ mdxCommentMarker(`api-example:${name}`, "start"), content.trim(), mdxCommentMarker(`api-example:${name}`, "end"), ].join("\n"); } function extractExampleSlots(source: string): Map { const examples = new Map(); const regex = /\{\/\*\s*api-example:([^:\s]+):start\s*\*\/\}([\s\S]*?)\{\/\*\s*api-example:\1:end\s*\*\/\}/g; for (const match of source.matchAll(regex)) { const name = match[1]!; examples.set(name, exampleSlot(name, match[2] ?? "")); } return examples; } function assertPairedMdxMarkers(filePath: string, source: string): void { const startMarkerRegex = /\{\/\*\s*([^*]+?):start\s*\*\/\}/g; const unclosed = [...source.matchAll(startMarkerRegex)] .map((match) => match[1]!.trim()) .filter((name) => !source.includes(mdxCommentMarker(name, "end"))); if (unclosed.length === 0) return; throw new Error( `Unclosed marker found in ${path.relative( REPO_ROOT, filePath, )}, generation stopped: ${unclosed.join(", ")}`, ); } function unquoteFrontmatterScalar(value: string): string { const trimmed = value.trim(); if (!trimmed) return ""; if ( (trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'")) ) { try { return trimmed.startsWith('"') ? JSON.parse(trimmed) : trimmed.slice(1, -1).replaceAll("''", "'"); } catch { throw new Error(`Invalid quoted frontmatter scalar: ${value}`); } } if (trimmed.startsWith("[") || trimmed.startsWith("{")) { throw new Error(`Unsupported frontmatter scalar: ${value}`); } return trimmed; } function readFrontmatter( filePath: string, source: string, ): AuthoredFrontmatter { if (!source.startsWith("---\n")) return {}; const end = source.indexOf("\n---", 4); if (end === -1) { throw new Error( `Malformed frontmatter in ${path.relative(REPO_ROOT, filePath)}`, ); } const fm: AuthoredFrontmatter = {}; for (const line of source.slice(4, end).split("\n")) { if (!line.trim()) continue; const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/); if (!match) { throw new Error( `Unsupported frontmatter line in ${path.relative(REPO_ROOT, filePath)}: ${line}`, ); } const [, key, value] = match; if (key === "title" || key === "description") { fm[key] = unquoteFrontmatterScalar(value ?? ""); } } return fm; } function emptyPageSlots(): PageSlots { return { namedManual: new Map(), examples: new Map(), explicit: [], }; } function readAuthoredPageParts( section: ApiSection, slug: string, items: ExportInfo[], ): AuthoredPageParts | undefined { const filePath = apiReferencePagePath(section, slug); let source: string; try { source = fs.readFileSync(filePath, "utf8"); } catch (error) { if ((error as { code?: string }).code === "ENOENT") return undefined; throw error; } assertPairedMdxMarkers(filePath, source); const fm = readFrontmatter(filePath, source); const manual = extractManualSlot(source); const namedManual = extractNamedManualSlots(source); const explicitExamples = extractExampleSlots(source); const examples = new Map(explicitExamples); const itemNames = new Set( items.flatMap((item) => [ item.name, ...(item.section === "primitives" ? readPrimitiveParts(item.name).map((part) => `${item.name}.${part}`) : []), ]), ); for (const name of examples.keys()) { if (!itemNames.has(name)) { console.warn( `Warning: api-example:${name} in ${path.relative(REPO_ROOT, filePath)} does not match an export on this page`, ); } } const explicit: AuthoredSlot[] = []; if (manual) explicit.push({ kind: "api-manual", content: manual }); for (const [name, content] of namedManual) { explicit.push({ kind: "api-manual", name, content }); } for (const [name, content] of explicitExamples) { explicit.push({ kind: "api-example", name, content }); } return { frontmatter: fm, slots: { manual, namedManual, examples, explicit }, skipAutoGeneration: source.includes(SKIP_AUTO_GENERATION_MARKER), }; } function assertPreservedSlots( filePath: string, slots: PageSlots, nextSource: string, ): void { const missing = slots.explicit.filter( (slot) => !nextSource.includes(slot.content), ); if (missing.length === 0) return; throw new Error( `Refusing to write ${path.relative( REPO_ROOT, filePath, )} because generated output dropped authored slots: ${missing .map((slot) => (slot.name ? `${slot.kind}:${slot.name}` : slot.kind)) .join(", ")}`, ); } const EXPORT_ORDER_BY_PAGE: Record = { "tools/interactables": [ "unstable_Interactables", "unstable_useInteractable", "unstable_useInteractableState", "unstable_useInteractableVersions", "unstable_interactableTool", "unstable_getInteractableSnapshots", "unstable_formatInteractableSnapshot", "unstable_getInteractableVersions", ], "tools/interactables-legacy": [ "Interactables", "useAssistantInteractable", "useInteractableState", ], }; const API_STATUS_ORDER = { stable: 0, experimental: 1, deprecated: 2, } satisfies Record; const PAGE_ROLE_ORDER = { primary: 0, related: 1, "supporting-type": 2, } satisfies Record; function exportSortKey( item: ExportInfo, ): [number, number, number, number, string] { const pageOrder = EXPORT_ORDER_BY_PAGE[`${item.section}/${item.page}`]; const pageIndex = pageOrder?.indexOf(item.name) ?? -1; const orderIndex = pageIndex === -1 ? (pageOrder?.length ?? 0) : pageIndex; return [ item.pageRole === "supporting-type" ? 1 : 0, API_STATUS_ORDER[apiStatusForDeprecatedTag(item.deprecated)], PAGE_ROLE_ORDER[item.pageRole], orderIndex, item.name, ]; } function sortExportsForPage(items: ExportInfo[]): ExportInfo[] { return [...items].sort((a, b) => { const [aSupportingGroup, aStatusGroup, aRoleGroup, aOrderIndex, aName] = exportSortKey(a); const [bSupportingGroup, bStatusGroup, bRoleGroup, bOrderIndex, bName] = exportSortKey(b); if (aSupportingGroup !== bSupportingGroup) { return aSupportingGroup - bSupportingGroup; } if (aStatusGroup !== bStatusGroup) return aStatusGroup - bStatusGroup; if (aRoleGroup !== bRoleGroup) return aRoleGroup - bRoleGroup; if (aOrderIndex !== bOrderIndex) return aOrderIndex - bOrderIndex; return aName.localeCompare(bName); }); } function exportSection( item: ExportInfo, typeDocNames: Set, slots: PageSlots, headingLevel = 2, typeDocBindings: TypeDocBindings = new Map(), ): string[] { if (!hasGeneratedEntryContent(item, typeDocNames, slots)) return []; const heading = "#".repeat(headingLevel); const lines = [`${heading} ${item.name}`, ""]; lines.push(...apiStatusCallout(item.deprecated)); if (item.jsDoc) { lines.push(mdxEscape(item.jsDoc), ""); } const example = slots.examples.get(item.name); if (example) { lines.push(example, ""); } else if (item.jsDocExamples) { for (const jsDocExample of item.jsDocExamples) { lines.push(renderJsDocExample(jsDocExample), ""); } } if (typeDocNames.has(item.name)) { lines.push( ``, "", ); } else if (item.signature) { lines.push(["```ts", item.signature, "```"].join("\n"), ""); } const manual = slots.namedManual.get(item.name); if (manual) lines.push(manual, ""); return lines; } function hasGeneratedEntryContent( item: ExportInfo, typeDocNames: Set, slots: PageSlots, ): boolean { return Boolean( item.jsDoc || item.jsDocExamples?.length || item.deprecated || typeDocNames.has(item.name) || item.signature || slots.namedManual.has(item.name) || slots.examples.has(item.name), ); } function isRenderedApiEntry( item: ExportInfo, typeDocNames: Set, slots: PageSlots, ): boolean { return ( item.pageRole !== "supporting-type" && hasGeneratedEntryContent(item, typeDocNames, slots) ); } function generatePrimitivePage( item: ExportInfo, companions: ExportInfo[], typeDocNames: Set, slots: PageSlots, title: string, description: string, ): string { const guideFile = path.join( REPO_ROOT, "apps/docs/content/docs/primitives", `${item.page}.mdx`, ); const guideLine = fs.existsSync(guideFile) ? `For examples and usage patterns, see [${titleForPage(item.page, [item]).replace(/Primitive$/, "")}](/docs/primitives/${item.page}).` : undefined; return generateApiPage({ title, description, imports: generatedImportsForPage([item, ...companions], typeDocNames), guideLine, slots, reference: generatePrimitiveReferenceRegion( item, companions, typeDocNames, slots, ), }); } function generatePrimitiveReferenceRegion( item: ExportInfo, companions: ExportInfo[], typeDocNames: Set, slots: PageSlots, ): string { const parts = readPrimitiveParts(item.name); const lines = ["## API Reference", ""]; if (parts.length > 0) { for (const part of parts) { lines.push(`### ${part}`, ""); const partName = `${item.name}.${part}`; const primitivePart = primitivePartFor( item.name, part, item.jsDocRenderOptions, ); const example = slots.examples.get(partName); const examples = example ? [example] : (primitivePart?.examples ?? []).map(renderJsDocExample); lines.push( primitiveParametersTable( item.name, part, typeDocNames, primitivePart, examples, ), "", ); const manual = slots.namedManual.get(partName); if (manual) lines.push(manual, ""); } } else { lines.push(...exportSection(item, typeDocNames, slots, 3)); } if (typeDocNames.has(item.name)) { lines.push( `### ${item.name}`, "", ``, "", ); const manual = slots.namedManual.get(item.name); if (manual) lines.push(manual, ""); } // Helpers bound to a specific primitive part (e.g. groupPartByType for // ) render after the primitive's own parts so // they live beside the part they configure instead of in a fallback drawer. for (const companion of sortExportsForPage(companions).filter((entry) => isRenderedApiEntry(entry, typeDocNames, slots), )) { lines.push(...exportSection(companion, typeDocNames, slots, 3)); } return lines.join("\n").trimEnd(); } function primitivePartFor( primitiveName: string, part: string, options: ExportInfo["jsDocRenderOptions"], ) { return extractPrimitivePartsFor(primitiveName, options).find( (primitivePart) => primitivePart.partName === part, ); } function primitiveParametersTable( primitiveName: string, part: string, typeDocNames: Set, primitivePart: PrimitivePartModel | undefined, examples: string[], ): string { const binding = `${primitiveName}Docs.${part}`; const typeDocName = primitivePartTypeDocName(primitiveName, part); const statusCallout = apiStatusForDeprecatedTag(primitivePart?.deprecated) === "experimental" ? [ `{${binding}?.deprecated && (`, ` `, ` Experimental. {${binding}.deprecated}`, ` `, `)}`, ] : [ `{${binding}?.deprecated && (`, ` `, ` Deprecated. {${binding}.deprecated}`, ` `, `)}`, ]; const table = typeDocNames.has(typeDocName) ? `` : [ `{${binding}?.props && (`, ` `, `)}`, ].join("\n"); return [ ...statusCallout, "", `{${binding}?.description}`, "", `{${binding}?.element && (`, `

This primitive renders a {\`<\${${binding}?.element}>\`} element unless asChild is set.

`, `)}`, "", ...examples.flatMap((example) => [example, ""]), table, ].join("\n"); } function generateReferenceRegion( items: ExportInfo[], typeDocNames: Set, slots: PageSlots, typeDocBindings: TypeDocBindings = new Map(), ): string { return [ "## API Reference", "", ...sortExportsForPage(items) .filter((item) => isRenderedApiEntry(item, typeDocNames, slots)) .flatMap((item) => exportSection(item, typeDocNames, slots, 3, typeDocBindings), ), ] .join("\n") .trimEnd(); } function generatedReferenceRegion(content: string): string { return [ API_REFERENCE_START, "{/* AUTO-GENERATED by scripts/generate-api-reference.mts */}", "{/* Do not edit this block manually. */}", "", content.trim(), API_REFERENCE_END, ].join("\n"); } function generateApiPage({ title, description, imports, guideLine, slots, reference, }: { title: string; description: string; imports: string; guideLine?: string | undefined; slots: PageSlots; reference: string; }): string { const lines = [ frontmatter(title, description), GENERATED_PAGE_MARKER, "{/* Do not edit manually. */}", "", ]; if (imports) lines.splice(1, 0, imports, ""); if (guideLine) lines.push(guideLine, ""); if (slots.manual) lines.push(slots.manual, ""); lines.push(generatedReferenceRegion(reference)); return `${lines.join("\n")}\n`; } function generatedImportsForPage( items: ExportInfo[], typeDocNames: Set, ): string { const visibleItems = items.filter( (item) => item.pageRole !== "supporting-type", ); const primitives = visibleItems .filter((item) => item.section === "primitives") .map((item) => ({ name: item.name, parts: readPrimitiveParts(item.name) })); const typeImports = visibleItems .filter((item) => typeDocNames.has(item.name)) .map((item) => item.name); const primitivePartTypeImports = primitives.flatMap((item) => item.parts .map((part) => primitivePartTypeDocName(item.name, part)) .filter((name) => typeDocNames.has(name)), ); const primitiveImports = primitives .filter((item) => item.parts.length > 0) .map((item) => item.name); return generatedImports({ typeDocNames: [...typeImports, ...primitivePartTypeImports], primitiveDocNames: primitiveImports, }); } function generatedIntegrationImports(typeDocBindings: TypeDocBindings): string { const bindings = [...typeDocBindings.values()].sort(); if (bindings.length === 0) return ""; return `import { ${bindings.join(", ")} } from "@/generated/integrationTypeDocs";`; } function generatedImports({ typeDocNames, primitiveDocNames, }: { typeDocNames: string[]; primitiveDocNames: string[]; }): string { const lines: string[] = []; const uniqueTypeDocs = [...new Set(typeDocNames)].sort(); if (uniqueTypeDocs.length > 0) { lines.push( `import { ${uniqueTypeDocs.join(", ")} } from "@/generated/typeDocs";`, ); } for (const primitiveName of [...new Set(primitiveDocNames)].sort()) { lines.push( `import { ${primitiveName} as ${primitiveName}Docs } from "@/generated/primitiveDocs";`, ); } return lines.join("\n"); } const PAGE_ORDER_BY_SECTION: Partial> = { tools: [ "toolkits", "component-tools", "rendering", "status", "interactables", "interactables-legacy", ], "generative-ui": [ "json-generative-ui", "components", "actions", "spec", "rendering", "tokens", ], }; function comparePageSlugs(section: ApiSection, a: string, b: string): number { const order = PAGE_ORDER_BY_SECTION[section]; const aIndex = order?.indexOf(a) ?? -1; const bIndex = order?.indexOf(b) ?? -1; if (aIndex !== bIndex) { if (aIndex === -1) return 1; if (bIndex === -1) return -1; return aIndex - bIndex; } return a.localeCompare(b); } function generateSectionOverviewPage( section: ApiSection, pages: PageSummary[], slots: PageSlots, title: string, description: string, ): string { const cards = pages.flatMap((page) => [ ` `, ` {${JSON.stringify(page.description)}}`, " ", ]); return [ frontmatter(title, description), GENERATED_PAGE_MARKER, "{/* The page list is generated from exported APIs; edit only the manual prose slot. */}", "", ...(slots.manual ? [slots.manual, ""] : []), "## Pages", "", "", ...cards, "", "", ].join("\n"); } function groupedBySectionAndPage( exports: ExportInfo[], ): Map> { const result = new Map>(); for (const section of SECTION_ORDER) result.set(section, new Map()); for (const item of exports) { const section = result.get(item.section)!; const items = section.get(item.page) ?? []; items.push(item); section.set(item.page, items); } for (const pages of result.values()) { for (const [page, items] of pages) { pages.set(page, sortExportsForPage(items)); } } return result; } function writeSectionIndex( section: ApiSection, pages: PageSummary[], description: string, ): void { fs.writeFileSync( path.join(API_REFERENCE_DIR, section, "meta.json"), formatMetaJson({ title: sectionTitle(section), description, pages: ["index", ...pages.map((page) => page.slug)], }), ); } function writeSectionOverviewIndex( section: ApiSection, sectionDir: string, pageSummaries: PageSummary[], fallbackTitle: string, ): void { const indexPath = path.join(sectionDir, "index.mdx"); const indexAuthored = readAuthoredPageParts(section, "index", []); const indexSlots = indexAuthored?.slots ?? emptyPageSlots(); const indexTitle = authoredTitleOrSeed( indexPath, indexAuthored?.frontmatter, fallbackTitle, ); const indexDescription = authoredDescription(indexAuthored?.frontmatter); const indexBody = generateSectionOverviewPage( section, pageSummaries, indexSlots, indexTitle, indexDescription, ); assertPreservedSlots(indexPath, indexSlots, indexBody); fs.writeFileSync(indexPath, indexBody); validatePageDescription(indexPath, indexAuthored?.frontmatter, []); writeSectionIndex(section, pageSummaries, indexDescription); } function writeApiReferenceRoot(): void { fs.mkdirSync(API_REFERENCE_DIR, { recursive: true }); fs.writeFileSync( path.join(API_REFERENCE_DIR, "meta.json"), formatMetaJson({ title: "API Reference", pages: ["overview", ...SECTION_ORDER], }), ); } function authoredTitleOrSeed( filePath: string, fm: AuthoredFrontmatter | undefined, seed: string, ): string { if (fm?.title) return fm.title; console.warn( `Missing title in ${path.relative(REPO_ROOT, filePath)}. Generated mechanical title: ${seed}`, ); return seed; } function authoredDescription(fm: AuthoredFrontmatter | undefined): string { return fm?.description?.trim() ?? ""; } function validatePageDescription( filePath: string, fm: AuthoredFrontmatter | undefined, exports: ExportInfo[], ): void { const desc = authoredDescription(fm); const primary = exports.find((item) => item.pageRole === "primary"); if (!desc) { console.warn( `Missing description in ${path.relative( REPO_ROOT, filePath, )} (new API export: ${primary?.name ?? "?"}).\n` + "You must manually write the description frontmatter for new generated pages, matching the style of the other api-reference pages.", ); return; } const minLen = 50; const maxLen = 250; if (desc.length < minLen || desc.length > maxLen) { console.warn( `Description in ${path.relative( REPO_ROOT, filePath, )} is ${desc.length} chars (expected ${minLen}-${maxLen}).`, ); } } function pruneGeneratedPage(filePath: string): void { let source: string; try { source = fs.readFileSync(filePath, "utf8"); } catch (error) { if ((error as { code?: string }).code === "ENOENT") return; throw error; } if (source.includes(SKIP_AUTO_GENERATION_MARKER)) return; if (source.includes(GENERATED_PAGE_MARKER)) fs.unlinkSync(filePath); } function pruneStaleGeneratedPages( sectionDir: string, expectedSlugs: Set, ): void { if (!fs.existsSync(sectionDir)) return; for (const entry of fs.readdirSync(sectionDir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".mdx")) continue; const slug = entry.name.replace(/\.mdx$/, ""); if (expectedSlugs.has(slug)) continue; const filePath = path.join(sectionDir, entry.name); pruneGeneratedPage(filePath); } } export function writeApiReferencePages( exports: ExportInfo[], typeDocs: Map, integrationsByPackage: Array<{ slug: string; items: ExportInfo[]; typeDocBindings: TypeDocBindings; typeDocNames: Set; }>, ): void { writeApiReferenceRoot(); const typeDocNames = new Set(typeDocs.keys()); const grouped = groupedBySectionAndPage(exports); for (const section of REACT_API_SECTIONS) { const sectionDir = path.join(API_REFERENCE_DIR, section); fs.mkdirSync(sectionDir, { recursive: true }); const pages = [ ...(grouped.get(section) ?? new Map()).entries(), ] .filter(([, items]) => items.some((item) => item.pageRole === "primary")) .sort(([a], [b]) => comparePageSlugs(section, a, b)); const pageSummaries: PageSummary[] = []; for (const [slug, items] of pages) { const filePath = path.join(sectionDir, `${slug}.mdx`); const authored = readAuthoredPageParts(section, slug, items); const title = authoredTitleOrSeed( filePath, authored?.frontmatter, titleForPage(slug, items), ); const description = authoredDescription(authored?.frontmatter); pageSummaries.push({ slug, title, description }); if (authored?.skipAutoGeneration) { console.log( `Skipping auto-generation for API page: ${path.relative(REPO_ROOT, filePath)}`, ); continue; } const slots = authored?.slots ?? emptyPageSlots(); // A primitive page is anchored by exactly one primary primitive; any // other exports on the page (related helpers, supporting types) render // as companions after the primitive's parts. This keeps the parts-table // layout while letting a part-bound helper share the page. const primitivePrimaries = section === "primitives" ? items.filter((item) => item.pageRole === "primary") : []; const primitivePrimary = primitivePrimaries.length === 1 ? primitivePrimaries[0] : undefined; const body = primitivePrimary ? generatePrimitivePage( primitivePrimary, items.filter((item) => item !== primitivePrimary), typeDocNames, slots, title, description, ) : generateApiPage({ title, description, imports: generatedImportsForPage(items, typeDocNames), slots, reference: generateReferenceRegion(items, typeDocNames, slots), }); assertPreservedSlots(filePath, slots, body); fs.writeFileSync(filePath, body); validatePageDescription(filePath, authored?.frontmatter, items); } writeSectionOverviewIndex( section, sectionDir, pageSummaries, `${sectionTitle(section)} API Reference`, ); pruneStaleGeneratedPages( sectionDir, new Set(["index", ...pageSummaries.map((page) => page.slug)]), ); } writeIntegrationPages(integrationsByPackage); } function writeIntegrationPages( integrationsByPackage: Array<{ slug: string; items: ExportInfo[]; typeDocBindings: TypeDocBindings; typeDocNames: Set; }>, ): void { const section: ApiSection = "integrations"; const sectionDir = path.join(API_REFERENCE_DIR, section); fs.mkdirSync(sectionDir, { recursive: true }); const integrationBySlug = new Map< string, (typeof INTEGRATION_PACKAGES)[number] >(INTEGRATION_PACKAGES.map((p) => [p.slug, p])); const pageSummaries: PageSummary[] = []; for (const { slug, items, typeDocBindings, typeDocNames, } of integrationsByPackage) { const integration = integrationBySlug.get(slug); if (!integration) continue; const filePath = path.join(sectionDir, `${integration.slug}.mdx`); const authored = readAuthoredPageParts(section, integration.slug, items); const slots = authored?.slots ?? emptyPageSlots(); const title = authoredTitleOrSeed( filePath, authored?.frontmatter, integration.packageName, ); const description = authoredDescription(authored?.frontmatter); pageSummaries.push({ slug: integration.slug, title, description }); const reference = generateReferenceRegion( items, typeDocNames, slots, typeDocBindings, ); const body = generateApiPage({ title, description, imports: generatedIntegrationImports(typeDocBindings), slots, reference, }); assertPreservedSlots(filePath, slots, body); fs.writeFileSync(filePath, body); validatePageDescription(filePath, authored?.frontmatter, items); } writeSectionOverviewIndex( section, sectionDir, pageSummaries, "Integrations API Reference", ); pruneStaleGeneratedPages( sectionDir, new Set([ "index", ...INTEGRATION_PACKAGES.map((integration) => integration.slug), ]), ); } export function printClassificationDiagnostics( exports: ExportInfo[], typeDocs: Map, ): void { const typeDocNames = new Set(typeDocs.keys()); // No authored slots here — "rendered" means the export carries generated // content (jsDoc, examples, signature, or a typeDoc table) on its own. const noSlots = emptyPageSlots(); const ruleCounts = new Map(); const renderedFallbacks: ExportInfo[] = []; let hiddenFallbackCount = 0; let fallbackSupportingTypeCount = 0; for (const item of exports) { ruleCounts.set( item.classificationRule, (ruleCounts.get(item.classificationRule) ?? 0) + 1, ); if (item.classificationConfidence !== "fallback") continue; if (item.pageRole === "supporting-type") { fallbackSupportingTypeCount += 1; } else if (hasGeneratedEntryContent(item, typeDocNames, noSlots)) { renderedFallbacks.push(item); } else { hiddenFallbackCount += 1; } } console.log("API reference classification rules:"); for (const [rule, count] of [...ruleCounts.entries()].sort(([a], [b]) => a.localeCompare(b), )) { console.log(` ${rule}: ${count}`); } // Only rendered fallbacks are actionable (they show up in the junk drawer and // want a real home); list them with source paths. Hidden fallbacks (no // rendered content) and supporting types are collapsed to a count. if (renderedFallbacks.length > 0) { console.log("Fallback-classified exports needing a home:"); for (const item of renderedFallbacks.sort((a, b) => a.name.localeCompare(b.name), )) { console.log(` ${item.name} (${item.sourcePath ?? "unknown source"})`); } } if (hiddenFallbackCount > 0) { console.log( `Fallback-classified exports with no rendered content: ${hiddenFallbackCount}`, ); } if (fallbackSupportingTypeCount > 0) { console.log( `Fallback-classified supporting types: ${fallbackSupportingTypeCount}`, ); } if (process.env.API_REFERENCE_CLASSIFICATION_VERBOSE === "1") { console.log("API reference export classifications:"); for (const item of [...exports].sort((a, b) => a.name.localeCompare(b.name), )) { console.log( ` ${item.name}: ${item.section}/${item.page} (${item.pageRole}, ${item.classificationRule})`, ); } } }