/** * `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 frameworkVersion = currentCliVersion(); /** 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 # Logs and runtime files *.log logs/ *.pid *.pid.lock # Local databases *.db *.db-shm *.db-wal *.sqlite *.sqlite3 # 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": "${frameworkVersion}" }, "scripts": { "dev": "wrnexus dev .", "build": "wrnexus build .", "start": "bun dist/server.js", "production": "bun run build && bun run start", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier . --write", "format:check": "prettier . --check", "check": "bun run lint && bun run format:check" }, "dependencies": { "@wrnexus/ai": "${frameworkVersion}", "@wrnexus/core": "${frameworkVersion}", "@wrnexus/dev-server": "${frameworkVersion}", "@wrnexus/helpers": "${frameworkVersion}", "@wrnexus/styles": "${frameworkVersion}", "@wrnexus/validation": "${frameworkVersion}", "@wrnexus/db": "${frameworkVersion}" }, "devDependencies": { "@wrnexus/cli": "${frameworkVersion}", "@eslint/js": "^9.0.0", "@iconify-json/lucide": "^1.2.118", "@iconify/tailwind4": "^1.2.3", "@tailwindcss/cli": "^4.0.0", "@types/bun": "latest", "eslint": "^9.0.0", "prettier": "latest", "tailwindcss": "^4.0.0", "typescript": "^5.5.0", "typescript-eslint": "latest" } } `, "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", "wrnexus.config.ts"], "exclude": ["node_modules", "dist", "**/dist", "**/.wrnexus"] } `, "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 = { 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 }], // security: { // cors: { // enabled: true, // origin: ["http://localhost:5173"], // }, // }, }; export default config; `, "public/robots.txt": `User-agent: * Allow: / `, "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-wire-theme-toggle flips it. */ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); body { font-family: var(--wire-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 /. // Request cookies, resolved theme, language, URL, and pathname are available // as SSR props, so document attributes do not need a client-side correction. layout Document { props { cookies = {} 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"; const ai = createAI(); // reads ANTHROPIC_API_KEY; defaults to claude-opus-4-8 export const POST = async (ctx) => { 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": `export default async function logger(ctx, next) { console.log(ctx.req.method, ctx.url.pathname); return next(); } `, "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 }); }, }); `, }; /** * 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`); }