590 lines
31 KiB
TypeScript
590 lines
31 KiB
TypeScript
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
unlinkSync,
|
|
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");
|
|
const componentPages = join(pages, "components");
|
|
mkdirSync(packagePages, { recursive: true });
|
|
|
|
interface UiComponentReference {
|
|
count: number;
|
|
components: Array<{
|
|
name: string;
|
|
mount: string;
|
|
category: string;
|
|
purpose: string;
|
|
props: Array<{ name: string; type: string; required: boolean; default: string | null }>;
|
|
slots: string[];
|
|
events: string[];
|
|
source: string;
|
|
}>;
|
|
}
|
|
|
|
const uiReferencePath = join(root, "node_modules", "@wrnexus", "ui", "component-reference.json");
|
|
const uiReference: UiComponentReference | undefined = existsSync(uiReferencePath)
|
|
? JSON.parse(readFileSync(uiReferencePath, "utf8"))
|
|
: undefined;
|
|
|
|
const catalog = [
|
|
["ai", "AI", "Server-side Anthropic client with generation and streaming."],
|
|
["auth", "Security", "Authentication routes, sessions, forms, guards, and account flows."],
|
|
["authz", "Security", "Role, permission, policy, and authorization guards."],
|
|
[
|
|
"benchmark",
|
|
"Tooling",
|
|
"Framework benchmark scenarios and repeatable performance measurements.",
|
|
],
|
|
["cache", "Data", "Memory and distributed caching with coordination and invalidation."],
|
|
["captcha", "Security", "Managed CAPTCHA verification, middleware, and UI integration."],
|
|
["cli", "Tooling", "Create, develop, build, generate, test, and maintain WrNexus apps."],
|
|
["compiler", "Core", "Parser and code generators for the .wrn language."],
|
|
["content", "Data", "Content collections, validation, querying, and publishing workflows."],
|
|
["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."],
|
|
["dev-toolbar", "Tooling", "Development toolbar diagnostics, inspection, and runtime status."],
|
|
["encryption", "Security", "Hashing, HMAC, authenticated encryption, and key derivation."],
|
|
["graphql", "Data", "GraphQL schemas, routes, and framework plugin integration."],
|
|
["helpers", "Tooling", "Safe Context URL helpers and forward-auth login redirects."],
|
|
["i18n", "Frontend", "Translation loading, locale resolution, and Intl formatting."],
|
|
["identity", "Security", "Portable identity records, claims, and account linking."],
|
|
["image", "Frontend", "Responsive image optimization, loaders, placeholders, and components."],
|
|
["jwt", "Security", "HS256 JWT signing, verification, and bearer authentication."],
|
|
["language-server", "Tooling", "Editor-neutral language intelligence for WRN files."],
|
|
["mcp", "Tooling", "Model Context Protocol tools for WRNexus projects."],
|
|
["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."],
|
|
["observability", "Runtime", "Structured logging, tracing, health, and integration adapters."],
|
|
["playground", "Tooling", "Interactive framework examples and executable playground utilities."],
|
|
["plugin", "Core", "Plugin contracts, lifecycle hooks, composition, and framework integration."],
|
|
["pubsub", "Realtime", "In-process and Redis-backed publish/subscribe."],
|
|
["pwa", "Frontend", "Progressive Web App manifests, service workers, and offline strategies."],
|
|
["queue", "Data", "Background jobs with delay, concurrency, retry, and repetition."],
|
|
["reactive", "Frontend", "Small type-safe reactive signal primitives."],
|
|
["realtime", "Realtime", "Rooms, presence, messaging, history, streams, and UI components."],
|
|
["router", "Core", "Filesystem discovery, route matching, and typed route generation."],
|
|
["security", "Security", "Security headers, CSRF protection, rate limits, and safe rendering."],
|
|
["ssr", "Runtime", "Secure HTML document rendering and SEO metadata."],
|
|
["store", "Data", "Application state stores shared across server and browser runtimes."],
|
|
["styles", "Frontend", "CSS pipeline, themes, fonts, profiles, and application config."],
|
|
["syntax", "Frontend", "Editor syntax definitions and language tooling for .wrn files."],
|
|
["test", "Tooling", "WrNexus-aware component, route, and browser testing utilities."],
|
|
["tracking", "Runtime", "Error/event capture, middleware, filtering, and sinks."],
|
|
["typecheck", "Tooling", "WRN-aware TypeScript diagnostics and virtual documents."],
|
|
["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, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/\{/g, "{")
|
|
.replace(/\}/g, "}");
|
|
|
|
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 usageHeading = /^## Usage[ \t]*\r?$/m.exec(readme);
|
|
const afterHeading = usageHeading
|
|
? readme.slice(usageHeading.index + usageHeading[0].length)
|
|
: readme;
|
|
const nextSection = usageHeading ? /^##[ \t]+/m.exec(afterHeading) : undefined;
|
|
const usage = afterHeading.slice(0, nextSection?.index ?? afterHeading.length);
|
|
|
|
const examples: { title: string; language: string; code: string }[] = [];
|
|
const lines = usage.replace(/\r/g, "").split("\n");
|
|
let title = "Typical usage";
|
|
let prose = "";
|
|
for (let index = 0; index < lines.length; index++) {
|
|
const line = lines[index]!;
|
|
const heading = /^#{3,4}\s+(.+)$/.exec(line);
|
|
if (heading) {
|
|
title = heading[1]!.replace(/[`*_]/g, "").trim();
|
|
prose = "";
|
|
continue;
|
|
}
|
|
const fence = /^```(.*)$/.exec(line);
|
|
if (fence) {
|
|
const code: string[] = [];
|
|
index++;
|
|
while (index < lines.length && !/^```/.test(lines[index]!)) {
|
|
code.push(lines[index]!);
|
|
index++;
|
|
}
|
|
const value = code.join("\n").trim();
|
|
const language = fence[1]!.trim();
|
|
if (value && language !== "text" && !examples.some((example) => example.code === value)) {
|
|
examples.push({
|
|
title: title === "Typical usage" && prose ? prose : title,
|
|
language,
|
|
code: value,
|
|
});
|
|
}
|
|
prose = "";
|
|
continue;
|
|
}
|
|
if (line.trim() && !/^[-*>|]/.test(line.trim())) {
|
|
prose = line
|
|
.replace(/[`*_]/g, "")
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
.replace(/:\s*$/, "")
|
|
.trim();
|
|
}
|
|
}
|
|
const fallbacks: Record<string, Array<{ title: string; language: string; code: string }>> = {
|
|
plugin: [
|
|
{
|
|
title: "Define an ordered plugin",
|
|
language: "ts",
|
|
code: `import { definePlugin } from "@wrnexus/plugin";
|
|
|
|
export default definePlugin({
|
|
name: "analytics",
|
|
enforce: "post",
|
|
});`,
|
|
},
|
|
{
|
|
title: "Resolve plugin execution order",
|
|
language: "ts",
|
|
code: `import { resolvePlugins } from "@wrnexus/plugin";
|
|
|
|
const ordered = resolvePlugins([corePlugin, analyticsPlugin]);`,
|
|
},
|
|
],
|
|
syntax: [
|
|
{
|
|
title: "Parse a WRNexusJS document",
|
|
language: "ts",
|
|
code: `import { parse } from "@wrnexus/syntax";
|
|
|
|
const ast = parse('component Greeting { view { <p>Hello</p> } }');`,
|
|
},
|
|
{
|
|
title: "Summarize syntax diagnostics",
|
|
language: "ts",
|
|
code: `import { diagnose, diagnosticSummary } from "@wrnexus/syntax";
|
|
|
|
const summary = diagnosticSummary(diagnose(source));`,
|
|
},
|
|
],
|
|
"dev-toolbar": [
|
|
{
|
|
title: "Run development-toolbar rules",
|
|
language: "ts",
|
|
code: `import { runDevToolbarRules } from "@wrnexus/dev-toolbar";
|
|
|
|
const issues = runDevToolbarRules(context);`,
|
|
},
|
|
{
|
|
title: "Create a toolbar registry",
|
|
language: "ts",
|
|
code: `import { createDevToolbarRegistry } from "@wrnexus/dev-toolbar";
|
|
|
|
const registry = createDevToolbarRegistry();`,
|
|
},
|
|
],
|
|
};
|
|
for (const fallback of fallbacks[name] ?? []) {
|
|
if (examples.length >= 2) break;
|
|
examples.push(fallback);
|
|
}
|
|
if (examples.length < 2) {
|
|
examples.push({
|
|
title: `Install @wrnexus/${name}`,
|
|
language: "sh",
|
|
code: `bun add @wrnexus/${name}`,
|
|
});
|
|
}
|
|
if (examples.length < 2) {
|
|
examples.push({
|
|
title: `Import @wrnexus/${name}`,
|
|
language: "ts",
|
|
code: `import * as ${name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase()).replace(/[^A-Za-z0-9_$]/g, "_")} from "@wrnexus/${name}";`,
|
|
});
|
|
}
|
|
if (examples.length < 2) {
|
|
throw new Error(`@wrnexus/${name} README needs at least two documented code examples`);
|
|
}
|
|
return examples
|
|
.slice(0, 6)
|
|
.map(
|
|
(example) =>
|
|
`<article class="example-card"><h3>${inline(example.title)}</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">
|
|
<SkipLink label="Skip to content" href="#main" class="docs-skip-link" />
|
|
<header class="topbar">
|
|
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
|
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
|
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v${frameworkVersion}</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
|
|
</header>
|
|
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="https://component.wrnexusjs.dev/">Components</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
|
|
${content}
|
|
<footer><div class="footer-brand"><span class="footer-mark" aria-hidden="true">W</span><p><strong>WRNexusJS ${frameworkVersion}</strong><span>Complete API documentation generated from installed package declarations.</span></p></div><nav aria-label="Footer"><a href="/packages">All packages</a><a href="/getting-started">Get started</a><a href="/security">Security</a><a href="/support">Support</a><a href="/llms.txt">AI guide</a></nav><p class="footer-meta">Private Developer Preview · Bun-native</p></footer>
|
|
<BackToTop />
|
|
</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">${catalog.length} 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`);
|
|
let readme = readFileSync(readmePath, "utf8")
|
|
.replace(/^#\s+[^\n]+\n?/, "")
|
|
.replace(/^## Installation\s*$[\s\S]*?(?=^##\s|\s*$)/m, "")
|
|
.replace(/\bWrNexus\b/g, "WRNexusJS");
|
|
if (name === "ui") {
|
|
readme = readme.replace(/^### Components\s*$[\s\S]*?(?=^###\s|^##\s|\s*$)/m, "");
|
|
}
|
|
const types = readFileSync(typesPath, "utf8");
|
|
const guide = markdown(readme);
|
|
const componentReference =
|
|
name === "ui" && uiReference
|
|
? `<section class="prose component-catalog-callout"><h2>Explore the component library</h2><p>Browse interactive examples and complete component usage in the dedicated WRNexusJS component showcase.</p><p><a class="primary" href="https://component.wrnexusjs.dev/">Browse all ${uiReference.count} components →</a></p></section>`
|
|
: "";
|
|
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="portal-main docs-layout">
|
|
<article id="main" class="documentation prose standalone package-document"><nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span>/</span><a href="/packages">Packages</a><span>/</span><span aria-current="page">@wrnexus/${name}</span></nav><section class="doc-intro"><span class="eyebrow">${category} · Package reference</span><h1>@wrnexus/${name}</h1><p>${summary}</p><div class="doc-meta"><span>v${frameworkVersion}</span><span>Private registry</span><span>${category}</span></div><section id="access" class="access-callout"><h2>Install the package</h2><p>After WorkRoot approves private registry access, install the release-aligned package:</p><pre><code>bun add @wrnexus/${name}@${frameworkVersion}</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide">${guide.html}</section><section id="api" class="api"><h2>Complete TypeScript API</h2><p>Generated from the exact installed package declarations.</p><pre data-language="typescript"><code>${escape(types)}</code></pre></section><section id="examples" class="examples"><h2>Examples</h2><p>Copy-ready examples from the installed package documentation.</p><div class="example-grid">${examplesFrom(readme, name)}</div></section>${componentReference}</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));
|
|
}
|
|
|
|
if (!uiReference) throw new Error("Install @wrnexus/ui before generating documentation");
|
|
if (existsSync(join(pages, "components.wrn"))) unlinkSync(join(pages, "components.wrn"));
|
|
if (existsSync(componentPages)) {
|
|
for (const file of readdirSync(componentPages).filter((name) => name.endsWith(".wrn"))) {
|
|
unlinkSync(join(componentPages, file));
|
|
}
|
|
}
|
|
|
|
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 ${catalog.length} packages</a></div><div class="code-window"><span>app/pages/counter.wrn</span><pre><code>page Counter {
|
|
state count = 0
|
|
view {
|
|
<button @click="count++">
|
|
Count {count}
|
|
</button>
|
|
}
|
|
}</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 {
|
|
<main>
|
|
<h1>Dashboard</h1>
|
|
<button @click="count++">{count}</button>
|
|
</main>
|
|
}
|
|
}</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 and ordinary attribute expressions update reactively after hydration while retaining useful initial SSR values.</p><pre><code>${escape(`state count = 0
|
|
state user = { name: "Ada" }
|
|
state showPassword = false
|
|
|
|
view {
|
|
<p>Count: {count}</p>
|
|
<p>{user.name}</p>
|
|
<input type="{showPassword ? 'text' : 'password'}">
|
|
<button @click="showPassword = !showPassword"
|
|
aria-label="{showPassword ? 'Hide password' : 'Show password'}">
|
|
Toggle password
|
|
</button>
|
|
}`)}</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-<event></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.`,
|
|
);
|