Files
WRNexusJSDoc/scripts/generate-docs.ts
T

403 lines
23 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
const root = resolve(import.meta.dir, "..");
const projectPackage = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
wrnexus?: { version?: string };
};
const frameworkVersion = projectPackage.wrnexus?.version;
if (!frameworkVersion) throw new Error("package.json is missing wrnexus.version");
const pages = join(root, "app", "pages");
const packagePages = join(pages, "packages");
mkdirSync(packagePages, { recursive: true });
const catalog = [
["ai", "AI", "Server-side Anthropic client with generation and streaming."],
["authz", "Security", "Role, permission, policy, and authorization guards."],
["cli", "Tooling", "Create, develop, build, generate, test, and maintain WrNexus apps."],
["compiler", "Core", "Parser and code generators for the .wrn language."],
["core", "Core", "Contexts, middleware, security, sessions, caching, JSX, and realtime."],
["csr", "Frontend", "Reactive, navigation, and realtime browser runtimes."],
["db", "Data", "Database adapters, typed queries, models, migrations, and sessions."],
["dev-server", "Runtime", "Development and production servers, HMR, assets, and gateways."],
["encryption", "Security", "Hashing, HMAC, authenticated encryption, and key derivation."],
["i18n", "Frontend", "Translation loading, locale resolution, and Intl formatting."],
["jwt", "Security", "HS256 JWT signing, verification, and bearer authentication."],
["mobile", "Native", "SSR-safe compatibility access to Capacitor plugins."],
["native", "Native", "Cross-platform browser and Capacitor capability registry."],
["oauth", "Security", "OAuth 2.0, PKCE, provider presets, and profile mapping."],
["pubsub", "Realtime", "In-process and Redis-backed publish/subscribe."],
["queue", "Data", "Background jobs with delay, concurrency, retry, and repetition."],
["reactive", "Frontend", "Small type-safe reactive signal primitives."],
["router", "Core", "Filesystem discovery, route matching, and typed route generation."],
["ssr", "Runtime", "Secure HTML document rendering and SEO metadata."],
["styles", "Frontend", "CSS pipeline, themes, fonts, profiles, and application config."],
["test", "Tooling", "WrNexus-aware component, route, and browser testing utilities."],
["tracking", "Runtime", "Error/event capture, middleware, filtering, and sinks."],
["ui", "Frontend", "Themeable server-rendered UI components and CSS."],
["uploader", "Data", "Validated local/S3 uploads and secure file serving."],
["validation", "Security", "Typed schemas, coercion, validation, and browser descriptors."],
] as const;
const escape = (value: string) =>
value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/\{/g, "&#123;")
.replace(/\}/g, "&#125;");
function inline(value: string): string {
return escape(value)
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" rel="noreferrer">$1</a>');
}
interface DocHeading {
id: string;
title: string;
level: number;
}
function markdown(source: string): { html: string; headings: DocHeading[] } {
const lines = source.replace(/\r/g, "").split("\n");
const out: string[] = [];
const headings: DocHeading[] = [];
const usedIds = new Map<string, number>();
let code: string[] | null = null;
let language = "";
let list = false;
let paragraph: string[] = [];
let table: string[][] = [];
const flushParagraph = () => {
if (paragraph.length) out.push(`<p>${inline(paragraph.join(" "))}</p>`);
paragraph = [];
};
const closeList = () => {
if (list) out.push("</ul>");
list = false;
};
const flushTable = () => {
if (!table.length) return;
const separator = table[1]?.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
const header = separator ? table[0]! : undefined;
const rows = separator ? table.slice(2) : table;
out.push('<div class="table-wrap"><table>');
if (header) {
out.push(
`<thead><tr>${header.map((cell) => `<th>${inline(cell.trim())}</th>`).join("")}</tr></thead>`,
);
}
out.push(
`<tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell.trim())}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`,
);
table = [];
};
for (const line of lines) {
if (/^\|.*\|\s*$/.test(line)) {
flushParagraph();
closeList();
table.push(line.slice(1, line.lastIndexOf("|")).split("|"));
continue;
}
flushTable();
const fence = /^```(.*)$/.exec(line);
if (fence) {
flushParagraph();
closeList();
if (code) {
out.push(
`<pre data-language="${escape(language)}"><code>${escape(code.join("\n"))}</code></pre>`,
);
code = null;
} else {
code = [];
language = fence[1]!.trim();
}
continue;
}
if (code) {
code.push(line);
continue;
}
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
if (heading) {
flushParagraph();
closeList();
const level = Math.min(4, heading[1]!.length + 1);
const title = heading[2]!.replace(/[`*_]/g, "").trim();
const base =
title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "") || "section";
const occurrence = usedIds.get(base) ?? 0;
usedIds.set(base, occurrence + 1);
const id = occurrence ? `${base}-${occurrence + 1}` : base;
headings.push({ id, title, level });
out.push(`<h${level} id="${id}">${inline(heading[2]!)}</h${level}>`);
continue;
}
const item = /^[-*]\s+(.+)$/.exec(line);
if (item) {
flushParagraph();
if (!list) out.push("<ul>");
list = true;
out.push(`<li>${inline(item[1]!)}</li>`);
continue;
}
if (!line.trim()) {
flushParagraph();
closeList();
continue;
}
if (/^>\s?/.test(line)) {
flushParagraph();
closeList();
out.push(`<blockquote>${inline(line.replace(/^>\s?/, ""))}</blockquote>`);
continue;
}
if (/^---+$/.test(line.trim())) continue;
paragraph.push(line.trim());
}
flushParagraph();
closeList();
flushTable();
if (code) out.push(`<pre><code>${escape(code.join("\n"))}</code></pre>`);
return { html: out.join("\n"), headings };
}
function examplesFrom(readme: string, name: string): string {
const examples = [...readme.matchAll(/```([^\n]*)\n([\s\S]*?)```/g)]
.map((match) => ({ language: match[1]!.trim(), code: match[2]!.trim() }))
.filter((example) => example.code)
.filter(
(example, index, all) => all.findIndex((value) => value.code === example.code) === index,
)
.slice(0, 4);
if (!examples.length) {
examples.push({
language: "ts",
code: `import * as packageApi from "@wrnexus/${name}";\n\nconsole.log(packageApi);`,
});
}
return examples
.map(
(example, index) =>
`<article class="example-card"><h3>Example ${index + 1}</h3><pre data-language="${escape(example.language || "text")}"><code>${escape(example.code)}</code></pre></article>`,
)
.join("");
}
function shell(title: string, description: string, content: string, state = ""): string {
const document = `page ${title.replace(/[^A-Za-z0-9]/g, "")} {
seo {
title = "${title.replace(/"/g, "'")}"
description = "${description.replace(/"/g, "'")}"
}
${state}
view {
<div class="docs-shell">
<header class="topbar">
<a class="brand" href="/"><span>W</span> WrNexus</a>
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
</header>
${content}
<footer>WrNexus ${frameworkVersion} · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
</div>
}
}
`;
return document.replace(/\bWrNexus\b/g, "WRNexusJS");
}
const cards = catalog
.map(
([
name,
category,
summary,
]) => `<a class="package-card" href="/packages/${name}" data-show="(category === 'All' ? true : category === '${category}') ? (query === '' ? true : '${name} ${category.toLowerCase()} ${summary.toLowerCase().replace(/'/g, "")} '.includes(query.toLowerCase())) : false">
<span class="category">${category}</span><h2>@wrnexus/${name}</h2><p>${summary}</p><span class="card-link">Open documentation →</span>
</a>`,
)
.join("\n");
const categories = ["All", ...new Set(catalog.map(([, category]) => category))];
const categoryButtons = categories
.map((category) => {
const count =
category === "All"
? catalog.length
: catalog.filter(([, value]) => value === category).length;
return `<button type="button" @click="category = '${category}'">${category}<span>${count}</span></button>`;
})
.join("");
writeFileSync(
join(pages, "packages.wrn"),
shell(
"Packages",
"Explore every WrNexus package, API, function, and copy-ready usage example.",
`<main class="page"><section class="hero compact"><span class="eyebrow">25 focused packages</span><h1>Package reference</h1><p>Everything in the framework, organized by responsibility and documented from the published ${frameworkVersion} APIs.</p><input class="search" type="search" placeholder="Search packages, features, or categories…" @input="query = event.target.value" /></section><section class="category-filter" aria-label="Filter packages by category"><div class="category-row">${categoryButtons}</div><p>Showing <strong>{category}</strong> packages</p></section><section class="package-grid">${cards}</section></main>`,
' state query = ""\n state category = "All"\n',
),
);
for (const [name, category, summary] of catalog) {
const packageRoot = join(root, "node_modules", "@wrnexus", name);
const readmePath = join(packageRoot, "README.md");
const typesPath = join(packageRoot, "dist", "index.d.ts");
if (!existsSync(readmePath) || !existsSync(typesPath))
throw new Error(`Install @wrnexus/${name} before generating docs`);
const readme = readFileSync(readmePath, "utf8").replace(/^#\s+[^\n]+\n?/, "");
const types = readFileSync(typesPath, "utf8");
const guide = markdown(readme);
const toc = [
{ id: "guide", title: "Guide", level: 2 },
...guide.headings,
{ id: "api", title: "Complete API", level: 2 },
{ id: "examples", title: "Examples", level: 2 },
]
.map(
(heading) =>
`<a class="toc-level-${heading.level}" href="#${heading.id}">${escape(heading.title)}</a>`,
)
.join("");
const content = `<main class="page package-page">
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">${category}</span><h1>@wrnexus/${name}</h1><p>${summary}</p><code>bun add @wrnexus/${name}@${frameworkVersion}</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
<article class="documentation"><section class="doc-intro"><span class="eyebrow">${category}</span><h1>@wrnexus/${name}</h1><p>${summary}</p><pre><code>bun add @wrnexus/${name}@${frameworkVersion}</code></pre></section><section id="guide" class="prose">${guide.html}</section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>${escape(types)}</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</p><div class="example-grid">${examplesFrom(readme, name)}</div></section></article>
<aside class="on-this-page"><h2>On this page</h2><nav>${toc}</nav></aside>
</main>`;
writeFileSync(join(packagePages, `${name}.wrn`), shell(`@wrnexus/${name}`, summary, content));
}
writeFileSync(
join(pages, "index.wrn"),
shell(
"Home",
"WrNexus documentation: build secure, server-rendered, reactive applications with Bun.",
`<main class="page"><section class="hero"><span class="eyebrow">WrNexus ${frameworkVersion}</span><h1>Build from the server.<br><em>Ship only what matters.</em></h1><p>An SSR-first, Bun-native framework with reactive .wrn components, typed data, realtime rooms, mobile capabilities, and production security built in.</p><div class="actions"><a class="primary" href="/getting-started">Start building</a><a href="/packages">Explore 25 packages</a></div><div class="code-window"><span>app/pages/counter.wrn</span><pre><code>page Counter {
state count = 0
view {
&lt;button @click=&quot;count++&quot;&gt;
Count {count}
&lt;/button&gt;
}
}</code></pre></div></section><section class="feature-grid"><article><h2>SSR by default</h2><p>Useful HTML reaches the browser immediately. Interactive pages hydrate only the runtime they use.</p></article><article><h2>Secure foundations</h2><p>CSP, Trusted Types, CSRF, sessions, validation, authorization, encryption, and safe rendering are integrated.</p></article><article><h2>Web to native</h2><p>Share markup through Capacitor or compile portable pages into Expo and React Native routes.</p></article></section></main>`,
),
);
const guides = {
"getting-started": [
"Getting started",
"Getting started with WrNexus",
`<main class="page"><article class="documentation prose standalone"><span class="eyebrow">Guide</span><h1>Getting started</h1><p>Create a production-ready WrNexus application with Bun.</p><h2>1. Create the project</h2><pre><code>bunx @wrnexus/cli create my-app
cd my-app
bun install
bun run dev</code></pre><h2>2. Add a page</h2><pre><code>page Dashboard {
state count = 0
view {
&lt;main&gt;
&lt;h1&gt;Dashboard&lt;/h1&gt;
&lt;button @click=&quot;count++&quot;&gt;{count}&lt;/button&gt;
&lt;/main&gt;
}
}</code></pre><h2>3. Verify and build</h2><pre><code>wrnexus doctor
wrnexus test
wrnexus build</code></pre><h2>Where things live</h2><ul><li><code>app/pages</code> contains routes.</li><li><code>app/components</code> contains reusable .wrn components.</li><li><code>app/api</code> contains server API handlers.</li><li><code>app/layouts</code> contains shared shells.</li><li><code>app/middleware</code> contains request middleware.</li><li><code>wrnexus.config.ts</code> configures security, styles, data, mobile, and deployment.</li></ul></article></main>`,
],
language: [
"Language and directives",
"Complete WrNexus language reference for events, directives, loops, conditionals, data, components, forms, realtime, and native behavior.",
`<main class="page"><article class="documentation prose standalone language-reference"><span class="eyebrow">Complete reference</span><h1>Language and directives</h1><p>This page documents the .wrn language and declarative browser features that span multiple packages.</p>
<h2 id="file-anatomy">File anatomy</h2><p>A file declares a <code>page</code> or <code>component</code> and can contain metadata, props, state, data, view, style, server functions, APIs, and realtime handlers.</p><pre><code>${escape(`page Dashboard {
layout = "default"
seo { title = "Dashboard" }
state count = 0
view { <button @click="count++">{count}</button> }
style { button { padding: 12px; } }
}`)}</code></pre>
<h2 id="state">State and interpolation</h2><p>State is scoped to the nearest generated <code>data-scope</code>. Text expressions update reactively after hydration.</p><pre><code>${escape(`state count = 0
state user = { name: "Ada" }
view {
<p>Count: {count}</p>
<p>{user.name}</p>
}`)}</code></pre>
<h2 id="events">Events</h2><p>Any DOM event can use <code>@event="statement"</code>. The compiler emits <code>data-on-event</code>. The expression receives <code>event</code> and can mutate state.</p><div class="table-wrap"><table><thead><tr><th>Syntax</th><th>Purpose</th></tr></thead><tbody><tr><td><code>@click</code></td><td>Pointer or keyboard activation.</td></tr><tr><td><code>@input</code></td><td>Read live field values.</td></tr><tr><td><code>@change</code></td><td>React to committed field changes.</td></tr><tr><td><code>@submit</code></td><td>Handle form submission behavior.</td></tr><tr><td><code>@browser-click</code></td><td>Run only in a browser target.</td></tr><tr><td><code>@mobile-click</code></td><td>Run only in a native/mobile target.</td></tr></tbody></table></div><pre><code>${escape(`<input @input="name = event.target.value">
<button @click="count++">Add</button>
<form @submit="submitted = true">...</form>`)}</code></pre>
<h2 id="directives">Reactive data attributes</h2><div class="table-wrap"><table><thead><tr><th>Directive</th><th>Behavior</th></tr></thead><tbody><tr><td><code>data-scope</code></td><td>Declares reactive state for a subtree.</td></tr><tr><td><code>data-text</code></td><td>Synchronizes textContent with an expression.</td></tr><tr><td><code>data-show</code></td><td>Shows or hides an element by truthiness.</td></tr><tr><td><code>data-for</code></td><td>Repeats an element for a client-side list.</td></tr><tr><td><code>data-on-&lt;event&gt;</code></td><td>Compiled form of an event binding.</td></tr><tr><td><code>data-component</code></td><td>Mounts a server-rendered component.</td></tr><tr><td><code>data-slot</code></td><td>Fills a named component or layout slot.</td></tr><tr><td><code>data-wrnexus-csr</code></td><td>Connects generated client data fetching.</td></tr></tbody></table></div>
<h2 id="conditionals">Conditional rendering</h2><h3>Server conditionals</h3><p>Server blocks render only the selected branch into the response.</p><pre><code>${escape(`{#if user.isAdmin}
<a href="/admin">Admin</a>
{:else if user}
<p>Welcome {user.name}</p>
{:else}
<a href="/login">Sign in</a>
{/if}`)}</code></pre><h3>Client visibility</h3><pre><code>${escape(`<section data-show="open">Visible while open is true</section>`)}</code></pre>
<h2 id="loops">Loops and lists</h2><h3>Server each block</h3><pre><code>${escape(`{#each users as user, i}
<p>{i + 1}. {user.name}</p>
{:empty}
<p>No users</p>
{/each}`)}</code></pre><h3>Reactive client loop</h3><pre><code>${escape(`<li data-for="item, i in items">
<span data-text="item.name"></span>
<button data-on-click="items = items.filter(x => x !== item)">Remove</button>
</li>`)}</code></pre>
<h2 id="components">Components, props, and slots</h2><pre><code>${escape(`component Card {
props { title = "Card" }
view {
<article><h2>{title}</h2><slot></slot></article>
}
}
<div data-component="card" title="Profile">
<p>Card content</p>
</div>`)}</code></pre>
<h2 id="data">Server and client data</h2><p>Use named data bindings for SSR data or client hydration. Secrets and database work stay on the server.</p><pre><code>${escape(`data users {
ssr GET "/api/users"
}
view {
{#each users as user}<p>{user.name}</p>{/each}
}`)}</code></pre>
<h2 id="forms">Forms and validation</h2><p>Schema-backed forms validate in the browser and on the server with the same descriptor.</p><pre><code>${escape(`<form data-schema="login" method="post" action="/api/login" data-redirect="/dashboard">
<input name="email" type="email">
<span data-error="email"></span>
<button>Sign in</button>
<p data-success="Signed in" hidden></p>
</form>`)}</code></pre>
<h2 id="i18n">Internationalization and themes</h2><pre><code>${escape(`<h1>{t:home.title}</h1>
<button data-wire-lang-set="fr">Français</button>
<button data-wire-theme-toggle>Toggle theme</button>
<button data-wire-theme-set="dark">Dark</button>`)}</code></pre>
<h2 id="realtime">Realtime rooms</h2><pre><code>${escape(`<div data-room="chat" data-room-user="Ada">
<span data-room-status></span>
<div data-room-log></div>
<template data-room-item="message"><p>%user%: %text%</p></template>
<form data-room-send><input name="text" data-room-reset></form>
</div>`)}</code></pre>
<h2 id="native">Browser and native directives</h2><pre><code>${escape(`<button data-native-browser="share" data-native-mobile="share"
data-native-options='{"title":"WrNexus"}'>Share</button>
<nav data-native-only="mobile">Mobile navigation</nav>
<p data-native-only="browser">Browser instructions</p>
<button data-native-requires="haptics">Haptic action</button>`)}</code></pre>
<h2 id="other">Other framework attributes</h2><div class="table-wrap"><table><thead><tr><th>Attribute</th><th>Purpose</th></tr></thead><tbody><tr><td><code>data-error</code></td><td>Field validation error destination.</td></tr><tr><td><code>data-success</code></td><td>Successful form message.</td></tr><tr><td><code>data-redirect</code></td><td>Navigation after form success.</td></tr><tr><td><code>data-room-*</code></td><td>Realtime status, templates, sending, and reset behavior.</td></tr><tr><td><code>data-uploader</code></td><td>Config-driven upload widget.</td></tr><tr><td><code>data-wire-theme-*</code></td><td>Theme selection and toggling.</td></tr><tr><td><code>data-wire-lang*</code></td><td>Language selection.</td></tr><tr><td><code>data-native-*</code></td><td>Cross-platform capability and visibility behavior.</td></tr></tbody></table></div></article></main>`,
],
architecture: [
"Architecture",
"Understand the WrNexus SSR, compiler, runtime, and package architecture.",
`<main class="page"><article class="documentation prose standalone"><span class="eyebrow">Concepts</span><h1>Architecture</h1><p>WrNexus separates server work, generated markup, and browser behavior so applications stay understandable and efficient.</p><h2>Request path</h2><pre><code>Request → Router → Middleware → Page/API → SSR document → Browser runtime</code></pre><h2>Compiler</h2><p>The compiler parses .wrn files and lowers state, events, interpolation, loops, conditionals, data bindings, components, and styles into server modules and small declarative browser directives.</p><h2>Runtime</h2><p>The server owns routing, data, secrets, sessions, validation, uploads, and rendering. The browser owns reactive scopes, navigation, forms, realtime clients, and native capability dispatch.</p><h2>Package boundaries</h2><p>Each package is independently installable. Start with the CLI and core, then add database, security, realtime, native, UI, and operational packages as required.</p><div class="actions"><a class="primary" href="/packages/core">Read core API</a><a href="/packages/compiler">Read compiler API</a></div></article></main>`,
],
} as const;
for (const [route, [title, description, content]] of Object.entries(guides)) {
writeFileSync(join(pages, `${route}.wrn`), shell(title, description, content));
}
console.log(
`Generated ${catalog.length} package pages and ${Object.keys(guides).length + 2} site pages.`,
);