Something went wrong. Try again.
a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt
hypermedia frontend signals
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305import { echo } from "$console/echo.js";import { getDocsPath, getLibSrcPath } from "$utils/paths.js";import { trackVersion } from "$versioning/tracker.js";import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";import path from "node:path";import ts from "typescript";
type Member = { name: string; type: string; docs?: string };
type EntryKind = "function" | "interface" | "type" | "class";
type DocumentEntry = { name: string; kind: EntryKind; description: string; examples: string[]; signature?: string; members?: Array<Member>;};
type JSDocumentParsed = { description: string; examples: string[] };
/** * Extract and parse JSDoc comment text */function extractJSDoc(node: ts.Node, sourceFile: ts.SourceFile): JSDocumentParsed { const fullText = sourceFile.getFullText(); const ranges = ts.getLeadingCommentRanges(fullText, node.getFullStart());
if (!ranges || ranges.length === 0) { return { description: "", examples: [] }; }
const comments = ranges.map((range) => fullText.slice(range.pos, range.end)); const jsdocComments = comments.filter((c) => c.trim().startsWith("/**"));
if (jsdocComments.length === 0) { return { description: "", examples: [] }; }
const comment = jsdocComments.at(-1); const lines = comment!.split("\n").map((line) => line.trim()).map((line) => line.replace(/^\/\*\*\s?/, "")).map(( line, ) => line.replace(/^\*\s?/, "")).map((line) => line.replace(/\*\/\s*$/, "")).filter((line) => line !== "/" && line !== "*" );
const description: string[] = []; const examples: string[] = []; let currentExample: string[] = []; let inExample = false;
for (const line of lines) { if (line.startsWith("@example")) { inExample = true; continue; }
if (line.startsWith("@") && !line.startsWith("@example")) { if (inExample && currentExample.length > 0) { examples.push(currentExample.join("\n").trim()); currentExample = []; } inExample = false; continue; }
if (inExample) { currentExample.push(line); } else { description.push(line); } }
if (currentExample.length > 0) { examples.push(currentExample.join("\n").trim()); }
return { description: description.join("\n").trim(), examples };}
/** * Extract function signature */function extractFnSig(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): string { const start = node.getStart(sourceFile); const end = node.body ? node.body.getStart(sourceFile) : node.getEnd(); return sourceFile.text.slice(start, end).trim().replaceAll(/\s+/g, " ");}
/** * Extract interface members */function extractIMembers(node: ts.InterfaceDeclaration, sourceFile: ts.SourceFile): Array<Member> { const members: Array<Member> = [];
for (const member of node.members) { if (ts.isPropertySignature(member) && member.name) { const name = member.name.getText(sourceFile); const type = member.type ? member.type.getText(sourceFile) : "unknown"; const { description } = extractJSDoc(member, sourceFile);
members.push({ name, type, docs: description || undefined }); } }
return members;}
/** * Generate markdown for a documentation entry */function generateMD(entries: DocumentEntry[], moduleName: string, moduleDocs: string): string { const lines: string[] = [];
lines.push(`# ${moduleName}`, "");
if (moduleDocs) { lines.push(moduleDocs, ""); }
for (const entry of entries) { lines.push(`## ${entry.name}`, "");
if (entry.description) { lines.push(entry.description, ""); }
if (entry.signature) { lines.push("```typescript", entry.signature, "```", ""); }
if (entry.examples && entry.examples.length > 0) { for (const example of entry.examples) { lines.push("**Example:**", "", "```typescript", example, "```", ""); } }
if (entry.members && entry.members.length > 0) { lines.push("### Members", "");
for (const member of entry.members) { lines.push(`- **${member.name}**: \`${member.type}\``); if (member.docs) { lines.push(` ${member.docs}`); } }
lines.push(""); } }
return lines.join("\n");}
/** * Extract module-level documentation */function extractModDocs(content: string): string { const lines = content.split("\n"); const docLines: string[] = []; let inDoc = false;
for (const line of lines) { const trimmed = line.trim();
if (trimmed === "/**") { inDoc = true; continue; }
if (inDoc) { if (trimmed === "*/") { break; }
const cleaned = trimmed.replace(/^\*\s?/, ""); if (!cleaned.startsWith("@packageDocumentation")) { docLines.push(cleaned); } } }
return docLines.join("\n").trim();}
/** * Parse a TypeScript file and extract documentation */function parseFile(filePath: string, content: string): DocumentEntry[] { const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); const entries: DocumentEntry[] = [];
function visit(node: ts.Node) { if (ts.isFunctionDeclaration(node) && node.name) { const modifiers = node.modifiers; const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
if (isExported) { const name = node.name.text; const { description, examples } = extractJSDoc(node, sourceFile); const signature = extractFnSig(node, sourceFile);
entries.push({ name, kind: "function", description, examples, signature }); } }
if (ts.isInterfaceDeclaration(node)) { const modifiers = node.modifiers; const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
if (isExported) { const name = node.name.text; const { description, examples } = extractJSDoc(node, sourceFile); const members = extractIMembers(node, sourceFile);
entries.push({ name, kind: "interface", description, examples, members }); } }
if (ts.isTypeAliasDeclaration(node)) { const modifiers = node.modifiers; const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
if (isExported) { const name = node.name.text; const { description, examples } = extractJSDoc(node, sourceFile); const signature = node.type.getText(sourceFile);
entries.push({ name, kind: "type", description, examples, signature }); } }
ts.forEachChild(node, visit); }
visit(sourceFile); return entries;}
/** * Process a TypeScript file and generate documentation */async function processFile(filePath: string, baseDir: string, outputDir: string): Promise<void> { const content = await readFile(filePath, "utf8"); const entries = parseFile(filePath, content);
if (entries.length === 0) { return; }
const moduleDocs = extractModDocs(content); const relativePath = path.relative(baseDir, filePath); const moduleName = path.basename(relativePath, ".ts"); const markdown = generateMD(entries, moduleName, moduleDocs);
const outputPath = path.join(outputDir, `${moduleName}.md`); const versionedContent = await trackVersion(outputPath, markdown); await writeFile(outputPath, versionedContent, "utf8");
echo.ok(` Generated: ${relativePath} -> api/${moduleName}.md`);}
/** * Recursively find all TypeScript files */async function findTsFiles(dir: string, files: string[] = []): Promise<string[]> { const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) { const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) { await findTsFiles(fullPath, files); } else if (entry.isFile() && entry.name.endsWith(".ts")) { files.push(fullPath); } }
return files;}
/** * Docs command implementation */export async function docsCommand(): Promise<void> { const srcDir = await getLibSrcPath(); const docsPath = await getDocsPath(); const docsDir = path.join(docsPath, "api");
echo.title("\nGenerating API Documentation\n");
await mkdir(docsDir, { recursive: true });
const files = await findTsFiles(srcDir);
echo.info(`Found ${files.length} TypeScript files\n`);
for (const file of files) { await processFile(file, srcDir, docsDir); }
echo.success(`\nAPI documentation generated in docs/api/\n`);}