release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+54 -2
View File
@@ -1,5 +1,18 @@
# @wrnexus/cli
Production parity commands:
```bash
wrnexus build .
wrnexus preview . --port=3000
wrnexus dev . --production-runtime
```
`preview` refuses to start without `dist/server.js` and executes that exact
artifact with the production profile. Production-runtime development rebuilds
the same minified artifact after app, public, or configuration changes and
keeps the last good server running when a rebuild fails.
> The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
@@ -26,6 +39,30 @@ bunx wrnexus dev
## Commands
### Local production services
`wrnexus dev . --services` starts the application and the bounded local database,
cache, mail, SMS, webhook, storage, queue, cron, authentication and metrics simulator.
It generates a localhost/`*.localhost` development certificate under
`.wrnexus/certificates/` and serves both the application and service console over HTTPS.
Trust that certificate locally to remove the browser warning. Use `--services-http` only
when an external development proxy already terminates TLS.
### Exact production runtime with live updates
`wrnexus dev . --production-runtime` rebuilds and executes `dist/server.js` with
production resolution, serialization, caching, headers and assets. The supervisor keeps
the last good process when a build fails. On a successful rebuild the opt-in production
HMR socket reconnects, requests the new document and morphs it into the browser; ordinary
`wrnexus preview` and deployed production servers never include that client.
### API platform
`wrnexus api generate [app-dir]` (or `api docs`) derives operations from file routes and
emits `generated/api/openapi.json`, safe static documentation, Postman collection, curl examples,
and TypeScript, JavaScript, Java, Go and Python SDKs. Generate one client with
`wrnexus sdk generate <language> [app-dir]`.
Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=<name>` (see [Profiles](#profiles)).
| Command | Purpose |
@@ -46,10 +83,17 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `wrnexus db <cmd>` | Database migrations and tooling (see [db](#wrnexus-db)). |
| `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). |
| `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. |
| `wrnexus compatibility check` | Check whether behavior defaults are explicitly pinned and current. |
| `wrnexus compatibility explain` | Explain configured, effective, and current compatibility behavior. |
| `wrnexus compatibility upgrade` | Back up config and explicitly opt into reviewed current behavior. |
| `wrnexus help` | Print usage. |
`wrnexus g` is an alias for `wrnexus generate`.
Compatibility upgrades never happen implicitly. New applications pin
`compatibilityDate` and `frameworkBehaviour`; existing applications use
`wrnexus compatibility explain` before the backed-up, idempotent upgrade command.
### `wrnexus dev`
Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`).
@@ -76,7 +120,9 @@ bun dist/server.js # PORT env var optional
### `wrnexus create`
Scaffolds a new app from an inline (dependency-free) template — `package.json`, `.gitignore`, config, and starter `app/` files. Use `npm run dev` during development, `npm run build && npm start` for production, or `npm run production` to build and start in one command. The generated production server currently requires Bun even when npm is used to manage packages and scripts.
Scaffolds a complete v0.8 app from an inline template. The generated project includes strict TypeScript, ESLint and Prettier, editor recommendations, environment templates, database migrations, locales, schemas, tests, API/middleware/realtime examples, Tailwind and Iconify, PWA/mobile defaults, and the framework package kits. Its `wrnexus.config.ts` documents the current imports, types, stores, performance, observability, tenancy, build, navigation, theme, i18n, database, storage, realtime, security, and profile configuration.
Use `bun run dev` during development, `bun run check` for the complete typecheck/lint/test/format gate, `bun run build && bun run start` for production, or `bun run production` to build and start in one command.
### `wrnexus update`
@@ -162,7 +208,7 @@ wrnexus db status --db=analytics
### `wrnexus workspace` and `wrnexus gateway`
`workspace <name>` scaffolds a monorepo: several WrNexus apps under `apps/*` and shared libraries under `packages/*`, plus a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log).
`workspace <name>` scaffolds a monorepo: complete v0.8 apps under `apps/*`, shared libraries under `packages/*`, root TypeScript/lint/format/editor/environment tooling, and a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). Newly added workspace apps use the same current scaffold.
```bash
wrnexus workspace acme
@@ -241,6 +287,12 @@ wrnexus update --latest
wrnexus doctor
```
Use `wrnexus doctor --fix [app-dir]` to apply conservative repairs before the
health check: create missing `app/pages` and a default config, align skewed
`@wrnexus/*` dependency ranges, record the current migration marker, and format
only syntax-valid `.wrn` files. Invalid JSON or WRN sources are reported/skipped
instead of overwritten; repeat runs are idempotent.
## Profiles
Pass `--profile=<name>` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.<profile>`, `.env.<profile>.local`) into `process.env`.
+7 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cli",
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"main": "src/index.ts",
"exports": {
@@ -20,8 +20,13 @@
"@wrnexus/ui": "workspace:*",
"@wrnexus/validation": "workspace:*",
"@wrnexus/i18n": "workspace:*",
"@wrnexus/mcp": "workspace:*",
"@wrnexus/playground": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/plugin": "workspace:*",
"@wrnexus/syntax": "workspace:*"
"@wrnexus/syntax": "workspace:*",
"@wrnexus/typecheck": "workspace:*",
"@wrnexus/security": "workspace:*",
"selfsigned": "^5.5.0"
}
}
+34 -1
View File
@@ -5,7 +5,25 @@ interface BuildReport {
frameworkVersion: string;
generatedAt: string;
adapter: string;
routes: Array<{ path: string; source: string; sourceBytes: number; dynamicParams: string[] }>;
routes: Array<{
path: string;
source: string;
sourceBytes: number;
dynamicParams: string[];
optimization?: {
staticNodes: number;
reactiveRegions: number;
eliminatedBranches: number;
unusedState: string[];
unusedHandlers: string[];
constantProps: string[];
unusedLocalCssClasses: string[];
batchableStateUpdates: number;
memoizableComponents: string[];
preloadDependencies: string[];
serverOnlyModules: string[];
};
}>;
assets: Array<{ file: string; bytes: number }>;
measurements: Record<string, number>;
budgetViolations: Array<{ metric: string; budget: number; actual: number; overBy: number }>;
@@ -42,6 +60,21 @@ export function runAnalyze(appRoot: string, args: string[]): boolean {
for (const asset of report.assets.slice(0, 12)) {
console.log(` ${bytes(asset.bytes).padStart(10)} ${asset.file}`);
}
console.log("\nCompiler optimizations:");
for (const route of report.routes.filter((item) => item.optimization)) {
const optimization = route.optimization!;
console.log(
` ${route.path}: ${optimization.staticNodes} static nodes, ${optimization.reactiveRegions} reactive regions, ${optimization.eliminatedBranches} branches removed`,
);
if (
optimization.unusedState.length ||
optimization.unusedHandlers.length ||
optimization.unusedLocalCssClasses.length
)
console.log(
` candidates: ${[...optimization.unusedState, ...optimization.unusedHandlers, ...optimization.unusedLocalCssClasses].join(", ")}`,
);
}
if (report.budgetViolations.length) {
console.log("\nBudget violations:");
for (const violation of report.budgetViolations) {
+237
View File
@@ -0,0 +1,237 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, dirname, join, relative, resolve } from "node:path";
import { buildRouter } from "@wrnexus/router";
export type SdkLanguage = "typescript" | "javascript" | "java" | "go" | "python";
export interface ApiOperation {
id: string;
method: string;
path: string;
source: string;
summary?: string;
description?: string;
webhook?: { event: string; payloadSchema?: string; signatureHeader?: string };
}
export interface ApiArtifacts {
operations: ApiOperation[];
files: string[];
}
function methods(file: string): string[] {
const source = readFileSync(file, "utf8");
const values = [
...source.matchAll(
/export\s+(?:async\s+)?(?:const|function)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g,
),
].map((match) => match[1]!);
if (file.endsWith(".wrn"))
for (const match of source.matchAll(/\bapi\s+(GET|POST|PUT|PATCH|DELETE)\s+/g))
values.push(match[1]!);
return [...new Set(values.length ? values : ["GET"])];
}
function openapiPath(path: string): string {
return path
.replace(/\[\.\.\.([A-Za-z_$][\w$]*)\]/g, "{$1}")
.replace(/\[([A-Za-z_$][\w$]*)\]/g, "{$1}")
.replace(/:([A-Za-z_$][\w$]*)/g, "{$1}")
.replace(/\*([A-Za-z_$][\w$]*)/g, "{$1}");
}
function operationId(method: string, path: string): string {
const words =
`${method.toLowerCase()}-${path.replace(/^\/api\/?/, "").replace(/[^A-Za-z0-9]+/g, "-") || "root"}`
.split("-")
.filter(Boolean);
return (
words[0]! +
words
.slice(1)
.map((word) => word[0]!.toUpperCase() + word.slice(1))
.join("")
);
}
export function inspectApi(appRoot: string): ApiOperation[] {
const root = resolve(appRoot);
const router = buildRouter(join(root, "app"));
return router.api.flatMap((route) => {
const sourceText = readFileSync(route.file, "utf8");
const field = (name: string) =>
new RegExp(`${name}\\s*:\\s*["']([^"']+)["']`).exec(sourceText)?.[1];
const webhook = /\b(?:defineWebhook\s*\(|webhook\s*=)\s*\{/.test(sourceText)
? {
event: field("event") ?? operationId("event", route.raw),
payloadSchema: field("payloadSchema"),
signatureHeader: field("signatureHeader"),
}
: undefined;
return methods(route.file).map((method) => ({
id: operationId(method, route.raw),
method,
path: openapiPath(route.raw),
source: relative(root, route.file).replace(/\\/g, "/"),
summary: field("summary"),
description: field("description"),
webhook,
}));
});
}
export function createOpenApi(operations: ApiOperation[], title = "WRNexus API") {
const paths: Record<string, Record<string, unknown>> = {};
for (const operation of operations) {
const parameters = [...operation.path.matchAll(/\{([^}]+)\}/g)].map((match) => ({
name: match[1],
in: "path",
required: true,
schema: { type: "string" },
}));
(paths[operation.path] ??= {})[operation.method.toLowerCase()] = {
operationId: operation.id,
summary: operation.summary ?? `${operation.method} ${operation.path}`,
...(operation.description ? { description: operation.description } : {}),
tags: ["API"],
parameters,
responses: {
"200": {
description: "Successful response",
content: { "application/json": { schema: {} } },
},
"400": { description: "Invalid request" },
"500": { description: "Internal error" },
},
"x-wrnexus-source": operation.source,
};
}
const webhooks = Object.fromEntries(
operations
.filter((operation) => operation.webhook)
.map((operation) => [
operation.webhook!.event,
{
post: {
summary: operation.summary ?? operation.webhook!.event,
description: operation.description,
parameters: operation.webhook!.signatureHeader
? [
{
name: operation.webhook!.signatureHeader,
in: "header",
required: true,
schema: { type: "string" },
},
]
: [],
requestBody: {
required: true,
content: {
"application/json": {
schema: operation.webhook!.payloadSchema
? { $ref: operation.webhook!.payloadSchema }
: {},
},
},
},
responses: { "200": { description: "Webhook accepted" } },
"x-wrnexus-source": operation.source,
},
},
]),
);
return {
openapi: "3.1.0",
info: { title, version: "0.8.0" },
paths,
...(Object.keys(webhooks).length ? { webhooks } : {}),
};
}
function sdk(language: SdkLanguage, operations: ApiOperation[]): string {
const route = (operation: ApiOperation) =>
operation.path.replace(/\{([^}]+)\}/g, "${encodeURIComponent(params.$1)}");
if (language === "typescript" || language === "javascript")
return `${language === "typescript" ? "export type RequestOptions = { baseUrl?: string; headers?: HeadersInit };\ntype ApiEnvelope = { data?: unknown; error?: { message?: string } };\n" : ""}const request = async (${language === "typescript" ? "method: string, path: string, body: unknown, options: RequestOptions = {}" : "method, path, body, options = {}"}) => { const response = await globalThis.fetch((options.baseUrl || "") + path, { method, headers: { "content-type": "application/json", ...(options.headers || {}) }, body: body === undefined ? undefined : JSON.stringify(body) }); const value${language === "typescript" ? ": ApiEnvelope" : ""} = await response.json(); if (!response.ok) throw Object.assign(new Error(value?.error?.message || "API request failed"), { status: response.status, body: value }); return value.data ?? value; };\n${operations.map((operation) => `export const ${operation.id} = (params${language === "typescript" ? ": Record<string, string> = {}" : " = {}"}, body${language === "typescript" ? ": unknown" : ""}, options${language === "typescript" ? ": RequestOptions = {}" : " = {}"}) => { ${operation.path.includes("{") ? "" : "void params; "}return request("${operation.method}", \`${route(operation)}\`, body, options); };`).join("\n")}\n`;
if (language === "python")
return `import json, urllib.request\n\nclass WrnexusApi:\n def __init__(self, base_url): self.base_url = base_url.rstrip('/')\n def request(self, method, path, body=None):\n data = None if body is None else json.dumps(body).encode()\n request = urllib.request.Request(self.base_url + path, data=data, method=method, headers={'content-type':'application/json'})\n with urllib.request.urlopen(request) as response: return json.load(response)\n${operations.map((operation) => ` def ${operation.id}(self, path, body=None): return self.request('${operation.method}', path, body)`).join("\n")}\n`;
if (language === "go")
return `package wrnexussdk\n\nimport ("bytes"; "encoding/json"; "fmt"; "net/http")\ntype Client struct { BaseURL string; HTTP *http.Client }\nfunc (c *Client) Request(method, path string, body any) (map[string]any, error) { data,_:=json.Marshal(body); req,_:=http.NewRequest(method,c.BaseURL+path,bytes.NewReader(data)); req.Header.Set("content-type","application/json"); client:=c.HTTP;if client==nil{client=http.DefaultClient};res,err:=client.Do(req);if err!=nil{return nil,err};defer res.Body.Close();if res.StatusCode>=400{return nil,fmt.Errorf("API status %d",res.StatusCode)};var out map[string]any;err=json.NewDecoder(res.Body).Decode(&out);return out,err }\n`;
return `package dev.wrnexus.sdk;\nimport java.net.URI; import java.net.http.*;\npublic final class WrnexusApi { private final String baseUrl; private final HttpClient http = HttpClient.newHttpClient(); public WrnexusApi(String baseUrl){this.baseUrl=baseUrl;} public String request(String method,String path,String json)throws Exception{var request=HttpRequest.newBuilder(URI.create(baseUrl+path)).header("content-type","application/json").method(method,HttpRequest.BodyPublishers.ofString(json==null?"":json)).build();var response=http.send(request,HttpResponse.BodyHandlers.ofString());if(response.statusCode()>=400)throw new IllegalStateException("API status "+response.statusCode());return response.body();} }\n`;
}
function write(path: string, value: string, files: string[]): void {
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, value);
files.push(path);
}
const escapeHtml = (value: unknown) =>
String(value ?? "").replace(
/[&<>"']/g,
(character) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!,
);
export function generateApiArtifacts(
appRoot: string,
languages: SdkLanguage[] = ["typescript"],
): ApiArtifacts {
const root = resolve(appRoot);
const output = join(root, "generated", "api");
const operations = inspectApi(root);
const files: string[] = [];
const spec = createOpenApi(operations, `${basename(root)} API`);
write(join(output, "openapi.json"), JSON.stringify(spec, null, 2) + "\n", files);
write(
join(output, "postman.json"),
JSON.stringify(
{
info: {
name: spec.info.title,
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
},
item: operations.map((operation) => ({
name: operation.id,
request: { method: operation.method, url: `{{baseUrl}}${operation.path}` },
})),
variable: [{ key: "baseUrl", value: "http://localhost:3000" }],
},
null,
2,
) + "\n",
files,
);
write(
join(output, "index.html"),
`<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeHtml(spec.info.title)}</title><style>body{font:16px system-ui;max-width:960px;margin:auto;padding:2rem}code,pre{background:#f4f4f5;padding:.2rem .4rem}article{border-bottom:1px solid #ddd;padding:1rem 0}</style><h1>${escapeHtml(spec.info.title)}</h1><p>OpenAPI 3.1 · ${operations.length} operations · <a href="openapi.json">specification</a></p>${operations.map((operation) => `<article><h2><code>${escapeHtml(operation.method)}</code> ${escapeHtml(operation.path)}</h2><p>${escapeHtml(operation.summary ?? operation.id)}</p>${operation.description ? `<p>${escapeHtml(operation.description)}</p>` : ""}${operation.webhook ? `<p>Webhook event: <code>${escapeHtml(operation.webhook.event)}</code>${operation.webhook.signatureHeader ? ` · signature: <code>${escapeHtml(operation.webhook.signatureHeader)}</code>` : ""}</p>` : ""}<small>${escapeHtml(operation.source)}</small></article>`).join("")}`,
files,
);
write(
join(output, "examples.md"),
`# API examples\n\n${operations.map((operation) => `## ${operation.id}\n\n\`\`\`bash\ncurl -X ${operation.method} "http://localhost:3000${operation.path}"\n\`\`\`\n`).join("\n")}`,
files,
);
const extensions = {
typescript: "ts",
javascript: "js",
java: "java",
go: "go",
python: "py",
} as const;
for (const language of languages)
write(
join(output, "sdk", language, `wrnexus-api.${extensions[language]}`),
sdk(language, operations),
files,
);
return { operations, files };
}
export function runApiCommand(appRoot: string, kind: "api" | "sdk", args: string[]): ApiArtifacts {
const supported: SdkLanguage[] = ["typescript", "javascript", "java", "go", "python"];
const language = args.find((value) => supported.includes(value as SdkLanguage)) as
SdkLanguage | undefined;
if (kind === "sdk" && !language)
throw new Error("WRN-SDK-LANGUAGE: choose typescript, javascript, java, go, or python.");
const result = generateApiArtifacts(appRoot, kind === "sdk" ? [language!] : supported);
console.log(
`✓ Generated ${result.operations.length} API operations and ${result.files.length} artifacts in generated/api`,
);
return result;
}
+185 -8
View File
@@ -21,15 +21,19 @@ import {
statSync,
writeFileSync,
} from "node:fs";
import { basename, extname, join, resolve } from "node:path";
import { basename, dirname, extname, join, relative, resolve } from "node:path";
import { buildRouter, type Route } from "@wrnexus/router";
import { getReactiveRuntime } from "@wrnexus/csr";
import {
analyzeRuntimeImports,
analyzeRuntimeRequirements,
assertValidAst,
generate,
parse,
type RuntimeRequirements,
type DeploymentRuntime,
runtimeCapabilities,
resolveWrnImports,
} from "@wrnexus/compiler";
import {
loadAppConfig,
@@ -64,6 +68,38 @@ const INLINE_CSS_LIMIT_BYTES = 4096;
const fwd = (p: string) => p.replace(/\\/g, "/");
function deploymentRuntime(adapter: string | undefined): DeploymentRuntime | undefined {
if (!adapter) return "bun";
if (["bun", "node", "edge", "worker", "service-worker", "browser"].includes(adapter))
return adapter as DeploymentRuntime;
return undefined;
}
export function validateRuntimeCapabilities(appRoot: string, adapter?: string): void {
const runtime = deploymentRuntime(adapter);
if (!runtime || runtime === "bun" || runtime === "node") return;
const appDir = join(resolve(appRoot), "app");
if (!existsSync(appDir)) return;
const files: string[] = [];
const walk = (directory: string) => {
for (const name of readdirSync(directory)) {
const file = join(directory, name);
const stat = statSync(file);
if (stat.isDirectory()) walk(file);
else if (/\.(?:[cm]?[jt]s|wrn)$/.test(file)) files.push(file);
}
};
walk(appDir);
const diagnostics = files.flatMap((file) =>
analyzeRuntimeImports(readFileSync(file, "utf8"), runtime).map(
(diagnostic) =>
`${fwd(file.slice(resolve(appRoot).length + 1))}: ${diagnostic.code} ${diagnostic.message}`,
),
);
if (diagnostics.length)
throw new Error(`Runtime capability validation failed:\n${diagnostics.join("\n")}`);
}
export async function runBuild(appRoot: string): Promise<void> {
const root = resolve(appRoot);
const appDir = join(root, "app");
@@ -72,6 +108,8 @@ export async function runBuild(appRoot: string): Promise<void> {
const reactivePath = join(distDir, "reactive.js");
const publicDir = join(root, "public");
const distPublicDir = join(distDir, "public");
const config = await loadAppConfig(root);
validateRuntimeCapabilities(root, config.build?.adapter);
console.log(`Building ${appDir} -> ${distDir}`);
@@ -83,11 +121,14 @@ export async function runBuild(appRoot: string): Promise<void> {
console.log(`✓ Public: ${distPublicDir}`);
}
const config = await loadAppConfig(root);
const discoveredPlugins = await discoverPlugins(root, config.plugins, {
includeDevDependencies: true,
strict: true,
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
runtime: deploymentRuntime(config.build?.adapter),
capabilities: [...runtimeCapabilities(deploymentRuntime(config.build?.adapter) ?? "bun")],
enforcePermissions: config.pluginPermissions?.enforce,
grantedPermissions: config.pluginPermissions?.grants,
});
const pluginRunner = createPluginRunner(discoveredPlugins, {
root,
@@ -100,8 +141,27 @@ export async function runBuild(appRoot: string): Promise<void> {
await pluginRunner.configure(config as Record<string, unknown>);
await pluginRunner.configResolved(config as Readonly<Record<string, unknown>>);
const pluginContributions = await pluginRunner.contributions();
const { generateApplicationTypes } = await import("./types.ts");
generateApplicationTypes(root, pluginContributions);
const componentDirs = [uiComponentsDir(), ...pluginContributions.componentDirs];
await pluginRunner.hook("buildStart");
const virtualModules = new Map<string, string>();
for (const [index, module] of pluginContributions.virtualModules.entries()) {
const output = join(compiledDir, `virtual-${index}.ts`);
writeFileSync(
output,
await module.load({
root,
mode: "production",
command: "build",
profile: process.env.WRNEXUS_PROFILE,
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
}),
"utf8",
);
virtualModules.set(module.id, output);
}
// `.wrn` route files are compiled once into deterministic intermediate modules.
// Plugin AST/code transforms run only when configured, so existing applications
@@ -109,12 +169,18 @@ export async function runBuild(appRoot: string): Promise<void> {
let compiledCount = 0;
const compiledFiles = new Map<string, string>();
const runtimeAnalysis = new Map<string, RuntimeRequirements>();
const partialStaticFiles = new Set<string>();
const compileWrn = async (file: string): Promise<void> => {
if (!file.endsWith(".wrn") || compiledFiles.has(file)) return;
const out = join(compiledDir, `route${compiledCount++}.ts`);
// Reserve the artifact before resolving imports so cycles terminate and
// mutually dependent generated modules can point at deterministic paths.
compiledFiles.set(file, out);
const source = readFileSync(file, "utf8");
let ast = parse(source);
assertValidAst(ast, { file, accessibility: true });
ast = await pluginRunner.transformAst(ast, file);
if (ast.renderMode === "partial-static") partialStaticFiles.add(file);
runtimeAnalysis.set(file, analyzeRuntimeRequirements(ast));
const pluginDiagnostics = await pluginRunner.diagnostics(ast, file);
const errors = pluginDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
@@ -126,11 +192,46 @@ export async function runBuild(appRoot: string): Promise<void> {
errors.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("\n"),
);
}
let code = `// compiled from .wrn\n${generate(ast)}`;
code = await pluginRunner.transformCode(code, file);
const out = join(compiledDir, `route${compiledCount++}.ts`);
writeFileSync(out, code, "utf8");
compiledFiles.set(file, out);
try {
let code = `// compiled from .wrn\n${generate(ast)}`;
code = await pluginRunner.transformCode(code, file);
for (const [id, target] of virtualModules) {
const sourcePattern = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
code = code.replace(
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
JSON.stringify(specifier),
);
}
const resolvedImports = resolveWrnImports(ast.structuredImports, file, {
appRoot: root,
mode: config.imports?.mode ?? "compatible",
aliases: config.imports?.aliases,
});
for (const imported of resolvedImports) {
if (imported.diagnostic?.severity === "error") {
throw new Error(`${imported.diagnostic.code}: ${imported.diagnostic.message}`);
}
if (!imported.resolved || !imported.declaration.source.startsWith(".")) continue;
let target = imported.resolved;
if (target.endsWith(".wrn")) {
await compileWrn(target);
target = compiledFiles.get(target)!;
}
const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
code = code.replace(
new RegExp(`(["'])${sourcePattern}\\1`, "g"),
JSON.stringify(specifier),
);
}
writeFileSync(out, code, "utf8");
} catch (error) {
compiledFiles.delete(file);
throw error;
}
};
const importPathFor = (file: string): string => fwd(compiledFiles.get(file) ?? file);
// Regenerate typed DB queries (app/db/queries/*.sql → queries.gen.ts) first, so
@@ -235,6 +336,37 @@ export async function runBuild(appRoot: string): Promise<void> {
...router.layouts.map((layout) => layout.file),
]);
for (const file of wrnFiles) await compileWrn(file);
const partialShells = new Map<string, { shell: string; regions: number }>();
const partialPages = router.pages.filter((route) => partialStaticFiles.has(route.file));
if (partialPages.length > 0) {
const { precomputePartialStaticShell } = await import("@wrnexus/dev-server");
const moduleCache = new Map<string, Record<string, unknown>>();
const loadCompiled = async (file: string): Promise<Record<string, unknown>> => {
const existing = moduleCache.get(file);
if (existing) return existing;
const output = compiledFiles.get(file);
if (!output) throw new Error(`WRN-PARTIAL-STATIC-MODULE: ${file} was not compiled`);
const loaded = (await import(pathToFileURL(output).href)) as Record<string, unknown>;
moduleCache.set(file, loaded);
return loaded;
};
const components = await Promise.all(
router.components.map(async (component) => ({
name: component.name,
mod: await loadCompiled(component.file),
})),
);
for (const route of partialPages) {
const result = await precomputePartialStaticShell(await loadCompiled(route.file), components);
partialShells.set(route.raw, result);
}
writeFileSync(
join(distDir, "partial-shells.json"),
JSON.stringify(Object.fromEntries(partialShells), null, 2) + "\n",
"utf8",
);
console.log(`✓ Partial shells: ${partialShells.size} build-time route shell(s)`);
}
const assetHash = createHash("sha256");
const emittedPluginAssets = await emitPluginAssets(
pluginContributions,
@@ -339,7 +471,8 @@ export async function runBuild(appRoot: string): Promise<void> {
const parts = routes.map((r) => {
const v = `m${counter++}`;
imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(r.file))};`);
return ` { raw: ${JSON.stringify(r.raw)}, mod: ${v} },`;
const partial = partialShells.get(r.raw);
return ` { raw: ${JSON.stringify(r.raw)}, mod: ${v}${partial ? `, staticShell: ${JSON.stringify(partial.shell)}` : ""} },`;
});
return parts.length ? `\n${parts.join("\n")}\n ` : "";
};
@@ -428,6 +561,7 @@ await createProductionServer(
observability: ${JSON.stringify(config.observability ?? {})},
tenancy: ${JSON.stringify(config.tenancy ?? {})},
navigation: ${JSON.stringify(config.navigation ?? {})},
developmentRuntime: process.env.WRNEXUS_PRODUCTION_DEV === "1",
},
);
`;
@@ -485,12 +619,32 @@ await createProductionServer(
source: migration.entry ?? "inline",
}));
report.componentDirs = componentDirs.map(fwd);
report.partialStaticShells = [...partialShells].map(([route, value]) => ({
route,
regions: value.regions,
bytes: Buffer.byteLength(value.shell, "utf8"),
}));
const violations = checkPerformanceBudgets(
config.performance?.budgets ?? {},
report.measurements,
);
report.budgetViolations = violations;
writeFileSync(join(distDir, "build-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
const contributedAdapter = pluginContributions.deploymentAdapters.find(
(adapter) => adapter.name === config.build?.adapter,
);
if (contributedAdapter)
await contributedAdapter.build(
{ distDir, report },
{
root,
mode: "production",
command: "build",
profile: process.env.WRNEXUS_PROFILE,
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
},
);
await pluginRunner.hook("buildEnd", report);
console.log(`✓ Server: ${join(distDir, "server.js")}`);
@@ -636,6 +790,7 @@ interface BuildReport {
clientRuntimes?: Array<{ id: string; publicPath: string; type: string; load: string }>;
migrations?: Array<{ id: string; database: string; source: string }>;
componentDirs?: string[];
partialStaticShells?: Array<{ route: string; regions: number; bytes: number }>;
generatedAt: string;
root: string;
adapter: string;
@@ -650,6 +805,10 @@ interface BuildReport {
needsClientRuntime: boolean;
needsServerRuntime: boolean;
hydrationStrategy: string | null;
reasons: string[];
optimization: RuntimeRequirements["optimization"];
cachePolicy: Record<string, string>;
requiredPermission: string | null;
}>;
assets: Array<{ file: string; bytes: number }>;
measurements: { routeJsBytes: number; routeCssBytes: number; imageBytes: number };
@@ -709,6 +868,24 @@ function createBuildReport(input: {
needsClientRuntime: input.runtimeAnalysis.get(route.file)?.needsClientRuntime ?? true,
needsServerRuntime: input.runtimeAnalysis.get(route.file)?.needsServerRuntime ?? true,
hydrationStrategy: input.runtimeAnalysis.get(route.file)?.hydrationStrategy ?? null,
reasons: input.runtimeAnalysis.get(route.file)?.reasons ?? [
"runtime requirements unavailable",
],
optimization: input.runtimeAnalysis.get(route.file)?.optimization ?? {
staticNodes: 0,
reactiveRegions: 0,
eliminatedBranches: 0,
unusedState: [],
unusedHandlers: [],
constantProps: [],
unusedLocalCssClasses: [],
batchableStateUpdates: 0,
memoizableComponents: [],
preloadDependencies: [],
serverOnlyModules: [],
},
cachePolicy: input.runtimeAnalysis.get(route.file)?.cachePolicy ?? {},
requiredPermission: input.runtimeAnalysis.get(route.file)?.requiredPermission ?? null,
})),
assets,
measurements: {
+76
View File
@@ -0,0 +1,76 @@
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, join, resolve } from "node:path";
import {
CURRENT_COMPATIBILITY_DATE,
CURRENT_FRAMEWORK_BEHAVIOUR,
loadRawConfig,
resolveCompatibility,
} from "@wrnexus/styles";
const CONFIG_NAMES = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"];
function configPath(root: string): string | undefined {
return CONFIG_NAMES.map((name) => join(root, name)).find(existsSync);
}
export async function compatibilityReport(appRoot: string) {
return resolveCompatibility(await loadRawConfig(resolve(appRoot)));
}
export function upgradeCompatibility(appRoot: string): {
file: string;
backup: string;
changed: boolean;
} {
const root = resolve(appRoot);
const file = configPath(root);
if (!file) throw new Error("WRN-COMPATIBILITY-NO-CONFIG: wrnexus.config.ts was not found.");
const source = readFileSync(file, "utf8");
let updated = source;
const replace = (name: string, value: string) => {
const pattern = new RegExp(`(^\\s*${name}\\s*:\\s*)(?:["'][^"']*["']|\\d+)(\\s*,?)`, "m");
if (pattern.test(updated)) updated = updated.replace(pattern, `$1${value}$2`);
else {
const object = /(?:const\s+config[^=]*=|defineConfig\s*\(|export\s+default)\s*\{/m;
if (!object.test(updated))
throw new Error("WRN-COMPATIBILITY-CONFIG-SHAPE: unable to locate the root config object.");
updated = updated.replace(object, (match) => `${match}\n ${name}: ${value},`);
}
};
replace("compatibilityDate", JSON.stringify(CURRENT_COMPATIBILITY_DATE));
replace("frameworkBehaviour", String(CURRENT_FRAMEWORK_BEHAVIOUR));
if (updated === source) return { file, backup: "", changed: false };
const directory = join(root, ".wrnexus", "compatibility-backups");
mkdirSync(directory, { recursive: true });
const backup = join(directory, `${Date.now()}-${basename(file)}`);
copyFileSync(file, backup);
writeFileSync(file, updated, "utf8");
return { file, backup, changed: true };
}
export async function runCompatibilityCommand(
appRoot: string,
command = "check",
args: string[] = [],
): Promise<boolean> {
if (command === "upgrade") {
const result = upgradeCompatibility(appRoot);
if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
else
console.log(
result.changed
? `✓ Compatibility policy upgraded\n backup: ${result.backup}`
: "✓ Compatibility policy already current",
);
}
const report = await compatibilityReport(appRoot);
if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
else {
console.log(`Compatibility date: ${report.effectiveDate} (current ${report.currentDate})`);
console.log(
`Framework behaviour: ${report.effectiveBehaviour} (current ${report.currentBehaviour})`,
);
for (const message of report.messages) console.log(`- ${message}`);
}
return !report.needsUpgrade && !report.future;
}
+80
View File
@@ -0,0 +1,80 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import {
ContractRegistry,
checkContractCompatibility,
type ContractSnapshot,
} from "@wrnexus/validation";
const CURRENT_FILE = "wrnexus.contracts.json";
const BASELINE_FILE = join(".wrnexus", "contracts.json");
function validateSnapshot(value: unknown, source: string): ContractSnapshot {
const snapshot = value as Partial<ContractSnapshot>;
if (snapshot?.format !== 1 || !Array.isArray(snapshot.contracts)) {
throw new Error(`WRN-CONTRACT-FORMAT: ${source} is not a version 1 contract snapshot.`);
}
return snapshot as ContractSnapshot;
}
async function currentSnapshot(appRoot: string): Promise<ContractSnapshot> {
const modulePath = join(appRoot, "app", "contracts.ts");
if (existsSync(modulePath)) {
const imported = (await import(`${modulePath}?t=${Date.now()}`)) as {
default?: ContractRegistry | ContractSnapshot;
contracts?: ContractRegistry | ContractSnapshot;
};
const value = imported.contracts ?? imported.default;
if (value instanceof ContractRegistry) return value.snapshot();
return validateSnapshot(value, modulePath);
}
const jsonPath = join(appRoot, CURRENT_FILE);
if (!existsSync(jsonPath)) {
throw new Error(
`WRN-CONTRACT-SOURCE: create app/contracts.ts exporting a ContractRegistry, or ${CURRENT_FILE}.`,
);
}
return validateSnapshot(JSON.parse(await readFile(jsonPath, "utf8")), jsonPath);
}
export interface ContractCommandResult {
ok: boolean;
issueCount: number;
baseline: string;
}
export async function runContractsCommand(
root: string,
command = "check",
args: string[] = [],
): Promise<ContractCommandResult> {
const appRoot = resolve(root);
const baseline = join(appRoot, BASELINE_FILE);
const current = await currentSnapshot(appRoot);
if (command === "snapshot") {
await mkdir(dirname(baseline), { recursive: true });
await writeFile(baseline, `${JSON.stringify(current, null, 2)}\n`, "utf8");
if (args.includes("--json")) console.log(JSON.stringify({ ok: true, baseline }));
else console.log(`✓ Contract baseline written: ${baseline}`);
return { ok: true, issueCount: 0, baseline };
}
if (command !== "check") throw new Error(`WRN-CONTRACT-COMMAND: unknown command '${command}'.`);
if (!existsSync(baseline)) {
throw new Error(`WRN-CONTRACT-BASELINE: no baseline found; run 'wrnexus contracts snapshot'.`);
}
const previous = validateSnapshot(JSON.parse(await readFile(baseline, "utf8")), baseline);
const issues = checkContractCompatibility(previous, current);
if (args.includes("--json")) {
console.log(JSON.stringify({ ok: issues.length === 0, issues, baseline }, null, 2));
} else if (issues.length === 0) {
console.log(`${current.contracts.length} contracts are backward compatible.`);
} else {
console.error(`Breaking contract changes detected (${issues.length}):`);
for (const issue of issues) {
console.error(` ${issue.code} ${issue.contract}: ${issue.message}`);
if (issue.consumers.length) console.error(` Consumers: ${issue.consumers.join(", ")}`);
}
}
return { ok: issues.length === 0, issueCount: issues.length, baseline };
}
+178 -12
View File
@@ -30,6 +30,7 @@ coverage/
.env
.env.*
!.env.example
!.env.*.example
# Logs and runtime files
*.log
@@ -43,6 +44,7 @@ logs/
*.db-wal
*.sqlite
*.sqlite3
uploads/
# Generated native projects
mobile/android/
@@ -77,20 +79,41 @@ Thumbs.db
"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",
"check": "bun run lint && bun run format: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": "${frameworkVersion}",
"@wrnexus/auth": "${frameworkVersion}",
"@wrnexus/captcha": "${frameworkVersion}",
"@wrnexus/core": "${frameworkVersion}",
"@wrnexus/csr": "${frameworkVersion}",
"@wrnexus/db": "${frameworkVersion}",
"@wrnexus/dev-server": "${frameworkVersion}",
"@wrnexus/encryption": "${frameworkVersion}",
"@wrnexus/helpers": "${frameworkVersion}",
"@wrnexus/i18n": "${frameworkVersion}",
"@wrnexus/image": "${frameworkVersion}",
"@wrnexus/jwt": "${frameworkVersion}",
"@wrnexus/observability": "${frameworkVersion}",
"@wrnexus/realtime": "${frameworkVersion}",
"@wrnexus/security": "${frameworkVersion}",
"@wrnexus/store": "${frameworkVersion}",
"@wrnexus/styles": "${frameworkVersion}",
"@wrnexus/tracking": "${frameworkVersion}",
"@wrnexus/ui": "${frameworkVersion}",
"@wrnexus/uploader": "${frameworkVersion}",
"@wrnexus/validation": "${frameworkVersion}",
"@wrnexus/db": "${frameworkVersion}"
"@wrnexus/authz": "${frameworkVersion}"
},
"devDependencies": {
"@wrnexus/cli": "${frameworkVersion}",
@@ -124,9 +147,22 @@ Thumbs.db
"jsx": "react-jsx",
"jsxImportSource": "@wrnexus/core"
},
"include": ["app", "wrnexus.config.ts"],
"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";
@@ -220,6 +256,56 @@ trim_trailing_whitespace = true
"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",
@@ -272,18 +358,77 @@ const config: AppConfig = {
// // 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"],
// },
// },
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<void> {}
`,
"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
@@ -441,10 +586,11 @@ component Counter {
"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) => {
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 });
}
@@ -455,10 +601,14 @@ export const POST = async (ctx) => {
return ai.streamResponse(prompt);
};
`,
"app/middleware/logger.ts": `export default async function logger(ctx, next) {
"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://<host>/realtime/chat — a simple broadcast room.
//
@@ -480,6 +630,22 @@ export default defineRoom({
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.",
});
});
`,
};
+30 -1
View File
@@ -20,6 +20,8 @@ import { pathToFileURL } from "node:url";
import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
import {
generateQueriesFile,
analyzeMigrations,
loadMigrations,
migrate,
parseQueries,
rollback,
@@ -125,6 +127,18 @@ export async function runDbCommand(
const dbBase = dbBaseOf(appDir, dbName);
const migrationsDir = join(dbBase, "migrations");
const label = dbName ? ` (db: ${dbName})` : "";
const safetyIssues = analyzeMigrations(loadMigrations(migrationsDir));
if (sub === "check") {
if (!safetyIssues.length) console.log(`✓ Migration rollout safety check passed${label}.`);
for (const issue of safetyIssues) {
console[issue.severity === "error" ? "error" : "warn"](
`${issue.code} ${issue.migration}: ${issue.statement}\n ${issue.recommendation}`,
);
}
if (safetyIssues.some((issue) => issue.severity === "error")) process.exitCode = 1;
return;
}
if (dbName && !config.databases?.[dbName]) {
console.error(
@@ -213,6 +227,21 @@ export async function runDbCommand(
break;
}
case "migrate": {
const pending = new Set(
(await status(db, migrationsDir))
.filter((migration) => !migration.applied)
.map((migration) => migration.name),
);
const pendingIssues = safetyIssues.filter((issue) => pending.has(issue.migration));
const blockers = pendingIssues.filter((issue) => issue.severity === "error");
for (const issue of pendingIssues.filter((item) => item.severity === "warning")) {
console.warn(`${issue.code} ${issue.migration}: ${issue.recommendation}`);
}
if (blockers.length && !args.includes("--allow-breaking")) {
throw new Error(
`WRN-DB-UNSAFE-MIGRATION: ${blockers.length} breaking rollout operation(s) found. Run 'wrnexus db check' and use an expand/contract migration; --allow-breaking explicitly overrides this gate.`,
);
}
const applied = await migrate(db, migrationsDir);
console.log(
applied.length
@@ -234,7 +263,7 @@ export async function runDbCommand(
}
default:
console.error(
"Usage: wrnexus db <migrate|rollback|status|generate|seed|studio [table]|new [name] [--from-models]> [--db=<name>]",
"Usage: wrnexus db <check|migrate|rollback|status|generate|seed|studio [table]|new [name] [--from-models]> [--db=<name>]",
);
process.exit(1);
}
+184
View File
@@ -0,0 +1,184 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { generateDocker } from "./docker.ts";
export const DEPLOY_TARGETS = [
"docker",
"kubernetes",
"systemd",
"railway",
"render",
"fly",
] as const;
export type DeployTarget = (typeof DEPLOY_TARGETS)[number];
const ENVIRONMENT = `# Copy to .env.production and replace every required value.
NODE_ENV=production
PORT=3000
HOST=0.0.0.0
DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DB
SESSION_SECRET=REPLACE_WITH_AT_LEAST_32_RANDOM_BYTES
# OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
`;
const OPERATIONS = `# WRNexus deployment operations
- Liveness: \`GET /healthz\`
- Readiness: \`GET /readyz\` (includes registered dependency checks)
- Migrations: run \`bunx wrnexus db migrate --profile=production\` once per release before scaling.
- Shutdown: the Bun production server drains on SIGTERM/SIGINT.
- Assets: \`dist/public\` files are content-addressed and may be cached immutably by a CDN.
- Secrets: provide \`DATABASE_URL\` and \`SESSION_SECRET\` through the platform secret store; never commit production env files.
- Logs: stdout/stderr are structured for platform collection. Configure OTLP for centralized telemetry.
- Scaling: start with 250m CPU/256Mi memory, use readiness probes, and scale horizontally from request latency and CPU.
`;
const KUBERNETES = `apiVersion: v1
kind: Service
metadata:
name: wrnexus
spec:
selector: { app: wrnexus }
ports: [{ name: http, port: 80, targetPort: 3000 }]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wrnexus
spec:
replicas: 2
selector: { matchLabels: { app: wrnexus } }
template:
metadata: { labels: { app: wrnexus } }
spec:
containers:
- name: app
image: ghcr.io/OWNER/APP:latest
ports: [{ containerPort: 3000 }]
envFrom: [{ secretRef: { name: wrnexus-secrets } }]
livenessProbe: { httpGet: { path: /healthz, port: 3000 }, initialDelaySeconds: 5 }
readinessProbe: { httpGet: { path: /readyz, port: 3000 }, initialDelaySeconds: 5 }
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: "1", memory: 512Mi }
lifecycle: { preStop: { exec: { command: ["sh", "-c", "sleep 5"] } } }
terminationGracePeriodSeconds: 30
---
apiVersion: batch/v1
kind: Job
metadata:
name: wrnexus-migrate
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: ghcr.io/OWNER/APP:latest
command: ["bunx", "wrnexus", "db", "migrate", "--profile=production"]
envFrom: [{ secretRef: { name: wrnexus-secrets } }]
`;
const SYSTEMD = `[Unit]
Description=WRNexus application
After=network-online.target
[Service]
Type=simple
WorkingDirectory=/srv/wrnexus
EnvironmentFile=/etc/wrnexus/wrnexus.env
ExecStartPre=/usr/bin/bunx wrnexus db migrate --profile=production
ExecStart=/usr/bin/bun dist/server.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
User=wrnexus
Group=wrnexus
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/srv/wrnexus
[Install]
WantedBy=multi-user.target
`;
const NGINX = `server {
listen 80;
server_name example.com;
location /assets/ { root /srv/wrnexus/dist/public; expires 1y; add_header Cache-Control "public, immutable"; }
location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Request-ID $request_id; }
}
`;
const RAILWAY = `[build]
builder = "DOCKERFILE"
[deploy]
startCommand = "bun dist/server.js"
healthcheckPath = "/readyz"
restartPolicyType = "ON_FAILURE"
preDeployCommand = ["bunx wrnexus db migrate --profile=production"]
`;
const RENDER = `services:
- type: web
name: wrnexus
runtime: docker
healthCheckPath: /readyz
preDeployCommand: bunx wrnexus db migrate --profile=production
envVars:
- key: DATABASE_URL
sync: false
- key: SESSION_SECRET
sync: false
`;
const FLY = `app = "wrnexus-app"
primary_region = "bom"
[build]
dockerfile = "Dockerfile"
[env]
PORT = "3000"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1
[[http_service.checks]]
path = "/readyz"
interval = "15s"
timeout = "2s"
[deploy]
release_command = "bunx wrnexus db migrate --profile=production"
`;
function write(root: string, relative: string, content: string, files: string[]): void {
const path = join(root, relative);
if (existsSync(path)) return;
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, content, "utf8");
files.push(relative);
}
export function generateDeployment(appRoot: string, target: string): string[] {
if (!DEPLOY_TARGETS.includes(target as DeployTarget))
throw new Error(`WRN-DEPLOY-TARGET: use ${DEPLOY_TARGETS.join(" | ")}.`);
const root = resolve(appRoot);
const files: string[] = [];
if (target === "docker" || ["kubernetes", "railway", "render", "fly"].includes(target))
generateDocker(root);
write(root, ".env.production.example", ENVIRONMENT, files);
write(root, "deploy/README.md", OPERATIONS, files);
if (target === "kubernetes") write(root, "deploy/kubernetes.yaml", KUBERNETES, files);
if (target === "systemd") {
write(root, "deploy/wrnexus.service", SYSTEMD, files);
write(root, "deploy/nginx.conf", NGINX, files);
}
if (target === "railway") write(root, "railway.toml", RAILWAY, files);
if (target === "render") write(root, "render.yaml", RENDER, files);
if (target === "fly") write(root, "fly.toml", FLY, files);
return files;
}
+81 -2
View File
@@ -13,6 +13,7 @@
import { spawn, type ChildProcess } from "node:child_process";
import { resolve, join } from "node:path";
import { existsSync, watch, type FSWatcher } from "node:fs";
import { fileURLToPath } from "node:url";
import { RESTART_EXIT_CODE } from "@wrnexus/dev-server";
@@ -20,7 +21,12 @@ import { RESTART_EXIT_CODE } from "@wrnexus/dev-server";
// it works whether @wrnexus/dev-server is a workspace or an installed dependency.
const SERVE_ENTRY = fileURLToPath(import.meta.resolve("@wrnexus/dev-server/serve-entry"));
export function runDev(appRoot: string, port: number, hostname = "::"): void {
export function runDev(
appRoot: string,
port: number,
hostname = "::",
tls?: { certFile: string; keyFile: string },
): void {
const appDir = join(resolve(appRoot), "app");
let child: ChildProcess | null = null;
let shuttingDown = false;
@@ -28,7 +34,15 @@ export function runDev(appRoot: string, port: number, hostname = "::"): void {
const spawnChild = (): void => {
child = spawn(
process.execPath, // the Bun binary
[SERVE_ENTRY, appDir, String(port), "development", hostname],
[
SERVE_ENTRY,
appDir,
String(port),
"development",
hostname,
"true",
...(tls ? [tls.certFile, tls.keyFile] : []),
],
{ stdio: "inherit" },
);
@@ -81,3 +95,68 @@ export function runDev(appRoot: string, port: number, hostname = "::"): void {
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
export async function runProductionDev(
appRoot: string,
port: number,
hostname = "::",
): Promise<void> {
const root = resolve(appRoot);
let child: ChildProcess | undefined;
let building = false;
let pending = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const { runBuild } = await import("./build.ts");
const rebuild = async (): Promise<void> => {
if (building) {
pending = true;
return;
}
building = true;
try {
await runBuild(root);
child?.kill();
const { runPreview } = await import("./preview.ts");
child = runPreview(root, { port, hostname, developmentRuntime: true });
} catch (error) {
console.error(
"[wrnexus] production-runtime rebuild failed; keeping the last good server.",
error,
);
} finally {
building = false;
if (pending) {
pending = false;
void rebuild();
}
}
};
console.log(`\n ⚡ WrNexus dev (exact production runtime) — ${root}`);
await rebuild();
const watchers: FSWatcher[] = [];
for (const name of [
"app",
"public",
"wrnexus.config.ts",
"wrnexus.config.js",
"wrnexus.config.mjs",
]) {
const target = join(root, name);
if (!existsSync(target)) continue;
watchers.push(
watch(target, { recursive: true }, () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => void rebuild(), 120);
}),
);
}
const shutdown = (): void => {
watchers.forEach((watcher) => watcher.close());
child?.kill();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
+90 -3
View File
@@ -1,6 +1,6 @@
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { extname, join, resolve } from "node:path";
import { diagnose } from "@wrnexus/syntax";
import { diagnose, formatWrn } from "@wrnexus/syntax";
import { buildRouter, findRouteConflicts } from "@wrnexus/router";
import { loadAppConfig, validateAppConfig } from "@wrnexus/styles";
import { createPluginRunner, discoverPlugins } from "@wrnexus/plugin";
@@ -12,6 +12,12 @@ export interface DoctorCheck {
level?: "error" | "warning";
}
export interface DoctorRepair {
name: string;
changed: boolean;
detail: string;
}
function parseVersion(value: string): [number, number, number] {
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.replace(/^[^\d]*/, ""));
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : [0, 0, 0];
@@ -53,6 +59,81 @@ function frameworkRanges(pkg: Record<string, unknown>): Map<string, string[]> {
return ranges;
}
export function repairProject(appRoot: string): DoctorRepair[] {
const root = resolve(appRoot);
const repairs: DoctorRepair[] = [];
const pages = join(root, "app", "pages");
if (!existsSync(pages)) {
mkdirSync(pages, { recursive: true });
repairs.push({ name: "app/pages", changed: true, detail: "created app/pages" });
}
const configNames = ["wrnexus.config.ts", "wrnexus.config.mjs", "wrnexus.config.js"];
if (!configNames.some((name) => existsSync(join(root, name)))) {
writeFileSync(join(root, "wrnexus.config.ts"), "export default {};\n", "utf8");
repairs.push({ name: "configuration", changed: true, detail: "created wrnexus.config.ts" });
}
const pkgPath = join(root, "package.json");
if (existsSync(pkgPath)) {
try {
const source = readFileSync(pkgPath, "utf8");
const pkg = JSON.parse(source) as Record<string, unknown>;
const ranges = frameworkRanges(pkg);
const preferred = [...ranges.keys()].sort((left, right) => {
const a = parseVersion(left);
const b = parseVersion(right);
return b[0] - a[0] || b[1] - a[1] || b[2] - a[2];
})[0];
let changed = false;
if (preferred && ranges.size > 1) {
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
const dependencies = pkg[field] as Record<string, string> | undefined;
for (const name of Object.keys(dependencies ?? {})) {
if (name.startsWith("@wrnexus/") && dependencies![name] !== preferred) {
dependencies![name] = preferred;
changed = true;
}
}
}
}
const marker = (pkg.wrnexus as Record<string, unknown> | undefined) ?? {};
if (!marker.version || !versionAtLeast(String(marker.version), "0.8.0")) {
marker.version = "0.8.0";
pkg.wrnexus = marker;
changed = true;
}
if (changed) {
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
repairs.push({
name: "package.json",
changed: true,
detail: preferred
? `aligned framework packages to ${preferred}`
: "recorded 0.8.0 marker",
});
}
} catch {
repairs.push({ name: "package.json", changed: false, detail: "skipped invalid JSON" });
}
}
let formatted = 0;
for (const file of walk(join(root, "app"), ".wrn")) {
const source = readFileSync(file, "utf8");
if (diagnose(source).some((item) => item.severity === "error")) continue;
const output = formatWrn(source, { tabSize: 2, printWidth: 100, multilineAttributes: true });
if (output !== source) {
writeFileSync(file, output, "utf8");
formatted++;
}
}
if (formatted) {
repairs.push({ name: "WRN formatting", changed: true, detail: `formatted ${formatted} files` });
}
return repairs;
}
export function inspectProject(appRoot: string): DoctorCheck[] {
const root = resolve(appRoot);
const checks: DoctorCheck[] = [];
@@ -174,8 +255,12 @@ export function inspectProject(appRoot: string): DoctorCheck[] {
return checks;
}
export async function runDoctor(appRoot: string): Promise<boolean> {
export async function runDoctor(
appRoot: string,
options: { fix?: boolean } = {},
): Promise<boolean> {
const root = resolve(appRoot);
const repairs = options.fix ? repairProject(root) : [];
const checks = inspectProject(root);
try {
const config = await loadAppConfig(root);
@@ -257,6 +342,8 @@ export async function runDoctor(appRoot: string): Promise<boolean> {
}
console.log("WRNexus doctor\n");
for (const repair of repairs) console.log(`${repair.name}: ${repair.detail}`);
if (repairs.length) console.log("");
for (const check of checks) {
const optional = check.level === "warning";
console.log(` ${check.ok ? "✓" : optional ? "⚠" : "✗"} ${check.name}: ${check.detail}`);
+147
View File
@@ -0,0 +1,147 @@
import { existsSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
interface ExplainRoute {
kind: "page" | "api" | "realtime";
path: string;
source: string;
execution: string;
canPrerender: boolean;
needsClientRuntime: boolean;
needsServerRuntime: boolean;
hydrationStrategy: string | null;
reasons?: string[];
cachePolicy?: Record<string, string>;
requiredPermission?: string | null;
}
interface ExplainReport {
frameworkVersion: string;
adapter: string;
routes: ExplainRoute[];
assets: Array<{ file: string; bytes: number }>;
measurements: Record<string, number>;
budgetViolations: Array<{ metric: string; budget: number; actual: number }>;
}
export interface Explanation {
target: string;
subject: string;
summary: string;
reasons: string[];
evidence: Record<string, unknown>;
}
function loadReport(root: string): ExplainReport {
const path = join(resolve(root), "dist", "build-report.json");
if (!existsSync(path)) {
throw new Error(
"WRN-EXPLAIN-NO-BUILD: run `wrnexus build` before requesting build explanations.",
);
}
return JSON.parse(readFileSync(path, "utf8")) as ExplainReport;
}
function routeMatch(routes: ExplainRoute[], subject: string): ExplainRoute | undefined {
const normalized = subject.startsWith("/") ? subject : `/${subject}`;
return routes.find((route) => route.path === normalized || route.source.includes(subject));
}
export function explainBuildDecision(root: string, target: string, subject = ""): Explanation {
const report = loadReport(root);
if (
target === "route" ||
target === "hydration" ||
target === "cache" ||
target === "permission"
) {
const route =
target === "permission"
? report.routes.find((item) => item.requiredPermission === subject)
: routeMatch(report.routes, subject);
if (!route) throw new Error(`WRN-EXPLAIN-NOT-FOUND: no route or source matches '${subject}'.`);
if (target === "cache") {
const policy = route.cachePolicy ?? {};
const entries = Object.entries(policy);
return {
target,
subject: route.path,
summary: entries.length
? `Route cache uses '${policy.strategy ?? "framework-default"}' strategy.`
: "Route has no explicit cache policy and uses safe framework defaults.",
reasons: entries.length
? entries.map(([name, value]) => `${name} = ${value}`)
: ["responses remain private/revalidated unless an explicit safe policy enables reuse"],
evidence: { source: route.source, cachePolicy: policy, execution: route.execution },
};
}
if (target === "permission") {
const requested = subject.startsWith("/") ? undefined : subject;
const matches = report.routes.filter((item) =>
requested ? item.requiredPermission === requested : item.path === route.path,
);
return {
target,
subject: requested ?? route.path,
summary: matches.length
? `${matches.length} route(s) require this permission.`
: "No built route declares this permission.",
reasons: matches.length
? matches.map((item) => `${item.path} declares security.permission in ${item.source}`)
: ["authorization may still be enforced programmatically; inspect authz policies"],
evidence: { routes: matches },
};
}
const reasons = route.reasons?.length ? route.reasons : ["no dynamic requirement was detected"];
return {
target,
subject: route.path,
summary:
target === "hydration"
? route.needsClientRuntime
? `Hydration uses '${route.hydrationStrategy ?? "load"}' because client runtime is required.`
: "Hydration is omitted because no client runtime is required."
: `Route execution is '${route.execution}'${route.canPrerender ? " and can prerender" : " and cannot prerender"}.`,
reasons,
evidence: { ...route },
};
}
if (target === "bundle") {
const assets = [...report.assets].sort((a, b) => b.bytes - a.bytes);
return {
target,
subject: subject || "production bundle",
summary: `${assets.length} emitted assets; largest is ${assets[0]?.file ?? "none"}.`,
reasons: assets.slice(0, 10).map((asset) => `${asset.file}: ${asset.bytes} bytes`),
evidence: { measurements: report.measurements, largestAssets: assets.slice(0, 10) },
};
}
if (target === "build") {
return {
target,
subject: "production build",
summary: `${report.routes.length} routes target the ${report.adapter} adapter.`,
reasons: report.budgetViolations.length
? report.budgetViolations.map(
(item) => `${item.metric} exceeds ${item.budget} with ${item.actual}`,
)
: ["all configured performance budgets pass"],
evidence: {
frameworkVersion: report.frameworkVersion,
adapter: report.adapter,
measurements: report.measurements,
},
};
}
throw new Error(`WRN-EXPLAIN-TARGET: unsupported target '${target}'.`);
}
export function runExplain(root: string, target: string, subject: string, args: string[]): void {
const explanation = explainBuildDecision(root, target, subject);
if (args.includes("--json")) {
console.log(JSON.stringify(explanation, null, 2));
return;
}
console.log(explanation.summary);
explanation.reasons.forEach((reason, index) => console.log(` ${index + 1}. ${reason}`));
}
+66
View File
@@ -0,0 +1,66 @@
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { extname, join, resolve } from "node:path";
import {
auditLocaleKeys,
extractTranslationKeysFromFiles,
flattenMessageKeys,
loadLocales,
} from "@wrnexus/i18n";
function sourceFiles(root: string): string[] {
const output: string[] = [];
const visit = (directory: string) => {
if (!existsSync(directory)) return;
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
continue;
const path = join(directory, entry.name);
if (entry.isDirectory()) visit(path);
else if ([".wrn", ".ts", ".tsx", ".js", ".jsx"].includes(extname(entry.name)))
output.push(path);
}
};
visit(join(root, "app"));
return output.sort();
}
export function runI18nCommand(appRoot: string, command: string): boolean {
const root = resolve(appRoot);
const extracted = extractTranslationKeysFromFiles(sourceFiles(root));
const keys = [...new Set(extracted.map((entry) => entry.key))].sort();
const messages = loadLocales(join(root, "app", "locales"), { strict: true });
const locales = Object.keys(messages).sort();
const reference = locales[0] ?? "en";
const audit = auditLocaleKeys(messages, reference);
const referenceKeys = new Set(flattenMessageKeys(messages[reference] ?? {}));
const unused = [...referenceKeys].filter((key) => !keys.includes(key)).sort();
const missingFromReference = keys.filter((key) => !referenceKeys.has(key));
const report = { reference, locales, extracted: keys, missingFromReference, unused, audit };
if (command === "extract") {
const directory = join(root, ".wrnexus");
mkdirSync(directory, { recursive: true });
const file = join(directory, "i18n-keys.json");
writeFileSync(file, `${JSON.stringify(report, null, 2)}\n`, "utf8");
console.log(`✓ Extracted ${keys.length} translation keys to ${file}`);
return true;
}
if (command !== "validate")
throw new Error("WRN-I18N-COMMAND: use i18n extract or i18n validate.");
for (const locale of locales) {
const missing = [
...new Set([...(audit[locale]?.missing ?? []), ...missingFromReference]),
].sort();
if (missing.length) {
console.log(`Missing in ${locale}:`);
missing.forEach((key) => console.log(`- ${key}`));
}
}
if (unused.length) {
console.log("Unused keys:");
unused.forEach((key) => console.log(`- ${key}`));
}
const healthy =
missingFromReference.length === 0 &&
Object.values(audit).every((value) => value.missing.length === 0);
if (healthy) console.log(`${locales.length} locales contain every extracted key.`);
return healthy;
}
+190 -7
View File
@@ -40,7 +40,12 @@ function help(): void {
Usage:
wrnexus dev [app-dir] [--port=3000] [--host=::]
Start the development server (live reload)
wrnexus dev [app-dir] --services Start local production-service simulators with the app
wrnexus dev [app-dir] --production-runtime
wrnexus dev [app-dir] --services [--services-port=3099] [--services-http]
Rebuild and reload the exact production artifact
wrnexus build [app-dir] Build a production server bundle + assets
wrnexus preview [app-dir] Serve the existing exact production output
wrnexus create <app-name> Scaffold a new app
wrnexus workspace <name> Scaffold a monorepo (apps/* + shared packages/*)
wrnexus workspace add <name> [--domain=name.localhost]
@@ -51,6 +56,9 @@ Usage:
wrnexus generate <type> <name> Scaffold a page | component | api | schema
wrnexus generate routes | docker | mobile
Generate routes or scaffold deployment targets
wrnexus generate types [app-dir] Generate application-wide route/component/key types
wrnexus routes [app-dir] Generate typed named routes
wrnexus typecheck [app-dir] Generate types and check TypeScript plus every .wrn file
wrnexus mobile add <package...> Install Capacitor or Expo native packages
wrnexus mobile compile Compile .wrn pages into native Expo routes
wrnexus native list List cross-platform native capabilities
@@ -58,12 +66,35 @@ Usage:
wrnexus eject <name...> Copy a Wire UI component into app/components
wrnexus update [dir] [--latest] Upgrade deps, migrate project files, and verify the app
wrnexus db <cmd> Migrations: migrate | rollback | status | seed | generate | new
wrnexus test [app-dir] [--watch] Run the app's tests (bun test, 'test' profile)
wrnexus test [level] [app-dir] [--watch]
Run unit | component | api | browser | visual | accessibility | performance
wrnexus profiles [app-dir] List config profiles (dev/prod/uat/…) and their env files
wrnexus doctor [app-dir] Check project structure, versions, syntax, routes, and config
wrnexus doctor [app-dir] [--fix] Check project health; optionally apply safe repairs
wrnexus compatibility <check|explain|upgrade> [app-dir]
Inspect or explicitly upgrade behavior defaults
wrnexus contracts <check|snapshot> [app-dir]
Detect breaking boundary contract changes
wrnexus security <audit|headers|test> [app-dir]
Audit ASVS controls, inspect headers, or run abuse tests
wrnexus api <generate|docs> [app-dir] Generate OpenAPI, docs, Postman, examples and SDKs
wrnexus sdk generate <language> [app-dir]
Generate TypeScript, JavaScript, Java, Go, or Python SDK
wrnexus deploy <target> [app-dir] Generate docker | kubernetes | systemd | railway | render | fly
wrnexus mcp [app-dir] Start the WRNexus MCP server over stdio
wrnexus i18n <extract|validate> [app-dir]
Extract and audit translation keys
wrnexus report [app-dir] [--file=app/pages/page.wrn]
Create a sanitized reproduction bundle
wrnexus playground [--port=4173] Start the shareable WRN compiler playground
wrnexus config [app-dir] --explain Print the fully resolved profile configuration
wrnexus analyze [app-dir] Inspect dist/build-report.json and performance budgets
wrnexus explain <route|build|hydration|bundle> [subject] [app-dir]
Explain compiler and production build decisions
wrnexus explain <cache|permission> <subject> [app-dir]
Explain route caching or permission enforcement
wrnexus inspect <target> [app-dir] Inspect packages | plugins | routes | assets | runtimes | styles | migrations | bundle
wrnexus inspect component <name> [app-dir]
Inspect a component's typed public contract
wrnexus generate system <name> Scaffold a complete framework-native package
Profiles: pass --profile=<name> to dev/build/db (or set WRNEXUS_PROFILE) to load
@@ -91,7 +122,31 @@ async function main(): Promise<void> {
const port = portArg ? Number(portArg.split("=")[1]) : 3000;
const host = hostArg?.split("=")[1] || "::";
bootstrapProfile(appRoot, "development", rest);
runDev(appRoot, port, host);
let developmentCertificate:
| { certFile: string; keyFile: string; cert: string; key: string; reused: boolean }
| undefined;
if (rest.includes("--services") && !rest.includes("--services-http")) {
const { ensureLocalCertificate } = await import("./services.ts");
developmentCertificate = await ensureLocalCertificate(appRoot);
}
if (rest.includes("--services")) {
const { startLocalServices } = await import("./services.ts");
await startLocalServices({
appRoot,
port: Number(
rest.find((value) => value.startsWith("--services-port="))?.split("=")[1] ?? 3099,
),
https: !rest.includes("--services-http"),
origin: `${rest.includes("--services-http") ? "http" : "https"}://localhost:${port}`,
certificate: developmentCertificate,
});
}
if (rest.includes("--production-runtime")) {
const { runProductionDev } = await import("./dev.ts");
await runProductionDev(appRoot, port, host);
} else {
runDev(appRoot, port, host, developmentCertificate);
}
break;
}
case "build": {
@@ -101,6 +156,15 @@ async function main(): Promise<void> {
await runBuild(appRoot);
break;
}
case "preview": {
const appRoot = rest.find((a) => !a.startsWith("--")) ?? ".";
const port = Number(rest.find((a) => a.startsWith("--port="))?.split("=")[1] ?? 3000);
const hostname = rest.find((a) => a.startsWith("--host="))?.split("=")[1] || "::";
bootstrapProfile(appRoot, "production", rest);
const { runPreview } = await import("./preview.ts");
runPreview(appRoot, { port, hostname });
break;
}
case "create":
createApp(rest[0] ?? "");
break;
@@ -129,10 +193,18 @@ async function main(): Promise<void> {
case "g": {
if (rest[0] === "routes") {
const { regenerateRoutes } = await import("./routes.ts");
const n = regenerateRoutes(join(process.cwd(), "app"));
const n = regenerateRoutes(join(resolve(rest[1] ?? "."), "app"));
console.log(`✓ Generated app/routes.gen.ts (${n} routes)`);
break;
}
if (rest[0] === "types") {
const { generateApplicationTypesWithPlugins } = await import("./types.ts");
const result = await generateApplicationTypesWithPlugins(rest[1] ?? ".");
console.log(
`✓ Generated ${result.file} (${result.routes} routes, ${result.components} components)`,
);
break;
}
if (rest[0] === "docker") {
const { generateDocker } = await import("./docker.ts");
generateDocker(process.cwd());
@@ -153,6 +225,20 @@ async function main(): Promise<void> {
runGenerate(".", rest[0], rest[1]);
break;
}
case "routes": {
const appRoot = resolve(rest.find((arg) => !arg.startsWith("--")) ?? ".");
const { regenerateRoutes } = await import("./routes.ts");
const count = regenerateRoutes(join(appRoot, "app"));
console.log(`✓ Generated app/routes.gen.ts (${count} routes)`);
break;
}
case "typecheck": {
const { runTypecheck } = await import("./types.ts");
const healthy = await runTypecheck(rest.find((arg) => !arg.startsWith("--")) ?? ".");
if (!healthy) process.exitCode = 1;
else console.log("✓ Application types are valid");
break;
}
case "eject": {
const { runEject } = await import("./eject.ts");
const args = rest.filter((a) => !a.startsWith("--"));
@@ -191,10 +277,87 @@ async function main(): Promise<void> {
}
case "doctor": {
const { runDoctor } = await import("./doctor.ts");
const healthy = await runDoctor(rest.find((a) => !a.startsWith("--")) ?? ".");
const healthy = await runDoctor(rest.find((a) => !a.startsWith("--")) ?? ".", {
fix: rest.includes("--fix"),
});
if (!healthy) process.exitCode = 1;
break;
}
case "compatibility": {
const { runCompatibilityCommand } = await import("./compatibility-command.ts");
const subcommand = rest.find((value) => !value.startsWith("--")) ?? "check";
const appRoot = rest.filter((value) => !value.startsWith("--"))[1] ?? ".";
if (!["check", "explain", "upgrade"].includes(subcommand))
throw new Error(`WRN-COMPATIBILITY-COMMAND: unknown command '${subcommand}'.`);
const current = await runCompatibilityCommand(appRoot, subcommand, rest);
if (!current && subcommand !== "explain") process.exitCode = 1;
break;
}
case "contracts": {
const { runContractsCommand } = await import("./contracts-command.ts");
const subcommand = rest.find((value) => !value.startsWith("--")) ?? "check";
const appRoot = rest.filter((value) => !value.startsWith("--"))[1] ?? ".";
const result = await runContractsCommand(appRoot, subcommand, rest);
if (!result.ok) process.exitCode = 1;
break;
}
case "security": {
const values = rest.filter((value) => !value.startsWith("--"));
const { runSecurityCommand } = await import("./security-command.ts");
const healthy = await runSecurityCommand(values[1] ?? ".", values[0] ?? "audit", rest);
if (!healthy) process.exitCode = 1;
break;
}
case "api": {
const values = rest.filter((value) => !value.startsWith("--"));
if (!["generate", "docs"].includes(values[0] ?? ""))
throw new Error("WRN-API-COMMAND: use api generate or api docs.");
const { runApiCommand } = await import("./api-command.ts");
runApiCommand(values[1] ?? ".", "api", rest);
break;
}
case "sdk": {
const values = rest.filter((value) => !value.startsWith("--"));
if (values[0] !== "generate")
throw new Error("WRN-SDK-COMMAND: use sdk generate <language>.");
const { runApiCommand } = await import("./api-command.ts");
runApiCommand(values[2] ?? ".", "sdk", rest);
break;
}
case "deploy": {
const values = rest.filter((value) => !value.startsWith("--"));
const { generateDeployment } = await import("./deploy.ts");
const files = generateDeployment(values[1] ?? ".", values[0] ?? "");
console.log(`✓ Deployment preset '${values[0]}' ready (${files.length} new files)`);
break;
}
case "mcp": {
const { runMcpStdio } = await import("@wrnexus/mcp/stdio");
await runMcpStdio(resolve(rest.find((value) => !value.startsWith("--")) ?? "."));
break;
}
case "i18n": {
const values = rest.filter((value) => !value.startsWith("--"));
const { runI18nCommand } = await import("./i18n-command.ts");
if (!runI18nCommand(values[1] ?? ".", values[0] ?? "validate")) process.exitCode = 1;
break;
}
case "report": {
const { runReport } = await import("./report.ts");
runReport(rest.find((value) => !value.startsWith("--")) ?? ".", rest);
break;
}
case "playground": {
const { createPlaygroundHandler } = await import("@wrnexus/playground");
const port = Number(rest.find((value) => value.startsWith("--port="))?.split("=")[1] ?? 4173);
const server = Bun.serve({
port,
hostname: "127.0.0.1",
fetch: createPlaygroundHandler(),
});
console.log(`▶ WRNexus playground: http://localhost:${server.port}`);
break;
}
case "config": {
const { runConfigCommand } = await import("./config-command.ts");
await runConfigCommand(rest.find((a) => !a.startsWith("--")) ?? ".", rest);
@@ -202,6 +365,12 @@ async function main(): Promise<void> {
}
case "inspect": {
const target = rest.find((arg) => !arg.startsWith("--"));
if (target === "component") {
const values = rest.filter((arg) => !arg.startsWith("--"));
const { runInspectComponent } = await import("./inspect.ts");
runInspectComponent(values[2] ?? ".", values[1] ?? "", rest);
break;
}
const appRoot = rest.filter((arg) => !arg.startsWith("--"))[1] ?? ".";
const { runInspect } = await import("./inspect.ts");
await runInspect(appRoot, target, rest);
@@ -213,13 +382,25 @@ async function main(): Promise<void> {
if (!healthy) process.exitCode = 1;
break;
}
case "explain": {
const values = rest.filter((value) => !value.startsWith("--"));
const target = values[0] ?? "build";
const subject = target === "build" || target === "bundle" ? "" : (values[1] ?? "");
const appRoot =
target === "build" || target === "bundle" ? (values[1] ?? ".") : (values[2] ?? ".");
const { runExplain } = await import("./explain.ts");
runExplain(appRoot, target, subject, rest);
break;
}
case "test": {
const appRoot = rest.find((a) => !a.startsWith("--")) ?? ".";
const { TEST_LEVELS, runTests } = await import("./test.ts");
const values = rest.filter((value) => !value.startsWith("--"));
const hasLevel = TEST_LEVELS.includes(values[0] as (typeof TEST_LEVELS)[number]);
const appRoot = (hasLevel ? values[1] : values[0]) ?? ".";
const flag = rest.find((a) => a.startsWith("--profile="));
// Tests default to the `test` profile (config + .env.test), unless overridden.
process.env.WRNEXUS_PROFILE = resolveProfile({ explicit: flag?.split("=")[1] ?? "test" });
loadEnv(resolve(appRoot), process.env.WRNEXUS_PROFILE);
const { runTests } = await import("./test.ts");
runTests(appRoot, rest);
break;
}
@@ -259,6 +440,8 @@ async function main(): Promise<void> {
}
} catch (error) {
if (error instanceof Error && error.message !== "unknown-environment-command") throw error;
const { runPluginCliCommand } = await import("./plugin-command.ts");
if (await runPluginCliCommand(workspaceRoot, command, rest)) break;
console.error(`Unknown command or workspace environment: ${command}\n`);
help();
process.exit(1);
+40
View File
@@ -4,10 +4,41 @@ import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
import { createPluginRunner, discoverPlugins } from "@wrnexus/plugin";
import { loadAppConfig } from "@wrnexus/styles";
import { uiComponentsDir } from "@wrnexus/ui";
import { parse } from "@wrnexus/syntax";
export type InspectTarget =
"packages" | "plugins" | "routes" | "assets" | "runtimes" | "styles" | "migrations" | "bundle";
export function inspectComponent(appRoot: string, requestedName: string): unknown {
const root = resolve(appRoot);
const router = buildRouter(join(root, "app"), { componentDirs: [uiComponentsDir()] });
const component = router.components.find(
(candidate) => candidate.name.toLowerCase() === requestedName.toLowerCase(),
);
if (!component) throw new Error(`Component not found: ${requestedName}`);
const ast = parse(readFileSync(component.file, "utf8"));
return {
name: ast.name,
file: relative(root, component.file).replace(/\\/g, "/"),
props: ast.props.map(({ name, valueType, required, default: defaultValue }) => ({
name,
type: valueType ?? "unknown",
required,
...(defaultValue === undefined || defaultValue === "undefined"
? {}
: { default: defaultValue }),
})),
outputs: ast.outputs.map(({ name, payload }) => ({ name, payload: payload ?? null })),
functions: ast.runtimeFunctions.map(({ name, runtime, async, parameters, returnType }) => ({
name,
runtime,
async,
parameters,
returnType: returnType ?? "unknown",
})),
};
}
function json(path: string): Record<string, any> | null {
try {
return JSON.parse(readFileSync(path, "utf8")) as Record<string, any>;
@@ -158,3 +189,12 @@ export async function runInspect(
);
else console.log(JSON.stringify(value, null, 2));
}
export function runInspectComponent(appRoot: string, name: string, args: string[] = []): void {
const value = inspectComponent(appRoot, name);
if (args.includes("--json")) console.log(JSON.stringify(value, null, 2));
else {
console.log(`WRNexus component ${name}\n`);
console.log(JSON.stringify(value, null, 2));
}
}
+37
View File
@@ -0,0 +1,37 @@
import { resolve } from "node:path";
import { createPluginRunner, discoverPlugins } from "@wrnexus/plugin";
import { loadAppConfig } from "@wrnexus/styles";
export async function runPluginCliCommand(
appRoot: string,
commandName: string,
args: string[],
): Promise<boolean> {
const root = resolve(appRoot);
const config = await loadAppConfig(root);
const discovered = await discoverPlugins(root, config.plugins, {
includeDevDependencies: true,
strict: true,
});
const runner = createPluginRunner(discovered, {
root,
mode: "development",
command: "cli",
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
});
await runner.configure(config as Record<string, unknown>);
await runner.configResolved(config as Readonly<Record<string, unknown>>);
const command = (await runner.contributions()).cliCommands.find(
(candidate) => candidate.name === commandName,
);
if (!command) return false;
await command.run(args, {
root,
mode: "development",
command: "cli",
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
});
return true;
}
+40
View File
@@ -0,0 +1,40 @@
import { existsSync } from "node:fs";
import { spawn, type ChildProcess } from "node:child_process";
import { join, resolve } from "node:path";
export interface PreviewOptions {
port?: number;
hostname?: string;
stdio?: "inherit" | "pipe";
/** Enable the production artifact's reconnecting DOM-morph client. */
developmentRuntime?: boolean;
}
export function productionEntry(appRoot: string): string {
const entry = join(resolve(appRoot), "dist", "server.js");
if (!existsSync(entry)) {
throw new Error("WRN-PREVIEW-NO-BUILD: run `wrnexus build` before `wrnexus preview`.");
}
return entry;
}
export function runPreview(appRoot: string, options: PreviewOptions = {}): ChildProcess {
const entry = productionEntry(appRoot);
const port = options.port ?? 3000;
const hostname = options.hostname ?? "::";
console.log(
`\n ▶ WrNexus production preview — http://${hostname === "::" ? "localhost" : hostname}:${port}`,
);
return spawn(process.execPath, [entry], {
cwd: resolve(appRoot),
stdio: options.stdio ?? "inherit",
env: {
...process.env,
NODE_ENV: "production",
WRNEXUS_PROFILE: process.env.WRNEXUS_PROFILE ?? "production",
PORT: String(port),
HOST: hostname,
...(options.developmentRuntime ? { WRNEXUS_PRODUCTION_DEV: "1" } : {}),
},
});
}
+127
View File
@@ -0,0 +1,127 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { basename, join, relative, resolve } from "node:path";
import { diagnose } from "@wrnexus/compiler";
import { currentCliVersion } from "./update-notifier.ts";
const SENSITIVE_KEY =
/(?:secret|token|password|passwd|credential|api[-_]?key|private[-_]?key|cookie|authorization|session|dsn|database[-_]?url)/i;
function sanitizeText(value: string): string {
return value
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]")
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[REDACTED_IP]")
.replace(/https?:\/\/[^\s"'`/]+/gi, (origin) =>
/localhost|127\.0\.0\.1|example\.(?:com|test|org)/i.test(origin)
? origin
: "https://[REDACTED_DOMAIN]",
)
.replace(/\b(?:sk|pk|wrn|ghp|xox[baprs])[_-][A-Za-z0-9_-]{12,}\b/g, "[REDACTED_TOKEN]")
.replace(
/((?:secret|token|password|apiKey|authorization|cookie)\s*[:=]\s*)(["'`])[^"'`]*\2/gi,
"$1$2[REDACTED]$2",
)
.slice(0, 512 * 1024);
}
function sanitizeValue(value: unknown, key = ""): unknown {
if (SENSITIVE_KEY.test(key)) return "[REDACTED]";
if (typeof value === "string") return sanitizeText(value);
if (Array.isArray(value)) return value.slice(0, 200).map((item) => sanitizeValue(item));
if (value && typeof value === "object")
return Object.fromEntries(
Object.entries(value)
.slice(0, 500)
.map(([name, item]) => [name, sanitizeValue(item, name)]),
);
return value;
}
function packageVersions(root: string) {
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
return Object.fromEntries(
Object.entries({ ...manifest.dependencies, ...manifest.devDependencies }).sort(
([left], [right]) => left.localeCompare(right),
),
);
}
export interface ReproductionReportOptions {
file?: string;
error?: string;
output?: string;
command?: string[];
}
export function generateReproductionReport(
appRoot: string,
options: ReproductionReportOptions = {},
) {
const root = resolve(appRoot);
const selected = options.file ? resolve(root, options.file) : undefined;
if (
selected &&
(!relative(root, selected) ||
relative(root, selected).startsWith("..") ||
!existsSync(selected))
)
throw new Error("WRN-REPORT-FILE: selected source must exist inside the application.");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const output = resolve(root, options.output ?? join(".wrnexus", "reports", stamp));
if (!relative(root, output) || relative(root, output).startsWith(".."))
throw new Error("WRN-REPORT-OUTPUT: output must stay inside the application.");
mkdirSync(output, { recursive: true });
let source: string | undefined;
let diagnostics: unknown[] = [];
if (selected) {
source = readFileSync(selected, "utf8");
diagnostics = selected.endsWith(".wrn")
? diagnose(source, { file: basename(selected), accessibility: true })
: [];
writeFileSync(
join(output, `sanitized-${basename(selected)}`),
`${sanitizeText(source)}\n`,
"utf8",
);
}
const configNames = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"];
const configFile = configNames.map((name) => join(root, name)).find(existsSync);
const commands = options.command?.length
? options.command
: [
"bun install --frozen-lockfile",
selected ? "bunx wrnexus typecheck ." : "bunx wrnexus doctor .",
"bunx wrnexus build .",
];
const report = sanitizeValue({
schemaVersion: 1,
frameworkVersion: currentCliVersion(),
runtime: { bun: Bun.version, platform: process.platform, architecture: process.arch },
source: selected ? relative(root, selected).replace(/\\/g, "/") : undefined,
diagnostics,
dependencies: packageVersions(root),
config: configFile ? sanitizeText(readFileSync(configFile, "utf8")) : undefined,
error: options.error,
commands,
});
const reportFile = join(output, "report.json");
writeFileSync(reportFile, `${JSON.stringify(report, null, 2)}\n`, "utf8");
writeFileSync(
join(output, "README.md"),
`# Sanitized WRNexus reproduction\n\nGenerated by WRNexus ${currentCliVersion()}. Review the bundle before sharing. Values matching secrets, credentials, emails, IPs and non-public domains are redacted.\n\n## Reproduce\n\n${commands.map((command) => `- \`${command}\``).join("\n")}\n`,
"utf8",
);
return {
directory: output,
reportFile,
sourceFile: selected ? join(output, `sanitized-${basename(selected)}`) : undefined,
};
}
export function runReport(appRoot: string, args: string[]) {
const option = (name: string) =>
args.find((value) => value.startsWith(`--${name}=`))?.slice(name.length + 3);
const result = generateReproductionReport(appRoot, {
file: option("file"),
error: option("error"),
output: option("output"),
});
console.log(`✓ Sanitized reproduction bundle: ${result.directory}`);
return result;
}
+145
View File
@@ -0,0 +1,145 @@
import { existsSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
import { withSecurityHeaders } from "@wrnexus/core";
import { isSafeUrl, secureCookieOptions } from "@wrnexus/security";
import { loadAppConfig } from "@wrnexus/styles";
export interface SecurityAuditCheck {
id: string;
asvs: string[];
passed: boolean;
message: string;
}
export interface SecurityAuditReport {
version: "ASVS 5.0.0";
root: string;
checks: SecurityAuditCheck[];
passed: boolean;
}
export async function securityHeaders(appRoot: string): Promise<Record<string, string>> {
const root = resolve(appRoot);
const config = await loadAppConfig(root);
const response = withSecurityHeaders(
new Request("https://security-audit.invalid/", {
headers: { origin: "https://untrusted.invalid" },
}),
new Response("audit"),
"production",
config.security,
"audit-nonce",
);
return Object.fromEntries(
[...response.headers.entries()].sort(([left], [right]) => left.localeCompare(right)),
);
}
export async function securityAudit(appRoot: string): Promise<SecurityAuditReport> {
const root = resolve(appRoot);
const config = await loadAppConfig(root);
const headers = await securityHeaders(root);
const cors = typeof config.security?.cors === "object" ? config.security.cors : undefined;
const checks: SecurityAuditCheck[] = [
{
id: "SEC-HEADERS",
asvs: ["v5.0.0-3.4.1", "v5.0.0-3.4.6"],
passed: config.security?.headers !== false && headers["x-content-type-options"] === "nosniff",
message: "Browser security headers are enabled.",
},
{
id: "SEC-CSP",
asvs: ["v5.0.0-3.4.6"],
passed:
config.security?.contentSecurityPolicy !== false &&
Boolean(headers["content-security-policy"]),
message: "Nonce-capable Content Security Policy is enabled.",
},
{
id: "SEC-CORS",
asvs: ["v5.0.0-3.4.2"],
passed:
!(cors?.credentials && cors.origin === "*") &&
!(cors?.credentials && Array.isArray(cors.origin) && cors.origin.includes("*")),
message: "Credentialed CORS does not use a wildcard origin.",
},
{
id: "SEC-CSRF",
asvs: ["v5.0.0-3.5.1"],
passed: true,
message: "The shared runtime verifies double-submit CSRF tokens on unsafe requests.",
},
{
id: "SEC-OUTPUT-ENCODING",
asvs: ["v5.0.0-1.1.2", "v5.0.0-1.2.1", "v5.0.0-1.2.3"],
passed: true,
message: "Compiler HTML/attribute/JSON boundaries use contextual escaping.",
},
{
id: "SEC-SSRF-REDIRECT",
asvs: ["v5.0.0-1.3.6", "v5.0.0-3.7.2"],
passed: !isSafeUrl("javascript:alert(1)"),
message:
"Unsafe URL protocols are rejected and outbound fetch uses allowlist/private-address controls.",
},
];
return { version: "ASVS 5.0.0", root, checks, passed: checks.every((check) => check.passed) };
}
function securityTests(root: string): string[] {
const output: string[] = [];
const walk = (dir: string): void => {
if (!existsSync(dir)) return;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (["node_modules", "dist", ".wrnexus"].includes(entry.name)) continue;
const path = join(dir, entry.name);
if (entry.isDirectory()) walk(path);
else if (/(?:security|abuse).*\.test\.[cm]?[jt]s$/i.test(entry.name)) output.push(path);
}
};
walk(join(root, "app"));
walk(join(root, "test"));
return output;
}
export async function runSecurityCommand(
appRoot: string,
subcommand = "audit",
args: string[] = [],
): Promise<boolean> {
const root = resolve(appRoot);
if (subcommand === "headers") {
const headers = await securityHeaders(root);
if (args.includes("--json")) console.log(JSON.stringify(headers, null, 2));
else for (const [name, value] of Object.entries(headers)) console.log(`${name}: ${value}`);
return true;
}
if (subcommand === "test") {
const report = await securityAudit(root);
secureCookieOptions({ url: new URL("https://security-audit.invalid/") });
const tests = securityTests(root);
if (!report.passed) return false;
if (!tests.length) {
console.log("✓ Built-in security probes passed; no application security test files found.");
return true;
}
const result = Bun.spawnSync(["bun", "test", ...tests], {
cwd: root,
stdout: "inherit",
stderr: "inherit",
});
return result.exitCode === 0;
}
if (subcommand !== "audit")
throw new Error(`WRN-SECURITY-COMMAND: unknown command '${subcommand}'.`);
const report = await securityAudit(root);
if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
else {
console.log(`${report.version} application security audit\n`);
for (const check of report.checks)
console.log(
`${check.passed ? "✓" : "✗"} ${check.id} [${check.asvs.join(", ")}] — ${check.message}`,
);
}
return report.passed;
}
+265
View File
@@ -0,0 +1,265 @@
export interface LocalServiceRecord {
id: string;
createdAt: string;
[key: string]: unknown;
}
export interface LocalServicesState {
database: Map<string, unknown>;
cache: Map<string, { value: unknown; expiresAt?: number }>;
mail: LocalServiceRecord[];
sms: LocalServiceRecord[];
webhooks: LocalServiceRecord[];
storage: Map<string, Uint8Array>;
queue: LocalServiceRecord[];
cron: LocalServiceRecord[];
auth: Map<string, LocalServiceRecord>;
metrics: LocalServiceRecord[];
}
export function createLocalServicesState(): LocalServicesState {
return {
database: new Map(),
cache: new Map(),
mail: [],
sms: [],
webhooks: [],
storage: new Map(),
queue: [],
cron: [],
auth: new Map(),
metrics: [],
};
}
const json = (value: unknown, status = 200, origin = "https://localhost:3000") =>
Response.json(value, {
status,
headers: {
"cache-control": "no-store",
"access-control-allow-origin": origin,
"x-content-type-options": "nosniff",
},
});
async function boundedJson(
request: Request,
maximum = 256 * 1024,
): Promise<Record<string, unknown>> {
const text = await request.text();
if (new TextEncoder().encode(text).byteLength > maximum)
throw new RangeError("payload-too-large");
const value = JSON.parse(text) as unknown;
if (!value || typeof value !== "object" || Array.isArray(value))
throw new TypeError("object-required");
return value as Record<string, unknown>;
}
function record(value: Record<string, unknown>): LocalServiceRecord {
return { id: crypto.randomUUID(), createdAt: new Date().toISOString(), ...value };
}
export function createLocalServicesHandler(
state = createLocalServicesState(),
options: { origin?: string } = {},
) {
const origin = options.origin ?? "https://localhost:3000";
return async (request: Request): Promise<Response> => {
const url = new URL(request.url);
const path = url.pathname;
if (request.method === "OPTIONS")
return new Response(null, {
status: 204,
headers: {
"access-control-allow-origin": origin,
"access-control-allow-methods": "GET,POST,PUT,DELETE",
"access-control-allow-headers": "content-type",
},
});
if (path === "/healthz" || path === "/readyz")
return json({ status: "up", service: "wrnexus-local-services" }, 200, origin);
if (path === "/" || path === "/__services")
return new Response(
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>WRNexus Local Services</title></head><body><h1>WRNexus Local Services</h1><nav>${["database", "cache", "mail", "sms", "webhooks", "storage", "queue", "cron", "auth", "metrics"].map((name) => `<a href="/${name}">${name}</a> `).join("")}</nav><p>Use the JSON endpoints to inspect or inject local development events.</p></body></html>`,
{
headers: {
"content-type": "text/html; charset=utf-8",
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
},
},
);
try {
if (path === "/database") {
if (request.method === "GET") return json(Object.fromEntries(state.database), 200, origin);
const body = await boundedJson(request);
if (typeof body.key !== "string") return json({ error: "key is required" }, 400);
state.database.set(body.key, body.value);
return json({ key: body.key, value: body.value }, 201, origin);
}
if (path === "/cache") {
if (request.method === "GET") {
const now = Date.now();
for (const [key, value] of state.cache)
if (value.expiresAt && value.expiresAt <= now) state.cache.delete(key);
return json(Object.fromEntries(state.cache), 200, origin);
}
const body = await boundedJson(request);
if (typeof body.key !== "string") return json({ error: "key is required" }, 400);
const ttlMs = typeof body.ttlMs === "number" ? Math.max(0, body.ttlMs) : undefined;
state.cache.set(body.key, {
value: body.value,
...(ttlMs ? { expiresAt: Date.now() + ttlMs } : {}),
});
return json({ stored: true }, 201, origin);
}
for (const [name, values] of [
["mail", state.mail],
["sms", state.sms],
["webhooks", state.webhooks],
["queue", state.queue],
["cron", state.cron],
["metrics", state.metrics],
] as const)
if (path === `/${name}`) {
if (request.method === "GET") return json(values, 200, origin);
const value = record(await boundedJson(request));
values.unshift(value);
if (values.length > 500) values.length = 500;
return json(value, 202, origin);
}
if (path === "/storage") {
if (request.method === "GET")
return json(
[...state.storage.entries()].map(([key, value]) => ({ key, bytes: value.byteLength })),
200,
origin,
);
const key = url.searchParams.get("key");
if (!key || key.includes("..") || key.length > 256)
return json({ error: "safe key is required" }, 400);
const bytes = new Uint8Array(await request.arrayBuffer());
if (bytes.byteLength > 10 * 1024 * 1024) return json({ error: "object too large" }, 413);
state.storage.set(key, bytes);
return json({ key, bytes: bytes.byteLength }, 201, origin);
}
if (path.startsWith("/storage/") && request.method === "GET") {
const key = decodeURIComponent(path.slice(9));
const value = state.storage.get(key);
return value
? new Response(Uint8Array.from(value), {
headers: {
"content-type": "application/octet-stream",
"content-disposition": "attachment",
"x-content-type-options": "nosniff",
},
})
: json({ error: "not found" }, 404);
}
if (path === "/auth") {
if (request.method === "GET") return json([...state.auth.values()], 200, origin);
const value = record(await boundedJson(request));
if (typeof value.email !== "string") return json({ error: "email is required" }, 400);
state.auth.set(value.id, value);
return json({ user: value, accessToken: `local_${value.id}` }, 201, origin);
}
return json({ error: "not found" }, 404);
} catch (error) {
if (error instanceof RangeError) return json({ error: error.message }, 413);
return json({ error: error instanceof Error ? error.message : "invalid request" }, 400);
}
};
}
export interface LocalCertificate {
cert: string;
key: string;
certFile: string;
keyFile: string;
reused: boolean;
}
/** Generate and cache a localhost-only development certificate without requiring OpenSSL. */
export async function ensureLocalCertificate(appRoot: string): Promise<LocalCertificate> {
const directory = join(resolve(appRoot), ".wrnexus", "certificates");
const certFile = join(directory, "localhost.pem");
const keyFile = join(directory, "localhost-key.pem");
if (existsSync(certFile) && existsSync(keyFile)) {
try {
const cert = readFileSync(certFile, "utf8");
const key = readFileSync(keyFile, "utf8");
const certificate = new X509Certificate(cert);
if (Date.parse(certificate.validTo) > Date.now() + 7 * 24 * 60 * 60 * 1000) {
return { cert, key, certFile, keyFile, reused: true };
}
} catch {
// Replace invalid or expired development material below.
}
}
const now = new Date();
const expires = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
const generated = await generate([{ name: "commonName", value: "localhost" }], {
algorithm: "sha256",
keyType: "ec",
curve: "P-256",
notBeforeDate: new Date(now.getTime() - 60_000),
notAfterDate: expires,
extensions: [
{ name: "basicConstraints", cA: false, critical: true },
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
{ name: "extKeyUsage", serverAuth: true },
{
name: "subjectAltName",
altNames: [
{ type: 2, value: "localhost" },
{ type: 2, value: "*.localhost" },
{ type: 7, ip: "127.0.0.1" },
{ type: 7, ip: "::1" },
],
},
],
});
mkdirSync(directory, { recursive: true });
writeFileSync(certFile, generated.cert, { encoding: "utf8", mode: 0o600 });
writeFileSync(keyFile, generated.private, { encoding: "utf8", mode: 0o600 });
try {
chmodSync(certFile, 0o600);
chmodSync(keyFile, 0o600);
} catch {
// Windows ACLs are inherited from the private workspace directory.
}
return {
cert: generated.cert,
key: generated.private,
certFile,
keyFile,
reused: false,
};
}
export async function startLocalServices(
options: {
appRoot?: string;
port?: number;
hostname?: string;
https?: boolean;
origin?: string;
certificate?: LocalCertificate;
} = {},
) {
const port = options.port ?? 3099;
const hostname = options.hostname ?? "127.0.0.1";
const secure = options.https !== false;
const tls = secure
? (options.certificate ?? (await ensureLocalCertificate(options.appRoot ?? ".")))
: undefined;
const server = Bun.serve({
port,
hostname,
fetch: createLocalServicesHandler(undefined, { origin: options.origin }),
...(tls ? { tls: { cert: tls.cert, key: tls.key } } : {}),
});
console.log(
` ▸ Local services: ${secure ? "https" : "http"}://${hostname}:${server.port}/__services`,
);
if (tls) console.log(` certificate: ${tls.certFile} (trust locally to remove warnings)`);
console.log(" database cache mail sms webhooks storage queue cron auth metrics");
return server;
}
import { X509Certificate } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { generate } from "selfsigned";
+157 -24
View File
@@ -1,26 +1,159 @@
/**
* `wrnexus test [app-dir] [--watch] [--profile=test]` — run the app's test files
* with `bun test`. Defaults to the `test` profile (config + .env.test). Extra
* args after `--` (or bun test flags) pass straight through.
*/
/** Level-aware `wrnexus test` runner with Bun and optional Playwright backends. */
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { spawn, type ChildProcess } from "node:child_process";
import { join, relative, resolve } from "node:path";
import { spawn } from "node:child_process";
import { resolve } from "node:path";
export function runTests(appRoot: string, args: string[]): void {
const root = resolve(appRoot);
const watch = args.includes("--watch");
const passthrough = args.filter(
(a) => !a.startsWith("--profile=") && a !== "--watch" && a !== appRoot,
);
const child = spawn(
process.execPath, // the Bun binary
["test", ...(watch ? ["--watch"] : []), ...passthrough],
{ stdio: "inherit", cwd: root },
);
child.on("exit", (code, signal) => {
if (signal) return;
process.exit(code ?? 0);
});
export const TEST_LEVELS = [
"unit",
"component",
"api",
"browser",
"visual",
"accessibility",
"performance",
] as const;
export type TestLevel = (typeof TEST_LEVELS)[number];
export interface TestCommandPlan {
command: string;
args: string[];
cwd: string;
level?: TestLevel;
files: string[];
setup?: { command: string; args: string[] };
}
function shardFiles(files: string[], value?: string): string[] {
if (!value) return files;
const match = /^(\d+)\/(\d+)$/.exec(value);
if (!match) throw new Error("WRN-TEST-SHARD: expected --shard=<index>/<total>");
const index = Number(match[1]);
const total = Number(match[2]);
if (index < 1 || total < 1 || index > total)
throw new Error("WRN-TEST-SHARD: index must be between 1 and total");
return files.filter((_file, position) => position % total === index - 1);
}
function testFiles(root: string): string[] {
const files: string[] = [];
const walk = (directory: string): void => {
if (!existsSync(directory)) return;
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (["node_modules", "dist", ".wrnexus", "coverage"].includes(entry.name)) continue;
const path = join(directory, entry.name);
if (entry.isDirectory()) walk(path);
else if (/\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(entry.name)) files.push(path);
}
};
for (const directory of ["app", "test", "tests"]) walk(join(root, directory));
return files.sort();
}
function hasPlaywright(root: string): boolean {
if (
["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"].some((file) =>
existsSync(join(root, file)),
)
)
return true;
try {
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
return Boolean(
manifest.dependencies?.["@playwright/test"] || manifest.devDependencies?.["@playwright/test"],
);
} catch {
return false;
}
}
export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan {
const root = resolve(appRoot);
const level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel));
const watch = args.includes("--watch");
const shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
.split(",")
.filter(Boolean);
const passthrough = args.filter(
(value) =>
!value.startsWith("--profile=") &&
value !== "--watch" &&
value !== appRoot &&
value !== level &&
!value.startsWith("--browsers=") &&
!value.startsWith("--shard=") &&
value !== "--install-browsers",
);
if ((level === "browser" || level === "visual") && hasPlaywright(root)) {
return {
command: process.execPath,
args: [
"x",
"playwright",
"test",
...(level === "visual" ? ["--grep", "@visual"] : []),
...browsers.flatMap((browser) => ["--project", browser]),
...(shard ? [`--shard=${shard}`] : []),
"--reporter=line,html",
...passthrough,
],
cwd: root,
level,
files: [],
...(args.includes("--install-browsers")
? {
setup: { command: process.execPath, args: ["x", "playwright", "install", ...browsers] },
}
: {}),
};
}
const pattern =
level === "accessibility"
? /(?:accessibility|a11y)/i
: level === "performance"
? /(?:performance|benchmark)/i
: level
? new RegExp(level, "i")
: null;
const files = shardFiles(
pattern ? testFiles(root).filter((file) => pattern.test(relative(root, file))) : [],
shard,
);
return {
command: process.execPath,
args: ["test", ...(watch ? ["--watch"] : []), ...(pattern ? files : []), ...passthrough],
cwd: root,
level,
files,
};
}
export function runTests(appRoot: string, args: string[]): ChildProcess | null {
const plan = createTestPlan(appRoot, args);
if (plan.level && !plan.files.length && !plan.args.includes("playwright")) {
console.error(
`WRN-TEST-NO-FILES: no ${plan.level} tests found. Name a file or directory with '${plan.level}' under app/, test/, or tests/.`,
);
process.exitCode = 1;
return null;
}
const launch = () => spawn(plan.command, plan.args, { stdio: "inherit", cwd: plan.cwd });
if (plan.setup) {
const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd });
setup.on("exit", (code, signal) => {
if (signal || code !== 0) {
process.exitCode = code ?? 1;
return;
}
const test = launch();
test.on("exit", (testCode, testSignal) => {
if (!testSignal) process.exitCode = testCode ?? 0;
});
});
return setup;
}
const child = launch();
child.on("exit", (code, signal) => {
if (!signal) process.exitCode = code ?? 0;
});
return child;
}
+283
View File
@@ -0,0 +1,283 @@
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { basename, extname, join, relative, resolve } from "node:path";
import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
import { checkWrnFile, type WrnTypeDiagnostic } from "@wrnexus/typecheck";
import { parse } from "@wrnexus/syntax";
import { regenerateRoutes } from "./routes.ts";
import { loadAppConfig } from "@wrnexus/styles";
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";
function files(root: string, predicate: (path: string) => boolean): string[] {
if (!existsSync(root)) return [];
const output: string[] = [];
for (const entry of readdirSync(root, { withFileTypes: true })) {
const path = join(root, entry.name);
if (entry.isDirectory()) output.push(...files(path, predicate));
else if (predicate(path)) output.push(path);
}
return output.sort();
}
function literalUnion(values: Iterable<string>): string {
const unique = [...new Set(values)].sort();
return unique.length ? unique.map((value) => JSON.stringify(value)).join(" | ") : "never";
}
function typeImport(fromDirectory: string, file: string): string {
const path = relative(fromDirectory, file).replace(/\\/g, "/");
return JSON.stringify(path.startsWith(".") ? path : `./${path}`);
}
function exportedHandlers(source: string): string[] {
return [
...new Set(
[
...source.matchAll(
/\bexport\s+(?:const|async\s+function|function)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g,
),
].map((match) => match[1]!),
),
];
}
function flatten(value: unknown, prefix = ""): string[] {
if (!value || typeof value !== "object" || Array.isArray(value)) return prefix ? [prefix] : [];
return Object.entries(value).flatMap(([key, child]) =>
flatten(child, prefix ? `${prefix}.${key}` : key),
);
}
function environmentKeys(root: string): string[] {
return files(root, (path) => /^\.env(?:\.[\w-]+)?(?:\.example)?$/.test(basename(path))).flatMap(
(path) =>
readFileSync(path, "utf8")
.split(/\r?\n/)
.map((line) => line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1])
.filter((key): key is string => Boolean(key)),
);
}
export interface GeneratedApplicationTypes {
file: string;
routes: number;
components: number;
}
function writePluginArtifacts(root: string, contributions?: PluginContributions): void {
if (!contributions) return;
const typeDir = join(root, "app", "types");
mkdirSync(typeDir, { recursive: true });
const references = contributions.typeDefinitions.map((entry) => {
const target = resolve(root, entry);
const specifier = relative(typeDir, target).replace(/\\/g, "/");
return `/// <reference path=${JSON.stringify(specifier.startsWith(".") ? specifier : `./${specifier}`)} />`;
});
writeFileSync(
join(typeDir, "wrnexus.plugins.generated.d.ts"),
`// AUTO-GENERATED plugin type aggregation - do not edit.\n${references.join("\n")}\n`,
"utf8",
);
const docsDir = join(root, ".wrnexus", "documentation");
mkdirSync(docsDir, { recursive: true });
writeFileSync(
join(docsDir, "plugins.md"),
`# Plugin documentation\n\n${contributions.documentation
.map(
(entry) => `- [${entry}](${relative(docsDir, resolve(root, entry)).replace(/\\/g, "/")})`,
)
.join("\n")}\n`,
"utf8",
);
}
export function generateApplicationTypes(
appRoot: string,
pluginContributions?: PluginContributions,
): GeneratedApplicationTypes {
const root = resolve(appRoot);
const app = join(root, "app");
const router = buildRouter(app);
regenerateRoutes(app);
const componentFiles = files(join(app, "components"), (path) => extname(path) === ".wrn");
const components = componentFiles.map((path) => parse(readFileSync(path, "utf8")));
const localeKeys = files(join(app, "locales"), (path) => extname(path) === ".json").flatMap(
(path) => {
try {
return flatten(JSON.parse(readFileSync(path, "utf8")));
} catch {
return [];
}
},
);
const source = files(app, (path) => /\.(?:ts|tsx|js|wrn)$/.test(path))
.map((path) => readFileSync(path, "utf8"))
.join("\n");
const cacheKeys = [
...source.matchAll(/\b(?:cache|invalidate(?:Tag)?)\s*\(\s*["'`]([^"'`]+)["'`]/g),
].map((match) => match[1]);
const queueNames = files(join(app, "queues"), (path) => /\.(?:ts|js)$/.test(path)).map((path) =>
basename(path, extname(path)),
);
const manifest = createRouteManifest(nameRoutes(router.pages));
const componentMap = components
.map((component) => {
const props = component.props
.map(
(prop) =>
`${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"}`,
)
.join("; ");
const outputs = component.outputs
.map(
(output) =>
`${JSON.stringify(output.name)}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`,
)
.join("; ");
return ` ${JSON.stringify(component.name)}: { props: ${props ? `{ ${props} }` : "Record<string, never>"}; outputs: ${outputs ? `{ ${outputs} }` : "Record<string, never>"} };`;
})
.join("\n");
const typeDir = join(app, "types");
const apiContracts = router.api
.map((route) => {
const source = readFileSync(route.file, "utf8");
const methods = exportedHandlers(source);
const module = `typeof import(${typeImport(typeDir, route.file)})`;
const entries = methods
.map((method) => `${method}: ApiContract<${module}[${JSON.stringify(method)}]>`)
.join("; ");
return ` ${JSON.stringify(route.raw)}: { ${entries || `default: ApiContract<${module}["default"]>`} };`;
})
.join("\n");
const middlewareContracts = router.middlewareFiles
.map((file) => {
const name = basename(file, extname(file));
return ` ${JSON.stringify(name)}: MiddlewareContext<(typeof import(${typeImport(typeDir, file)}))["default"]>;`;
})
.join("\n");
const databaseContracts = files(join(app, "db"), (path) => /queries\.gen\.ts$/.test(path))
.flatMap((file) => {
const module = `typeof import(${typeImport(typeDir, file)})`;
return [
...readFileSync(file, "utf8").matchAll(/\bexport\s+async\s+function\s+([A-Za-z_$][\w$]*)/g),
].map(
(match) =>
` ${JSON.stringify(match[1]!)}: QueryContract<${module}[${JSON.stringify(match[1]!)}]>;`,
);
})
.join("\n");
const realtimeContracts = router.realtime
.map(
(route) =>
` ${JSON.stringify(route.raw)}: RealtimeMessage<(typeof import(${typeImport(typeDir, route.file)}))["default"]>;`,
)
.join("\n");
const queueContracts = files(join(app, "queues"), (path) => /\.(?:ts|js)$/.test(path))
.map((file) => {
const name = basename(file, extname(file));
return ` ${JSON.stringify(name)}: QueuePayload<(typeof import(${typeImport(typeDir, file)}))["default"]>;`;
})
.join("\n");
const configFile = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]
.map((name) => join(root, name))
.find(existsSync);
const code = `// AUTO-GENERATED by \`wrnexus generate types\` - do not edit.
declare namespace WRNexusGenerated {
type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O>
? { input: I; output: O }
: T extends (...args: infer A) => infer R
? { input: A extends [any, infer I, ...any[]] ? I : unknown; output: Awaited<R> }
: { input: unknown; output: unknown };
type MiddlewareContext<T> = T extends (ctx: infer C, ...args: any[]) => any ? C : never;
type QueryContract<T> = T extends (db: any, args: infer A, ...rest: any[]) => infer R
? { args: A; result: Awaited<R> }
: T extends (db: any, ...rest: any[]) => infer R
? { args: Record<string, never>; result: Awaited<R> }
: never;
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
type RouteName = ${literalUnion(manifest.map((route) => route.name))};
type ApiRoute = ${literalUnion(router.api.map((route) => route.raw))};
type RealtimeRoute = ${literalUnion(router.realtime.map((route) => route.raw))};
type EnvironmentKey = ${literalUnion(environmentKeys(root))};
type TranslationKey = ${literalUnion(localeKeys)};
type QueueName = ${literalUnion(queueNames)};
type CacheKey = ${literalUnion(cacheKeys)};
interface Components {
${componentMap}
}
interface ApiContracts {
${apiContracts}
}
interface MiddlewareContexts {
${middlewareContracts}
}
interface DatabaseQueries {
${databaseContracts}
}
interface RealtimeMessages {
${realtimeContracts}
}
interface QueuePayloads {
${queueContracts}
}
type ApplicationConfig = ${configFile ? `(typeof import(${typeImport(typeDir, configFile)}))["default"]` : "Record<string, never>"};
}
`;
mkdirSync(typeDir, { recursive: true });
const output = join(typeDir, "wrnexus.generated.d.ts");
writeFileSync(output, code, "utf8");
writePluginArtifacts(root, pluginContributions);
return {
file: relative(root, output).replace(/\\/g, "/"),
routes: manifest.length,
components: components.length,
};
}
/** Discover configured plugins and include their type/docs contributions in generated artifacts. */
export async function generateApplicationTypesWithPlugins(
appRoot: string,
): Promise<GeneratedApplicationTypes> {
const root = resolve(appRoot);
const config = await loadAppConfig(root);
const input = await discoverPlugins(root, config.plugins, {
includeDevDependencies: true,
strict: true,
});
const runner = createPluginRunner(input, {
root,
mode: "development",
command: "cli",
profile: process.env.WRNEXUS_PROFILE,
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
});
await runner.configure(config as Record<string, unknown>);
await runner.configResolved(config as Readonly<Record<string, unknown>>);
return generateApplicationTypes(root, await runner.contributions());
}
export function checkApplication(appRoot: string): WrnTypeDiagnostic[] {
const root = resolve(appRoot);
return files(join(root, "app"), (path) => extname(path) === ".wrn").flatMap((file) =>
checkWrnFile(file, { appRoot: root }),
);
}
export async function runTypecheck(appRoot: string): Promise<boolean> {
const root = resolve(appRoot);
await generateApplicationTypesWithPlugins(root);
const diagnostics = checkApplication(root);
for (const item of diagnostics)
console.error(`${item.file}:${item.line}:${item.column} ${item.code} ${item.message}`);
let tsOk = true;
if (existsSync(join(root, "tsconfig.json"))) {
const process = Bun.spawnSync(["bunx", "tsc", "--noEmit"], {
cwd: root,
stdout: "inherit",
stderr: "inherit",
});
tsOk = process.exitCode === 0;
}
return diagnostics.every((item) => item.category !== "error") && tsOk;
}
+115 -1
View File
@@ -30,6 +30,7 @@ import {
import { spawnSync } from "node:child_process";
import { basename, dirname, join, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { formatWrn, parse } from "@wrnexus/syntax";
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
import { inspectProject, type DoctorCheck } from "./doctor.ts";
@@ -189,7 +190,20 @@ function quoteLegacyDynamicAttributes(source: string): string {
}
export function migrateWrnSource(source: string): string {
return formatInlineProps(quoteLegacyDynamicAttributes(source));
return formatInlineProps(quoteLegacyDynamicAttributes(source))
.replace(/[ \t]+$/gm, "")
.replace(/\r\n?/g, "\n")
.replace(/\n{3,}$/g, "\n\n")
.replace(/\s*$/, "\n");
}
export function formatCurrentWrnSource(source: string): string {
return formatWrn(migrateWrnSource(source), {
insertSpaces: true,
tabSize: 2,
printWidth: 100,
multilineAttributes: true,
});
}
function findMatching(source: string, open: number, openChar = "{", closeChar = "}"): number {
@@ -1857,6 +1871,106 @@ const MIGRATIONS: Migration[] = [
if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review);
},
},
{
version: "0.8.0",
id: "0.8.0-01-package-kits",
description:
"Adds package-owned helper kits, reusable UI blocks, standalone realtime rooms, repaired i18n, image helpers, JWT utilities, and encrypted HTTP envelopes.",
apply(ctx) {
const file = join(ctx.appRoot, "package.json");
if (!existsSync(file)) return;
const pkg = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
const dependencies = (pkg.dependencies ??= {});
const additions = ["@wrnexus/realtime", "@wrnexus/csr"];
const added: string[] = [];
for (const name of additions) {
if (dependencies[name] === `^${ctx.to}`) continue;
dependencies[name] = `^${ctx.to}`;
added.push(name);
}
if (added.length) {
ctx.log(`+ package-kit dependencies: ${added.join(", ")}`);
if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8");
}
const review =
"Review package-owned components and helpers, i18n locale layout, encrypted HTTP trust boundaries, and realtime room authorization before enabling them in production.";
if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review);
},
},
{
version: "0.8.0",
id: "0.8.0-02-current-wrn-source",
description:
"Upgrades every application WRN source to current syntax, adds resolvable imports, normalizes formatting, and records unresolved work.",
apply(ctx) {
const appDirectory = join(ctx.appRoot, "app");
if (!existsSync(appDirectory)) return;
const index = buildV060SymbolIndex(ctx.appRoot);
const changedFiles: string[] = [];
for (const file of walkProjectFiles(appDirectory, ".wrn")) {
const relativeFile = relative(ctx.appRoot, file).replace(/\\/g, "/");
const before = readFileSync(file, "utf8");
let after = migrateV060WrnSource(before, ctx.report, relativeFile);
after = migrateImportedLayout(after, relativeFile, ctx.appRoot, index, ctx.report);
after = addExplicitImportsToSource(after, relativeFile, ctx.appRoot, index, ctx.report);
after = formatCurrentWrnSource(after);
if (after === before) continue;
try {
parse(after);
} catch (error) {
const message = error instanceof Error ? error.message.split("\n", 1)[0] : String(error);
ctx.report.parseFailures.push(`${relativeFile}: ${message}`);
ctx.report.needsReview.push(
`${relativeFile}: automatic modernization was skipped because the migrated source did not parse`,
);
ctx.log(`! ${relativeFile}: left unchanged because migrated source did not parse`);
continue;
}
changedFiles.push(relativeFile);
if (!ctx.report.changedAutomatically.includes(relativeFile)) {
ctx.report.changedAutomatically.push(relativeFile);
}
ctx.log(`~ ${relativeFile}: current WRN syntax, imports, and formatting`);
if (!ctx.dryRun) writeFileSync(file, after, "utf8");
}
const reportFile = join(
ctx.appRoot,
".wrnexus",
"migrations",
"0.8.0-source-modernization.json",
);
ctx.log(
`+ .wrnexus/migrations/0.8.0-source-modernization.json (${changedFiles.length} WRN files updated)`,
);
if (!ctx.dryRun) {
mkdirSync(dirname(reportFile), { recursive: true });
writeFileSync(
reportFile,
JSON.stringify(
{
version: "0.8.0",
from: ctx.from,
to: ctx.to,
appliedAt: new Date().toISOString(),
changedFiles,
unresolvedImports: ctx.report.unresolvedImports,
ambiguousFunctions: ctx.report.ambiguousFunctions,
legacyOutputPayloads: ctx.report.legacyOutputPayloads,
parseFailures: ctx.report.parseFailures,
needsReview: ctx.report.needsReview,
},
null,
2,
) + "\n",
"utf8",
);
}
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
+122 -2
View File
@@ -75,10 +75,25 @@ export const workspaceFiles = (name: string): Record<string, string> => ({
"scripts": {
"dev": "wrnexus gateway",
"gateway": "wrnexus gateway",
"production": "wrnexus production"
"staging": "wrnexus staging",
"production": "wrnexus production",
"typecheck": "tsc --noEmit && bun run --filter './apps/*' typecheck",
"test": "bun run --filter './apps/*' test",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier . --write",
"format:check": "prettier . --check",
"doctor": "bun run --filter './apps/*' doctor",
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check"
},
"devDependencies": {
"@wrnexus/cli": "${frameworkVersion}"
"@wrnexus/cli": "${frameworkVersion}",
"@eslint/js": "^9.0.0",
"@types/bun": "latest",
"eslint": "^9.0.0",
"prettier": "latest",
"typescript": "^5.5.0",
"typescript-eslint": "latest"
}
}
`,
@@ -119,8 +134,109 @@ export default config;
".gitignore": `node_modules/
dist/
.wrnexus/
**/.wrnexus/
coverage/
.env
.env.*
!.env.example
!.env.*.example
*.log
*.db
*.db-shm
*.db-wal
*.sqlite
*.sqlite3
uploads/
mobile/android/
mobile/ios/
mobile/.expo/
.idea/
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
*.tsbuildinfo
.eslintcache
`,
".env.example": `REDIS_URL=redis://localhost:6379
AUTH_SECRET=replace-with-at-least-32-random-characters
`,
".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
`,
"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/**", "**/dist/**", ".wrnexus/**", "**/.wrnexus/**"] },
{ 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: "^_" }],
},
},
);
`,
"tsconfig.json": `{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext", "DOM"],
"types": ["bun"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true
},
"include": ["wrnexus.workspace.ts", "packages/**/*.ts"],
"exclude": ["node_modules", "dist", "apps"]
}
`,
".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"]
}
`,
"packages/shared/package.json": `{
"name": "@app/shared",
@@ -129,6 +245,10 @@ dist/
"type": "module",
"main": "src/index.ts",
"exports": { ".": "./src/index.ts" },
"scripts": {
"typecheck": "tsc --noEmit",
"test": "bun test"
},
"dependencies": {
"@wrnexus/pubsub": "${frameworkVersion}"
}
+67
View File
@@ -0,0 +1,67 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createOpenApi, generateApiArtifacts, inspectApi } from "../src/api-command.ts";
const roots: string[] = [];
afterEach(async () =>
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
);
async function fixture() {
const root = join(tmpdir(), `wrnexus-api-${crypto.randomUUID()}`);
roots.push(root);
await mkdir(join(root, "app", "api", "users"), { recursive: true });
await writeFile(
join(root, "app", "api", "users", "[id].ts"),
"export async function GET(){}\nexport const PATCH = () => {};\n",
);
return root;
}
describe("API and SDK generation", () => {
test("derives methods, paths and OpenAPI operations from file routes", async () => {
const operations = inspectApi(await fixture());
expect(operations.map((operation) => `${operation.method} ${operation.path}`)).toEqual([
"GET /api/users/{id}",
"PATCH /api/users/{id}",
]);
expect(createOpenApi(operations).openapi).toBe("3.1.0");
});
test("emits docs, Postman, examples and all requested SDK languages", async () => {
const root = await fixture();
const result = generateApiArtifacts(root, ["typescript", "javascript", "java", "go", "python"]);
expect(result.files).toHaveLength(9);
expect(
JSON.parse(await readFile(join(root, "generated/api/openapi.json"), "utf8")).paths[
"/api/users/{id}"
].get.operationId,
).toBe("getUsersId");
expect(await readFile(join(root, "generated/api/sdk/python/wrnexus-api.py"), "utf8")).toContain(
"class WrnexusApi",
);
});
test("extracts webhook prose and schemas into OpenAPI 3.1 webhooks", async () => {
const root = await fixture();
await mkdir(join(root, "app", "api", "webhooks"), { recursive: true });
await writeFile(
join(root, "app", "api", "webhooks", "payment.ts"),
`
export const webhook = defineWebhook({
event: "payment.completed",
summary: "Payment completed",
description: "Sent after settlement.",
payloadSchema: "#/components/schemas/Payment",
signatureHeader: "x-payment-signature"
});
export const POST = () => new Response("ok");
`,
);
const spec = createOpenApi(inspectApi(root)) as any;
expect(spec.webhooks["payment.completed"].post.description).toBe("Sent after settlement.");
expect(
spec.webhooks["payment.completed"].post.requestBody.content["application/json"].schema.$ref,
).toBe("#/components/schemas/Payment");
expect(spec.webhooks["payment.completed"].post.parameters[0].name).toBe("x-payment-signature");
});
});
+17
View File
@@ -0,0 +1,17 @@
import { expect, test } from "bun:test";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { compatibilityReport, upgradeCompatibility } from "../src/compatibility-command.ts";
test("compatibility upgrade is backed up, current, and idempotent", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-compatibility-"));
const file = join(root, "wrnexus.config.ts");
writeFileSync(file, `export default { port: 3000 };\n`);
const first = upgradeCompatibility(root);
expect(first.changed).toBe(true);
expect(readFileSync(first.backup, "utf8")).toContain("port: 3000");
expect(readFileSync(file, "utf8")).toContain('compatibilityDate: "2026-08-02"');
expect(upgradeCompatibility(root).changed).toBe(false);
expect((await compatibilityReport(root)).needsUpgrade).toBe(false);
});
+59
View File
@@ -0,0 +1,59 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { runContractsCommand } from "../src/contracts-command.ts";
const roots: string[] = [];
afterEach(async () =>
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
);
async function fixture(): Promise<string> {
const root = join(tmpdir(), `wrnexus-contracts-${crypto.randomUUID()}`);
roots.push(root);
await mkdir(root, { recursive: true });
await writeFile(
join(root, "wrnexus.contracts.json"),
JSON.stringify({
format: 1,
contracts: [
{
kind: "queue",
name: "mail",
version: 1,
consumers: ["worker"],
payload: {
type: "object",
fields: { to: { type: "string", rules: [] } },
},
},
],
}),
);
return root;
}
describe("contracts command", () => {
test("snapshots and checks compatible contracts", async () => {
const root = await fixture();
expect((await runContractsCommand(root, "snapshot")).ok).toBe(true);
expect((await runContractsCommand(root, "check")).ok).toBe(true);
});
test("returns a failed result for breaking changes", async () => {
const root = await fixture();
await runContractsCommand(root, "snapshot");
await writeFile(
join(root, "wrnexus.contracts.json"),
JSON.stringify({ format: 1, contracts: [] }),
);
const result = await runContractsCommand(root, "check");
expect(result).toMatchObject({ ok: false, issueCount: 1 });
});
test("requires an explicit baseline", async () => {
const root = await fixture();
await expect(runContractsCommand(root, "check")).rejects.toThrow("WRN-CONTRACT-BASELINE");
});
});
+75
View File
@@ -22,8 +22,10 @@ test("scaffoldApp creates a comprehensive .gitignore", () => {
".wrnexus/",
".env.*",
"!.env.example",
"!.env.*.example",
"*.log",
"*.db",
"uploads/",
"coverage/",
"mobile/android/",
".vscode/",
@@ -47,6 +49,79 @@ test("scaffoldApp includes production build and start scripts", () => {
expect(pkg.scripts.build).toBe("wrnexus build .");
expect(pkg.scripts.start).toBe("bun dist/server.js");
expect(pkg.scripts.production).toBe("bun run build && bun run start");
expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
expect(pkg.scripts.test).toBe("wrnexus test .");
expect(pkg.scripts.check).toBe(
"bun run typecheck && bun run lint && bun run test && bun run format:check",
);
} finally {
rmSync(parent, { recursive: true, force: true });
}
});
test("scaffoldApp includes the complete v0.8 configuration and starter structure", () => {
const parent = mkdtempSync(join(tmpdir(), "wrnexus-create-"));
const root = join(parent, "complete-app");
try {
scaffoldApp(root, "complete-app");
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
for (const packageName of [
"@wrnexus/auth",
"@wrnexus/captcha",
"@wrnexus/db",
"@wrnexus/encryption",
"@wrnexus/i18n",
"@wrnexus/image",
"@wrnexus/jwt",
"@wrnexus/observability",
"@wrnexus/realtime",
"@wrnexus/security",
"@wrnexus/store",
"@wrnexus/ui",
"@wrnexus/uploader",
"@wrnexus/validation",
]) {
expect(pkg.dependencies[packageName]).toBe(currentCliVersion());
}
for (const block of [
"plugins:",
"imports:",
"types:",
"stores:",
"compatibility:",
"performance:",
"observability:",
"tenancy:",
"build:",
"navigation:",
"devToolbar:",
"theme:",
"i18n:",
"db:",
"databases:",
"storage:",
"realtime:",
"profiles:",
]) {
expect(config).toContain(block);
}
for (const relative of [
".env.example",
".env.test.example",
"app/locales/en.json",
"app/db/migrations/0001_init.sql",
"app/db/seed.ts",
"app/schemas/contact.ts",
"app/realtime/chat.ts",
"test/smoke.test.ts",
]) {
expect(existsSync(join(root, relative))).toBe(true);
}
} finally {
rmSync(parent, { recursive: true, force: true });
}
+30
View File
@@ -0,0 +1,30 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DEPLOY_TARGETS, generateDeployment } from "../src/deploy.ts";
describe("deployment presets", () => {
for (const target of DEPLOY_TARGETS) {
test(`generates ${target}`, () => {
const root = mkdtempSync(join(tmpdir(), `wrnexus-${target}-`));
const files = generateDeployment(root, target);
expect(files).toContain(".env.production.example");
expect(readFileSync(join(root, "deploy/README.md"), "utf8")).toContain("/readyz");
expect(generateDeployment(root, target)).toEqual([]);
});
}
test("Kubernetes includes probes, limits and release migration", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-k8s-"));
generateDeployment(root, "kubernetes");
const manifest = readFileSync(join(root, "deploy/kubernetes.yaml"), "utf8");
expect(manifest).toContain("readinessProbe");
expect(manifest).toContain("kind: Job");
expect(manifest).toContain("resources:");
});
test("rejects unknown targets", () => {
expect(() => generateDeployment(".", "unknown")).toThrow("WRN-DEPLOY-TARGET");
});
});
+28 -2
View File
@@ -1,8 +1,8 @@
import { expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { inspectProject } from "../src/doctor.ts";
import { inspectProject, repairProject } from "../src/doctor.ts";
test("doctor reports a healthy minimal project", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-doctor-"));
@@ -19,3 +19,29 @@ test("doctor returns actionable missing-project checks", () => {
expect(checks.find((check) => check.name === "package.json")?.ok).toBe(false);
expect(checks.find((check) => check.name === "app/pages")?.detail).toBe("Create app/pages");
});
test("doctor --fix applies safe repairs and is idempotent", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-doctor-fix-"));
writeFileSync(
join(root, "package.json"),
JSON.stringify({
name: "app",
dependencies: { "@wrnexus/core": "^0.8.0", "@wrnexus/router": "^0.7.0" },
wrnexus: { version: "0.7.0" },
}),
);
mkdirSync(join(root, "app", "components"), { recursive: true });
writeFileSync(
join(root, "app", "components", "Greeting.wrn"),
'component Greeting { props { name:string="World" } view { <p>{name}</p> } }',
);
const repairs = repairProject(root);
expect(repairs.map(({ name }) => name)).toContain("app/pages");
expect(repairs.map(({ name }) => name)).toContain("configuration");
expect(existsSync(join(root, "wrnexus.config.ts"))).toBe(true);
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
expect(manifest.dependencies["@wrnexus/router"]).toBe("^0.8.0");
expect(manifest.wrnexus.version).toBe("0.8.0");
expect(repairProject(root)).toEqual([]);
});
+74
View File
@@ -0,0 +1,74 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { explainBuildDecision } from "../src/explain.ts";
const roots: string[] = [];
afterEach(async () =>
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
);
async function fixture(): Promise<string> {
const root = join(tmpdir(), `wrnexus-explain-${crypto.randomUUID()}`);
roots.push(root);
await mkdir(join(root, "dist"), { recursive: true });
await writeFile(
join(root, "dist", "build-report.json"),
JSON.stringify({
frameworkVersion: "0.8.0",
adapter: "edge",
measurements: { routeJsBytes: 12 },
budgetViolations: [],
assets: [{ file: "server.js", bytes: 12 }],
routes: [
{
kind: "page",
path: "/users/[id]",
source: "app/pages/users/[id].wrn",
execution: "authenticated-ssr",
canPrerender: false,
needsClientRuntime: true,
needsServerRuntime: true,
hydrationStrategy: "visible",
reasons: ["client interactivity", "authentication required"],
cachePolicy: { strategy: "stale-while-revalidate", ttl: "30s" },
requiredPermission: "users.read",
},
],
}),
);
return root;
}
describe("causal build explanations", () => {
test("explains route execution and hydration from persisted compiler evidence", async () => {
const root = await fixture();
const route = explainBuildDecision(root, "route", "/users/[id]");
expect(route.summary).toContain("authenticated-ssr");
expect(route.reasons).toContain("authentication required");
expect(explainBuildDecision(root, "hydration", "users/[id]").summary).toContain("visible");
});
test("explains build and bundle measurements", async () => {
const root = await fixture();
expect(explainBuildDecision(root, "build").reasons).toContain(
"all configured performance budgets pass",
);
expect(explainBuildDecision(root, "bundle").reasons[0]).toBe("server.js: 12 bytes");
});
test("explains cache and permission decisions", async () => {
const root = await fixture();
expect(explainBuildDecision(root, "cache", "/users/[id]").summary).toContain(
"stale-while-revalidate",
);
expect(explainBuildDecision(root, "permission", "users.read").reasons[0]).toContain(
"security.permission",
);
});
test("uses stable diagnostics for missing evidence", () => {
expect(() => explainBuildDecision("missing", "build")).toThrow("WRN-EXPLAIN-NO-BUILD");
});
});
+35
View File
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test";
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runI18nCommand } from "../src/i18n-command.ts";
test("i18n extract and validate audit native WRN translation keys", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-i18n-cli-"));
mkdirSync(join(root, "app/pages"), { recursive: true });
mkdirSync(join(root, "app/locales"), { recursive: true });
writeFileSync(
join(root, "app/pages/index.wrn"),
`page Home { view { <h1>{t:home.title}</h1> } }`,
);
writeFileSync(join(root, "app/locales/en.json"), JSON.stringify({ home: { title: "Home" } }));
writeFileSync(
join(root, "app/locales/mr.json"),
JSON.stringify({ home: { title: "मुख्यपृष्ठ" } }),
);
expect(runI18nCommand(root, "extract")).toBe(true);
expect(existsSync(join(root, ".wrnexus/i18n-keys.json"))).toBe(true);
expect(runI18nCommand(root, "validate")).toBe(true);
});
test("i18n validate fails missing locale keys", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-i18n-cli-"));
mkdirSync(join(root, "app/pages"), { recursive: true });
mkdirSync(join(root, "app/locales"), { recursive: true });
writeFileSync(
join(root, "app/pages/index.wrn"),
`page Home { view { <h1>{t:home.title}</h1> } }`,
);
writeFileSync(join(root, "app/locales/en.json"), JSON.stringify({ home: { title: "Home" } }));
writeFileSync(join(root, "app/locales/es.json"), JSON.stringify({}));
expect(runI18nCommand(root, "validate")).toBe(false);
});
+32
View File
@@ -0,0 +1,32 @@
import { afterEach, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { runPluginCliCommand } from "../src/plugin-command.ts";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
delete (globalThis as Record<string, unknown>).__pluginCommandArgs;
});
test("application plugins can register executable CLI commands", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-command-"));
roots.push(root);
writeFileSync(
join(root, "wrnexus.config.ts"),
`export default {
plugins: [{
name: "command-test",
cliCommands: [{
name: "greet",
run(args) { globalThis.__pluginCommandArgs = args }
}]
}]
};
`,
);
expect(await runPluginCliCommand(root, "greet", ["Ada"])).toBe(true);
expect((globalThis as Record<string, unknown>).__pluginCommandArgs).toEqual(["Ada"]);
expect(await runPluginCliCommand(root, "missing", [])).toBe(false);
});
+31
View File
@@ -0,0 +1,31 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { productionEntry, runPreview } from "../src/preview.ts";
const roots: string[] = [];
afterEach(async () =>
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
);
describe("production preview", () => {
test("refuses to approximate a missing production build", () => {
expect(() => productionEntry("missing-preview-root")).toThrow("WRN-PREVIEW-NO-BUILD");
});
test("executes the exact dist server with production environment", async () => {
const root = join(tmpdir(), `wrnexus-preview-${crypto.randomUUID()}`);
roots.push(root);
await mkdir(join(root, "dist"), { recursive: true });
await writeFile(
join(root, "dist", "server.js"),
"console.log(process.env.NODE_ENV + ':' + process.env.PORT)",
);
expect(productionEntry(root)).toBe(join(root, "dist", "server.js"));
const child = runPreview(root, { port: 4100, stdio: "pipe" });
const output = await new Response(child.stdout as never).text();
expect(await new Promise<number | null>((resolve) => child.on("exit", resolve))).toBe(0);
expect(output.trim()).toBe("production:4100");
});
});
+42
View File
@@ -0,0 +1,42 @@
import { expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { generateReproductionReport } from "../src/report.ts";
test("report bundles actionable diagnostics while redacting secrets and user/internal data", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-report-"));
mkdirSync(join(root, "app/pages"), { recursive: true });
writeFileSync(
join(root, "package.json"),
JSON.stringify({ dependencies: { "@wrnexus/core": "0.8.0" } }),
);
writeFileSync(
join(root, "wrnexus.config.ts"),
`export default { apiKey: "sk_secretsecretsecret", endpoint: "https://internal.police.local/api" }`,
);
writeFileSync(
join(root, "app/pages/index.wrn"),
`page Home { view { <p>officer@example.com</p> } }`,
);
const result = generateReproductionReport(root, {
file: "app/pages/index.wrn",
error: "token=wrn_supersecrettoken at 10.0.0.1",
output: ".wrnexus/report-test",
});
const all = readFileSync(result.reportFile, "utf8") + readFileSync(result.sourceFile!, "utf8");
expect(all).toContain("frameworkVersion");
expect(all).toContain("diagnostics");
expect(all).not.toContain("sk_secretsecretsecret");
expect(all).not.toContain("officer@example.com");
expect(all).not.toContain("internal.police.local");
expect(all).not.toContain("10.0.0.1");
});
test("report rejects traversal inputs and outputs", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-report-"));
writeFileSync(join(root, "package.json"), "{}");
expect(() => generateReproductionReport(root, { file: "../secret" })).toThrow("WRN-REPORT-FILE");
expect(() => generateReproductionReport(root, { output: "../outside" })).toThrow(
"WRN-REPORT-OUTPUT",
);
});
@@ -0,0 +1,14 @@
import { expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { validateRuntimeCapabilities } from "../src/build.ts";
test("production targets fail before bundling incompatible application imports", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-edge-build-"));
mkdirSync(join(root, "app", "api"), { recursive: true });
writeFileSync(join(root, "app", "api", "files.ts"), `import fs from "node:fs";`);
expect(() => validateRuntimeCapabilities(root, "edge")).toThrow(/WRN-RUNTIME-CAPABILITY/);
expect(() => validateRuntimeCapabilities(root, "worker")).toThrow(/filesystem/);
expect(() => validateRuntimeCapabilities(root, "bun")).not.toThrow();
});
@@ -0,0 +1,50 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { runSecurityCommand, securityAudit, securityHeaders } from "../src/security-command.ts";
const roots: string[] = [];
afterEach(async () =>
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
);
async function fixture(config = "export default {};"): Promise<string> {
const root = join(tmpdir(), `wrnexus-security-${crypto.randomUUID()}`);
roots.push(root);
await mkdir(root, { recursive: true });
await writeFile(join(root, "wrnexus.config.ts"), config);
return root;
}
describe("security command", () => {
test("audits secure framework defaults against mapped ASVS controls", async () => {
const report = await securityAudit(await fixture());
expect(report.passed).toBe(true);
expect(report.version).toBe("ASVS 5.0.0");
expect(report.checks.every((check) => check.asvs.length > 0)).toBe(true);
});
test("reports deliberately disabled headers", async () => {
const report = await securityAudit(
await fixture("export default { security: { headers: false } };"),
);
expect(report.passed).toBe(false);
expect(report.checks.find((check) => check.id === "SEC-HEADERS")?.passed).toBe(false);
});
test("prints the effective production headers", async () => {
const headers = await securityHeaders(await fixture());
expect(headers["content-security-policy"]).toContain("nonce-audit-nonce");
expect(headers["strict-transport-security"]).toContain("max-age=");
expect(headers["x-content-type-options"]).toBe("nosniff");
});
test("rejects credentialed wildcard CORS and unknown commands", async () => {
const root = await fixture(
'export default { security: { cors: { origin: "*", credentials: true } } };',
);
expect((await securityAudit(root)).passed).toBe(false);
await expect(runSecurityCommand(root, "unknown")).rejects.toThrow("WRN-SECURITY-COMMAND");
});
});
+99
View File
@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { X509Certificate } from "node:crypto";
import {
createLocalServicesHandler,
ensureLocalCertificate,
startLocalServices,
} from "../src/services.ts";
describe("local production service simulator", () => {
test("simulates bounded mail, cache, storage, auth and health APIs", async () => {
const handler = createLocalServicesHandler();
expect((await handler(new Request("http://local/healthz"))).status).toBe(200);
expect(
(
await handler(
new Request("http://local/mail", {
method: "POST",
body: JSON.stringify({ to: "u@test", subject: "Welcome" }),
}),
)
).status,
).toBe(202);
expect(await (await handler(new Request("http://local/mail"))).json()).toHaveLength(1);
await handler(
new Request("http://local/cache", {
method: "POST",
body: JSON.stringify({ key: "user", value: 1 }),
}),
);
expect(await (await handler(new Request("http://local/cache"))).json()).toHaveProperty(
"user.value",
1,
);
expect(
(
await handler(
new Request("http://local/storage?key=file.txt", { method: "POST", body: "hello" }),
)
).status,
).toBe(201);
expect(await (await handler(new Request("http://local/storage/file.txt"))).text()).toBe(
"hello",
);
expect(
(
await handler(
new Request("http://local/auth", {
method: "POST",
body: JSON.stringify({ email: "u@test" }),
}),
)
).status,
).toBe(201);
});
test("rejects unsafe storage keys and oversized declared JSON", async () => {
const handler = createLocalServicesHandler();
expect(
(
await handler(
new Request("http://local/storage?key=../secret", { method: "POST", body: "bad" }),
)
).status,
).toBe(400);
const response = await handler(
new Request("http://local/mail", {
method: "POST",
body: JSON.stringify({ value: "x".repeat(300_000) }),
}),
);
expect(response.status).toBe(413);
});
test("generates and safely reuses a localhost HTTPS certificate", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-services-cert-"));
const first = await ensureLocalCertificate(root);
const certificate = new X509Certificate(first.cert);
expect(certificate.subjectAltName).toContain("DNS:localhost");
expect(certificate.subjectAltName).toContain("IP Address:127.0.0.1");
expect(statSync(first.keyFile).size).toBeGreaterThan(100);
const second = await ensureLocalCertificate(root);
expect(second.reused).toBe(true);
expect(second.cert).toBe(first.cert);
});
test("serves the simulator over generated HTTPS", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-services-tls-"));
const server = await startLocalServices({ appRoot: root, port: 0, hostname: "127.0.0.1" });
try {
const response = await fetch(`https://127.0.0.1:${server.port}/healthz`, {
tls: { rejectUnauthorized: false },
});
expect(response.status).toBe(200);
expect(await response.json()).toHaveProperty("service", "wrnexus-local-services");
} finally {
server.stop(true);
}
});
});
+55
View File
@@ -0,0 +1,55 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createTestPlan } from "../src/test.ts";
const roots: string[] = [];
afterEach(async () =>
Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))),
);
async function fixture(): Promise<string> {
const root = join(tmpdir(), `wrnexus-test-command-${crypto.randomUUID()}`);
roots.push(root);
await mkdir(join(root, "test", "component"), { recursive: true });
await writeFile(join(root, "test", "math.unit.test.ts"), "export {};\n");
await writeFile(join(root, "test", "component", "card.test.ts"), "export {};\n");
await writeFile(join(root, "test", "home.a11y.test.ts"), "export {};\n");
await writeFile(join(root, "package.json"), "{}");
return root;
}
describe("test command planning", () => {
test("discovers the requested Bun test level", async () => {
const root = await fixture();
expect(createTestPlan(root, ["unit"]).files).toHaveLength(1);
expect(createTestPlan(root, ["component"]).files[0]).toContain("card.test.ts");
expect(createTestPlan(root, ["accessibility"]).files[0]).toContain("a11y.test.ts");
});
test("delegates browser and visual suites to Playwright", async () => {
const root = await fixture();
await writeFile(join(root, "playwright.config.ts"), "export default {};\n");
expect(createTestPlan(root, ["browser"]).args).toContain("playwright");
expect(createTestPlan(root, ["visual"]).args).toContain("@visual");
const matrix = createTestPlan(root, [
"browser",
"--browsers=chromium,firefox",
"--shard=2/3",
"--install-browsers",
]);
expect(matrix.args).toContain("firefox");
expect(matrix.args).toContain("--shard=2/3");
expect(matrix.args).toContain("--reporter=line,html");
expect(matrix.setup?.args).toEqual(["x", "playwright", "install", "chromium", "firefox"]);
});
test("deterministically shards convention-based suites", async () => {
const root = await fixture();
await writeFile(join(root, "test", "second.unit.test.ts"), "export {};\n");
expect(createTestPlan(root, ["unit", "--shard=1/2"]).files).toHaveLength(1);
expect(() => createTestPlan(root, ["unit", "--shard=3/2"])).toThrow("WRN-TEST-SHARD");
});
test("keeps the unfiltered legacy command", async () => {
const plan = createTestPlan(await fixture(), ["--watch"]);
expect(plan.level).toBeUndefined();
expect(plan.args).toEqual(["test", "--watch"]);
});
});
+61
View File
@@ -0,0 +1,61 @@
import { afterEach, expect, test } from "bun:test";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { checkApplication, generateApplicationTypes } from "../src/types.ts";
import { inspectComponent } from "../src/inspect.ts";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
function fixture(): string {
const root = mkdtempSync(join(tmpdir(), "wrnexus-types-"));
roots.push(root);
for (const dir of ["pages/users", "components", "api", "realtime", "queues", "locales"])
mkdirSync(join(root, "app", dir), { recursive: true });
writeFileSync(join(root, ".env.example"), "PUBLIC_API_URL=https://example.test\n");
writeFileSync(join(root, "app/pages/index.wrn"), "page Home { view { <h1>Home</h1> } }\n");
writeFileSync(
join(root, "app/pages/users/[id].wrn"),
"page User { props { id: string } view { <p>{id}</p> } }\n",
);
writeFileSync(
join(root, "app/components/Button.wrn"),
"component Button { props { label: string } outputs { press(event: MouseEvent) } view { <button>{label}</button> } }\n",
);
writeFileSync(join(root, "app/api/users.ts"), "export default () => new Response('ok');\n");
writeFileSync(join(root, "app/realtime/chat.ts"), "export default {};\n");
writeFileSync(join(root, "app/queues/email.ts"), "export default {};\n");
writeFileSync(join(root, "app/locales/en.json"), JSON.stringify({ common: { save: "Save" } }));
return root;
}
test("generate types emits application-wide deterministic contracts", () => {
const root = fixture();
const result = generateApplicationTypes(root);
const output = readFileSync(join(root, result.file), "utf8");
expect(existsSync(join(root, "app/routes.gen.ts"))).toBe(true);
expect(output).toContain('type RouteName = "index" | "users.id"');
expect(output).toContain('type EnvironmentKey = "PUBLIC_API_URL"');
expect(output).toContain('type TranslationKey = "common.save"');
expect(output).toContain('type QueueName = "email"');
expect(output).toContain('"Button": { props: { "label": string }');
expect(output).toContain("interface ApiContracts");
expect(output).toContain('"/api/users": { default: ApiContract<');
expect(output).toContain("interface RealtimeMessages");
expect(output).toContain('"/realtime/chat": RealtimeMessage<');
expect(output).toContain("interface QueuePayloads");
expect(output).toContain('"email": QueuePayload<');
});
test("application checker validates every wrn source", () => {
expect(checkApplication(fixture()).filter((item) => item.category === "error")).toEqual([]);
}, 15_000);
test("component inspection exposes its typed public contract", () => {
const value = inspectComponent(fixture(), "button") as { name: string; props: unknown[] };
expect(value.name).toBe("Button");
expect(value.props).toEqual([{ name: "label", type: "string", required: true }]);
});
+13 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { migrateV060WrnSource } from "../src/update.ts";
import { formatCurrentWrnSource, migrateV060WrnSource } from "../src/update.ts";
const report = () => ({
changedAutomatically: [],
@@ -30,4 +30,16 @@ describe("v0.6 source migration", () => {
expect(first).toContain("output.confirm({ ok: true })");
expect(second).toBe(first);
});
test("uses the canonical framework formatter idempotently", () => {
const source = `page Home {
view {
<button type="button" class="one two three four five six seven eight nine ten eleven twelve" @click='save()'>Save</button>
}
}`;
const formatted = formatCurrentWrnSource(source);
expect(formatted).toContain("<button\n");
expect(formatCurrentWrnSource(formatted)).toBe(formatted);
});
});
+53
View File
@@ -286,3 +286,56 @@ test("0.4 migration removes manual CAPTCHA runtime wiring and archives copied as
rmSync(root, { recursive: true, force: true });
}
});
test("0.8 migration modernizes every WRN source with imports and a review report", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-update-current-source-"));
mkdirSync(join(root, "app", "components"), { recursive: true });
mkdirSync(join(root, "app", "layouts"), { recursive: true });
mkdirSync(join(root, "app", "pages"), { recursive: true });
writeFileSync(
join(root, "package.json"),
JSON.stringify({
name: "source-app",
dependencies: { "@wrnexus/core": "^0.7.0" },
wrnexus: { version: "0.7.0" },
}),
);
writeFileSync(
join(root, "app", "components", "Notice.wrn"),
'component Notice {\r\n props { label = "Ready" count = 1 } \r\n view { <p>{label}</p> }\r\n}',
);
writeFileSync(
join(root, "app", "layouts", "shell.wrn"),
"layout Shell {\n view { <main><slot /></main> }\n}\n",
);
writeFileSync(
join(root, "app", "pages", "index.wrn"),
'page Home {\n layout = "shell"\n view { <Notice label={"Updated"} /> <Missing /> }\n}\n',
);
try {
updateApp(root, "0.8.0", false);
const first = readFileSync(join(root, "app", "pages", "index.wrn"), "utf8");
expect(first).toContain('import Notice from "@/components/Notice.wrn"');
expect(first).toContain('import Shell from "@/layouts/shell.wrn"');
expect(first).toContain("layout = Shell");
expect(first).toContain("label='{\"Updated\"}'");
expect(first.endsWith("\n")).toBe(true);
const component = readFileSync(join(root, "app", "components", "Notice.wrn"), "utf8");
expect(component).toContain('props {\n label = "Ready"\n count = 1\n }');
expect(component).not.toContain("\r");
const reportPath = join(root, ".wrnexus", "migrations", "0.8.0-source-modernization.json");
const report = JSON.parse(readFileSync(reportPath, "utf8"));
expect(report.changedFiles).toContain("app/pages/index.wrn");
expect(report.unresolvedImports).toContain(
"app/pages/index.wrn: component 'Missing' could not be resolved",
);
updateApp(root, "0.8.0", false);
expect(readFileSync(join(root, "app", "pages", "index.wrn"), "utf8")).toBe(first);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
+6
View File
@@ -47,8 +47,14 @@ test("workspace templates pin the running framework release", () => {
expect(files["README.md"]).toContain("http://127.0.0.1:3000");
expect(files["README.md"]).toContain("internal gateway targets");
expect(JSON.parse(files["package.json"]!).scripts.production).toBe("wrnexus production");
expect(JSON.parse(files["package.json"]!).scripts.check).toContain("typecheck");
expect(JSON.parse(files["package.json"]!).scripts.check).toContain("format:check");
expect(files["wrnexus.workspace.ts"]).toContain('runtime: "development"');
expect(files["wrnexus.workspace.ts"]).toContain("hmr: false");
expect(files[".env.example"]).toContain("REDIS_URL");
expect(files["eslint.config.js"]).toContain("typescript-eslint");
expect(files["tsconfig.json"]).toContain('"strict": true');
expect(files[".vscode/extensions.json"]).toContain("wrnexus.wrnexus");
});
test("production workspace detects default and named SQL migrations", () => {