t3-code-android-nightly/.repos/alchemy-effect/scripts/generate-api-reference.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

1161 lines
36 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as ts from "typescript-api/unstable/ast";
import type { Node, SourceFile } from "typescript-api/unstable/ast";
import {
docCommentLines,
jsdocCopyId,
LINK_TAG_RE,
markDocLines,
normalizeLinkTarget,
parseLinkTag,
} from "./jsdoc-blocks.ts";
import { createSyntaxProject } from "./typescript-source.ts";
const websiteRoot = path.join(import.meta.dir, "../website");
interface SourceRoot {
/** Directory scanned for documented source files. */
srcRoot: string;
/**
* Synthetic provider name for flat single-provider packages (their files
* sit directly at `srcRoot`); empty for the alchemy package, whose
* top-level directories ARE the providers.
*/
providerPrefix: string;
/** Display prefix for the page's `Source:` line. */
sourceDisplayPrefix: string;
}
const repoRoot = path.join(import.meta.dir, "..");
const config = {
outRoot: path.join(websiteRoot, "src/content/docs/providers"),
/**
* Emit `<!--copy:jsdoc:...-->` markers after each prose block so the
* website's dev-time copy editor can map rendered text back to the JSDoc
* it came from. Dev only — production builds never set this.
*/
copyMarkers:
process.env.API_REFERENCE_COPY_MARKERS === "1" ||
process.argv.includes("--copy-markers"),
roots: [
{
srcRoot: path.join(import.meta.dir, "../packages/alchemy/src"),
providerPrefix: "",
sourceDisplayPrefix: "src",
},
{
srcRoot: path.join(import.meta.dir, "../packages/better-auth/src"),
providerPrefix: "BetterAuth",
sourceDisplayPrefix: "packages/better-auth/src",
},
] satisfies SourceRoot[],
};
interface FileEntry {
/** Output-relative path (provider-prefixed for flat packages). */
relativePath: string;
absolutePath: string;
/** Display path for the page's `Source:` blockquote. */
sourceDisplay: string;
}
interface ExampleBlock {
title: string;
body: string;
}
interface ExampleSection {
title: string;
description: string;
examples: ExampleBlock[];
}
interface PageDoc {
title: string;
/** Display path for the `Source:` blockquote. */
sourceDisplay: string;
summary: string;
sections: ExampleSection[];
/** True for `@layer` pages — renders the Layer metadata line. */
isLayer: boolean;
/** Service tags the Layer provides (`@provides`). */
provides: string[];
/** Optional peer dependencies (`@peer`). */
peers: string[];
}
const normalizeSlashes = (value: string) => value.split(path.sep).join("/");
const isSourceFile = (baseName: string) =>
(baseName.endsWith(".ts") || baseName.endsWith(".tsx")) &&
!baseName.endsWith(".d.ts") &&
baseName !== "index.ts";
async function discoverFiles(root: SourceRoot): Promise<FileEntry[]> {
const entries: FileEntry[] = [];
// Flat single-provider packages: every file under srcRoot belongs to the
// synthetic provider directory.
if (root.providerPrefix) {
const files = (await fs.readdir(root.srcRoot, {
recursive: true,
})) as string[];
for (const file of files) {
if (!isSourceFile(path.basename(file))) continue;
entries.push({
relativePath: path.join(root.providerPrefix, file),
absolutePath: path.join(root.srcRoot, file),
sourceDisplay: `${root.sourceDisplayPrefix}/${normalizeSlashes(file)}`,
});
}
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
return entries;
}
const topLevelEntries = await fs.readdir(root.srcRoot, {
withFileTypes: true,
});
const dirs = topLevelEntries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
for (const dir of dirs) {
const dirPath = path.join(root.srcRoot, dir);
let files: string[];
try {
files = (await fs.readdir(dirPath, { recursive: true })) as string[];
} catch {
continue;
}
for (const file of files) {
if (!isSourceFile(path.basename(file))) continue;
const relativePath = path.join(dir, file);
entries.push({
relativePath,
absolutePath: path.join(root.srcRoot, relativePath),
sourceDisplay: `${root.sourceDisplayPrefix}/${normalizeSlashes(relativePath)}`,
});
}
}
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
return entries;
}
function getJsDocText(node: Node): string {
return node.jsDoc?.map((doc) => doc.getText()).join("\n") ?? "";
}
function cleanDocComment(raw: string): string {
return raw
.replace(/^\/\*\*?/, "")
.replace(/\*\/$/, "")
.split("\n")
.map((line) => line.replace(/^\s*\*\s?/, ""))
.join("\n");
}
interface ParsedJSDoc {
summary: string;
sections: ExampleSection[];
hasResourceTag: boolean;
hasBindingTag: boolean;
/** `@layer` — a Layer factory providing a Context service. */
hasLayerTag: boolean;
/** `@provides <Service.Tag>` — service tags the Layer satisfies. */
provides: string[];
/** `@peer <package>` — optional peer dependencies the Layer needs. */
peers: string[];
category: string;
product: string;
}
/** Where a declaration's JSDoc lives, for copy markers. */
interface MarkerContext {
/** Repo-relative path of the source file. */
file: string;
/** Full source text (JSDoc offsets index into it). */
text: string;
}
function docLines(node: Node, raw: string, markers?: MarkerContext): string[] {
const docs = node.jsDoc ?? [];
// Markers need one comment per declaration to map blocks to source.
const start = docs[0]?.getStart();
// `getStart()` can land on leading `//` trivia; only map clean comments.
if (
markers &&
docs.length === 1 &&
start !== undefined &&
markers.text.startsWith("/**", start)
) {
return markDocLines(docCommentLines(markers.text, start), (block) =>
jsdocCopyId(markers.file, start, block),
);
}
return cleanDocComment(raw).split("\n");
}
function parseJSDoc(node: Node, markers?: MarkerContext): ParsedJSDoc {
const raw = getJsDocText(node);
if (!raw) {
return {
summary: "",
sections: [],
hasResourceTag: false,
hasBindingTag: false,
hasLayerTag: false,
provides: [],
peers: [],
category: "",
product: "",
};
}
const lines = docLines(node, raw, markers);
const summaryLines: string[] = [];
const sections: ExampleSection[] = [];
const provides: string[] = [];
const peers: string[] = [];
let hasResourceTag = false;
let hasBindingTag = false;
let hasLayerTag = false;
let category = "";
let product = "";
let sawTag = false;
let currentSection: ExampleSection | undefined;
let currentExample: ExampleBlock | undefined;
let sectionDescLines: string[] = [];
let collectingSectionDesc = false;
let insideFence = false;
const flushExample = () => {
if (!currentExample) return;
currentExample.body = currentExample.body.trim();
if (!currentSection) {
currentSection = { title: "Examples", description: "", examples: [] };
sections.push(currentSection);
}
currentSection.examples.push(currentExample);
currentExample = undefined;
};
const flushSectionDesc = () => {
if (currentSection && sectionDescLines.length > 0) {
currentSection.description = sectionDescLines.join("\n").trim();
}
sectionDescLines = [];
collectingSectionDesc = false;
};
for (const line of lines) {
// Track fenced code blocks so an `@`-prefixed line inside an example
// (e.g. a decorator) is never mistaken for a JSDoc tag.
if (line.trim().startsWith("```")) {
insideFence = !insideFence;
}
const proseHeading = insideFence
? null
: line.trim().match(/^###\s+(.+?)\s+<!-- api-prose -->$/);
if (proseHeading) {
if (!sawTag) summaryLines.push(`### ${proseHeading[1]!.trim()}`);
continue;
}
const section = insideFence ? null : line.trim().match(/^###\s+(.+)$/);
if (section) {
sawTag = true;
flushExample();
flushSectionDesc();
currentSection = {
title: section[1]!.trim(),
description: "",
examples: [],
};
sections.push(currentSection);
collectingSectionDesc = true;
continue;
}
const example = insideFence
? null
: line.trim().match(/^\*\*Example:\*\*\s*(.*)$/);
if (example) {
sawTag = true;
flushSectionDesc();
flushExample();
currentExample = {
title: example[1]!.trim() || "Example",
body: "",
};
continue;
}
const tag = insideFence ? null : line.trim().match(/^@(\w+)\s*(.*)$/);
if (tag) {
sawTag = true;
const [, name, rest] = tag;
const value = (rest ?? "").trim();
switch (name) {
case "resource":
hasResourceTag = true;
break;
case "binding":
hasBindingTag = true;
break;
case "layer":
hasLayerTag = true;
break;
case "provides":
if (value) provides.push(value);
break;
case "peer":
if (value) peers.push(value);
break;
case "category":
case "group":
if (value) category = value;
break;
case "product":
case "label":
if (value) product = value;
break;
case "section":
flushExample();
flushSectionDesc();
currentSection = {
title: value || "Examples",
description: "",
examples: [],
};
sections.push(currentSection);
collectingSectionDesc = true;
break;
case "example":
flushSectionDesc();
flushExample();
currentExample = { title: value || "Example", body: "" };
break;
}
continue;
}
if (!sawTag) {
summaryLines.push(line);
continue;
}
if (currentExample) {
currentExample.body += `${line}\n`;
} else if (collectingSectionDesc) {
sectionDescLines.push(line);
}
}
flushSectionDesc();
flushExample();
return {
summary: summaryLines.join("\n").trim(),
sections,
hasResourceTag,
hasBindingTag,
hasLayerTag,
provides,
peers,
category,
product,
};
}
function declName(node: Node): string {
if (ts.isVariableStatement(node)) {
return node.declarationList.declarations[0]?.name.getText() ?? "";
}
if (
ts.isClassDeclaration(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node)
) {
return node.name?.getText() ?? "";
}
return "";
}
interface Primary {
name: string;
doc: ParsedJSDoc;
category: string;
product: string;
}
const hasContent = (doc: ParsedJSDoc) =>
Boolean(doc.summary) || doc.sections.length > 0;
/**
* Map a public export name back to its local declaration name when a file
* re-exports under an alias, e.g. `export { VpcLinkResource as VpcLink }`
* lets us find the documented `VpcLinkResource` const from the tagged
* `VpcLink` interface.
*/
export function exportedNames(sourceFile: SourceFile): string[] {
const names = new Set<string>();
const addBinding = (name: ts.BindingName): void => {
if (ts.isIdentifier(name)) names.add(name.text);
else
for (const element of name.elements) {
if (ts.isBindingElement(element) && element.name)
addBinding(element.name);
}
};
for (const statement of sourceFile.statements) {
if (ts.isExportDeclaration(statement)) {
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
for (const spec of statement.exportClause.elements)
names.add(spec.name.text);
} else if (
statement.exportClause &&
ts.isNamespaceExport(statement.exportClause)
) {
names.add(statement.exportClause.name.text);
}
} else if (ts.isExportAssignment(statement)) {
if (!statement.isExportEquals) names.add("default");
} else if (
(ts.isVariableStatement(statement) ||
ts.isClassDeclaration(statement) ||
ts.isFunctionDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
ts.isEnumDeclaration(statement) ||
ts.isModuleDeclaration(statement) ||
ts.isImportEqualsDeclaration(statement)) &&
statement.modifierFlags & ts.ModifierFlags.Export
) {
if (statement.modifierFlags & ts.ModifierFlags.Default)
names.add("default");
else if (ts.isVariableStatement(statement)) {
for (const declaration of statement.declarationList.declarations)
addBinding(declaration.name);
} else if (
ts.isClassDeclaration(statement) ||
ts.isFunctionDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
ts.isEnumDeclaration(statement) ||
ts.isModuleDeclaration(statement) ||
ts.isImportEqualsDeclaration(statement)
) {
if (statement.name) names.add(statement.name.text);
}
}
}
return [...names];
}
function localNameForExport(
sourceFile: SourceFile,
publicName: string,
): string | undefined {
for (const ed of sourceFile.statements.filter(ts.isExportDeclaration)) {
if (ed.moduleSpecifier) continue;
for (const spec of ed.exportClause && ts.isNamedExports(ed.exportClause)
? ed.exportClause.elements
: []) {
if (spec.name.text === publicName) {
return (spec.propertyName ?? spec.name).text;
}
}
}
return undefined;
}
/**
* The page for a file is owned by the single exported declaration tagged
* `@resource` or `@binding`, and named after it. Authors sometimes write the
* docs (summary/@section/@example) on a sibling declaration of the same name
* (an `interface X` paired with `const X`) or on an internal const that is
* re-exported under the tagged name (the ApiGateway `XResource as X` pattern).
* When the tagged declaration itself has no content, pull it from that related
* declaration so the page isn't dropped as empty.
*/
export function findTaggedPrimary(
sourceFile: SourceFile,
markers?: MarkerContext,
): Primary | undefined {
const candidates: Node[] = [
...sourceFile.statements
.filter(ts.isVariableStatement)
.filter((s) => Boolean(s.modifierFlags & ts.ModifierFlags.Export)),
...sourceFile.statements
.filter(ts.isClassDeclaration)
.filter((c) => Boolean(c.modifierFlags & ts.ModifierFlags.Export)),
...sourceFile.statements
.filter(ts.isInterfaceDeclaration)
.filter((i) => Boolean(i.modifierFlags & ts.ModifierFlags.Export)),
];
for (const node of candidates) {
const doc = parseJSDoc(node, markers);
if (!doc.hasResourceTag && !doc.hasBindingTag && !doc.hasLayerTag) {
continue;
}
const name = declName(node);
if (!name) continue;
const category = doc.category;
const product = doc.product;
if (hasContent(doc)) return { name, doc, category, product };
// Tagged declaration has no prose — look for the related declaration that
// carries the docs (same name, or re-exported under this name).
const localName = localNameForExport(sourceFile, name);
const related: Node[] = [
...sourceFile.statements.filter(ts.isVariableStatement),
...sourceFile.statements.filter(ts.isClassDeclaration),
...sourceFile.statements.filter(ts.isInterfaceDeclaration),
...sourceFile.statements.filter(ts.isTypeAliasDeclaration),
].filter((d) => {
if (d === node) return false;
const dn = declName(d);
return dn === name || (localName !== undefined && dn === localName);
});
let best: ParsedJSDoc | undefined;
for (const d of related) {
const pd = parseJSDoc(d, markers);
if (pd.sections.length > 0) {
best = pd;
break;
}
if (!best && pd.summary) best = pd;
}
// Borrowed prose keeps the TAGGED declaration's kind + metadata.
const merged: ParsedJSDoc = {
...(best ?? doc),
hasResourceTag: doc.hasResourceTag,
hasBindingTag: doc.hasBindingTag,
hasLayerTag: doc.hasLayerTag,
provides: doc.provides,
peers: doc.peers,
};
return { name, doc: merged, category, product };
}
return undefined;
}
/** Resolves a `{@link}` symbol target to a generated page URL, if any. */
type LinkResolver = (target: string) => string | undefined;
/**
* Build a per-page resolver factory. A symbol resolves to another generated
* page preferring (in order): a same-directory page named after it, a
* same-directory page whose source file exports it (an access-level service
* like `ReadNamespace` documented on its `ReadWriteNamespace` page), a unique
* name match within the same provider, a unique exporter within the same
* provider, then a unique global name match. Member suffixes
* (`Cluster#capacityProviders`), provider-qualified names (`Cloudflare.Worker`)
* and `FooProps` interfaces resolve to the base resource's page.
*/
function makeLinkResolverFactory(
pages: PageEntry[],
): (fromDir: string) => LinkResolver {
const byName = new Map<string, PageEntry[]>();
const byExport = new Map<string, PageEntry[]>();
for (const p of pages) {
if (!byName.has(p.resource)) byName.set(p.resource, []);
byName.get(p.resource)!.push(p);
for (const name of p.exports) {
if (!byExport.has(name)) byExport.set(name, []);
byExport.get(name)!.push(p);
}
}
const providers = new Set(pages.map((p) => p.provider));
return (fromDir: string) => {
const fromProvider = fromDir.split("/")[0] ?? "";
const lookup = (name: string, provider?: string): PageEntry | undefined => {
const scope = (list: PageEntry[] | undefined) =>
(list ?? []).filter((c) => !provider || c.provider === provider);
const named = scope(byName.get(name));
const exporting = scope(byExport.get(name));
const sameDirNamed = named.filter((c) => c.dir === fromDir);
if (sameDirNamed.length === 1) return sameDirNamed[0];
const sameDirExporting = exporting.filter((c) => c.dir === fromDir);
if (sameDirExporting.length === 1) return sameDirExporting[0];
const providerNamed = named.filter((c) => c.provider === fromProvider);
if (providerNamed.length === 1) return providerNamed[0];
const providerExporting = exporting.filter(
(c) => c.provider === fromProvider,
);
if (providerExporting.length === 1) return providerExporting[0];
if (named.length === 1) return named[0];
return undefined;
};
return (target: string) => {
// Strip a `#member` suffix — the link lands on the owning resource page.
const base = target.split("#")[0];
const attempts: { name: string; provider?: string }[] = [{ name: base }];
if (base.includes(".")) {
const segments = base.split(".");
const first = segments[0];
const last = segments[segments.length - 1];
if (providers.has(first)) {
// Provider-qualified: `Cloudflare.Worker`, `Cloudflare.KV.Namespace`.
attempts.push({ name: last, provider: first });
} else {
// Member access on a symbol: `KeyPairProps.publicKeyMaterial`.
attempts.push({ name: first });
if (first.endsWith("Props")) {
attempts.push({ name: first.slice(0, -"Props".length) });
}
}
} else if (base.endsWith("Props")) {
attempts.push({ name: base.slice(0, -"Props".length) });
}
for (const attempt of attempts) {
const found = lookup(attempt.name, attempt.provider);
if (found) return found.link;
}
return undefined;
};
};
}
const isUrl = (target: string) => /^https?:\/\//.test(target);
/**
* Replace `{@link ...}` tags with markdown links, skipping fenced code
* blocks. Symbols that don't resolve to a generated page render as inline
* code (or their label) instead of leaking the raw tag.
*/
/** Symbol targets that didn't resolve to a page, for the end-of-run summary. */
const unresolvedLinkTargets = new Map<string, number>();
function linkifyMarkdown(markdown: string, resolve: LinkResolver): string {
const replaceTags = (chunk: string) =>
chunk.replace(LINK_TAG_RE, (_, inner: string) => {
// A tag wrapped across source lines carries the line break in its
// label — collapse it so the markdown link stays intact.
const parsed = parseLinkTag(inner.replace(/\s+/g, " "));
const label = parsed.label;
if (isUrl(parsed.target)) {
return `[${label ?? parsed.target}](${parsed.target})`;
}
const target = normalizeLinkTarget(parsed.target);
const url = resolve(target);
if (!url) {
unresolvedLinkTargets.set(
target,
(unresolvedLinkTargets.get(target) ?? 0) + 1,
);
}
const text = label ?? `\`${target}\``;
return url ? `[${text}](${url})` : text;
});
// Apply across contiguous non-fence chunks (a `{@link}` may span lines);
// leave fenced code blocks untouched.
const out: string[] = [];
let chunk: string[] = [];
let insideFence = false;
const flushChunk = () => {
if (chunk.length > 0) {
out.push(replaceTags(chunk.join("\n")));
chunk = [];
}
};
for (const line of markdown.split("\n")) {
if (line.trim().startsWith("```")) {
if (!insideFence) flushChunk();
insideFence = !insideFence;
out.push(line);
continue;
}
if (insideFence) {
out.push(line);
} else {
chunk.push(line);
}
}
flushChunk();
return out.join("\n");
}
function yamlString(value: string): string {
if (/[\n:"{}[\],&*?|>!%@`#]/.test(value) || value.trim() !== value) {
return JSON.stringify(value);
}
return value;
}
function renderPageBody(doc: PageDoc): string {
const parts: string[] = [];
if (doc.summary) {
parts.push(doc.summary);
}
for (const section of doc.sections) {
const secParts = [`## ${section.title}`];
if (section.description) {
secParts.push(section.description);
}
for (const example of section.examples) {
if (section.examples.length > 1) {
secParts.push(`**${example.title}**`);
}
secParts.push(example.body);
}
parts.push(secParts.join("\n\n"));
}
return parts.join("\n\n");
}
function renderReferenceFrontmatter(title: string): string {
return [
"---",
`title: ${yamlString(`${title} reference`)}`,
`description: ${yamlString(`Resources and capabilities for ${title}.`)}`,
// Keep the existing search-engine indexing policy during this experiment.
"head:",
" - tag: meta",
" attrs:",
" name: robots",
' content: "noindex, follow"',
"prev: false",
"next: false",
"tableOfContents:",
" minHeadingLevel: 2",
" maxHeadingLevel: 2",
"---",
].join("\n");
}
function renderResource(doc: PageDoc, resolve: LinkResolver): string {
const headerLines = [`> **Source:** \`${doc.sourceDisplay}\``];
if (doc.isLayer) {
const meta = ["**Kind:** Layer"];
if (doc.provides.length > 0) {
meta.push(
`**Provides:** ${doc.provides.map((tag) => `\`${tag}\``).join(", ")}`,
);
}
if (doc.peers.length > 0) {
meta.push(
`**Peer dependencies:** ${doc.peers
.map((peer) => `\`${peer}\``)
.join(", ")}`,
);
}
headerLines.push(`> ${meta.join(" · ")}`);
}
const sourceBlock = headerLines.join("\n");
const body = linkifyMarkdown(renderPageBody(doc).trim(), resolve);
if (body) {
return `${sourceBlock}\n\n${body}\n`;
}
return `${sourceBlock}\n`;
}
/** Change this grouping to experiment with larger or smaller reference pages. */
function referenceLocation(outputRelative: string, product: string) {
const parts = normalizeSlashes(outputRelative)
.replace(/\.md$/, "")
.split("/");
// Flat providers declare service-sized pages with @product instead of folders.
const group =
parts.length > 2
? parts.slice(0, 2)
: product
? [
parts[0],
"reference",
product
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, ""),
]
: [parts[0], "reference"];
const title = parts.slice(parts.length > 2 ? 2 : 1).join("-");
return {
outputRelative: `${group.join("/")}.md`,
title:
parts.length > 2
? group.join(".")
: product
? `${parts[0]}.${product}`
: parts[0],
resourceTitle: title,
link: `/providers/${group.join("/").toLowerCase()}#${title.toLowerCase()}`,
};
}
/** Keep example headings below their resource, with resource-scoped slugs. */
function nestResourceHeadings(markdown: string, resource: string): string {
let fence: string | undefined;
return markdown
.split("\n")
.map((line) => {
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/);
if (marker) {
if (!fence) fence = marker[1];
else if (marker[1][0] === fence[0] && marker[1].length >= fence.length)
fence = undefined;
return line;
}
if (fence) return line;
return line.replace(
/^(#{1,6})\s+(.+)$/,
(_, hashes: string, title: string) =>
`${"#".repeat(Math.min(6, Math.max(3, hashes.length + 1)))} ${resource}: ${title}`,
);
})
.join("\n");
}
/** Providers shown first in the sidebar; the rest follow alphabetically. */
const PROVIDER_ORDER = ["AWS", "Cloudflare"];
interface SidebarLeaf {
label: string;
link: string;
}
interface SidebarGroup {
label: string;
collapsed: true;
items: SidebarItem[];
}
type SidebarItem = SidebarLeaf | SidebarGroup;
interface PageEntry {
provider: string;
service: string;
resource: string;
category: string;
product: string;
link: string;
/** Source directory relative to srcRoot (slash-normalized), e.g. `Cloudflare/KV`. */
dir: string;
/** All names the page's source file exports — `{@link}` targets documented on this page. */
exports: string[];
}
const byLabel = (a: { label: string }, b: { label: string }) =>
a.label.localeCompare(b.label);
function orderedKeys(keys: string[], order: string[]): string[] {
const ranked = keys.filter((k) => order.includes(k));
ranked.sort((a, b) => order.indexOf(a) - order.indexOf(b));
const rest = keys.filter((k) => !order.includes(k)).sort();
return [...ranked, ...rest];
}
/** The sidebar lists documents; resource anchors live in the page's TOC. */
function buildServiceItems(pages: PageEntry[]): SidebarItem[] {
return pages
.map((page) => ({
label: page.product || page.service || page.provider,
link: page.link,
}))
.sort(byLabel);
}
function buildProvidersSidebar(entries: PageEntry[]): SidebarItem[] {
const byProvider = new Map<string, PageEntry[]>();
for (const e of entries) {
if (!byProvider.has(e.provider)) byProvider.set(e.provider, []);
byProvider.get(e.provider)!.push(e);
}
const providers: SidebarGroup[] = [];
for (const provider of orderedKeys([...byProvider.keys()], PROVIDER_ORDER)) {
const byPage = new Map<string, PageEntry[]>();
for (const entry of byProvider.get(provider)!) {
const link = entry.link.split("#")[0];
if (!byPage.has(link)) byPage.set(link, []);
byPage.get(link)!.push(entry);
}
const pages = [...byPage].map(([link, resources]) => ({
...resources[0],
link,
product: resources.every(
(resource) => resource.product === resources[0].product,
)
? resources[0].product
: "",
}));
const items = buildServiceItems(pages);
providers.push({ label: provider, collapsed: true, items });
}
assertNoDuplicateSiblings(providers, []);
return providers;
}
/**
* Duplicate sibling labels are always a tagging bug (e.g. two products
* resolving to the same name in one category) and render as confusing
* twin sections — fail the generation instead of shipping them.
*/
function assertNoDuplicateSiblings(items: SidebarItem[], path: string[]) {
const seen = new Map<string, number>();
for (const item of items) {
seen.set(item.label, (seen.get(item.label) ?? 0) + 1);
}
const dups = [...seen.entries()].filter(([, n]) => n > 1);
if (dups.length > 0) {
throw new Error(
`Duplicate sidebar sibling label(s) under "${path.join(" > ") || "(root)"}": ${dups
.map(([label, n]) => `"${label}" ×${n}`)
.join(", ")} — fix the @product/@category tags on the offending files.`,
);
}
for (const item of items) {
if ("items" in item) {
assertNoDuplicateSiblings(item.items, [...path, item.label]);
}
}
}
/**
* Writes `content` only when it differs from what's on disk, so a dev server
* watching the output reloads just the pages that changed.
*/
async function writeIfChanged(file: string, content: string): Promise<boolean> {
const current = await fs.readFile(file, "utf8").catch(() => undefined);
if (current === content) return false;
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, content, "utf8");
return true;
}
/** Deletes files under `dir` not in `keep`, then any directories left empty. */
async function pruneOutput(dir: string, keep: Set<string>): Promise<void> {
const entries = await fs
.readdir(dir, { withFileTypes: true })
.catch(() => []);
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
await pruneOutput(full, keep);
const left = await fs.readdir(full).catch(() => []);
if (left.length === 0) await fs.rmdir(full).catch(() => {});
} else if (!keep.has(full)) {
await fs.rm(full, { force: true });
}
}
}
async function main() {
await fs.mkdir(config.outRoot, { recursive: true });
const outputs = new Set<string>();
const seen = new Map<string, string>();
const pageEntries: PageEntry[] = [];
const pending: { outputRelative: string; product: string; doc: PageDoc }[] =
[];
let written = 0;
const redirects: Record<string, string> = {};
const anchors = new Set<string>();
let skipped = 0;
for (const root of config.roots) {
const entries = await discoverFiles(root);
console.log(
`Discovered ${entries.length} source files in ${normalizeSlashes(
path.relative(path.join(import.meta.dir, ".."), root.srcRoot),
)}.`,
);
await using syntax = await createSyntaxProject(
entries.map((entry) => entry.absolutePath),
);
for (const entry of entries) {
const sourceFile = await syntax.project.program.getSourceFile(
entry.absolutePath,
);
if (!sourceFile) {
throw new Error(`Missing source file ${entry.absolutePath}`);
}
const primary = findTaggedPrimary(
sourceFile,
config.copyMarkers
? {
file: normalizeSlashes(
path.relative(repoRoot, entry.absolutePath),
),
text: sourceFile.text,
}
: undefined,
);
if (!primary) {
skipped++;
continue;
}
// Only emit a page when there's actual documented content; a bare
// frontmatter + source link stub is noise.
if (!primary.doc.summary && primary.doc.sections.length === 0) {
skipped++;
continue;
}
// Mirror the source directory structure; name the page after the
// tagged declaration (e.g. Cloudflare.AI.Search/AiSearchInstance.md).
const relDir = path.dirname(entry.relativePath);
const outputRelative = path.join(relDir, `${primary.name}.md`);
const existing = seen.get(outputRelative);
if (existing) {
console.warn(
` collision: ${outputRelative} from ${entry.relativePath} (already from ${existing})`,
);
}
seen.set(outputRelative, entry.relativePath);
const doc: PageDoc = {
title: primary.name,
sourceDisplay: entry.sourceDisplay,
summary: primary.doc.summary,
sections: primary.doc.sections,
isLayer: primary.doc.hasLayerTag,
provides: primary.doc.provides,
peers: primary.doc.peers,
};
pending.push({ outputRelative, product: primary.product, doc });
const exportNames = exportedNames(sourceFile);
const segments = normalizeSlashes(outputRelative).split("/");
const location = referenceLocation(outputRelative, primary.product);
const oldLink = `/providers/${normalizeSlashes(outputRelative).replace(/\.md$/, "").toLowerCase()}`;
if (anchors.has(location.link)) {
throw new Error(`Duplicate reference anchor: ${location.link}`);
}
anchors.add(location.link);
redirects[oldLink] = location.link;
pageEntries.push({
provider: segments[0] ?? "",
service: segments.length > 2 ? segments[1] : "",
resource: primary.name,
category: primary.category,
product: primary.product,
link: location.link,
dir: normalizeSlashes(relDir),
exports: exportNames,
});
}
}
// Second pass: render with `{@link}` resolution — the full page set must be
// known before symbol targets can resolve to their pages.
const resolverFor = makeLinkResolverFactory(pageEntries);
const groups = new Map<string, { title: string; sections: string[] }>();
for (const page of pending) {
const location = referenceLocation(page.outputRelative, page.product);
const resolve = resolverFor(
normalizeSlashes(path.dirname(page.outputRelative)),
);
const group = groups.get(location.outputRelative) ?? {
title: location.title,
sections: [],
};
group.sections.push(
`## ${location.resourceTitle}\n\n${nestResourceHeadings(renderResource(page.doc, resolve), location.resourceTitle)}`,
);
groups.set(location.outputRelative, group);
}
for (const [relative, group] of groups) {
const outputPath = path.join(config.outRoot, relative);
outputs.add(outputPath);
if (
await writeIfChanged(
outputPath,
`${renderReferenceFrontmatter(group.title)}\n\n${group.sections.join("\n\n")}\n`,
)
) {
written++;
}
}
const sidebar = buildProvidersSidebar(pageEntries);
// Landing page for the Reference tab (/providers): a provider directory.
// Each provider's reference belongs to its docs hub — the directory just
// routes there (or into the tree, for providers without a hub). The
// ProviderDirectory component reads the generated sidebar for counts.
// Regenerated with the rest of the tree on every run.
const referenceIndex = [
"---",
"title: API Reference",
"description: Every provider alchemy can manage — pick a provider to open its docs hub and resource reference.",
"---",
"",
'import ProviderDirectory from "../../../components/ProviderDirectory.astro";',
"",
"Every resource alchemy can manage, documented from the source JSDoc and",
"organized by provider. A provider's reference lives in its docs hub —",
"pick one below, or search with `⌘K`.",
"",
"<ProviderDirectory />",
"",
].join("\n");
const indexPath = path.join(config.outRoot, "index.mdx");
outputs.add(indexPath);
await writeIfChanged(indexPath, referenceIndex);
await pruneOutput(config.outRoot, outputs);
const sidebarPath = path.join(
websiteRoot,
"src/generated/providers-sidebar.json",
);
await writeIfChanged(
path.join(websiteRoot, "src/generated/reference-redirects.json"),
`${JSON.stringify(redirects, null, 2)}\n`,
);
// astro.config imports the sidebar, so rewriting it restarts a dev server.
await writeIfChanged(sidebarPath, `${JSON.stringify(sidebar, null, 2)}\n`);
console.log(
`Done. Wrote ${written} changed reference pages containing ${pending.length} resources (skipped ${skipped} untagged${
config.copyMarkers ? ", with copy markers" : ""
}) to ${normalizeSlashes(
path.relative(path.join(import.meta.dir, ".."), config.outRoot),
)}.`,
);
if (unresolvedLinkTargets.size > 0) {
const list = [...unresolvedLinkTargets.entries()]
.sort((a, b) => b[1] - a[1])
.map(([target, n]) => (n > 1 ? `${target} ×${n}` : target))
.join(", ");
console.log(
`{@link} targets without a page (rendered as inline code): ${list}`,
);
}
console.log(
`Wrote provider sidebar to ${normalizeSlashes(
path.relative(path.join(import.meta.dir, ".."), sidebarPath),
)}.`,
);
}
if (import.meta.main) await main();