/** * `wrnexus create ` — scaffold a new app from an inline template. * * Kept dependency-free and explicit: the template files live here as strings so * scaffolding works without copying from anywhere on disk. */ 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 cliVersion = currentCliVersion(); // Framework packages use independent patch versions. Keep scaffolds on the // compatible 0.8 line instead of assuming every package shares the CLI patch. export const scaffoldFrameworkRange = "^0.8.8"; /** Map of file (relative to app root) -> contents. */ const TEMPLATE: Record = { // AI/agent context: teaches Claude Code / Cursor / Copilot the WrNexus conventions. "CLAUDE.md": CLAUDE_MD, "public/llms.txt": AI_GUIDE, ".gitignore": `# Dependencies node_modules/ # WRNexusJS and production builds dist/ .wrnexus/ **/.wrnexus/ coverage/ # Environment files and local secrets .env .env.* !.env.example !.env.*.example # Logs and runtime files *.log logs/ *.pid *.pid.lock # Local databases *.db *.db-shm *.db-wal *.sqlite *.sqlite3 uploads/ # Generated native projects mobile/android/ mobile/ios/ mobile/.expo/ # Editors and operating systems .idea/ .vscode/* !.vscode/settings.json !.vscode/extensions.json *.swp *.swo .DS_Store Thumbs.db # TypeScript and test caches *.tsbuildinfo .eslintcache .nyc_output/ `, "package.json": `{ "name": "APP_NAME", "version": "0.1.0", "private": true, "type": "module", "wrnexus": { "version": "${cliVersion}" }, "scripts": { "dev": "wrnexus dev .", "build": "wrnexus build .", "start": "bun dist/server.js", "production": "bun run build && bun run start", "typecheck": "tsc --noEmit", "test": "wrnexus test .", "test:watch": "wrnexus test . --watch", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier . --write", "format:check": "prettier . --check", "doctor": "wrnexus doctor .", "analyze": "wrnexus analyze .", "inspect": "wrnexus inspect packages .", "check": "bun run typecheck && bun run lint && bun run test && bun run format:check" }, "dependencies": { "@wrnexus/ai": "${scaffoldFrameworkRange}", "@wrnexus/auth": "${scaffoldFrameworkRange}", "@wrnexus/captcha": "${scaffoldFrameworkRange}", "@wrnexus/core": "${scaffoldFrameworkRange}", "@wrnexus/csr": "${scaffoldFrameworkRange}", "@wrnexus/db": "${scaffoldFrameworkRange}", "@wrnexus/dev-server": "${scaffoldFrameworkRange}", "@wrnexus/encryption": "${scaffoldFrameworkRange}", "@wrnexus/helpers": "${scaffoldFrameworkRange}", "@wrnexus/i18n": "${scaffoldFrameworkRange}", "@wrnexus/image": "${scaffoldFrameworkRange}", "@wrnexus/jwt": "${scaffoldFrameworkRange}", "@wrnexus/observability": "${scaffoldFrameworkRange}", "@wrnexus/realtime": "${scaffoldFrameworkRange}", "@wrnexus/security": "${scaffoldFrameworkRange}", "@wrnexus/store": "${scaffoldFrameworkRange}", "@wrnexus/styles": "${scaffoldFrameworkRange}", "@wrnexus/tracking": "${scaffoldFrameworkRange}", "@wrnexus/ui": "${scaffoldFrameworkRange}", "@wrnexus/uploader": "${scaffoldFrameworkRange}", "@wrnexus/validation": "${scaffoldFrameworkRange}", "@wrnexus/authz": "${scaffoldFrameworkRange}" }, "devDependencies": { "@wrnexus/cli": "${cliVersion}", "@eslint/js": "^10.0.1", "@iconify-json/lucide": "^1.2.123", "@iconify/tailwind4": "^1.2.3", "@tailwindcss/cli": "^4.3.3", "@types/bun": "^1.3.14", "eslint": "^10.8.1", "prettier": "^3.9.6", "tailwindcss": "^4.3.3", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0" } } `, "tsconfig.json": `{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ESNext", "DOM", "DOM.Iterable"], "types": ["bun"], "strict": true, "skipLibCheck": true, "noEmit": true, "allowImportingTsExtensions": true, "verbatimModuleSyntax": false, "esModuleInterop": true, "resolveJsonModule": true, "jsx": "react-jsx", "jsxImportSource": "@wrnexus/core" }, "include": ["app", "test", "wrnexus.config.ts"], "exclude": ["node_modules", "dist", "**/dist", "**/.wrnexus"] } `, ".env.example": `# Copy to .env for local development. Never commit real secrets. WRNEXUS_PUBLIC_ORIGIN=http://localhost:3000 DATABASE_URL=file:./dev.db REDIS_URL=redis://localhost:6379 AUTH_SECRET=replace-with-at-least-32-random-characters ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key ANTHROPIC_API_KEY= OTEL_EXPORTER_OTLP_ENDPOINT= `, ".env.test.example": `WRNEXUS_PUBLIC_ORIGIN=http://localhost:3000 DATABASE_URL=file:./test.db AUTH_SECRET=test-only-secret-replace-outside-tests `, "eslint.config.js": `import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import js from "@eslint/js"; import tseslint from "typescript-eslint"; const tsconfigRootDir = dirname(fileURLToPath(import.meta.url)); export default tseslint.config( { ignores: [ "node_modules/**", "dist/**", ".wrnexus/**", "**/.wrnexus/**", "mobile/android/**", "mobile/ios/**", ], }, { languageOptions: { parserOptions: { tsconfigRootDir, }, }, }, js.configs.recommended, ...tseslint.configs.recommended, { files: ["**/*.{ts,tsx}"], rules: { "no-undef": "off", "no-console": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": [ "warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_", }, ], }, }, ); `, ".prettierrc.json": `{ "printWidth": 100, "tabWidth": 2, "useTabs": false, "semi": true, "singleQuote": false, "trailingComma": "all", "endOfLine": "lf" } `, ".prettierignore": `node_modules/ dist/ .wrnexus/ **/.wrnexus/ *.log CLAUDE.md `, ".editorconfig": `root = true [*] charset = utf-8 end_of_line = lf indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true `, ".vscode/settings.json": `{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "prettier.requireConfig": true, "[wrn]": { "editor.defaultFormatter": "wrnexus.wrnexus", "editor.formatOnSave": true } } `, ".vscode/extensions.json": `{ "recommendations": ["wrnexus.wrnexus", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] } `, "wrnexus.config.ts": `import type { AppConfig } from "@wrnexus/styles"; const config: AppConfig = { compatibilityDate: "2026-08-02", frameworkBehaviour: 1, // v0.8 defaults: explicit imports, strict template types, safe stores, and // automatic progressive navigation. Package plugins are discovered from the // installed packages above; add custom plugins to this array when needed. plugins: [], imports: { mode: "explicit", autoImport: true, aliases: { "@": "./app" } }, types: { strict: true, noImplicitAny: true, strictNullChecks: true, checkTemplates: true, checkComponentProps: true, generateDeclarations: true, }, functions: { legacyDefaultRuntime: "current" }, stores: { strictMutations: true, persistence: true }, compatibility: { legacyEmit: false, legacyEventProps: false, legacyComponentDiscovery: false, stringLayouts: false, }, experimental: {}, performance: { enforcement: "warn", analyze: true, budgets: { routeJsBytes: 50 * 1024, routeCssBytes: 25 * 1024, lcpMs: 2_500, inpMs: 200, cls: 0.1, }, }, observability: { enabled: true, serviceName: "APP_SLUG", serverTiming: true, sampleRate: 1, exporter: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ? "otlp" : "none", endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, webVitals: true, }, tenancy: { mode: "domain", required: false, rootDomains: ["localhost"] }, build: { cache: true, sourceMaps: true, report: true, adapter: "bun" }, navigation: { mode: "auto" }, devToolbar: { enabled: true, position: "bottom-center", openEditor: true }, mobile: { enabled: true, appId: "com.example.APP_SLUG", appName: "APP_NAME", userAgent: "WrNexusMobile", backgroundColor: "#0f172a", // layout: "mobile", // app/layouts/mobile.wrn // icon: "resources/icon.png", }, // PWA support is enabled automatically. Override any install metadata here. pwa: { name: "APP_NAME", shortName: "APP_NAME", display: "standalone", themeColor: "#6366f1", backgroundColor: "#0f172a", }, seo: { title: "APP_NAME", titleTemplate: "%s | APP_NAME", // Set WRNEXUS_PUBLIC_ORIGIN in production when TLS terminates at a proxy. canonicalBase: process.env.WRNEXUS_PUBLIC_ORIGIN, description: "An SSR-first WrNexus app.", robots: "index,follow", themeColor: "#6366f1", }, styles: { entry: "app/styles/global.css", // Tailwind v4 build. Runs once at dev-serve time (cached; re-run on restart) // and at \`wrnexus build\`. \`@tailwindcss/cli\` writes to stdout, so we capture // and return the final CSS. Delete this hook to drop Tailwind — global.css is // still bundled and served as-is. process: async ({ entryPath, appRoot, mode }) => { const args = ["@tailwindcss/cli", "-i", entryPath!]; if (mode === "production") args.push("--minify"); return await Bun.$.cwd(appRoot)\`bunx \${args}\`.text(); }, }, // Fonts — optimized preconnect, subsetted weights, font-display, and CSP. fonts: { sans: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif', google: [{ family: "Plus Jakarta Sans", weights: [400, 500, 600, 700] }], }, // // // Or self-host (fastest, no third party) — drop files in public/fonts/: // // local: [{ family: "Inter", src: "/fonts/inter.woff2", weight: "100 900", preload: true }], theme: { palette: "violet", default: "light" }, i18n: { default: "en", locales: ["en"] }, db: { driver: "sqlite", url: process.env.DATABASE_URL ?? "file:./dev.db" }, databases: {}, storage: { default: "public", stores: { public: { driver: "local", access: "public", dir: "uploads/public", maxBytes: 10_000_000, accept: ["image/*", "application/pdf"], }, private: { driver: "local", access: "private", dir: "uploads/private", maxBytes: 10_000_000, }, }, }, realtime: { scale: Boolean(process.env.REDIS_URL), redisUrl: process.env.REDIS_URL }, port: Number(process.env.PORT ?? 3000), security: { cors: { enabled: false }, }, profiles: { development: {}, test: { db: { driver: "sqlite", url: "file:./test.db" }, observability: { exporter: "none", sampleRate: 0 }, }, staging: { seo: { robots: "noindex,nofollow" }, performance: { enforcement: "error" }, build: { sourceMaps: true, report: true }, }, production: { seo: { canonicalBase: process.env.WRNEXUS_PUBLIC_ORIGIN }, performance: { enforcement: "error" }, build: { sourceMaps: false, report: true }, devToolbar: false, }, }, }; export default config; `, "public/robots.txt": `User-agent: * Allow: / `, "app/locales/en.json": `{ "common": { "appName": "APP_NAME", "welcome": "Welcome to APP_NAME" } } `, "app/db/migrations/0001_init.sql": `-- Create application tables here. -- Run with: bunx wrnexus db migrate `, "app/db/seed.ts": `// Add deterministic development seed data here. export async function seed(): Promise {} `, "app/schemas/contact.ts": `import { v } from "@wrnexus/validation"; export const contactSchema = v.object({ email: v.string().email(), message: v.string().min(10).max(2_000), }); `, "app/styles/global.css": `/* * Global stylesheet. Tailwind v4 is compiled by the styles.process hook in * wrnexus.config.ts and served at /__wrnexus/styles.css on every page. * * @source tells Tailwind which files to scan for class names. */ @import "tailwindcss"; @plugin "@iconify/tailwind4"; @source "../**/*.wrn"; @source "../**/*.tsx"; /* Make Tailwind's \`dark:\` variant follow the framework's data-theme attribute * (set on by the theme system), not the OS setting. Any element with * data-wrn-theme-toggle flips it. */ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); body { font-family: var(--wrn-font-sans, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif); } `, "app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected // page layout and merges SEO metadata, styles, and scripts into /. // Resolved theme, language, URL, and pathname are available as safe SSR props, // so document attributes do not need a client-side correction. Request cookies // remain server-private and are never passed into document hydration. layout Document { runtime = "server" props { theme = "light" language = "en" url = "" pathname = "/" } view {
} } `, "app/pages/index.wrn": `// Home page (route: /). SSR-first: the view is server-rendered, then components // (.wrn files under app/components) hydrate in the browser. Styled with Tailwind. page Home { seo { title = "Home" description = "APP_NAME — built with WrNexus, an SSR-first Bun framework." } view {
W APP_NAME

SSR-first · Bun-native

Server-rendered.
Instantly interactive.

APP_NAME runs on WrNexus — write .wrn components, ship no client boilerplate, and let the server do the work.

live · hydrated on the server
This button works. You wrote zero client JavaScript.

edit app/pages/index.wrn to make it yours

} } `, "app/pages/about.wrn": `page About { seo { title = "About" description = "Learn how APP_NAME is built with WrNexus." } view {
← Home

WrNexus application

About APP_NAME

This page is server-rendered from app/pages/about.wrn. Add state, events, components, APIs, and data without switching to another UI framework.

} } `, "app/components/counter.wrn": `// A reusable component. Route: none — mounted inside a page with //
. // // Components render on the SERVER (with their props applied) and are hydrated in // the browser by the generic reactive runtime — they ship no JS of their own. component Counter { // Props arrive as mount attributes, each coerced to the type of its default // (so start="5" arrives as the number 5). props { start = 0 label = "Count" } // State can reference props. \`count\` seeds the reactive scope. state count = start view { } } `, "app/api/hello.ts": `export const GET = async () => { return Response.json({ message: "Hello API" }); }; `, "app/api/ai.ts": `// POST /api/ai { "prompt": "..." } → Claude's reply. // Set ANTHROPIC_API_KEY in your environment (e.g. a .env file) to enable this. import { createAI } from "@wrnexus/ai"; import type { Context } from "@wrnexus/core"; const ai = createAI(); // reads ANTHROPIC_API_KEY; defaults to claude-opus-4-8 export const POST = async (ctx: Context) => { if (!process.env.ANTHROPIC_API_KEY) { return Response.json({ error: "Set ANTHROPIC_API_KEY to use AI." }, { status: 501 }); } const { prompt } = await ctx.req.json().catch(() => ({})); if (!prompt) return Response.json({ error: "Provide a 'prompt'." }, { status: 400 }); // Stream the reply back as plain text. Use \`ai.generate(prompt)\` for a one-shot string. return ai.streamResponse(prompt); }; `, "app/middleware/logger.ts": `import type { Middleware } from "@wrnexus/core"; const logger: Middleware = async (ctx, next) => { console.log(ctx.req.method, ctx.url.pathname); return next(); }; export default logger; `, "app/realtime/chat.ts": `// ws:///realtime/chat — a simple broadcast room. // // The client side is the framework's realtime runtime; a page opts in with // \`data-room="chat"\`. Here we only handle room events. // // client.send(msg) → just this connection // client.broadcast(msg) → everyone else in the room // client.room.broadcast(msg) → everyone, including the sender import { defineRoom } from "@wrnexus/core"; export default defineRoom({ onConnect(client) { client.send({ type: "system", text: "connected" }); }, onMessage(client, msg) { // Echo each message to the whole room so every tab stays in sync. client.room.broadcast({ type: "message", data: msg }); }, }); `, "test/smoke.test.ts": `import { expect, test } from "bun:test"; import { parseOrThrow } from "@wrnexus/validation"; import { contactSchema } from "../app/schemas/contact.ts"; test("starter validation schema accepts a contact request", () => { expect( parseOrThrow(contactSchema, { email: "hello@example.com", message: "Hello from the generated application.", }), ).toEqual({ email: "hello@example.com", message: "Hello from the generated application.", }); }); `, }; /** * Write the app template into `root` (absolute), substituting the app name. * Reused by `wrnexus create` and the workspace scaffolder. Refuses to overwrite. */ export function scaffoldApp(root: string, appName: string): void { if (existsSync(root)) { console.error(`Refusing to overwrite existing directory: ${root}`); process.exit(1); } for (const [rel, contents] of Object.entries(TEMPLATE)) { const target = join(root, rel); mkdirSync(dirname(target), { recursive: true }); const appSlug = appName.toLowerCase().replace(/[^a-z0-9]+/g, "") || "app"; writeFileSync( target, contents.replaceAll("APP_NAME", appName).replaceAll("APP_SLUG", appSlug), "utf8", ); } } export function createApp(name: string): void { if (!name) { console.error("Usage: wrnexus create "); process.exit(1); } scaffoldApp(resolve(process.cwd(), name), name); console.log(`✓ Created ${name}`); console.log(`\nNext steps:`); console.log(` cd ${name}`); console.log(` bun install`); console.log(` bun run dev`); }