fix: stabilize workspace gateway and scaffolding

This commit is contained in:
2026-07-13 12:54:07 +05:30
parent 6dfef7da3c
commit 88e907783a
63 changed files with 380 additions and 206 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
/**
* Framework conventions shipped into scaffolded apps as CLAUDE.md and llms.txt so
* Framework conventions shipped into scaffolded apps as CLAUDE.md and public/llms.txt so
* AI coding tools (Claude Code, Cursor, Copilot) generate correct WrNexus code.
* Generated from the repo-root llms.txt - do not edit by hand.
*/
/** The canonical WrNexus conventions reference (shipped as llms.txt). */
/** The canonical WrNexus conventions reference (served as /llms.txt). */
export const AI_GUIDE = `# WrNexus
> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
+35 -8
View File
@@ -8,12 +8,15 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
import { currentCliVersion } from "./update-notifier.ts";
const frameworkVersion = currentCliVersion();
/** Map of file (relative to app root) -> contents. */
const TEMPLATE: Record<string, string> = {
// AI/agent context: teaches Claude Code / Cursor / Copilot the WrNexus conventions.
"CLAUDE.md": CLAUDE_MD,
"llms.txt": AI_GUIDE,
"public/llms.txt": AI_GUIDE,
".gitignore": `# Dependencies
node_modules/
@@ -64,6 +67,9 @@ Thumbs.db
"version": "0.1.0",
"private": true,
"type": "module",
"wrnexus": {
"version": "${frameworkVersion}"
},
"scripts": {
"dev": "wrnexus dev .",
"build": "wrnexus build .",
@@ -76,14 +82,14 @@ Thumbs.db
"check": "bun run lint && bun run format:check"
},
"dependencies": {
"@wrnexus/ai": "^0.2.0",
"@wrnexus/core": "^0.2.0",
"@wrnexus/styles": "^0.2.0",
"@wrnexus/validation": "^0.2.0",
"@wrnexus/db": "^0.2.0"
"@wrnexus/ai": "${frameworkVersion}",
"@wrnexus/core": "${frameworkVersion}",
"@wrnexus/styles": "${frameworkVersion}",
"@wrnexus/validation": "${frameworkVersion}",
"@wrnexus/db": "${frameworkVersion}"
},
"devDependencies": {
"@wrnexus/cli": "^0.2.0",
"@wrnexus/cli": "${frameworkVersion}",
"@eslint/js": "^9.0.0",
"@tailwindcss/cli": "^4.0.0",
"@types/bun": "latest",
@@ -306,7 +312,7 @@ page Home {
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<a href="/about" class="rounded-lg bg-slate-900 px-5 py-2.5 text-sm font-medium text-white shadow-sm transition hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200">Get started</a>
<a href="/hello" class="rounded-lg border border-slate-200 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:border-slate-300 dark:border-white/10 dark:text-slate-300 dark:hover:border-white/20">View demo</a>
<a href="/api/hello" class="rounded-lg border border-slate-200 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:border-slate-300 dark:border-white/10 dark:text-slate-300 dark:hover:border-white/20">View API</a>
</div>
<div class="mt-14 w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 text-left shadow-sm dark:border-white/10 dark:bg-white/5">
@@ -332,6 +338,27 @@ page Home {
</main>
}
}
`,
"app/pages/about.wrn": `page About {
seo {
title = "About"
description = "Learn how APP_NAME is built with WrNexus."
}
view {
<main class="min-h-screen bg-white px-6 py-20 text-slate-900 dark:bg-[#0b0f1e] dark:text-slate-100">
<article class="mx-auto max-w-2xl">
<a href="/" class="text-sm text-indigo-600 hover:underline dark:text-indigo-400">← Home</a>
<p class="mt-12 font-mono text-xs uppercase tracking-[0.2em] text-indigo-500">WrNexus application</p>
<h1 class="mt-4 text-4xl font-bold tracking-tight">About APP_NAME</h1>
<p class="mt-6 text-lg leading-8 text-slate-600 dark:text-slate-400">
This page is server-rendered from <code>app/pages/about.wrn</code>. Add state,
events, components, APIs, and data without switching to another UI framework.
</p>
</article>
</main>
}
}
`,
"app/components/counter.wrn": `// A reusable component. Route: none — mounted inside a page with
// <div data-component="counter" ...props></div>.
+15 -8
View File
@@ -4,7 +4,7 @@
* Upgrade an app (or every app in a workspace) to a WrNexus release:
* 1. Bump every `@wrnexus/*` dependency to the target version.
* 2. `bun install`.
* 3. Refresh framework-owned reference files (llms.txt) and apply any
* 3. Refresh framework-owned reference files (public/llms.txt) and apply any
* versioned, idempotent migrations that newer releases introduce.
* 4. Record the applied version in package.json (`"wrnexus": { version }`).
*
@@ -19,7 +19,7 @@
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join, resolve } from "node:path";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
import { inspectProject } from "./doctor.ts";
@@ -190,11 +190,14 @@ function appVersion(appRoot: string, pkg: Record<string, unknown>): string {
/** Refresh pure framework-owned reference files. Never touches user-edited CLAUDE.md. */
function refreshFrameworkFiles(appRoot: string, dryRun: boolean, log: (m: string) => void): void {
// llms.txt is a generated reference — always safe to overwrite.
const llms = join(appRoot, "llms.txt");
// Public llms.txt is generated and web-visible, so it is safe to refresh.
const llms = join(appRoot, "public", "llms.txt");
if (!existsSync(llms) || readFileSync(llms, "utf8") !== AI_GUIDE) {
log("~ llms.txt refreshed");
if (!dryRun) writeFileSync(llms, AI_GUIDE, "utf8");
log("~ public/llms.txt refreshed");
if (!dryRun) {
mkdirSync(join(appRoot, "public"), { recursive: true });
writeFileSync(llms, AI_GUIDE, "utf8");
}
}
// CLAUDE.md is often user-edited — only create it when absent.
const claude = join(appRoot, "CLAUDE.md");
@@ -223,10 +226,14 @@ function backupProjectFiles(appRoot: string, from: string, target: string): stri
"tsconfig.json",
".gitignore",
"CLAUDE.md",
"llms.txt",
"public/llms.txt",
]) {
const source = join(appRoot, name);
if (existsSync(source)) cpSync(source, join(backup, name));
if (existsSync(source)) {
const destination = join(backup, name);
mkdirSync(dirname(destination), { recursive: true });
cpSync(source, destination);
}
}
return backup;
}
+13 -6
View File
@@ -13,8 +13,11 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { scaffoldApp } from "./create.ts";
import { currentCliVersion } from "./update-notifier.ts";
import type { GatewayAuth, GatewaySecurity } from "@wrnexus/dev-server";
const frameworkVersion = currentCliVersion();
export interface WorkspaceApp {
name: string;
dir: string;
@@ -29,7 +32,7 @@ export interface WorkspaceConfig {
security?: GatewaySecurity;
}
const files = (name: string): Record<string, string> => ({
export const workspaceFiles = (name: string): Record<string, string> => ({
"package.json": `{
"name": "${name}",
"private": true,
@@ -40,7 +43,7 @@ const files = (name: string): Record<string, string> => ({
"gateway": "wrnexus gateway"
},
"devDependencies": {
"@wrnexus/cli": "^0.2.0"
"@wrnexus/cli": "${frameworkVersion}"
}
}
`,
@@ -86,7 +89,7 @@ dist/
"main": "src/index.ts",
"exports": { ".": "./src/index.ts" },
"dependencies": {
"@wrnexus/pubsub": "^0.2.0"
"@wrnexus/pubsub": "${frameworkVersion}"
}
}
`,
@@ -124,7 +127,7 @@ ${name}/
\`\`\`bash
bun install
bun run dev # = wrnexus gateway → http://localhost:3000
bun run dev # = wrnexus gateway → http://127.0.0.1:3000
\`\`\`
Add the hosts to your machine (e.g. /etc/hosts):
@@ -133,6 +136,10 @@ Add the hosts to your machine (e.g. /etc/hosts):
127.0.0.1 web.localhost admin.localhost
\`\`\`
Open \`http://localhost:3000\` for the web app or
\`http://admin.localhost:3000\` for the admin app. The ports printed for individual
apps are internal gateway targets, not public workspace URLs.
## Interconnect
- **Shared code:** import \`@app/shared\` in any app.
@@ -162,7 +169,7 @@ export function createWorkspace(name: string): void {
process.exit(1);
}
for (const [rel, contents] of Object.entries(files(name))) {
for (const [rel, contents] of Object.entries(workspaceFiles(name))) {
const target = join(root, rel);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, contents, "utf8");
@@ -198,7 +205,7 @@ export async function runGateway(root: string, args: string[]): Promise<void> {
const portArg = args.find((a) => a.startsWith("--port="));
const hostArg = args.find((a) => a.startsWith("--host="));
const port = portArg ? Number(portArg.split("=")[1]) : 3000;
const hostname = hostArg?.split("=")[1] || "::";
const hostname = hostArg?.split("=")[1];
const mode = args.includes("--prod") ? "production" : "development";
const { startGateway } = await import("@wrnexus/dev-server");