1/**2 * Generates the Docusaurus API reference from `deno doc --json` (schema v2)3 * into `website/api-gen/`. One page per entry in {@link entrypoints}, with the4 * six CLI command wrappers sharing a single page. Run via `deno task site:api`.5 */6import { type Entrypoint, entrypoints } from "./entrypoints.ts";78// `--node` renders the lofi-node API instead: same renderer, sourced from the9// pinned checkout of FelineStateMachine/lofi-node (LOFI_NODE_DIR, default10// ../lofi-node) so pages match the ref the /node docs render from; the11// package itself is published as jsr:@nzip/lofi-node.12const NODE_MODE = Deno.args.includes("--node");13const NODE_DIR = NODE_MODE ? (Deno.env.get("LOFI_NODE_DIR") ?? "../lofi-node") : "";1415const nodeEntrypoints: readonly Entrypoint[] = [16 {17 jsrName: ".",18 file: `${NODE_DIR}/mod.ts`,19 page: "node",20 title: "Sync node",21 },22 {23 jsrName: "./testing",24 file: `${NODE_DIR}/testing/mod.ts`,25 page: "testing",26 title: "Testing",27 },28];2930const activeEntrypoints = NODE_MODE ? nodeEntrypoints : entrypoints;31const packageName = NODE_MODE ? "@nzip/lofi-node" : "@nzip/lofi";3233const OUT_DIR = NODE_MODE ? "website/node-api-gen" : "website/api-gen";3435type JsDocTag = {36 kind: string;37 name?: string;38 doc?: string;39 type?: string;40};41type JsDoc = { doc?: string; tags?: JsDocTag[] };42type TsType = {43 repr?: string;44 kind?: string;45 // deno-lint-ignore no-explicit-any46 value?: any;47 // deno-lint-ignore no-explicit-any48 [key: string]: any;49};50type Param = {51 kind: string;52 name?: string;53 optional?: boolean;54 tsType?: TsType;55 // deno-lint-ignore no-explicit-any56 [k: string]: any;57};58type TypeParam = { name: string; constraint?: TsType; default?: TsType };59type Declaration = {60 kind: string;61 declarationKind: string;62 jsDoc?: JsDoc;63 // deno-lint-ignore no-explicit-any64 def?: any;65};66type SymbolNode = { name: string; declarations: Declaration[] };67type ModuleNodes = { module_doc?: JsDoc; symbols?: SymbolNode[] };68type DocOutput = { version: number; nodes: Record<string, ModuleNodes> };6970function printType(t: TsType | undefined | null): string {71 if (!t) return "unknown";72 switch (t.kind) {73 case "keyword":74 return String(t.value ?? t.repr ?? "unknown");75 case "literal": {76 const v = t.value ?? {};77 if (v.kind === "string") return JSON.stringify(v.string);78 if (v.kind === "number") return String(v.number);79 if (v.kind === "boolean") return String(v.boolean);80 if (v.kind === "bigInt") return `${v.string}n`;81 if (v.kind === "template") {82 return t.repr ? `\`${t.repr}\`` : "`...`";83 }84 return t.repr ?? "unknown";85 }86 case "typeRef": {87 const v = t.value ?? {};88 const args: TsType[] = v.typeParams ?? [];89 const name = v.typeName ?? t.repr ?? "unknown";90 return args.length > 0 ? `${name}<${args.map(printType).join(", ")}>` : name;91 }92 case "union":93 return (t.value as TsType[]).map(printType).join(" | ");94 case "intersection":95 return (t.value as TsType[]).map(printType).join(" & ");96 case "array": {97 const inner = printType(t.value);98 return /[|&]/.test(inner) ? `(${inner})[]` : `${inner}[]`;99 }100 case "tuple":101 return `[${(t.value as TsType[]).map(printType).join(", ")}]`;102 case "typeOperator": {103 const v = t.value ?? {};104 return `${v.operator} ${printType(v.tsType)}`;105 }106 case "optional":107 return printType(t.value);108 case "parenthesized":109 return `(${printType(t.value)})`;110 case "rest":111 return `...${printType(t.value)}`;112 case "fnOrConstructor": {113 const v = t.value ?? {};114 const params = (v.params ?? []).map(printParam).join(", ");115 const tp = printTypeParams(v.typeParams ?? []);116 const prefix = v.constructor ? "new " : "";117 return `${prefix}${tp}(${params}) => ${printType(v.tsType)}`;118 }119 case "typeLiteral": {120 const v = t.value ?? {};121 const parts: string[] = [];122 for (const sig of v.indexSignatures ?? []) {123 parts.push(124 `[${(sig.params ?? []).map(printParam).join(", ")}]: ${printType(sig.tsType)}`,125 );126 }127 for (const p of v.properties ?? []) {128 parts.push(129 `${p.readonly ? "readonly " : ""}${p.name}${p.optional ? "?" : ""}: ${130 printType(p.tsType)131 }`,132 );133 }134 for (const m of v.methods ?? []) {135 const fd = m.functionDef ?? m;136 parts.push(137 `${m.name}${m.optional ? "?" : ""}${printTypeParams(fd.typeParams ?? [])}(${138 (fd.params ?? m.params ?? []).map(printParam).join(", ")139 }): ${printType(fd.returnType ?? m.returnType)}`,140 );141 }142 if (parts.length === 0) return "Record<never, never>";143 return `{ ${parts.join("; ")} }`;144 }145 case "conditional": {146 const v = t.value ?? {};147 return `${printType(v.checkType)} extends ${printType(v.extendsType)} ? ${148 printType(v.trueType)149 } : ${printType(v.falseType)}`;150 }151 case "indexedAccess": {152 const v = t.value ?? {};153 return `${printType(v.objType)}[${printType(v.indexType)}]`;154 }155 case "typeQuery":156 return `typeof ${t.value}`;157 case "typePredicate": {158 const v = t.value ?? {};159 const name = v.param?.name ?? "value";160 const asserts = v.asserts ? "asserts " : "";161 return v.type ? `${asserts}${name} is ${printType(v.type)}` : `${asserts}${name}`;162 }163 case "importType": {164 const v = t.value ?? {};165 const qualifier = v.qualifier ? `.${v.qualifier}` : "";166 return `import(${JSON.stringify(v.specifier ?? "?")})${qualifier}`;167 }168 case "infer":169 return `infer ${t.value?.typeParam?.name ?? "T"}`;170 case "this":171 return "this";172 case "mapped":173 return t.repr && t.repr.length > 0 ? t.repr : "{ [mapped type] }";174 default:175 return t.repr && t.repr.length > 0 ? t.repr : "unknown";176 }177}178179function printParam(p: Param): string {180 switch (p.kind) {181 case "identifier":182 return `${p.name}${p.optional ? "?" : ""}${p.tsType ? `: ${printType(p.tsType)}` : ""}`;183 case "rest":184 return `...${printParam(p.arg)}${p.tsType ? `: ${printType(p.tsType)}` : ""}`;185 case "assign": {186 const left = printParam(p.left);187 return p.left?.tsType || !p.tsType ? left : `${left}: ${printType(p.tsType)}`;188 }189 case "object":190 return `{ ${(p.props ?? []).map((prop: Param) => prop.name ?? "...").join(", ")} }${191 p.tsType ? `: ${printType(p.tsType)}` : ""192 }`;193 case "array":194 return `[...]${p.tsType ? `: ${printType(p.tsType)}` : ""}`;195 default:196 return p.name ?? "arg";197 }198}199200function printTypeParams(typeParams: TypeParam[]): string {201 if (!typeParams || typeParams.length === 0) return "";202 const parts = typeParams.map((tp) => {203 let out = tp.name;204 if (tp.constraint) out += ` extends ${printType(tp.constraint)}`;205 if (tp.default) out += ` = ${printType(tp.default)}`;206 return out;207 });208 return `<${parts.join(", ")}>`;209}210211// deno-lint-ignore no-explicit-any212function functionSignature(name: string, def: any, isAsync = false): string {213 const asyncPrefix = def?.isAsync || isAsync ? "async " : "";214 const params = (def?.params ?? []).map(printParam).join(", ");215 const ret = def?.returnType ? `: ${printType(def.returnType)}` : "";216 return `${asyncPrefix}function ${name}${printTypeParams(def?.typeParams ?? [])}(${params})${ret}`;217}218219// deno-lint-ignore no-explicit-any220function classSignature(name: string, def: any): string {221 const lines: string[] = [];222 let head = `class ${name}${printTypeParams(def?.typeParams ?? [])}`;223 if (def?.extends) head += ` extends ${def.extends}`;224 if (def?.implements?.length) {225 head += ` implements ${def.implements.map(printType).join(", ")}`;226 }227 lines.push(`${head} {`);228 for (const ctor of def?.constructors ?? []) {229 lines.push(230 ` constructor(${(ctor.params ?? []).map(printParam).join(", ")});`,231 );232 }233 for (const prop of def?.properties ?? []) {234 const mods = [235 prop.isStatic ? "static " : "",236 prop.readonly ? "readonly " : "",237 ].join("");238 lines.push(239 ` ${mods}${prop.name}${prop.optional ? "?" : ""}${240 prop.tsType ? `: ${printType(prop.tsType)}` : ""241 };`,242 );243 }244 for (const method of def?.methods ?? []) {245 const fd = method.functionDef ?? {};246 const mods = method.isStatic ? "static " : "";247 const accessor = method.kind === "getter" ? "get " : method.kind === "setter" ? "set " : "";248 const params = (fd.params ?? []).map(printParam).join(", ");249 const ret = fd.returnType ? `: ${printType(fd.returnType)}` : "";250 lines.push(251 ` ${mods}${accessor}${method.name}${printTypeParams(fd.typeParams ?? [])}(${params})${ret};`,252 );253 }254 lines.push("}");255 return lines.join("\n");256}257258// deno-lint-ignore no-explicit-any259function interfaceSignature(name: string, def: any): string {260 const lines: string[] = [];261 let head = `interface ${name}${printTypeParams(def?.typeParams ?? [])}`;262 if (def?.extends?.length) {263 head += ` extends ${def.extends.map(printType).join(", ")}`;264 }265 lines.push(`${head} {`);266 for (const prop of def?.properties ?? []) {267 lines.push(268 ` ${prop.readonly ? "readonly " : ""}${prop.name}${prop.optional ? "?" : ""}${269 prop.tsType ? `: ${printType(prop.tsType)}` : ""270 };`,271 );272 }273 for (const method of def?.methods ?? []) {274 const params = (method.params ?? []).map(printParam).join(", ");275 const ret = method.returnType ? `: ${printType(method.returnType)}` : "";276 lines.push(277 ` ${method.name}${method.optional ? "?" : ""}${278 printTypeParams(method.typeParams ?? [])279 }(${params})${ret};`,280 );281 }282 lines.push("}");283 return lines.join("\n");284}285286// deno-lint-ignore no-explicit-any287function typeAliasSignature(name: string, def: any): string {288 const printed = printTypeAliasBody(def?.tsType);289 return `type ${name}${printTypeParams(def?.typeParams ?? [])} = ${printed};`;290}291292function printTypeAliasBody(t: TsType | undefined): string {293 if (t?.kind === "typeLiteral") {294 const v = t.value ?? {};295 const parts: string[] = [];296 for (const sig of v.indexSignatures ?? []) {297 parts.push(298 ` [${(sig.params ?? []).map(printParam).join(", ")}]: ${printType(sig.tsType)};`,299 );300 }301 for (const p of v.properties ?? []) {302 parts.push(303 ` ${p.readonly ? "readonly " : ""}${p.name}${p.optional ? "?" : ""}: ${304 printType(p.tsType)305 };`,306 );307 }308 for (const m of v.methods ?? []) {309 const fd = m.functionDef ?? m;310 parts.push(311 ` ${m.name}${m.optional ? "?" : ""}${printTypeParams(fd.typeParams ?? [])}(${312 (fd.params ?? m.params ?? []).map(printParam).join(", ")313 }): ${printType(fd.returnType ?? m.returnType)};`,314 );315 }316 if (parts.length > 0) return `{\n${parts.join("\n")}\n}`;317 }318 if (t?.kind === "union" && (t.value as TsType[]).length > 3) {319 return `\n | ${(t.value as TsType[]).map(printType).join("\n | ")}`;320 }321 return printType(t);322}323324// deno-lint-ignore no-explicit-any325function variableSignature(name: string, def: any): string {326 const keyword = def?.kind ?? "const";327 return `${keyword} ${name}${def?.tsType ? `: ${printType(def.tsType)}` : ""};`;328}329330function declarationSignature(name: string, dec: Declaration): string | null {331 switch (dec.kind) {332 case "function":333 return functionSignature(name, dec.def);334 case "class":335 return classSignature(name, dec.def);336 case "interface":337 return interfaceSignature(name, dec.def);338 case "typeAlias":339 return typeAliasSignature(name, dec.def);340 case "variable":341 return variableSignature(name, dec.def);342 case "enum":343 // deno-lint-ignore no-explicit-any344 return `enum ${name} { ${(dec.def?.members ?? []).map((m: any) => m.name).join(", ")} }`;345 case "namespace":346 return `namespace ${name}`;347 default:348 return null;349 }350}351352/** Converts `{@link X}` / `{@linkcode X}` JSDoc tags to plain Markdown. */353function renderJsDocText(text: string): string {354 return text.replace(355 /\{@link(?:code|plain)?\s+([^}|\s]+)(?:\s*\|\s*([^}]+))?\}/g,356 (_match, target: string, label?: string) => {357 const display = (label ?? target).trim();358 if (/^https?:\/\//.test(target)) return `[${display}](${target})`;359 return `\`${display}\``;360 },361 );362}363364function renderJsDoc(jsDoc: JsDoc | undefined): string {365 if (!jsDoc) return "";366 const out: string[] = [];367 if (jsDoc.doc) out.push(renderJsDocText(jsDoc.doc.trim()));368 const tags = jsDoc.tags ?? [];369 const params = tags.filter((tag) => tag.kind === "param" && tag.doc);370 if (params.length > 0) {371 out.push("| Parameter | Description |");372 out.push("| --- | --- |");373 for (const tag of params) {374 out.push(375 `| \`${tag.name}\` | ${renderJsDocText(tag.doc ?? "").replaceAll("\n", " ")} |`,376 );377 }378 }379 for (const tag of tags) {380 if (tag.kind === "return" && tag.doc) {381 out.push(`**Returns:** ${renderJsDocText(tag.doc)}`);382 }383 if (tag.kind === "deprecated") {384 out.push(`**Deprecated.** ${renderJsDocText(tag.doc ?? "")}`.trim());385 }386 if (tag.kind === "default") {387 out.push(`**Default:** \`${tag.doc ?? tag.type ?? ""}\``);388 }389 }390 for (const tag of tags) {391 if (tag.kind === "example" && tag.doc) {392 const body = tag.doc.trim();393 const rendered = body.includes("```") ? body : `\`\`\`ts\n${body}\n\`\`\``;394 out.push(`**Example**\n\n${renderJsDocText(rendered)}`);395 }396 }397 return out.join("\n\n");398}399400async function docModule(file: string): Promise<ModuleNodes> {401 const command = new Deno.Command(Deno.execPath(), {402 args: ["doc", "--json", file],403 stdout: "piped",404 stderr: "piped",405 });406 const result = await command.output();407 if (!result.success) {408 throw new Error(409 `deno doc --json ${file} failed:\n${new TextDecoder().decode(result.stderr)}`,410 );411 }412 const parsed = JSON.parse(413 new TextDecoder().decode(result.stdout),414 ) as DocOutput;415 if (parsed.version !== 2) {416 throw new Error(417 `deno doc --json for ${file} returned schema v${parsed.version}; expected v2.`,418 );419 }420 const modules = Object.values(parsed.nodes);421 if (modules.length === 0) {422 throw new Error(`deno doc --json ${file} returned no modules.`);423 }424 return modules[0];425}426427const KIND_LABEL: Record<string, string> = {428 function: "function",429 class: "class",430 interface: "interface",431 typeAlias: "type",432 variable: "const",433 enum: "enum",434 namespace: "namespace",435};436437function renderSymbol(symbol: SymbolNode): string {438 const out: string[] = [];439 const kinds = [440 ...new Set(441 symbol.declarations.map((dec) => KIND_LABEL[dec.kind] ?? dec.kind),442 ),443 ];444 // h2 keeps the heading hierarchy sequential under the page h1445 out.push(`## ${symbol.name}`);446 out.push(`<sup>${kinds.join(", ")}</sup>`);447 const signatures = symbol.declarations448 .map((dec) => declarationSignature(symbol.name, dec))449 .filter((sig): sig is string => sig !== null);450 if (signatures.length > 0) {451 out.push(`\`\`\`ts\n${signatures.join("\n\n")}\n\`\`\``);452 }453 const jsDoc = symbol.declarations.find((dec) => dec.jsDoc)?.jsDoc;454 const rendered = renderJsDoc(jsDoc);455 if (rendered) out.push(rendered);456 return out.join("\n\n");457}458459function symbolSortKey(symbol: SymbolNode): [number, string] {460 const kindRank =461 symbol.declarations.some((dec) =>462 dec.kind === "function" || dec.kind === "class" ||463 dec.kind === "variable"464 )465 ? 0466 : 1;467 return [kindRank, symbol.name.toLowerCase()];468}469470function moduleSummary(moduleDoc: JsDoc | undefined): string {471 const doc = moduleDoc?.doc ?? "";472 const firstSentence = doc.split(/\n\s*\n/)[0]?.replaceAll("\n", " ").trim() ??473 "";474 return renderJsDocText(firstSentence);475}476477type PageGroup = {478 page: string;479 position: number;480 entries: typeof entrypoints[number][];481};482483const pageOrder = NODE_MODE484 ? ["node", "testing"]485 : ["runtime", "astro", "access", "preact", "testing", "cli"];486const groups = new Map<string, PageGroup>();487for (const entry of activeEntrypoints) {488 const group = groups.get(entry.page) ?? {489 page: entry.page,490 position: entry.page.startsWith("recipes/")491 ? 1 +492 [...activeEntrypoints.filter((e) => e.page.startsWith("recipes/"))]493 .findIndex((e) => e.page === entry.page)494 : 1 + pageOrder.indexOf(entry.page),495 entries: [],496 };497 group.entries.push(entry);498 groups.set(entry.page, group);499}500501const denoJson = JSON.parse(502 await Deno.readTextFile(NODE_MODE ? `${NODE_DIR}/deno.json` : "deno.json"),503) as {504 version: string;505 description: string;506};507508await Deno.mkdir(NODE_MODE ? OUT_DIR : `${OUT_DIR}/recipes`, {509 recursive: true,510});511512const indexRows: string[] = [];513let totalSymbols = 0;514515for (const group of groups.values()) {516 const sections: string[] = [];517 let pageTitle: string;518 if (group.page === "cli") {519 pageTitle = "CLI commands";520 sections.push(521 "Executable entrypoints run with `deno run` (the generated project wires them up as `deno task` commands). They expose behavior, not exported symbols.",522 );523 } else {524 pageTitle = group.entries[0].title;525 }526527 for (const entry of group.entries) {528 const module = await docModule(entry.file);529 const symbols = (module.symbols ?? []).filter((symbol) =>530 symbol.declarations.some((dec) => dec.declarationKind === "export")531 );532 symbols.sort((a, b) => {533 const [rankA, nameA] = symbolSortKey(a);534 const [rankB, nameB] = symbolSortKey(b);535 return rankA - rankB || nameA.localeCompare(nameB);536 });537 const isCommand = group.page === "cli";538 if (!isCommand && symbols.length === 0) {539 throw new Error(540 `No exported symbols found for ${entry.file}; refusing to emit empty page.`,541 );542 }543 if (!module.module_doc?.doc && symbols.length === 0) {544 throw new Error(`No module doc or symbols for ${entry.file}.`);545 }546 totalSymbols += symbols.length;547548 const jsrSpecifier = entry.jsrName === "."549 ? packageName550 : `${packageName}${entry.jsrName.slice(1)}`;551 if (isCommand) {552 sections.push(`## ${entry.title}`);553 sections.push(`\`\`\`sh\ndeno run -A jsr:${jsrSpecifier}\n\`\`\``);554 if (module.module_doc?.doc) {555 sections.push(renderJsDocText(module.module_doc.doc.trim()));556 }557 } else {558 sections.push(`\`\`\`ts\nimport * as mod from "jsr:${jsrSpecifier}";\n\`\`\``);559 if (module.module_doc?.doc) {560 sections.push(renderJsDocText(module.module_doc.doc.trim()));561 }562 sections.push(...symbols.map(renderSymbol));563 }564 indexRows.push(565 `| [\`${jsrSpecifier}\`](./${group.page}.md${isCommand ? `#${entry.title}` : ""}) | ${566 moduleSummary(module.module_doc)567 } |`,568 );569 }570571 const frontmatter = [572 "---",573 `title: ${JSON.stringify(pageTitle)}`,574 `sidebar_position: ${group.position}`,575 "---",576 ].join("\n");577 const body = `${frontmatter}\n\n# ${pageTitle}\n\n${sections.join("\n\n")}\n`;578 await Deno.writeTextFile(`${OUT_DIR}/${group.page}.md`, body);579}580581const indexIntro = NODE_MODE582 ? `Generated from the JSDoc of [\`@nzip/lofi-node\`](https://jsr.io/@nzip/lofi-node) v${denoJson.version}.583${denoJson.description}`584 : `Generated from the JSDoc of [\`@nzip/lofi\`](https://jsr.io/@nzip/lofi) v${denoJson.version}.585${denoJson.description}`;586587const index = `---588title: "API reference"589sidebar_position: 0590slug: /591---592593# ${NODE_MODE ? "lofi-node API reference" : "API reference"}594595${indexIntro}596597| Export | Summary |598| --- | --- |599${indexRows.join("\n")}600`;601await Deno.writeTextFile(`${OUT_DIR}/index.md`, index);602603if (!NODE_MODE) {604 await Deno.writeTextFile(605 `${OUT_DIR}/recipes/_category_.json`,606 `${607 JSON.stringify(608 { label: "Recipes", position: 8, collapsed: true },609 null,610 2,611 )612 }\n`,613 );614}615616console.log(617 `Generated ${groups.size + 1} API pages (${totalSymbols} exported symbols) into ${OUT_DIR}/.`,618);