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
+32
View File
@@ -1,5 +1,9 @@
# @wrnexus/ai
Provider-neutral AI orchestration for OpenAI, Anthropic, Google and local OpenAI-compatible models,
with streaming, structured output, tools, embeddings, vector search/RAG, conversation persistence,
templates, guardrails, usage events, fallback, rate limits and evaluation reports.
> A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
@@ -120,6 +124,34 @@ try {
}
```
### Multi-provider client
`createAIClient` adds named-provider selection and fallback, capability discovery,
validated JSON output, validated tool execution, abort-aware exponential retries,
and per-provider circuit breakers. Attempt events intentionally contain metadata
only: prompts, credentials, and raw model responses are never passed to telemetry.
```ts
import { anthropicProvider, createAIClient } from "@wrnexus/ai";
const ai = createAIClient({
providers: [anthropicProvider()],
retry: { attempts: 3, baseDelayMs: 100, maxDelayMs: 2_000 },
circuitBreaker: { failureThreshold: 5, resetAfterMs: 30_000 },
});
const result = await ai.generateObject<{ title: string }>("Return a JSON title", {
validate: (value): value is { title: string } =>
typeof value === "object" && value !== null && "title" in value,
});
```
Providers can return normalized `usage` (`inputTokens`, `outputTokens`,
`totalTokens`, and `costUsd`) and `toolCalls`. Use `executeTools` with a named,
validated tool registry; unknown tools and invalid arguments are rejected before
application code runs. `deterministicAIProvider` supplies ordered or computed
offline responses for tests and examples without API keys or network calls.
## Usage
### Return generated JSON from an API route
+3 -2
View File
@@ -1,11 +1,12 @@
{
"name": "@wrnexus/ai",
"version": "0.7.0",
"version": "0.8.0",
"private": true,
"type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./platform": "./src/platform.ts"
}
}
+43 -2
View File
@@ -245,5 +245,46 @@ export function createAI(config: AIConfig = {}): AI {
return { generate, stream, streamResponse };
}
export { anthropicProvider, aiProvider, createAIClient } from "./providers.ts";
export type { AIUsage, AIResult, AIProvider, AIClient, AIClientOptions } from "./providers.ts";
export {
anthropicProvider,
aiProvider,
createAIClient,
deterministicAIProvider,
} from "./providers.ts";
export type {
AIAttemptEvent,
AICircuitBreakerOptions,
AIClient,
AIClientOptions,
AIProvider,
AIProviderCapabilities,
AIResult,
AIRetryOptions,
AITool,
AIToolCall,
AIUsage,
DeterministicAIProviderOptions,
} from "./providers.ts";
export {
aiRateLimiter,
createRagPipeline,
evaluateAI,
googleAIProvider,
guardedProvider,
localAIProvider,
maxPromptLength,
memoryConversationStore,
memoryVectorStore,
openAIEmbeddings,
openAIProvider,
promptTemplate,
} from "./platform.ts";
export type {
AIGuardrail,
ConversationStore,
EmbeddingProvider,
HttpAIProviderOptions,
VectorMatch,
VectorRecord,
VectorStore,
} from "./platform.ts";
+353
View File
@@ -0,0 +1,353 @@
import { AIError, type GenerateOptions, type Message } from "./index.ts";
import type { AIProvider, AIResult } from "./providers.ts";
export interface HttpAIProviderOptions {
apiKey?: string;
model: string;
baseUrl?: string;
fetch?: typeof fetch;
headers?: HeadersInit;
}
function key(configured: string | undefined, name: string): string {
const value = configured ?? process.env[name];
if (!value) throw new AIError(`Missing ${name}.`, 0, "authentication_error");
return value;
}
function promptMessages(prompt: string | Message[], options?: GenerateOptions) {
const messages =
options?.messages ??
(typeof prompt === "string" ? [{ role: "user" as const, content: prompt }] : prompt);
return options?.system ? [{ role: "system", content: options.system }, ...messages] : messages;
}
export function openAIProvider(options: HttpAIProviderOptions): AIProvider {
const send = options.fetch ?? fetch;
const base = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
return {
name: "openai",
capabilities: { streaming: false, structuredOutput: true, tools: true, usage: true },
async generate(prompt, call = {}) {
const response = await send(`${base}/chat/completions`, {
method: "POST",
headers: {
authorization: `Bearer ${key(options.apiKey, "OPENAI_API_KEY")}`,
"content-type": "application/json",
...Object.fromEntries(new Headers(options.headers)),
},
body: JSON.stringify({
model: call.model ?? options.model,
messages: promptMessages(prompt, call),
...(call.maxTokens ? { max_completion_tokens: call.maxTokens } : {}),
}),
signal: call.signal,
});
if (!response.ok)
throw new AIError(`OpenAI returned ${response.status}.`, response.status, "provider_error");
const body = (await response.json()) as any;
return {
value: String(body.choices?.[0]?.message?.content ?? ""),
provider: "openai",
model: body.model,
finishReason: body.choices?.[0]?.finish_reason,
usage: {
inputTokens: body.usage?.prompt_tokens,
outputTokens: body.usage?.completion_tokens,
totalTokens: body.usage?.total_tokens,
},
toolCalls: body.choices?.[0]?.message?.tool_calls?.map((tool: any) => ({
id: String(tool.id),
name: String(tool.function?.name),
arguments: JSON.parse(tool.function?.arguments ?? "{}"),
})),
raw: body,
};
},
};
}
export function googleAIProvider(options: HttpAIProviderOptions): AIProvider {
const send = options.fetch ?? fetch;
const base = (options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(
/\/$/,
"",
);
return {
name: "google",
capabilities: { streaming: false, structuredOutput: true, tools: true, usage: true },
async generate(prompt, call = {}) {
const response = await send(
`${base}/models/${encodeURIComponent(call.model ?? options.model)}:generateContent?key=${encodeURIComponent(key(options.apiKey, "GOOGLE_AI_API_KEY"))}`,
{
method: "POST",
headers: {
"content-type": "application/json",
...Object.fromEntries(new Headers(options.headers)),
},
body: JSON.stringify({
contents: promptMessages(prompt, call)
.filter((message) => message.role !== "system")
.map((message) => ({
role: message.role === "assistant" ? "model" : "user",
parts: [{ text: message.content }],
})),
...(call.system ? { systemInstruction: { parts: [{ text: call.system }] } } : {}),
}),
signal: call.signal,
},
);
if (!response.ok)
throw new AIError(
`Google AI returned ${response.status}.`,
response.status,
"provider_error",
);
const body = (await response.json()) as any;
return {
value: String(
body.candidates?.[0]?.content?.parts?.map((part: any) => part.text ?? "").join("") ?? "",
),
provider: "google",
model: call.model ?? options.model,
finishReason: body.candidates?.[0]?.finishReason,
usage: {
inputTokens: body.usageMetadata?.promptTokenCount,
outputTokens: body.usageMetadata?.candidatesTokenCount,
totalTokens: body.usageMetadata?.totalTokenCount,
},
raw: body,
};
},
};
}
/** OpenAI-compatible local servers including Ollama, llama.cpp and vLLM. */
export function localAIProvider(
options: Omit<HttpAIProviderOptions, "apiKey"> & { apiKey?: string },
): AIProvider {
const provider = openAIProvider({
...options,
apiKey: options.apiKey ?? "local",
baseUrl: options.baseUrl ?? "http://localhost:11434/v1",
});
return {
...provider,
name: "local",
generate: async (prompt, call) => ({
...(await provider.generate(prompt, call)),
provider: "local",
}),
};
}
export interface EmbeddingProvider {
name: string;
embed(
values: string[],
options?: { model?: string; signal?: AbortSignal },
): Promise<{ vectors: number[][]; usage?: { tokens?: number } }>;
}
export function openAIEmbeddings(options: HttpAIProviderOptions): EmbeddingProvider {
return {
name: "openai",
async embed(values, call = {}) {
const response = await (options.fetch ?? fetch)(
`${(options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")}/embeddings`,
{
method: "POST",
headers: {
authorization: `Bearer ${key(options.apiKey, "OPENAI_API_KEY")}`,
"content-type": "application/json",
},
body: JSON.stringify({ model: call.model ?? options.model, input: values }),
signal: call.signal,
},
);
if (!response.ok)
throw new AIError(`Embedding provider returned ${response.status}.`, response.status);
const body = (await response.json()) as any;
return {
vectors: body.data.map((item: any) => item.embedding as number[]),
usage: { tokens: body.usage?.total_tokens },
};
},
};
}
export interface VectorRecord<T = Record<string, unknown>> {
id: string;
vector: number[];
text: string;
metadata: T;
}
export interface VectorMatch<T = Record<string, unknown>> extends VectorRecord<T> {
score: number;
}
export interface VectorStore<T = Record<string, unknown>> {
upsert(records: VectorRecord<T>[]): Promise<void>;
query(
vector: number[],
limit?: number,
filter?: (metadata: T) => boolean,
): Promise<VectorMatch<T>[]>;
delete(ids: string[]): Promise<void>;
}
export function memoryVectorStore<T = Record<string, unknown>>(): VectorStore<T> {
const records = new Map<string, VectorRecord<T>>();
const cosine = (a: number[], b: number[]) => {
if (a.length !== b.length || !a.length) return 0;
let dot = 0,
aa = 0,
bb = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i]! * b[i]!;
aa += a[i]! ** 2;
bb += b[i]! ** 2;
}
return aa && bb ? dot / Math.sqrt(aa * bb) : 0;
};
return {
async upsert(values) {
for (const value of values) records.set(value.id, { ...value, vector: [...value.vector] });
},
async query(vector, limit = 5, filter) {
return [...records.values()]
.filter((record) => !filter || filter(record.metadata))
.map((record) => ({ ...record, score: cosine(vector, record.vector) }))
.sort((a, b) => b.score - a.score)
.slice(0, Math.max(0, limit));
},
async delete(ids) {
for (const id of ids) records.delete(id);
},
};
}
export function createRagPipeline<T>(options: {
embeddings: EmbeddingProvider;
store: VectorStore<T>;
generate: (prompt: string, options?: GenerateOptions) => Promise<AIResult<string>>;
maxContextCharacters?: number;
}) {
return {
async index(documents: Array<{ id: string; text: string; metadata: T }>) {
const embedded = await options.embeddings.embed(documents.map((document) => document.text));
await options.store.upsert(
documents.map((document, index) => ({ ...document, vector: embedded.vectors[index]! })),
);
return documents.length;
},
async ask(
question: string,
call: GenerateOptions & { limit?: number; filter?: (metadata: T) => boolean } = {},
) {
const embedded = await options.embeddings.embed([question], { signal: call.signal });
const matches = await options.store.query(embedded.vectors[0]!, call.limit, call.filter);
const context = matches
.map((match, index) => `[${index + 1}] ${match.text}`)
.join("\n\n")
.slice(0, options.maxContextCharacters ?? 12_000);
const result = await options.generate(
`Answer using only the supplied context. Cite sources as [n].\n\nContext:\n${context}\n\nQuestion: ${question}`,
call,
);
return { ...result, sources: matches };
},
};
}
export interface ConversationStore {
load(id: string): Promise<Message[]>;
append(id: string, messages: Message[]): Promise<void>;
clear(id: string): Promise<void>;
}
export function memoryConversationStore(maxMessages = 100): ConversationStore {
const conversations = new Map<string, Message[]>();
return {
async load(id) {
return [...(conversations.get(id) ?? [])];
},
async append(id, messages) {
conversations.set(id, [...(conversations.get(id) ?? []), ...messages].slice(-maxMessages));
},
async clear(id) {
conversations.delete(id);
},
};
}
export function promptTemplate(template: string) {
return (variables: Record<string, string | number>) =>
template.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_match, name: string) => {
if (!(name in variables)) throw new Error(`WRN-AI-PROMPT-VARIABLE:${name}`);
return String(variables[name]);
});
}
export type AIGuardrail = (input: {
prompt: string | Message[];
output?: string;
}) => void | Promise<void>;
export const maxPromptLength =
(maximum: number): AIGuardrail =>
({ prompt }) => {
const length =
typeof prompt === "string"
? prompt.length
: prompt.reduce((sum, message) => sum + message.content.length, 0);
if (length > maximum)
throw new AIError("Prompt exceeds configured guardrail.", 400, "guardrail");
};
export function guardedProvider(provider: AIProvider, guardrails: AIGuardrail[]): AIProvider {
return {
...provider,
async generate(prompt, options) {
for (const guardrail of guardrails) await guardrail({ prompt });
const result = await provider.generate(prompt, options);
for (const guardrail of guardrails) await guardrail({ prompt, output: result.value });
return result;
},
};
}
export function aiRateLimiter(options: { limit: number; windowMs: number; now?: () => number }) {
const buckets = new Map<string, { count: number; reset: number }>();
const now = options.now ?? Date.now;
return (key: string) => {
const time = now();
const bucket = buckets.get(key);
if (!bucket || bucket.reset <= time) {
buckets.set(key, { count: 1, reset: time + options.windowMs });
return { allowed: true, remaining: options.limit - 1 };
}
if (bucket.count >= options.limit)
return { allowed: false, remaining: 0, retryAfterMs: bucket.reset - time };
bucket.count++;
return { allowed: true, remaining: options.limit - bucket.count };
};
}
export async function evaluateAI(
cases: Array<{
name: string;
prompt: string;
expected?: string;
score?: (output: string) => number | Promise<number>;
}>,
generate: (prompt: string) => Promise<string>,
) {
const results = [];
for (const item of cases) {
const started = performance.now();
const output = await generate(item.prompt);
const score = item.score
? await item.score(output)
: item.expected === undefined
? 1
: output.includes(item.expected)
? 1
: 0;
results.push({ name: item.name, output, score, durationMs: performance.now() - started });
}
return {
results,
score: results.length
? results.reduce((sum, result) => sum + result.score, 0) / results.length
: 0,
};
}
+229 -16
View File
@@ -11,28 +11,79 @@ export interface AIUsage {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
costUsd?: number;
}
export interface AIResult<T = string> {
value: T;
provider: string;
model?: string;
usage?: AIUsage;
finishReason?: string;
toolCalls?: AIToolCall[];
raw?: unknown;
}
export interface AIProviderCapabilities {
streaming?: boolean;
structuredOutput?: boolean;
tools?: boolean;
usage?: boolean;
}
export interface AIToolCall {
id: string;
name: string;
arguments: unknown;
}
export interface AITool<TInput = unknown, TOutput = unknown> {
name: string;
description?: string;
validate?: (value: unknown) => value is TInput;
execute(input: TInput, context: { signal?: AbortSignal }): TOutput | Promise<TOutput>;
}
export interface AIProvider {
name: string;
capabilities?: AIProviderCapabilities;
generate(prompt: string | Message[], options?: GenerateOptions): Promise<AIResult<string>>;
stream?(
prompt: string | Message[],
options?: GenerateOptions,
): AsyncGenerator<string, void, unknown>;
}
export interface AIRetryOptions {
attempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
retry?: (error: unknown) => boolean;
}
export interface AICircuitBreakerOptions {
failureThreshold?: number;
resetAfterMs?: number;
}
export interface AIClientOptions {
providers: AIProvider[];
fallback?: boolean;
onAttempt?: (provider: string, error?: unknown) => void | Promise<void>;
retry?: AIRetryOptions;
circuitBreaker?: AICircuitBreakerOptions;
/** Receives metadata only; prompts, provider raw responses, and credentials are never emitted. */
onAttempt?: (event: AIAttemptEvent) => void | Promise<void>;
}
export interface AIAttemptEvent {
provider: string;
attempt: number;
outcome: "start" | "success" | "error" | "circuit-open";
durationMs?: number;
error?: { name: string; type?: string; status?: number; message: string };
usage?: AIUsage;
}
export interface AIClient {
generate(
prompt: string | Message[],
@@ -42,40 +93,82 @@ export interface AIClient {
prompt: string | Message[],
options?: GenerateOptions & { provider?: string; validate?: (value: unknown) => value is T },
): Promise<AIResult<T>>;
executeTools(
result: AIResult,
tools: AITool[],
options?: { signal?: AbortSignal },
): Promise<Array<{ call: AIToolCall; value: unknown }>>;
stream(
prompt: string | Message[],
options?: GenerateOptions & { provider?: string },
): AsyncGenerator<string, void, unknown>;
capabilities(provider?: string): Record<string, AIProviderCapabilities>;
}
export function anthropicProvider(config: AIConfig = {}): AIProvider {
const client = createAI(config);
return {
name: "anthropic",
async generate(prompt: string | Message[], options?: GenerateOptions) {
capabilities: { streaming: true, structuredOutput: true },
async generate(prompt, options) {
return {
value: await client.generate(prompt, options),
provider: "anthropic",
model: options?.model ?? config.model,
};
},
stream: (prompt: string | Message[], options?: GenerateOptions) =>
client.stream(prompt, options),
stream: (prompt, options) => client.stream(prompt, options),
};
}
export function aiProvider(name: string, client: AI): AIProvider {
export function aiProvider(
name: string,
client: AI,
capabilities: AIProviderCapabilities = {},
): AIProvider {
return {
name,
async generate(prompt: string | Message[], options?: GenerateOptions) {
capabilities: { streaming: true, ...capabilities },
async generate(prompt, options) {
return {
value: await client.generate(prompt, options),
provider: name,
model: options?.model,
};
},
stream: (prompt: string | Message[], options?: GenerateOptions) =>
client.stream(prompt, options),
stream: (prompt, options) => client.stream(prompt, options),
};
}
export interface DeterministicAIProviderOptions {
name?: string;
responses?: Array<string | AIResult<string>>;
handler?: (
prompt: string | Message[],
options?: GenerateOptions,
) => string | AIResult<string> | Promise<string | AIResult<string>>;
}
/** Offline provider for examples and tests. Responses are consumed in order. */
export function deterministicAIProvider(options: DeterministicAIProviderOptions = {}): AIProvider {
let index = 0;
const name = options.name ?? "deterministic";
return {
name,
capabilities: { streaming: true, structuredOutput: true, tools: true, usage: true },
async generate(prompt, callOptions) {
const selected = options.handler
? await options.handler(prompt, callOptions)
: options.responses?.[index++];
if (selected === undefined)
throw new AIError("No deterministic response configured", 0, "provider_error");
return typeof selected === "string"
? { value: selected, provider: name }
: { ...selected, provider: name };
},
async *stream(prompt, callOptions) {
yield (await this.generate(prompt, callOptions)).value;
},
};
}
@@ -84,27 +177,116 @@ function jsonText(value: string): string {
return (fenced?.[1] ?? value).trim();
}
function safeError(error: unknown): AIAttemptEvent["error"] {
if (error instanceof AIError)
return {
name: error.name,
type: error.type,
status: error.status,
message: error.message.slice(0, 300),
};
if (error instanceof Error) return { name: error.name, message: error.message.slice(0, 300) };
return { name: "Error", message: "Unknown provider error" };
}
function defaultRetry(error: unknown): boolean {
return (
error instanceof AIError &&
(error.status === 408 || error.status === 429 || error.status >= 500)
);
}
function abortError(): Error {
return new DOMException("The operation was aborted", "AbortError");
}
async function delay(ms: number, signal?: AbortSignal): Promise<void> {
if (signal?.aborted) throw signal.reason ?? abortError();
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(signal.reason ?? abortError());
},
{ once: true },
);
});
}
export function createAIClient(options: AIClientOptions): AIClient {
if (!options.providers.length) throw new Error("WRN-AI-NO-PROVIDERS");
const duplicate = options.providers.find(
(provider, index) =>
options.providers.findIndex((other) => other.name === provider.name) !== index,
);
if (duplicate) throw new Error(`WRN-AI-DUPLICATE-PROVIDER:${duplicate.name}`);
const circuits = new Map<string, { failures: number; openedAt?: number }>();
const select = (name?: string) =>
name ? options.providers.filter((provider) => provider.name === name) : options.providers;
const attempts = Math.max(1, options.retry?.attempts ?? 1);
const failureThreshold = Math.max(1, options.circuitBreaker?.failureThreshold ?? 5);
const resetAfterMs = Math.max(0, options.circuitBreaker?.resetAfterMs ?? 30_000);
const generate: AIClient["generate"] = async (prompt, callOptions = {}) => {
const providers = select(callOptions.provider);
if (!providers.length)
throw new AIError(`Unknown AI provider: ${callOptions.provider}`, 0, "provider_error");
let last: unknown;
for (const provider of providers) {
try {
await options.onAttempt?.(provider.name);
return await provider.generate(prompt, callOptions);
} catch (error) {
last = error;
await options.onAttempt?.(provider.name, error);
if (options.fallback === false || callOptions.provider) throw error;
const circuit = circuits.get(provider.name) ?? { failures: 0 };
if (circuit.openedAt !== undefined && Date.now() - circuit.openedAt < resetAfterMs) {
await options.onAttempt?.({ provider: provider.name, attempt: 0, outcome: "circuit-open" });
last = new AIError(`Circuit is open for provider: ${provider.name}`, 0, "circuit_open");
if (callOptions.provider) throw last;
continue;
}
if (circuit.openedAt !== undefined) {
circuit.failures = 0;
circuit.openedAt = undefined;
}
for (let attempt = 1; attempt <= attempts; attempt++) {
if (callOptions.signal?.aborted) throw callOptions.signal.reason ?? abortError();
const started = Date.now();
await options.onAttempt?.({ provider: provider.name, attempt, outcome: "start" });
try {
const result = await provider.generate(prompt, callOptions);
circuits.set(provider.name, { failures: 0 });
await options.onAttempt?.({
provider: provider.name,
attempt,
outcome: "success",
durationMs: Date.now() - started,
usage: result.usage,
});
return result;
} catch (error) {
last = error;
circuit.failures++;
if (circuit.failures >= failureThreshold) circuit.openedAt = Date.now();
circuits.set(provider.name, circuit);
await options.onAttempt?.({
provider: provider.name,
attempt,
outcome: "error",
durationMs: Date.now() - started,
error: safeError(error),
});
const retryable = (options.retry?.retry ?? defaultRetry)(error);
if (!retryable || attempt === attempts || callOptions.signal?.aborted) break;
const backoff = Math.min(
options.retry?.maxDelayMs ?? 5_000,
(options.retry?.baseDelayMs ?? 100) * 2 ** (attempt - 1),
);
await delay(backoff, callOptions.signal);
}
}
if (options.fallback === false || callOptions.provider) throw last;
}
throw last;
};
return {
generate,
async generateObject<T>(
@@ -114,6 +296,16 @@ export function createAIClient(options: AIClientOptions): AIClient {
validate?: (value: unknown) => value is T;
} = {},
) {
const candidates = select(callOptions.provider);
if (
candidates.length &&
candidates.every((provider) => provider.capabilities?.structuredOutput === false)
)
throw new AIError(
"Selected provider does not support structured output",
0,
"capability_error",
);
const result = await generate(prompt, callOptions);
let value: unknown;
try {
@@ -129,11 +321,27 @@ export function createAIClient(options: AIClientOptions): AIClient {
);
return { ...result, value: value as T };
},
async executeTools(result, tools, toolOptions = {}) {
const registry = new Map(tools.map((tool) => [tool.name, tool]));
const output: Array<{ call: AIToolCall; value: unknown }> = [];
for (const call of result.toolCalls ?? []) {
if (toolOptions.signal?.aborted) throw toolOptions.signal.reason ?? abortError();
const tool = registry.get(call.name);
if (!tool) throw new AIError(`Unknown AI tool: ${call.name}`, 0, "tool_error");
if (tool.validate && !tool.validate(call.arguments))
throw new AIError(`Invalid arguments for AI tool: ${call.name}`, 0, "tool_error");
output.push({
call,
value: await tool.execute(call.arguments, { signal: toolOptions.signal }),
});
}
return output;
},
async *stream(prompt, callOptions = {}) {
const providers = select(callOptions.provider);
let last: unknown;
for (const provider of providers) {
if (!provider.stream) continue;
if (!provider.stream || provider.capabilities?.streaming === false) continue;
try {
yield* provider.stream(prompt, callOptions);
return;
@@ -146,5 +354,10 @@ export function createAIClient(options: AIClientOptions): AIClient {
const result = await generate(prompt, callOptions);
yield result.value;
},
capabilities(provider) {
return Object.fromEntries(
select(provider).map((item) => [item.name, { ...item.capabilities }]),
);
},
};
}
+80 -1
View File
@@ -1,5 +1,11 @@
import { afterEach, expect, test } from "bun:test";
import { AIError, createAI } from "../src/index.ts";
import {
AIError,
createAI,
createAIClient,
deterministicAIProvider,
type AIProvider,
} from "../src/index.ts";
const originalFetch = globalThis.fetch;
afterEach(() => {
@@ -54,3 +60,76 @@ test("streams fragmented SSE and keeps a final event without a newline", async (
for await (const chunk of createAI({ apiKey: "secret" }).stream("Hi")) chunks.push(chunk);
expect(chunks).toEqual(["A", "B"]);
});
test("generates and validates structured output with an offline provider", async () => {
const client = createAIClient({
providers: [deterministicAIProvider({ responses: ['```json\n{"ok":true}\n```'] })],
});
const result = await client.generateObject<{ ok: boolean }>("ignored", {
validate: (value): value is { ok: boolean } =>
typeof value === "object" && value !== null && "ok" in value,
});
expect(result.value).toEqual({ ok: true });
expect(client.capabilities().deterministic?.structuredOutput).toBe(true);
});
test("retries transient failures and reports redacted metadata", async () => {
let calls = 0;
const events: unknown[] = [];
const provider: AIProvider = {
name: "retrying",
async generate() {
calls++;
if (calls === 1) throw new AIError("temporary secret-token", 503, "overloaded");
return { value: "done", provider: "retrying", usage: { totalTokens: 3, costUsd: 0.01 } };
},
};
const result = await createAIClient({
providers: [provider],
retry: { attempts: 2, baseDelayMs: 0 },
onAttempt: (event) => {
events.push(event);
},
}).generate("private prompt");
expect(result.value).toBe("done");
expect(calls).toBe(2);
expect(JSON.stringify(events)).not.toContain("private prompt");
expect(JSON.stringify(events)).not.toContain("raw");
});
test("opens a provider circuit and falls back", async () => {
let failedCalls = 0;
const failing: AIProvider = {
name: "failing",
async generate() {
failedCalls++;
throw new AIError("down", 503);
},
};
const client = createAIClient({
providers: [failing, deterministicAIProvider({ responses: ["one", "two"] })],
retry: { attempts: 1 },
circuitBreaker: { failureThreshold: 1, resetAfterMs: 60_000 },
});
expect((await client.generate("first")).value).toBe("one");
expect((await client.generate("second")).value).toBe("two");
expect(failedCalls).toBe(1);
});
test("executes validated tool calls and forwards cancellation", async () => {
const client = createAIClient({ providers: [deterministicAIProvider({ responses: ["ok"] })] });
const result = {
value: "",
provider: "deterministic",
toolCalls: [{ id: "1", name: "double", arguments: { value: 4 } }],
};
const output = await client.executeTools(result, [
{
name: "double",
validate: (value): value is { value: number } =>
typeof value === "object" && value !== null && "value" in value,
execute: ({ value }) => value * 2,
},
]);
expect(output[0]?.value).toBe(8);
});
+96
View File
@@ -0,0 +1,96 @@
import { describe, expect, test } from "bun:test";
import {
aiRateLimiter,
createRagPipeline,
evaluateAI,
googleAIProvider,
guardedProvider,
localAIProvider,
maxPromptLength,
memoryConversationStore,
memoryVectorStore,
openAIProvider,
promptTemplate,
} from "../src/platform.ts";
describe("AI platform", () => {
test("adapts OpenAI, Google and local providers", async () => {
const openFetch = (async () =>
Response.json({
model: "gpt",
choices: [{ message: { content: "hello" }, finish_reason: "stop" }],
usage: { total_tokens: 3 },
})) as unknown as typeof fetch;
expect(
(await openAIProvider({ model: "gpt", apiKey: "key", fetch: openFetch }).generate("hi"))
.value,
).toBe("hello");
const googleFetch = (async () =>
Response.json({
candidates: [{ content: { parts: [{ text: "hola" }] } }],
usageMetadata: { totalTokenCount: 2 },
})) as unknown as typeof fetch;
expect(
(
await googleAIProvider({ model: "gemini", apiKey: "key", fetch: googleFetch }).generate(
"hi",
)
).value,
).toBe("hola");
expect(
(await localAIProvider({ model: "llama", fetch: openFetch }).generate("hi")).provider,
).toBe("local");
});
test("indexes and retrieves a RAG answer", async () => {
const store = memoryVectorStore<{ source: string }>();
const embeddings = {
name: "test",
embed: async (values: string[]) => ({
vectors: values.map((value) => (value.includes("Bun") ? [1, 0] : [0, 1])),
}),
};
const rag = createRagPipeline({
embeddings,
store,
generate: async (prompt) => ({
value: prompt.includes("fast") ? "Bun [1]" : "none",
provider: "test",
}),
});
await rag.index([
{ id: "bun", text: "Bun is fast", metadata: { source: "docs" } },
{ id: "other", text: "Other", metadata: { source: "other" } },
]);
const result = await rag.ask("Bun runtime");
expect(result.value).toBe("Bun [1]");
expect(result.sources[0]?.id).toBe("bun");
});
test("persists conversations, renders prompts, guards and rate limits", async () => {
const conversations = memoryConversationStore(2);
await conversations.append("one", [
{ role: "user", content: "a" },
{ role: "assistant", content: "b" },
{ role: "user", content: "c" },
]);
expect(await conversations.load("one")).toHaveLength(2);
expect(promptTemplate("Hello {{name}}")({ name: "Wire" })).toBe("Hello Wire");
const provider = guardedProvider(
{ name: "test", generate: async () => ({ value: "ok", provider: "test" }) },
[maxPromptLength(3)],
);
await expect(provider.generate("long")).rejects.toThrow("guardrail");
const limit = aiRateLimiter({ limit: 1, windowMs: 100, now: () => 0 });
expect(limit("u").allowed).toBe(true);
expect(limit("u").allowed).toBe(false);
});
test("evaluates model output", async () => {
const report = await evaluateAI(
[{ name: "answer", prompt: "question", expected: "42" }],
async () => "42",
);
expect(report.score).toBe(1);
});
});
+11
View File
@@ -368,3 +368,14 @@ bun run validate:auth
```
Read [SECURITY.md](./SECURITY.md) before production deployment.
## Package-owned UI blocks and route helpers
Authentication forms continue to compose `@wrnexus/ui` inputs, buttons, cards, alerts, badges, avatars, and PIN controls. The package also provides:
- `<AuthShell />`
- `<AuthProviderButtons />`
- `<AuthSecurityNotice />`
- complete sign-in, sign-up, MFA, passkey, recovery, account-status, session, and impersonation blocks
Server helpers include `authRoute`, `authSuccess`, `authFailure`, `requireAuthUser`, `optionalAuthUser`, `currentAuthSession`, and `authComponentProps`.
+23 -7
View File
@@ -1,11 +1,27 @@
component AccountStatus {
props { status = "active" title = "Account status" activeMessage = "Your account is active and ready to use." pendingMessage = "Verify your contact details to activate your account." lockedMessage = "Your account is temporarily locked for security." disabledMessage = "Your account has been disabled." supportHref = "/support" color = "primary" size = "md" class = "" }
props {
status: string = "active"
title: string = "Account status"
activeMessage: string = "Your account is active and ready to use."
pendingMessage: string = "Verify your contact details to activate your account."
lockedMessage: string = "Your account is temporarily locked for security."
disabledMessage: string = "Your account has been disabled."
supportHref: string = "/support"
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<section {...attrs} data-status='{status}' class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-center {class}'>
<span class='mx-auto flex size-14 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)] text-[var(--wire-color-primary)] data-[status=locked]:text-[var(--wire-color-warning)] data-[status=disabled]:text-[var(--wire-color-danger)]'>{#if status == "active"}<span class="icon-[lucide--circle-check] size-7"></span>{:else if status == "pending"}<span class="icon-[lucide--clock-3] size-7"></span>{:else}<span class="icon-[lucide--shield-alert] size-7"></span>{/if}</span>
<h1 class="mb-0 mt-4 text-2xl font-semibold">{title}</h1>
<p class="mx-auto mt-2 max-w-md text-sm text-[var(--wire-color-muted)]">{status == "active" ? activeMessage : status == "pending" ? pendingMessage : status == "locked" ? lockedMessage : disabledMessage}</p>
{#if status != "active"}<a href='{supportHref}' class="mt-4 inline-flex h-10 items-center justify-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white">Contact support</a>{/if}
</section>
<Card {...attrs} title='{title}' color='{color}' size='{size}' align="center" class='w-full max-w-lg {class}'>
<div class="flex flex-col items-center text-center" data-status='{status}'>
<span class='flex size-14 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)] text-[var(--wire-color-primary)]'>
{#if status == "active"}<span class="icon-[lucide--circle-check] size-7"></span>{:else if status == "pending"}<span class="icon-[lucide--clock-3] size-7"></span>{:else}<span class="icon-[lucide--shield-alert] size-7"></span>{/if}
</span>
<p class="mx-auto mt-3 max-w-md text-sm text-[var(--wire-color-muted)]">{status == "active" ? activeMessage : status == "pending" ? pendingMessage : status == "locked" ? lockedMessage : disabledMessage}</p>
<Badge label='{status}' color='{status == "active" ? "success" : status == "pending" ? "warning" : "danger"}' variant="soft" size="sm" />
{#if status != "active"}<Button href='{supportHref}' label="Contact support" color='{color}' size='{size}' class="mt-4" />{/if}
</div>
</Card>
}
}
@@ -0,0 +1,43 @@
component AuthProviderButtons {
outputs {
select(payload: { provider: string; href: string })
}
props {
providers: unknown[] = []
title: string = "Continue with"
dividerLabel: string = "or"
color: string = "primary"
size: string = "md"
class: string = ""
}
functions {
client function choose(provider) {
output.select({ provider: provider.id || provider.name || provider.label, href: provider.href || "" })
}
}
view {
<div {...attrs} class='space-y-3 {class}'>
{#if title}<p class="m-0 text-sm font-semibold text-[var(--wire-color-text)]">{title}</p>{/if}
<div class="grid gap-2 sm:grid-cols-2">
{#each providers as provider}
<Button
label='{provider.label || provider.name || "Continue"}'
href='{provider.href || ""}'
icon='{provider.icon || "icon-[lucide--log-in]"}'
variant='{provider.variant || "outline"}'
color='{provider.color || color}'
size='{size}'
fullWidth="true"
@click='choose(provider)'
/>
{/each}
</div>
{#if dividerLabel}
<div class="flex items-center gap-3 text-xs text-[var(--wire-color-muted)]"><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span><span>{dividerLabel}</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
{/if}
</div>
}
}
@@ -0,0 +1,22 @@
component AuthSecurityNotice {
props {
title: string = "Security notice"
description: string = "Your session and credentials are protected by WRNexusJS security controls."
color: string = "info"
size: string = "sm"
class: string = ""
}
view {
<Alert
{...attrs}
title='{title}'
description='{description}'
icon="icon-[lucide--shield-check]"
color='{color}'
size='{size}'
variant="soft"
class='text-[var(--wire-color-text)] {class}'
/>
}
}
+37
View File
@@ -0,0 +1,37 @@
component AuthShell {
props {
title: string = "Welcome"
description: string = ""
eyebrow: string = ""
icon: string = "icon-[lucide--shield-check]"
footer: string = ""
color: string = "primary"
size: string = "md"
maxWidth: string = "md"
class: string = ""
}
view {
<Card
{...attrs}
title='{title}'
description='{description}'
header='{eyebrow}'
footer='{footer}'
color='{color}'
size='{size}'
class='w-full {class}'
class:max-w-sm='maxWidth === "sm"'
class:max-w-md='maxWidth === "md"'
class:max-w-lg='maxWidth === "lg"'
class:max-w-xl='maxWidth === "xl"'
class:max-w-2xl='maxWidth === "2xl"'
class:max-w-none='maxWidth === "full"'
>
<div class="mb-5 flex size-12 items-center justify-center rounded-full bg-[color-mix(in_srgb,var(--wire-color-primary)_12%,transparent)] text-[var(--wire-color-primary)]">
<span class='{icon}' aria-hidden="true"></span>
</div>
<slot></slot>
</Card>
}
}
+18 -19
View File
@@ -1,40 +1,39 @@
component DeviceSessions {
props {
sessions = []
currentSessionId = ""
title = "Active sessions"
description = "Review devices signed in to your account."
revokeAction = "/api/auth/sessions/revoke"
revokeSchema = "auth-session-revoke"
revokeLabel = "Sign out"
successMessage = "Session revoked."
color = "primary"
size = "md"
class = ""
sessions: unknown[] = []
currentSessionId: string = ""
title: string = "Active sessions"
description: string = "Review devices signed in to your account."
revokeAction: string = "/api/auth/sessions/revoke"
revokeSchema: string = "auth-session-revoke"
revokeLabel: string = "Sign out"
successMessage: string = "Session revoked."
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-2xl rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] {class}'>
<h2 class="m-0 text-xl font-semibold">{title}</h2>
<p class="mt-1 text-sm text-[var(--wire-color-muted)]">{description}</p>
<div class="mt-5 divide-y divide-[var(--wire-color-border)]">
<Card {...attrs} title='{title}' description='{description}' color='{color}' size='{size}' class='w-full max-w-2xl {class}'>
<div class="divide-y divide-[var(--wire-color-border)]">
{#each sessions as session}
<article class="flex items-center gap-3 py-4">
<span class="flex size-10 items-center justify-center rounded-full bg-[var(--wire-color-surface-2)]"><span class="icon-[lucide--monitor-smartphone] size-5"></span></span>
<Avatar fallback='{session.userAgent || "Device"}' icon="icon-[lucide--monitor-smartphone]" size="sm" color='{color}' />
<div class="min-w-0 flex-1"><p class="m-0 truncate text-sm font-semibold">{session.userAgent || "Unknown device"}</p><p class="m-0 mt-1 text-xs text-[var(--wire-color-muted)]">{session.ip || "Unknown IP"} · Last active {session.lastSeenAt}</p></div>
{#if session.id == currentSessionId}
<span class="rounded-full bg-[color-mix(in_srgb,var(--wire-color-success)_12%,transparent)] px-2 py-1 text-xs font-semibold text-[var(--wire-color-success)]">Current</span>
<Badge label="Current" color="success" variant="soft" size="sm" />
{:else}
<form method="post" action='{revokeAction}' data-schema='{revokeSchema}' novalidate>
<input type="hidden" name="sessionId" value='{session.id}' />
<p data-error="sessionId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wire-color-success)]">{successMessage}</p>
<button type="submit" class="text-sm font-semibold text-[var(--wire-color-danger)] disabled:opacity-60">{revokeLabel}</button>
<Button type="submit" label='{revokeLabel}' color="danger" variant="ghost" size="sm" />
</form>
{/if}
</article>
{/each}
</div>
</section>
</Card>
}
}
@@ -1,22 +1,25 @@
component ImpersonationBanner {
props {
visible = true
targetName = "this user"
stopAction = "/api/auth/impersonation/stop"
stopSchema = "auth-empty"
redirect = "/account"
message = "You are viewing the application as"
stopLabel = "Stop impersonating"
color = "warning"
size = "md"
class = ""
visible: boolean = true
targetName: string = "this user"
stopAction: string = "/api/auth/impersonation/stop"
stopSchema: string = "auth-empty"
redirect: string = "/account"
message: string = "You are viewing the application as"
stopLabel: string = "Stop impersonating"
color: string = "warning"
size: string = "md"
class: string = ""
}
view {
{#if visible}
<aside {...attrs} data-wrnexus-runtime="auth" role="status" class='flex w-full flex-wrap items-center justify-between gap-3 border-b border-[var(--wire-color-warning)] bg-[color-mix(in_srgb,var(--wire-color-warning)_14%,var(--wire-color-surface))] px-4 py-2 text-sm text-[var(--wire-color-text)] {class}'>
<span class="flex items-center gap-2"><span class="icon-[lucide--scan-face] size-4 text-[var(--wire-color-warning)]"></span><span>{message} <strong>{targetName}</strong>.</span></span>
<form method="post" action='{stopAction}' data-schema='{stopSchema}' data-redirect='{redirect}' novalidate><p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p><button type="submit" class="inline-flex h-8 items-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-warning)] px-3 text-xs font-semibold text-black disabled:opacity-60">{stopLabel}</button></form>
</aside>
<Alert {...attrs} title='{message + " " + targetName}' icon="icon-[lucide--scan-face]" color='{color}' size='{size}' variant="soft" class='{class}'>
<form method="post" action='{stopAction}' data-schema='{stopSchema}' data-redirect='{redirect}' novalidate class="mt-2">
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
<Button type="submit" label='{stopLabel}' color='{color}' size="sm" />
</form>
</Alert>
{/if}
}
}
+35 -26
View File
@@ -1,33 +1,42 @@
component PasskeyButton {
props {
mode = "authenticate"
label = "Continue with a passkey"
registerLabel = "Add a passkey"
identifier = ""
rpId = ""
rpName = "WRNexusJS"
passkeyName = "Passkey"
optionsEndpoint = ""
verifyEndpoint = ""
redirect = ""
mfaHref = "/two-factor"
conditional = false
fullWidth = false
loadingMessage = "Waiting for your passkey…"
successMessage = "Passkey verified."
color = "primary"
size = "md"
class = ""
@event passkeyRegistered = function
@event passkeyAuthenticated = function
@event error = function
outputs {
passkeyRegistered(payload: { credentialId: string; response?: object })
passkeyAuthenticated(payload: { userId?: string; sessionId?: string; response?: object })
error(payload: { code: string; message: string })
}
props {
mode: string = "authenticate"
label: string = "Continue with a passkey"
registerLabel: string = "Add a passkey"
identifier: string = ""
rpId: string = ""
rpName: string = "WRNexusJS"
passkeyName: string = "Passkey"
optionsEndpoint: string = ""
verifyEndpoint: string = ""
redirect: string = ""
mfaHref: string = "/two-factor"
conditional: boolean = false
fullWidth: boolean = false
loadingMessage: string = "Waiting for your passkey…"
successMessage: string = "Passkey verified."
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<div {...attrs} data-wrnexus-runtime="auth" data-auth-passkey='{mode}' data-identifier='{identifier}' data-rp-id='{rpId}' data-rp-name='{rpName}' data-passkey-name='{passkeyName}' data-options-endpoint='{optionsEndpoint}' data-verify-endpoint='{verifyEndpoint}' data-redirect='{redirect}' data-mfa-href='{mfaHref}' data-conditional='{conditional}' data-loading-message='{loadingMessage}' data-success-message='{successMessage}' data-auth-state="idle" aria-busy="false" class='space-y-2 {class}'>
<button type="button" class='inline-flex h-11 items-center justify-center gap-2 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] px-4 text-sm font-semibold text-[var(--wire-color-text)] hover:bg-[var(--wire-color-surface-2)] disabled:cursor-wait disabled:opacity-60 {fullWidth ? "w-full" : ""}'>
<span aria-hidden="true" class="icon-[lucide--key-round] size-4"></span>
{mode == "register" ? registerLabel : label}
</button>
<Button
type="button"
label='{mode == "register" ? registerLabel : label}'
icon="icon-[lucide--key-round]"
variant="outline"
color='{color}'
size='{size}'
fullWidth='{fullWidth}'
/>
<p data-auth-status hidden role="status" class="m-0 text-xs text-[var(--wire-color-muted)]"></p>
</div>
}
+22 -73
View File
@@ -1,86 +1,35 @@
component RecoveryCodes {
props {
codes = []
schema = "auth-recovery-codes"
title = "Recovery codes"
description = "Store these codes somewhere safe. Each code can be used once."
downloadLabel = "Download codes"
regenerateLabel = "Generate new codes"
regenerateAction = "/api/auth/recovery-codes"
successMessage = "New recovery codes generated. Previous unused codes are no longer valid."
count = 10
color = "primary"
size = "md"
class = ""
codes: unknown[] = []
schema: string = "auth-recovery-codes"
title: string = "Recovery codes"
description: string = "Store these codes somewhere safe. Each code can be used once."
downloadLabel: string = "Download codes"
regenerateLabel: string = "Generate new codes"
regenerateAction: string = "/api/auth/recovery-codes"
successMessage: string = "New recovery codes generated. Previous unused codes are no longer valid."
count: number = 10
color: string = "primary"
size: string = "md"
class: string = ""
}
view {
<section
{...attrs}
data-wrnexus-runtime="auth"
data-auth-recovery-codes
data-recovery-filename="wrnexus-recovery-codes.txt"
class='w-full max-w-xl rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 {class}'
>
<div class="flex items-start justify-between gap-4">
<div>
<h2 class="m-0 text-xl font-semibold">{title}</h2>
<p class="mt-1 text-sm text-[var(--wire-color-muted)]">{description}</p>
</div>
<span class="icon-[lucide--shield-keyhole] size-6 text-[var(--wire-color-primary)]"></span>
<Card {...attrs} title='{title}' description='{description}' color='{color}' size='{size}' class='w-full max-w-xl {class}' data-wrnexus-runtime="auth" data-auth-recovery-codes data-recovery-filename="wrnexus-recovery-codes.txt">
<Alert title="Keep these codes private" description="Anyone with a recovery code may be able to access your account." icon="icon-[lucide--shield-alert]" color="warning" variant="soft" size="sm" />
<div data-recovery-code-list class="mt-4 grid grid-cols-2 gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-4 font-mono text-sm sm:grid-cols-3">
{#each codes as code}<code data-recovery-code class="rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center">{code}</code>{/each}
</div>
<div
data-recovery-code-list
class="mt-5 grid grid-cols-2 gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-4 font-mono text-sm sm:grid-cols-3"
>
{#each codes as code}
<code
data-recovery-code
class="rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center"
>{code}</code>
{/each}
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button
type="button"
data-recovery-download
class="h-10 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-4 text-sm font-semibold"
>
{downloadLabel}
</button>
<form
method="post"
action='{regenerateAction}'
data-schema='{schema}'
novalidate
class="flex-1"
>
<Button type="button" label='{downloadLabel}' icon="icon-[lucide--download]" variant="outline" color='{color}' size='{size}' data-recovery-download />
<form method="post" action='{regenerateAction}' data-schema='{schema}' novalidate class="min-w-48 flex-1">
<input type="hidden" name="count" value='{count}' />
<p data-error="count" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
<p
data-error="_form"
role="alert"
class="m-0 hidden text-xs text-[var(--wire-color-danger)]"
></p>
<p
data-success='{successMessage}'
role="status"
hidden
class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-xs text-[var(--wire-color-success)]"
>
{successMessage}
</p>
<button
type="submit"
class="h-10 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] px-4 text-sm font-semibold disabled:cursor-wait disabled:opacity-60"
>
{regenerateLabel}
</button>
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
<p data-success='{successMessage}' role="status" hidden class="m-0 text-xs text-[var(--wire-color-success)]">{successMessage}</p>
<Button type="submit" label='{regenerateLabel}' icon="icon-[lucide--refresh-cw]" variant="outline" color='{color}' size='{size}' fullWidth="true" />
</form>
</div>
</section>
</Card>
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.7.0",
"version": "0.8.0",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
@@ -52,7 +52,7 @@
"@wrnexus/db": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest",
"@types/bun": "^1.3.14",
"@wrnexus/syntax": "workspace:*",
"typescript": "^5.9.2"
},
+89
View File
@@ -0,0 +1,89 @@
import type { Context } from "@wrnexus/core";
import type { AuthEngine } from "./engine.ts";
import type { AuthSession, AuthUser } from "./types.ts";
export type AuthRouteName =
| "signIn"
| "signUp"
| "signOut"
| "forgotPassword"
| "resetPassword"
| "verifyEmail"
| "verifyPhone"
| "twoFactor"
| "sessions"
| "passkeys";
const DEFAULT_AUTH_ROUTES: Record<AuthRouteName, string> = {
signIn: "/sign-in",
signUp: "/sign-up",
signOut: "/api/auth/logout",
forgotPassword: "/forgot-password",
resetPassword: "/reset-password",
verifyEmail: "/verify-email",
verifyPhone: "/verify-phone",
twoFactor: "/two-factor",
sessions: "/account/sessions",
passkeys: "/account/passkeys",
};
export function authRoute(
name: AuthRouteName,
options: { basePath?: string; overrides?: Partial<Record<AuthRouteName, string>> } = {},
): string {
const route = options.overrides?.[name] ?? DEFAULT_AUTH_ROUTES[name];
if (!options.basePath || route.startsWith("http://") || route.startsWith("https://"))
return route;
return `${options.basePath.replace(/\/$/, "")}/${route.replace(/^\//, "")}`;
}
export function authSuccess<T extends Record<string, unknown>>(
data: T,
init: ResponseInit = {},
): Response {
return Response.json({ ok: true, ...data }, { status: init.status ?? 200, ...init });
}
export function authFailure(
code: string,
message: string,
status = 400,
details?: Record<string, unknown>,
): Response {
return Response.json(
{ ok: false, error: { code, message, ...(details ? { details } : {}) } },
{ status },
);
}
export function requireAuthUser(ctx: Context): AuthUser {
if (!ctx.user || typeof ctx.user !== "object") {
throw new Response("Unauthorized", { status: 401 });
}
return ctx.user as AuthUser;
}
export function optionalAuthUser(ctx: Context): AuthUser | null {
return ctx.user && typeof ctx.user === "object" ? (ctx.user as AuthUser) : null;
}
export async function currentAuthSession(
engine: AuthEngine,
sessionId: string | undefined,
): Promise<AuthSession | null> {
if (!sessionId) return null;
const session = await engine.store.findSession(sessionId);
return session ?? null;
}
export function authComponentProps(
input: Record<string, unknown>,
defaults: { color?: string; size?: string; class?: string } = {},
): Record<string, unknown> {
return {
color: defaults.color ?? "primary",
size: defaults.size ?? "md",
class: defaults.class ?? "",
...input,
};
}
+2
View File
@@ -101,3 +101,5 @@ export {
type AuthSchemaSet,
type AuthSchemaOverrides,
} from "./validation.ts";
export * from "./helpers.ts";
+1 -1
View File
@@ -297,7 +297,7 @@ export function authPlugin(options: AuthPluginOptions = {}) {
return definePlugin({
name: "@wrnexus/auth",
version: "0.5.0",
version: "0.8.0",
enforce: "post",
componentDirs(context) {
+46 -1
View File
@@ -166,10 +166,55 @@ export interface LoginAttempt {
riskLevel: AuthRiskLevel;
}
export const AUTH_SECURITY_EVENT_TYPES = [
"account.registered",
"account.status-changed",
"delivery.failed",
"device.revoked",
"device.trusted",
"identity.email-verification-requested",
"identity.email-verified",
"identity.otp-verified",
"identity.phone-verification-requested",
"identity.phone-verified",
"impersonation.denied",
"impersonation.ended",
"impersonation.started",
"invitation.accepted",
"invitation.created",
"login.failed",
"login.magic-link",
"login.oauth",
"login.otp",
"login.otp-verified",
"login.passkey",
"login.succeeded",
"mfa.failed",
"mfa.otp-verified",
"mfa.recovery-code-used",
"mfa.recovery-codes-generated",
"mfa.succeeded",
"mfa.totp-disabled",
"mfa.totp-enabled",
"mfa.totp-verified",
"oauth.linked",
"oauth.unlinked",
"passkey.registered",
"password.changed",
"password.reset",
"password.reset-requested",
"session.revoked",
"session.revoked-all",
] as const;
export type KnownAuthSecurityEventType = (typeof AUTH_SECURITY_EVENT_TYPES)[number];
/** Known framework events plus application-defined extension events. */
export type AuthSecurityEventType = KnownAuthSecurityEventType | (string & {});
export interface AuthSecurityEvent {
id: string;
userId?: string;
type: string;
type: AuthSecurityEventType;
severity: "info" | "warning" | "critical";
actorUserId?: string;
sessionId?: string;
@@ -0,0 +1,32 @@
import { expect, test } from "bun:test";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { parse } from "@wrnexus/syntax";
const componentsDir = join(import.meta.dir, "..", "components");
test("all auth package components parse", () => {
for (const file of readdirSync(componentsDir).filter((name) => name.endsWith(".wrn"))) {
expect(() => parse(readFileSync(join(componentsDir, file), "utf8"))).not.toThrow();
}
});
test("auth form blocks use WRNexus UI controls", () => {
const formFiles = [
"SignIn.wrn",
"SignUp.wrn",
"ForgotPassword.wrn",
"ResetPassword.wrn",
"OtpSignIn.wrn",
"TwoFactorChallenge.wrn",
"VerifyEmail.wrn",
"VerifyPhone.wrn",
"InvitationAccept.wrn",
"MagicLinkSignIn.wrn",
];
for (const file of formFiles) {
const source = readFileSync(join(componentsDir, file), "utf8");
expect(source).toMatch(/<(?:Input|PinInput|StrongPassword|TogglePassword|Checkbox|Select)\b/);
expect(source).toContain("<Button");
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.7.0",
"version": "0.8.0",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/benchmark",
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.",
"main": "src/index.ts",
+65 -1
View File
@@ -3,7 +3,71 @@
Bounded in-memory/tag caching and HTTP response caching for WRNexusJS. Supports request deduplication, tag invalidation, ETags, fresh/stale states, and optional detached stale revalidation.
```ts
import { TagCache, responseCache } from "@wrnexus/cache";
import { connectCacheInvalidation, TagCache, responseCache } from "@wrnexus/cache";
const cache = new TagCache({ ttlMs: 60_000, staleWhileRevalidateMs: 300_000 });
export default responseCache({ cache, tags: ["products"] });
```
`TagCache` bounds entries with LRU-style eviction, deduplicates concurrent
loaders, and prevents an invalidated in-flight loader from repopulating stale
data. Use `lookup()` when fresh/stale state matters, or `getOrLoad()` for
stampede-safe loading.
For multi-instance applications, connect the cache to any compatible pub/sub
bus (including `@wrnexus/pubsub`). Namespaces isolate applications sharing the
same broker. Local invalidation happens first and the returned promise confirms
cross-instance publication; failures remain visible to the caller.
```ts
import { connectCacheInvalidation, TagCache } from "@wrnexus/cache";
import { createPubSub } from "@wrnexus/pubsub";
import { redisDriver } from "@wrnexus/pubsub/redis";
const cache = new TagCache({ maxEntries: 10_000 });
const bus = createPubSub(redisDriver(process.env.REDIS_URL));
const invalidation = connectCacheInvalidation(cache, bus, {
namespace: "storefront-production",
onError: (error) => logger.error("cache invalidation failed", { error }),
});
await invalidation.invalidateTag("products");
await invalidation.delete("product:42");
// Unsubscribes this cache only; the shared bus remains owned by the app.
invalidation.close();
await bus.close();
```
## Framework cache layers
`CacheCoordinator` keeps the four cache lifetimes explicit:
- `coordinator.request()` creates request-only deduplication.
- `coordinator.data` caches loader/query results.
- `coordinator.component` caches reusable rendered fragments.
- `coordinator.page` caches complete safe documents.
All cross-request layers are bounded, tag-aware, stale-while-revalidate capable,
stampede-safe, and expose `withLock()` for exclusive per-key work. `inspect()`
returns metadata without cached values. Development applications expose that
inspection through the Cache panel and `GET /__wrnexus/cache`.
Pages and components can opt in declaratively:
```wrn
cache {
scope = "page"
strategy = "stale-while-revalidate"
ttl = "5m"
stale = "10m"
tags = ["catalog", "marketing"]
vary = ["tenant", "language"]
}
```
Omit `scope` to cache named loader data. Use `scope = "page"` for full-page
caching. Component policies cache their rendered fragment. Authenticated user
and tenant identities are always included automatically; page caches also vary
by language, theme, and accent. Add header names or `cookie:name` entries for
other application-specific variation. Pages containing CSRF forms are never
stored in the full-page cache.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/cache",
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.",
"main": "src/index.ts",
+100
View File
@@ -0,0 +1,100 @@
import { TagCache, type CacheEvent, type CacheSetOptions, type TagCacheOptions } from "./memory.ts";
export type CacheLayerName = "data" | "component" | "page";
export interface CacheInspection {
layers: Record<CacheLayerName, ReturnType<TagCache<unknown>["snapshot"]>>;
recentEvents: Array<CacheEvent & { layer: CacheLayerName }>;
}
export interface CacheCoordinatorOptions extends Omit<TagCacheOptions, "onEvent"> {
eventLimit?: number;
onEvent?: (event: CacheEvent & { layer: CacheLayerName }) => void;
}
/** A request-lifetime cache: deduplicates work without leaking values between requests. */
export class RequestCache {
private pending = new Map<string, Promise<unknown>>();
getOrLoad<V>(key: string, loader: () => V | Promise<V>): Promise<V> {
const existing = this.pending.get(key);
if (existing) return existing as Promise<V>;
const value = Promise.resolve().then(loader);
this.pending.set(key, value);
return value as Promise<V>;
}
clear(): void {
this.pending.clear();
}
}
/** Owns the three cross-request cache layers and creates isolated request caches. */
export class CacheCoordinator {
readonly data: TagCache<unknown>;
readonly component: TagCache<unknown>;
readonly page: TagCache<unknown>;
private readonly events: Array<CacheEvent & { layer: CacheLayerName }> = [];
private readonly eventLimit: number;
constructor(options: CacheCoordinatorOptions = {}) {
const { eventLimit = 200, onEvent, ...cacheOptions } = options;
this.eventLimit = Math.max(1, eventLimit);
const create = (layer: CacheLayerName) =>
new TagCache<unknown>({
...cacheOptions,
onEvent: (event) => {
const item = { ...event, layer };
this.events.push(item);
if (this.events.length > this.eventLimit)
this.events.splice(0, this.events.length - this.eventLimit);
onEvent?.(item);
},
});
this.data = create("data");
this.component = create("component");
this.page = create("page");
}
request(): RequestCache {
return new RequestCache();
}
layer(name: CacheLayerName): TagCache<unknown> {
return this[name];
}
getOrLoad<V>(
layer: CacheLayerName,
key: string,
loader: () => V | Promise<V>,
options?: CacheSetOptions,
): Promise<V> {
return this.layer(layer).getOrLoad(key, loader, options) as Promise<V>;
}
invalidateTags(tags: Iterable<string>): number {
return (
this.data.invalidateTags(tags) +
this.component.invalidateTags(tags) +
this.page.invalidateTags(tags)
);
}
inspect(): CacheInspection {
return {
layers: {
data: this.data.snapshot(),
component: this.component.snapshot(),
page: this.page.snapshot(),
},
recentEvents: this.events.map((event) => ({ ...event, tags: event.tags && [...event.tags] })),
};
}
clear(): void {
this.data.clear();
this.component.clear();
this.page.clear();
}
}
+94
View File
@@ -0,0 +1,94 @@
import type { TagCache } from "./memory.ts";
export interface CacheInvalidationBus {
publish(topic: string, message: unknown): void | Promise<void>;
subscribe(pattern: string, handler: (message: unknown) => void | Promise<void>): () => void;
}
export interface DistributedInvalidationOptions {
namespace?: string;
instanceId?: string;
onError?: (error: unknown) => void;
}
type InvalidationMessage =
| { source: string; operation: "tags"; tags: string[] }
| { source: string; operation: "key"; key: string }
| { source: string; operation: "clear" };
export interface DistributedInvalidation {
invalidateTag(tag: string): Promise<number>;
invalidateTags(tags: Iterable<string>): Promise<number>;
delete(key: string): Promise<boolean>;
clear(): Promise<void>;
close(): void;
}
/**
* Propagate cache invalidations over any structurally compatible pub/sub bus.
* The bus is intentionally not closed because applications commonly share it.
*/
export function connectCacheInvalidation<V>(
cache: TagCache<V>,
bus: CacheInvalidationBus,
options: DistributedInvalidationOptions = {},
): DistributedInvalidation {
const namespace = options.namespace?.trim() || "default";
if (!/^[a-zA-Z0-9._-]+$/.test(namespace)) {
throw new TypeError("cache invalidation namespace contains unsupported characters");
}
const source = options.instanceId?.trim() || crypto.randomUUID();
const topic = `wrnexus:cache:${namespace}:invalidate`;
let closed = false;
const publish = async (message: InvalidationMessage): Promise<void> => {
if (closed) throw new Error("WRN-CACHE-INVALIDATION-CLOSED: invalidation channel is closed");
try {
await bus.publish(topic, message);
} catch (error) {
options.onError?.(error);
throw error;
}
};
const unsubscribe = bus.subscribe(topic, (value) => {
if (!value || typeof value !== "object") return;
const message = value as Partial<InvalidationMessage>;
if (message.source === source) return;
if (message.operation === "tags" && Array.isArray(message.tags)) {
cache.invalidateTags(message.tags.filter((tag): tag is string => typeof tag === "string"));
} else if (message.operation === "key" && typeof message.key === "string") {
cache.delete(message.key);
} else if (message.operation === "clear") {
cache.clear();
}
});
const invalidateTags = async (input: Iterable<string>): Promise<number> => {
const tags = [...new Set(input)].filter(Boolean);
const removed = cache.invalidateTags(tags);
await publish({ source, operation: "tags", tags });
return removed;
};
return {
async invalidateTag(tag) {
return invalidateTags([tag]);
},
invalidateTags,
async delete(key) {
const removed = cache.delete(key);
await publish({ source, operation: "key", key });
return removed;
},
async clear() {
cache.clear();
await publish({ source, operation: "clear" });
},
close() {
if (closed) return;
closed = true;
unsubscribe();
},
};
}
+9
View File
@@ -1,4 +1,13 @@
export { TagCache } from "./memory.ts";
export type { CacheEntry, CacheLookup, CacheSetOptions, TagCacheOptions } from "./memory.ts";
export type { CacheEvent, CacheSnapshotEntry } from "./memory.ts";
export { CacheCoordinator, RequestCache } from "./coordinator.ts";
export type { CacheCoordinatorOptions, CacheInspection, CacheLayerName } from "./coordinator.ts";
export { responseCache } from "./response.ts";
export type { CachedResponse, ResponseCacheOptions } from "./response.ts";
export { connectCacheInvalidation } from "./distributed.ts";
export type {
CacheInvalidationBus,
DistributedInvalidation,
DistributedInvalidationOptions,
} from "./distributed.ts";
+95 -7
View File
@@ -19,35 +19,66 @@ export interface TagCacheOptions {
staleWhileRevalidateMs?: number;
maxEntries?: number;
clock?: () => number;
onEvent?: (event: CacheEvent) => void;
}
export interface CacheEvent {
operation: "hit" | "stale" | "miss" | "set" | "delete" | "invalidate" | "clear" | "load";
key?: string;
tags?: string[];
at: number;
}
export interface CacheSnapshotEntry {
key: string;
state: "fresh" | "stale";
createdAt: number;
expiresAt: number;
staleUntil: number;
tags: string[];
}
export class TagCache<V = unknown> {
private entries = new Map<string, CacheEntry<V>>();
private tagIndex = new Map<string, Set<string>>();
private pending = new Map<string, Promise<V>>();
private locks = new Map<string, Promise<void>>();
private revisions = new Map<string, number>();
private readonly ttlMs: number;
private readonly staleMs: number;
private readonly maxEntries: number;
private readonly clock: () => number;
private readonly onEvent?: (event: CacheEvent) => void;
constructor(options: TagCacheOptions = {}) {
this.ttlMs = options.ttlMs ?? 60_000;
this.staleMs = options.staleWhileRevalidateMs ?? 0;
this.maxEntries = Math.max(1, options.maxEntries ?? 10_000);
this.clock = options.clock ?? Date.now;
this.onEvent = options.onEvent;
}
private emit(event: Omit<CacheEvent, "at">): void {
this.onEvent?.({ ...event, at: this.clock() });
}
lookup(key: string): CacheLookup<V> {
const entry = this.entries.get(key);
if (!entry) return { state: "miss" };
if (!entry) {
this.emit({ operation: "miss", key });
return { state: "miss" };
}
const now = this.clock();
if (entry.staleUntil <= now) {
this.delete(key);
this.emit({ operation: "miss", key });
return { state: "miss" };
}
this.entries.delete(key);
this.entries.set(key, entry);
return { state: entry.expiresAt > now ? "fresh" : "stale", entry };
const state = entry.expiresAt > now ? "fresh" : "stale";
this.emit({ operation: state === "fresh" ? "hit" : "stale", key, tags: entry.tags });
return { state, entry };
}
get(key: string): V | undefined {
@@ -56,7 +87,12 @@ export class TagCache<V = unknown> {
}
set(key: string, value: V, options: CacheSetOptions = {}): void {
this.delete(key);
this.bump(key);
this.store(key, value, options);
}
private store(key: string, value: V, options: CacheSetOptions): void {
this.removeEntry(key);
const now = this.clock();
const ttlMs = Math.max(0, options.ttlMs ?? this.ttlMs);
const staleMs = Math.max(0, options.staleWhileRevalidateMs ?? this.staleMs);
@@ -69,6 +105,7 @@ export class TagCache<V = unknown> {
tags,
};
this.entries.set(key, entry);
this.emit({ operation: "set", key, tags });
for (const tag of tags) {
const keys = this.tagIndex.get(tag) ?? new Set<string>();
keys.add(key);
@@ -90,10 +127,12 @@ export class TagCache<V = unknown> {
if (hit.state === "fresh") return hit.entry.value;
if (hit.state === "stale") {
if (!this.pending.has(key)) {
const revision = this.revision(key);
this.emit({ operation: "load", key, tags: options.tags });
const refresh = Promise.resolve()
.then(loader)
.then((value) => {
this.set(key, value, options);
if (this.revision(key) === revision) this.store(key, value, options);
return value;
})
.finally(() => this.pending.delete(key));
@@ -103,10 +142,12 @@ export class TagCache<V = unknown> {
}
const existing = this.pending.get(key);
if (existing) return existing;
const revision = this.revision(key);
this.emit({ operation: "load", key, tags: options.tags });
const pending = Promise.resolve()
.then(loader)
.then((value) => {
this.set(key, value, options);
if (this.revision(key) === revision) this.store(key, value, options);
return value;
})
.finally(() => this.pending.delete(key));
@@ -114,7 +155,30 @@ export class TagCache<V = unknown> {
return pending;
}
/** Serialize arbitrary cache-adjacent work for a key without storing its result. */
async withLock<T>(key: string, task: () => T | Promise<T>): Promise<T> {
const previous = this.locks.get(key) ?? Promise.resolve();
let release!: () => void;
const current = new Promise<void>((resolve) => (release = resolve));
const queued = previous.then(() => current);
this.locks.set(key, queued);
await previous;
try {
return await task();
} finally {
release();
if (this.locks.get(key) === queued) this.locks.delete(key);
}
}
delete(key: string): boolean {
const removed = this.removeEntry(key);
if (removed || this.pending.has(key)) this.bump(key);
if (removed) this.emit({ operation: "delete", key });
return removed;
}
private removeEntry(key: string): boolean {
const entry = this.entries.get(key);
if (!entry) return false;
this.entries.delete(key);
@@ -129,23 +193,47 @@ export class TagCache<V = unknown> {
invalidateTag(tag: string): number {
const keys = [...(this.tagIndex.get(tag) ?? [])];
for (const key of keys) this.delete(key);
this.emit({ operation: "invalidate", tags: [tag] });
return keys.length;
}
invalidateTags(tags: Iterable<string>): number {
const requested = [...tags];
const keys = new Set<string>();
for (const tag of tags) for (const key of this.tagIndex.get(tag) ?? []) keys.add(key);
for (const tag of requested) for (const key of this.tagIndex.get(tag) ?? []) keys.add(key);
for (const key of keys) this.delete(key);
this.emit({ operation: "invalidate", tags: requested });
return keys.size;
}
clear(): void {
for (const key of new Set([...this.entries.keys(), ...this.pending.keys()])) this.bump(key);
this.entries.clear();
this.tagIndex.clear();
this.pending.clear();
this.emit({ operation: "clear" });
}
get size(): number {
return this.entries.size;
}
snapshot(): CacheSnapshotEntry[] {
const now = this.clock();
return [...this.entries.entries()].map(([key, entry]) => ({
key,
state: entry.expiresAt > now ? "fresh" : "stale",
createdAt: entry.createdAt,
expiresAt: entry.expiresAt,
staleUntil: entry.staleUntil,
tags: [...entry.tags],
}));
}
private revision(key: string): number {
return this.revisions.get(key) ?? 0;
}
private bump(key: string): void {
this.revisions.set(key, this.revision(key) + 1);
}
}
+104 -1
View File
@@ -1,7 +1,32 @@
import { describe, expect, test } from "bun:test";
import { TagCache, responseCache } from "../src/index.ts";
import {
CacheCoordinator,
connectCacheInvalidation,
RequestCache,
TagCache,
responseCache,
} from "../src/index.ts";
describe("@wrnexus/cache", () => {
test("separates request, data, component, and page cache lifetimes", async () => {
const coordinator = new CacheCoordinator({ ttlMs: 100, eventLimit: 10 });
coordinator.data.set("user:1", { id: 1 }, { tags: ["users"] });
coordinator.component.set("avatar:1", "html", { tags: ["users"] });
coordinator.page.set("/users", "document", { tags: ["users"] });
expect(coordinator.inspect().layers.data[0]?.key).toBe("user:1");
expect(coordinator.invalidateTags(["users"])).toBe(3);
expect(
coordinator.inspect().recentEvents.some((event) => event.operation === "invalidate"),
).toBe(true);
const request = new RequestCache();
let calls = 0;
const [first, second] = await Promise.all([
request.getOrLoad("permissions", async () => ++calls),
request.getOrLoad("permissions", async () => ++calls),
]);
expect([first, second, calls]).toEqual([1, 1, 1]);
});
test("supports fresh, stale, and tag invalidation", async () => {
let now = 0;
const cache = new TagCache<number>({ ttlMs: 10, staleWhileRevalidateMs: 10, clock: () => now });
@@ -23,6 +48,84 @@ describe("@wrnexus/cache", () => {
expect(calls).toBe(1);
});
test("provides exclusive per-key locks", async () => {
const cache = new TagCache();
const order: string[] = [];
let release!: () => void;
const first = cache.withLock("catalog", async () => {
order.push("first:start");
await new Promise<void>((resolve) => (release = resolve));
order.push("first:end");
});
await Promise.resolve();
const second = cache.withLock("catalog", () => order.push("second"));
await Promise.resolve();
expect(order).toEqual(["first:start"]);
release();
await Promise.all([first, second]);
expect(order).toEqual(["first:start", "first:end", "second"]);
});
test("invalidation during a load prevents stale work from repopulating the cache", async () => {
const cache = new TagCache<number>();
let release!: (value: number) => void;
const loading = cache.getOrLoad(
"x",
() => new Promise<number>((resolve) => (release = resolve)),
);
await Promise.resolve();
cache.clear();
release(7);
expect(await loading).toBe(7);
expect(cache.lookup("x").state).toBe("miss");
});
test("propagates tag, key, and clear invalidations across instances", async () => {
const subscriptions = new Map<string, Set<(message: unknown) => void | Promise<void>>>();
const bus = {
async publish(topic: string, message: unknown) {
await Promise.all(
[...(subscriptions.get(topic) ?? [])].map((handler) => Promise.resolve(handler(message))),
);
},
subscribe(topic: string, handler: (message: unknown) => void | Promise<void>) {
const handlers = subscriptions.get(topic) ?? new Set();
handlers.add(handler);
subscriptions.set(topic, handlers);
return () => handlers.delete(handler);
},
};
const first = new TagCache<number>();
const second = new TagCache<number>();
const firstChannel = connectCacheInvalidation(first, bus, {
namespace: "catalog",
instanceId: "one",
});
const secondChannel = connectCacheInvalidation(second, bus, {
namespace: "catalog",
instanceId: "two",
});
for (const cache of [first, second]) cache.set("product:1", 1, { tags: ["products"] });
expect(await firstChannel.invalidateTag("products")).toBe(1);
expect(second.lookup("product:1").state).toBe("miss");
first.set("one", 1);
second.set("one", 1);
await secondChannel.delete("one");
expect(first.lookup("one").state).toBe("miss");
first.set("all", 1);
second.set("all", 1);
await firstChannel.clear();
expect(second.size).toBe(0);
firstChannel.close();
firstChannel.close();
await expect(firstChannel.invalidateTag("products")).rejects.toThrow(
"WRN-CACHE-INVALIDATION-CLOSED",
);
secondChannel.close();
});
test("response middleware does not call next twice for stale entries", async () => {
let now = 0;
const cache = new TagCache<any>({ ttlMs: 1, staleWhileRevalidateMs: 100, clock: () => now });
+11
View File
@@ -360,3 +360,14 @@ const engine = createCaptchaEngine({ secret, generators: [wordChallenge] });
```
Applications may also implement `CaptchaStore`, `CaptchaAudioRenderer`, or use `defineCaptchaProvider()` for a completely custom service.
## Helper and block kit
The package exports `captchaTokenFrom`, `captchaHeaders`, `captchaFields`, `verifyCaptcha`, `verifyCaptchaOrThrow`, `captchaResultResponse`, and `captchaContext` for consistent server and client integration.
Enable the CAPTCHA plugin to use the low-level `<Captcha />` challenge plus complete UI-composed blocks:
- `<CaptchaField />`
- `<CaptchaStatus />`
`CaptchaField` composes `Card` from `@wrnexus/ui` and keeps the CAPTCHA-specific size separate from the surrounding UI size.
@@ -0,0 +1,18 @@
component CaptchaField {
props {
title: string = "Security verification"
description: string = "Complete the challenge before submitting."
provider: string = "self-hosted"
type: string = "alphanumeric"
action: string = "form-submit"
color: string = "primary"
size: string = "md"
captchaSize: string = "normal"
class: string = ""
}
view {
<Card {...attrs} title='{title}' description='{description}' color='{color}' size='{size}' class='{class}'>
<Captcha provider='{provider}' type='{type}' action='{action}' color='{color}' size='{captchaSize}' compact='{size == "sm"}' />
</Card>
}
}
@@ -0,0 +1,23 @@
component CaptchaStatus {
props {
success: boolean = false
message: string = ""
successMessage: string = "Security verification completed."
failureMessage: string = "Security verification is required."
color: string = "primary"
size: string = "sm"
class: string = ""
}
view {
<Alert
{...attrs}
title='{success ? "Verified" : "Verification required"}'
description='{message || (success ? successMessage : failureMessage)}'
icon='{success ? "icon-[lucide--shield-check]" : "icon-[lucide--shield-alert]"}'
color='{success ? "success" : color}'
variant="soft"
size='{size}'
class='{class}'
/>
}
}
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
"version": "0.7.0",
"version": "0.8.0",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
@@ -43,10 +43,11 @@
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
"@wrnexus/validation": "workspace:*"
"@wrnexus/validation": "workspace:*",
"@wrnexus/ui": "workspace:*"
},
"devDependencies": {
"@types/bun": "latest",
"@types/bun": "^1.3.14",
"typescript": "^5.9.2",
"@wrnexus/syntax": "workspace:*"
},
+90
View File
@@ -0,0 +1,90 @@
import type { Context } from "@wrnexus/core";
import type {
CaptchaEngine,
CaptchaProvider,
CaptchaVerificationResult,
VerifyCaptchaInput,
} from "./types.ts";
import { selfHostedProvider } from "./providers/self-hosted.ts";
export function captchaTokenFrom(
value: Request | Headers | FormData | URLSearchParams | Record<string, unknown>,
field = "wrn-captcha-response",
): Promise<string | undefined> | string | undefined {
if (value instanceof Request) {
const header = value.headers.get("x-wrn-captcha-token");
if (header) return header;
return value
.clone()
.formData()
.then((form) => {
const token = form.get(field) ?? form.get("captchaToken") ?? form.get("responseToken");
return typeof token === "string" ? token : undefined;
})
.catch(async () => {
try {
const body = (await value.clone().json()) as Record<string, unknown>;
const token = body[field] ?? body.captchaToken ?? body.responseToken;
return token === undefined ? undefined : String(token);
} catch {
return undefined;
}
});
}
if (value instanceof Headers) {
return value.get("x-wrn-captcha-token") ?? value.get("x-captcha-token") ?? undefined;
}
if (value instanceof FormData || value instanceof URLSearchParams) {
const token = value.get(field) ?? value.get("captchaToken") ?? value.get("responseToken");
return typeof token === "string" ? token : undefined;
}
const token = value[field] ?? value.captchaToken ?? value.responseToken;
return token === undefined ? undefined : String(token);
}
export function captchaHeaders(token: string): HeadersInit {
return { "x-wrn-captcha-token": token };
}
export function captchaFields(
token: string,
field = "wrn-captcha-response",
): Record<string, string> {
return { [field]: token };
}
export async function verifyCaptcha(
providerOrEngine: CaptchaProvider | CaptchaEngine,
input: VerifyCaptchaInput,
): Promise<CaptchaVerificationResult> {
const provider: CaptchaProvider =
"client" in providerOrEngine
? (providerOrEngine as CaptchaProvider)
: selfHostedProvider(providerOrEngine as CaptchaEngine);
return provider.verify(input);
}
export async function verifyCaptchaOrThrow(
providerOrEngine: CaptchaProvider | CaptchaEngine,
input: VerifyCaptchaInput,
): Promise<CaptchaVerificationResult> {
const result = await verifyCaptcha(providerOrEngine, input);
if (!result.success) {
throw new Error(
`WRN-CAPTCHA-${String(result.code ?? "FAILED").toUpperCase()}: ${result.message ?? "CAPTCHA verification failed"}`,
);
}
return result;
}
export function captchaResultResponse(result: CaptchaVerificationResult): Response {
return Response.json(result, {
status: result.success ? 200 : 403,
headers: { "cache-control": "no-store", "x-content-type-options": "nosniff" },
});
}
export function captchaContext(ctx: Context): CaptchaVerificationResult | null {
const result = ctx.locals.captcha;
return result && typeof result === "object" ? (result as CaptchaVerificationResult) : null;
}
+1
View File
@@ -12,3 +12,4 @@ export * from "./providers/index.ts";
export * from "./challenges/index.ts";
export * from "./audio/index.ts";
export * from "./crypto.ts";
export * from "./helpers.ts";
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, test } from "bun:test";
import { captchaFields, captchaHeaders, captchaTokenFrom } from "../src/index.ts";
describe("CAPTCHA helper kit", () => {
test("reads standard response tokens from forms, headers, and objects", async () => {
const form = new FormData();
form.set("wrn-captcha-response", "form-token");
expect(await captchaTokenFrom(form)).toBe("form-token");
expect(await captchaTokenFrom(new Headers({ "x-wrn-captcha-token": "header-token" }))).toBe(
"header-token",
);
expect(await captchaTokenFrom({ "wrn-captcha-response": "object-token" })).toBe("object-token");
});
test("creates consistent fields and headers", () => {
expect(captchaFields("token")).toEqual({ "wrn-captcha-response": "token" });
expect(new Headers(captchaHeaders("token")).get("x-wrn-captcha-token")).toBe("token");
});
});
+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", () => {
+35
View File
@@ -1,9 +1,44 @@
# @wrnexus/compiler
## Partial-static rendering
Pages can select `render = "partial-static"` and divide their view with `<Static>` and
`<Dynamic>` boundaries. The compiler emits a build-only shell renderer that never evaluates
dynamic-boundary children. `wrnexus build` expands static component mounts into
`dist/partial-shells.json`, records byte/region evidence in `build-report.json`, and embeds
the shell in the production route manifest. At request time the production runtime retains
request-aware layouts, locale/theme metadata and security nonces while streaming dynamic
regions into stable placeholders.
> Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
Production adapters use `analyzeRuntimeImports` before bundling. Edge, worker,
service-worker, and browser targets reject Node filesystem, TCP, and process
modules with `WRN-RUNTIME-CAPABILITY`. Package manifests can declare supported
`wrnexus.runtimes` and required `wrnexus.requires` capabilities; discovery fails
when the selected deployment cannot satisfy them.
## Server actions
```wrn
action createUser using CreateUserSchema {
const user = await users.create(input)
invalidate("users")
return user
}
view {
<form @submit="createUser">...</form>
}
```
The compiler produces a schema-aware server registry, a fully inferred action
client, and progressively enhanced form metadata. The shared runtime performs
validation, authentication/permission checks, CSRF verification, serialization,
invalidation reporting, and browser lifecycle events.
## Overview
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
+4 -2
View File
@@ -1,13 +1,15 @@
{
"name": "@wrnexus/compiler",
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@wrnexus/csr": "workspace:*",
"@wrnexus/syntax": "workspace:*",
"@wrnexus/store": "workspace:*"
"@wrnexus/store": "workspace:*",
"@wrnexus/validation": "workspace:*"
}
}
+212 -3
View File
@@ -15,6 +15,185 @@ export interface RuntimeRequirements {
needsServerRuntime: boolean;
hydrationStrategy: string | null;
reasons: string[];
optimization: OptimizationReport;
cachePolicy: Record<string, string>;
requiredPermission: string | null;
}
export interface OptimizationReport {
staticNodes: number;
reactiveRegions: number;
eliminatedBranches: number;
unusedState: string[];
unusedHandlers: string[];
constantProps: string[];
unusedLocalCssClasses: string[];
batchableStateUpdates: number;
memoizableComponents: string[];
preloadDependencies: string[];
serverOnlyModules: string[];
}
function identifiers(value: string): Set<string> {
return new Set(value.match(/[A-Za-z_$][\w$]*/g) ?? []);
}
function literalBoolean(expression: string | null): boolean | undefined {
if (expression === null) return true;
const value = expression.trim();
if (value === "true") return true;
if (
value === "false" ||
value === "null" ||
value === "undefined" ||
value === "0" ||
value === "''" ||
value === '""'
)
return false;
if (/^-?(?:[1-9]\d*|0?\.\d+)$/.test(value) || /^(['"]).+\1$/.test(value)) return true;
return undefined;
}
function optimizeNodes(nodes: ViewNode[], report: { eliminated: number }): ViewNode[] {
const output: ViewNode[] = [];
for (const node of nodes) {
if (node.type === "element")
output.push({
...node,
attrs: node.attrs.map((attribute) => ({ ...attribute })),
children: optimizeNodes(node.children, report),
});
else if (node.type === "each")
output.push({
...node,
body: optimizeNodes(node.body, report),
empty: optimizeNodes(node.empty, report),
});
else if (node.type === "if") {
let selected: ViewNode[] | undefined;
let dynamic = false;
for (const branch of node.branches) {
const value = literalBoolean(branch.cond);
if (value === undefined) {
dynamic = true;
break;
}
report.eliminated++;
if (value) {
selected = branch.body;
break;
}
}
if (dynamic)
output.push({
...node,
branches: node.branches.map((branch) => ({
...branch,
body: optimizeNodes(branch.body, report),
})),
});
else if (selected) output.push(...optimizeNodes(selected, report));
} else output.push({ ...node });
}
return output;
}
/** Safe compile-time folding for literal conditional branches. */
export function optimizeAst(ast: PageAst): { ast: PageAst; eliminatedBranches: number } {
const report = { eliminated: 0 };
return {
ast: { ...ast, view: optimizeNodes(ast.view, report) },
eliminatedBranches: report.eliminated,
};
}
export function analyzeOptimizations(ast: PageAst): OptimizationReport {
const used = new Set<string>();
let staticNodes = 0;
let reactiveRegions = 0;
const componentNames = new Set<string>();
const staticClasses = new Set<string>();
const visit = (nodes: ViewNode[]) => {
for (const node of nodes) {
if (node.type === "text") {
const refs = identifiers(node.value);
refs.forEach((name) => used.add(name));
if (node.value.includes("{")) reactiveRegions++;
else staticNodes++;
} else if (node.type === "element") {
if (/^[A-Z]/.test(node.tag)) componentNames.add(node.tag);
let reactive = false;
for (const attribute of node.attrs) {
identifiers(attribute.value).forEach((name) => used.add(name));
reactive ||= attribute.event || attribute.value.includes("{");
if (attribute.name === "class" && !attribute.value.includes("{"))
for (const name of attribute.value.split(/\s+/)) if (name) staticClasses.add(name);
}
if (reactive) reactiveRegions++;
else staticNodes++;
visit(node.children);
} else if (node.type === "each") {
identifiers(`${node.list} ${node.key ?? ""}`).forEach((name) => used.add(name));
reactiveRegions++;
visit(node.body);
visit(node.empty);
} else {
for (const branch of node.branches) {
identifiers(branch.cond ?? "").forEach((name) => used.add(name));
visit(branch.body);
}
reactiveRegions++;
}
}
};
visit(ast.view);
const handlerReferences = new Set(used);
const executable = [
...ast.runtimeFunctions.map((fn) => fn.body),
...ast.functions,
...ast.effects.map((effect) => effect.body),
...ast.watches.map((watch) => watch.body),
...ast.actions.map((action) => action.body),
].join("\n");
identifiers(executable).forEach((name) => used.add(name));
const localCss = new Set(
ast.styles.flatMap((style) =>
[...style.matchAll(/\.([_a-zA-Z][\w-]*)/g)].map((match) => match[1]!),
),
);
const optimized = optimizeAst(ast);
const assignmentCounts = ast.runtimeFunctions.map(
(fn) =>
ast.states.filter((state) =>
new RegExp(`\\b${state.name}\\s*(?:[+*/-]?=|\\+\\+|--)`).test(fn.body),
).length,
);
return {
staticNodes,
reactiveRegions,
eliminatedBranches: optimized.eliminatedBranches,
unusedState: ast.states.filter((state) => !used.has(state.name)).map((state) => state.name),
unusedHandlers: ast.runtimeFunctions
.filter((fn) => fn.runtime !== "server" && !handlerReferences.has(fn.name))
.map((fn) => fn.name),
constantProps: ast.props
.filter((prop) =>
/^(?:-?\d+(?:\.\d+)?|true|false|null|(['"]).*\1)$/.test(prop.default.trim()),
)
.map((prop) => prop.name),
unusedLocalCssClasses: [...localCss].filter((name) => !staticClasses.has(name)).sort(),
batchableStateUpdates: assignmentCounts
.filter((count) => count > 1)
.reduce((sum, count) => sum + count - 1, 0),
memoizableComponents: [...componentNames].sort(),
preloadDependencies: ast.structuredImports
.filter((entry) => !entry.typeOnly && !entry.source.startsWith("node:"))
.map((entry) => entry.source),
serverOnlyModules: ast.structuredImports
.filter((entry) => entry.source.startsWith("node:") || ast.runtime === "server")
.map((entry) => entry.source),
};
}
function hasEvent(nodes: ViewNode[]): boolean {
@@ -67,12 +246,42 @@ export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements {
else if (interactive) kind = "static-interactive";
else kind = "static";
if (ast.renderMode === "static") {
kind = "static";
reasons.push("explicit static rendering");
} else if (ast.renderMode === "server") {
kind = requestData ? "request-ssr" : "static";
reasons.push("explicit server rendering");
} else if (ast.renderMode === "client") {
kind = "static-interactive";
reasons.push("explicit client rendering");
} else if (ast.renderMode === "partial-static") {
kind = "streaming-ssr";
reasons.push("partial-static shell with streamed dynamic regions");
}
const clientDisabled = ast.renderMode === "static" || ast.renderMode === "server";
const serverDisabled = ast.renderMode === "client";
return {
kind,
canPrerender: kind === "static" || kind === "static-interactive",
needsClientRuntime: interactive && ast.hydrate !== "none" && ast.runtime !== "server",
needsServerRuntime: requestData || authenticated || streaming || ast.runtime === "server",
hydrationStrategy: interactive ? (ast.hydrate ?? "load") : null,
needsClientRuntime:
!clientDisabled &&
(interactive || ast.renderMode === "client") &&
ast.hydrate !== "none" &&
ast.runtime !== "server",
needsServerRuntime:
!serverDisabled &&
(requestData ||
authenticated ||
streaming ||
ast.renderMode === "server" ||
["server", "edge", "worker", "service-worker"].includes(ast.runtime ?? "")),
hydrationStrategy: clientDisabled ? null : interactive ? (ast.hydrate ?? "load") : null,
reasons,
optimization: analyzeOptimizations(ast),
cachePolicy: { ...(ast.cache ?? {}) },
requiredPermission: ast.security.permission ?? null,
};
}
+371 -19
View File
@@ -21,6 +21,7 @@ import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode }
import { eraseFunctionTypes, runtimeTypeOf } from "./types.ts";
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
import { generateStoreModule } from "./store-codegen.ts";
import { optimizeAst } from "./analysis.ts";
interface RenderBinding {
method: string;
@@ -352,6 +353,55 @@ function renderLoopBody(node: ViewNode): string {
const inner = node.children.map(renderLoopBody).join("");
if (node.tag === "Static") return inner;
if (node.tag === "Dynamic")
return (
escLit('<wrn-dynamic-region data-wrn-dynamic="true">') +
inner +
escLit("</wrn-dynamic-region>")
);
if (node.tag === "KeepAlive") {
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
return (
escLit('<div data-wrn-keepalive="') +
bakeLoopAttr(key) +
escLit(`">`) +
inner +
escLit("</div>")
);
}
if (node.tag === "Portal") {
const target = node.attrs.find((attribute) => attribute.name === "to")?.value ?? "body";
return (
escLit('<div data-wrn-portal="') +
bakeLoopAttr(target) +
escLit('">') +
inner +
escLit("</div>")
);
}
if (node.tag === "Transition") {
const name =
node.attrs.find((attribute) => attribute.name === "name")?.value ?? "wrn-transition";
return (
escLit('<div data-wrn-transition="') +
bakeLoopAttr(name) +
escLit('">') +
inner +
escLit("</div>")
);
}
if (node.tag === "Component") {
const selected = node.attrs.find((attribute) => attribute.name === "is")?.value ?? "";
return (
escLit('<div data-wrn-dynamic-component="') +
bakeLoopAttr(selected) +
escLit('">') +
inner +
escLit("</div>")
);
}
if (componentTag) {
return (
escLit(`<div data-component="${attrEscape(node.tag)}"`) +
@@ -417,6 +467,7 @@ function compileIfExpr(node: IfNode): string {
*/
function collectControlExprs(nodes: ViewNode[], out: string[] = []): string[] {
for (const node of nodes) {
if (node.type === "text") continue;
if (node.type === "each") {
out.push(node.list);
collectControlExprs(node.body, out);
@@ -450,6 +501,73 @@ function renderNode(
return `\x00WRNEACH${loops.length - 1}\x00`;
}
if (node.tag === "Static" || node.tag === "Dynamic") {
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return node.tag === "Static"
? inner
: `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
}
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
const attribute =
node.tag === "Portal"
? "data-wrn-portal"
: node.tag === "Transition"
? "data-wrn-transition"
: "data-wrn-dynamic-component";
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
const fallback =
node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
const original = node.attrs.find((item) => item.name === source);
const rendered = original
? renderAttrs([{ ...original, name: attribute }], undefined, reactive, loops)
: ` ${attribute}="${attrEscape(fallback)}"`;
return `<div${rendered}>${inner}</div>`;
}
if (node.tag === "Async") {
const source = attrValue(node.attrs, "source") ?? "data";
const retries = attrValue(node.attrs, "retries") ?? "2";
const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
const asyncIndex = serverResolved ? loops.push("") - 1 : -1;
const branch = (name: string) => {
const element = node.children.find(
(child): child is Extract<ViewNode, { type: "element" }> =>
child.type === "element" && child.tag === name,
);
return (element?.children ?? [])
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
};
const loading = branch("Loading");
const success = branch("Success");
const error = branch("Error");
let initial = loading;
if (serverResolved) {
const nested = (value: string) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
const sourcePattern = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const serverSuccess = success.replace(
new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"),
(_whole, expression: string) => `\${__wrnexusEscapeHtml(${expression})}`,
);
loops[asyncIndex] =
`\${ctx[${JSON.stringify(source)}] !== undefined ? \`${nested(serverSuccess)}\` : \`${nested(loading)}\`}`;
initial = `\x00WRNEACH${asyncIndex}\x00`;
}
return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success>${success}</template><template data-wrn-async-error>${error}</template></section>`;
}
if (node.tag === "KeepAlive") {
const key = attrValue(node.attrs, "key") ?? "default";
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return `<div data-wrn-keepalive="${attrEscape(key)}">${inner}</div>`;
}
if (isComponentTag(node.tag)) {
return renderPageComponentInvocation(
node,
@@ -848,7 +966,9 @@ function generateSsrStateAliases(stateNames: string[]): string {
}
function hydrationAttribute(ast: PageAst): string {
const strategy = ast.hydrate ?? "load";
const strategy = ["static", "server"].includes(ast.renderMode ?? "")
? "none"
: (ast.hydrate ?? "load");
const hasBrowserModule = ast.runtimeFunctions.some((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
);
@@ -879,13 +999,88 @@ function publicOutputNames(ast: PageAst): string[] {
];
}
function prepareActionForms(nodes: ViewNode[], actions: ReadonlySet<string>): void {
for (const node of nodes) {
if (node.type === "text") continue;
if (node.type === "each") {
prepareActionForms(node.body, actions);
prepareActionForms(node.empty, actions);
continue;
}
if (node.type === "if") {
node.branches.forEach((branch) => prepareActionForms(branch.body, actions));
continue;
}
prepareActionForms(node.children, actions);
if (node.tag.toLowerCase() !== "form") continue;
const submit = node.attrs.find((attr) => attr.event && attr.name === "submit");
if (!submit || !actions.has(submit.value.trim())) continue;
const name = submit.value.trim();
node.attrs = node.attrs.filter((attr) => attr !== submit);
if (!node.attrs.some((attr) => !attr.event && attr.name === "method")) {
node.attrs.push({ name: "method", value: "post", event: false });
}
node.attrs.push({ name: "data-wrn-action", value: name, event: false });
node.children.unshift({
type: "element",
tag: "input",
attrs: [
{ name: "type", value: "hidden", event: false },
{ name: "name", value: "_wrnexus_action", event: false },
{ name: "value", value: name, event: false },
],
children: [],
});
}
}
function markServerAsyncBoundaries(nodes: ViewNode[], serverLoads: ReadonlySet<string>): void {
for (const node of nodes) {
if (node.type === "text") continue;
if (node.type === "each") {
markServerAsyncBoundaries(node.body, serverLoads);
markServerAsyncBoundaries(node.empty, serverLoads);
continue;
}
if (node.type === "if") {
node.branches.forEach((branch) => markServerAsyncBoundaries(branch.body, serverLoads));
continue;
}
if (node.tag === "Async") {
const source = attrValue(node.attrs, "source") ?? "data";
if (
serverLoads.has(source) &&
!node.attrs.some((attribute) => attribute.name === "data-wrn-async-server")
) {
node.attrs.push({ name: "data-wrn-async-server", value: "true", event: false });
}
}
markServerAsyncBoundaries(node.children, serverLoads);
}
}
export function generate(ast: PageAst): string {
ast = optimizeAst(ast).ast;
if (ast.kind === "global-store" || ast.kind === "page-store") return generateStoreModule(ast);
if (ast.kind === "component" || ast.kind === "layout") {
return generateComponent(ast);
}
const out: string[] = [];
prepareActionForms(ast.view, new Set(ast.actions.map((action) => action.name)));
markServerAsyncBoundaries(
ast.view,
new Set(
ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => load.name!),
),
);
if (ast.actions.length > 0) {
out.push(
`import { createActionClient } from "@wrnexus/csr";\nimport type { InferSchema } from "@wrnexus/validation";`,
);
}
if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
const ssrBindings: SsrBinding[] = [];
const csrBindings: CsrBinding[] = [];
@@ -909,11 +1104,19 @@ export function generate(ast: PageAst): string {
`export const layout = ${ast.layoutIsSymbol ? ast.layout : JSON.stringify(ast.layout)};`,
);
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
out.push(`export const __wrnexusHydrate = ${JSON.stringify(ast.hydrate ?? "load")};`);
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
out.push(
`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`,
);
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.cache ?? {}).length > 0)
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
if (Object.keys(ast.navigation).length > 0) {
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
}
// --- View -> default page component ---
const browserStates = ast.states.filter((state) => state.runtime !== "server");
@@ -960,6 +1163,10 @@ export function generate(ast: PageAst): string {
if (pageStyleTag) {
html = `${pageStyleTag}${html}`;
}
if (ast.renderMode === "client") {
const clientRoot = hydrationId(ast);
html = `<div data-wrn-client-root="${clientRoot}" aria-busy="true"></div><template data-wrn-client-template="${clientRoot}">${html}</template>`;
}
const pageStyleExport = localStyleExport(ast, styles);
if (pageStyleExport) out.push(pageStyleExport);
if (csrBindings.length > 0) {
@@ -972,6 +1179,14 @@ export function generate(ast: PageAst): string {
// Escape the static HTML for the template literal, then swap loop sentinels for
// their real `${…}` code (which must NOT be escaped).
let body = templateEscape(html);
let staticShellBody: string | undefined;
if (ast.renderMode === "partial-static") {
const shellHtml = html.replace(
/<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi,
'<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>',
);
staticShellBody = templateEscape(shellHtml);
}
const dynamicStateScope = ast.states
.map(
(state) =>
@@ -994,9 +1209,16 @@ export function generate(ast: PageAst): string {
(entry) => ` const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
)
.join("\n");
const serverLoadAliases = ast.loads
.filter((load) => load.mode === "server" && !load.deferred && load.name)
.map((load) => ` const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
.join("\n");
loops.forEach((code, idx) => {
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
staticShellBody = staticShellBody.replace(`\x00WRNEACH${idx}\x00`, () => code);
}
});
// Server loops iterate raw SSR data. Declare a named const for every `ssr` data
@@ -1024,6 +1246,7 @@ export function generate(ast: PageAst): string {
out.push(
`export default async function ${ast.name}(ctx: any) {
${storeDeclarations}
${serverLoadAliases}
${decls}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
@@ -1055,6 +1278,7 @@ export function generate(ast: PageAst): string {
out.push(
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
${storeDeclarations}
${serverLoadAliases}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
@@ -1081,30 +1305,122 @@ export function generate(ast: PageAst): string {
);
}
if (staticShellBody !== undefined) {
out.push(
`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
${storeDeclarations}
${serverLoadAliases}
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
const encoded = typeof value === "number" || typeof value === "boolean"
? String(value)
: JSON.stringify(value == null ? "" : String(value));
return key + ": " + encoded;
})
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return \`${staticShellBody}\`.replace("__WRNEXUS_DYNAMIC_SCOPE__", __scopeValue);
}`,
);
}
if (ast.loads.length > 0) {
const serverLoads = ast.loads.filter((entry) => entry.mode === "server");
const clientLoads = ast.loads.filter((entry) => entry.mode === "client");
if (serverLoads.length > 0) {
out.push(
`export async function __wrnexusLoad(ctx: any) {\n${serverLoads.map((entry) => entry.body).join("\n")}\n}`,
);
}
if (clientLoads.length > 0) {
out.push(
`export async function __wrnexusClientLoad(ctx: any) {
${clientLoads.map((entry) => entry.body).join("\n")}
}`,
);
}
const serverLoads = ast.loads.filter((entry) => entry.mode === "server" && !entry.deferred);
const publicClientLoads = ast.loads.filter(
(entry) => entry.mode === "client" || entry.deferred,
);
const namedByName = new Map(
ast.loads.filter((entry) => entry.name).map((entry) => [entry.name!, entry]),
);
const clientNames = new Set(
publicClientLoads.flatMap((entry) => (entry.name ? [entry.name] : [])),
);
const includeDependencies = (name: string): void => {
for (const dependency of namedByName.get(name)?.dependsOn ?? []) {
if (clientNames.has(dependency)) continue;
clientNames.add(dependency);
includeDependencies(dependency);
}
};
for (const name of [...clientNames]) includeDependencies(name);
const clientLoads = ast.loads.filter((entry) => !entry.name || clientNames.has(entry.name));
const renderLoads = (
exportName: string,
execution: typeof ast.loads,
exposed: typeof ast.loads,
): string => {
const declarations = execution
.filter((entry) => entry.name)
.map((entry) => {
const dependencies = (entry.dependsOn ?? [])
.map((dependency) => `const ${dependency} = await __load_${dependency}();`)
.join("\n");
return ` let __promise_${entry.name}: Promise<unknown> | undefined;
const __load_${entry.name} = () => (__promise_${entry.name} ??= (async () => {
${dependencies}
${entry.body}
})());`;
})
.join("\n");
const visible = exposed.filter((entry) => entry.name);
return `export async function ${exportName}(ctx: any) {
${exposed
.filter((entry) => !entry.name)
.map((entry) => entry.body)
.join("\n")}
${declarations}
${
visible.length
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
: ""
}
}`;
};
if (serverLoads.length > 0) out.push(renderLoads("__wrnexusLoad", serverLoads, serverLoads));
if (publicClientLoads.length > 0)
out.push(renderLoads("__wrnexusClientLoad", clientLoads, publicClientLoads));
}
if (ast.actions.length > 0) {
for (const action of ast.actions) {
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
if (!action.schema) {
out.push(
`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`,
);
continue;
}
out.push(`export async function ${action.name}(input: any, ctx: any) {
const invalidate = (...tags: string[]) => {
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
bucket.push(...tags.flat());
};
${action.body}
}`);
}
out.push(
`export const __wrnexusActions = { ${ast.actions.map((action) => action.name).join(", ")} };`,
`export const __wrnexusActions = { ${ast.actions
.map(
(action) =>
`${action.name}: { run: ${action.name}, schema: ${action.schema ?? "undefined"} }`,
)
.join(", ")} };`,
);
out.push(`export const __wrnexusActionClients = {
${ast.actions
.map(
(action) =>
` ${action.name}: createActionClient<${action.schema ? `InferSchema<typeof ${action.schema}>` : "Record<string, unknown>"}, Awaited<ReturnType<typeof ${action.name}>>>("", ${JSON.stringify(action.name)}),`,
)
.join("\n")}
};`);
}
// --- API blocks -> method handlers ---
@@ -1545,6 +1861,34 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
return renderComponentIfNode(node, ctx);
}
if (node.tag === "Static" || node.tag === "Dynamic") {
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
return node.tag === "Static"
? inner
: `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
}
if (node.tag === "KeepAlive") {
const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
return `<div data-wrn-keepalive="${compileAttrValue(key, ctx)}">${inner}</div>`;
}
if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
const attribute =
node.tag === "Portal"
? "data-wrn-portal"
: node.tag === "Transition"
? "data-wrn-transition"
: "data-wrn-dynamic-component";
const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
const fallback =
node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
const raw = node.attrs.find((item) => item.name === source)?.value ?? fallback;
return `<div ${attribute}="${compileAttrValue(raw, ctx)}">${inner}</div>`;
}
if (isComponentTag(node.tag)) {
return renderNestedComponentInvocation(node, ctx);
}
@@ -1865,11 +2209,19 @@ function generateComponent(ast: PageAst): string {
out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
}
out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
out.push(`export const __wrnexusHydrate = ${JSON.stringify(ast.hydrate ?? "load")};`);
out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
out.push(
`export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`,
);
out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
if (Object.keys(ast.cache ?? {}).length > 0)
out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
if (Object.keys(ast.security).length > 0) {
out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
}
if (Object.keys(ast.navigation).length > 0) {
out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
}
const componentStyleExport = localStyleExport(ast, styles);
if (componentStyleExport) out.push(componentStyleExport);
+10 -2
View File
@@ -15,6 +15,8 @@ import {
type PageAst,
type WrnDiagnostic,
} from "@wrnexus/syntax";
export { formatWrn } from "@wrnexus/syntax";
export type { FormatWrnOptions } from "@wrnexus/syntax";
import { generate } from "./codegen.ts";
import { generateNative } from "./native-codegen.ts";
@@ -35,8 +37,14 @@ export { generateStoreBrowserModule, generateStoreModule } from "./store-codegen
export { createComponentContract } from "./component-contract.ts";
export { resolveWrnImport, resolveWrnImports } from "./import-resolver.ts";
export { createWrnSourceMap } from "./source-map.ts";
export { analyzeRuntimeRequirements } from "./analysis.ts";
export type { RouteExecutionKind, RuntimeRequirements } from "./analysis.ts";
export { analyzeOptimizations, analyzeRuntimeRequirements, optimizeAst } from "./analysis.ts";
export type { OptimizationReport, RouteExecutionKind, RuntimeRequirements } from "./analysis.ts";
export { analyzeRuntimeImports, runtimeCapabilities } from "./runtime-capabilities.ts";
export type {
DeploymentRuntime,
RuntimeCapability,
RuntimeCapabilityDiagnostic,
} from "./runtime-capabilities.ts";
export { generateNative, NativeCompileError } from "./native-codegen.ts";
export { Lexer, LexError } from "@wrnexus/syntax";
export { eraseFunctionTypes, inferredRuntimeType, runtimeTypeOf } from "@wrnexus/syntax";
@@ -0,0 +1,69 @@
export type DeploymentRuntime = "bun" | "node" | "edge" | "worker" | "service-worker" | "browser";
export type RuntimeCapability =
"filesystem" | "tcp" | "process" | "websocket" | "crypto" | "streams" | "background-tasks";
const CAPABILITIES: Record<DeploymentRuntime, ReadonlySet<RuntimeCapability>> = {
bun: new Set([
"filesystem",
"tcp",
"process",
"websocket",
"crypto",
"streams",
"background-tasks",
]),
node: new Set([
"filesystem",
"tcp",
"process",
"websocket",
"crypto",
"streams",
"background-tasks",
]),
edge: new Set(["websocket", "crypto", "streams", "background-tasks"]),
worker: new Set(["websocket", "crypto", "streams", "background-tasks"]),
"service-worker": new Set(["crypto", "streams", "background-tasks"]),
browser: new Set(["websocket", "crypto", "streams"]),
};
const MODULE_CAPABILITIES: Array<[RegExp, RuntimeCapability]> = [
[/^(?:node:)?(?:fs|path|os)(?:\/|$)/, "filesystem"],
[/^(?:node:)?(?:net|tls|dgram|http2)(?:\/|$)/, "tcp"],
[/^(?:node:)?(?:child_process|cluster|worker_threads)(?:\/|$)/, "process"],
];
export interface RuntimeCapabilityDiagnostic {
code: "WRN-RUNTIME-CAPABILITY";
runtime: DeploymentRuntime;
module: string;
capability: RuntimeCapability;
message: string;
}
export function runtimeCapabilities(runtime: DeploymentRuntime): ReadonlySet<RuntimeCapability> {
return CAPABILITIES[runtime];
}
export function analyzeRuntimeImports(
source: string,
runtime: DeploymentRuntime,
): RuntimeCapabilityDiagnostic[] {
const modules = [
...source.matchAll(/\b(?:import\s+(?:[\s\S]*?\s+from\s+)?|require\s*\()\s*["']([^"']+)["']/g),
].map((match) => match[1]!);
const available = runtimeCapabilities(runtime);
return modules.flatMap((module) => {
const requirement = MODULE_CAPABILITIES.find(([pattern]) => pattern.test(module));
if (!requirement || available.has(requirement[1])) return [];
return [
{
code: "WRN-RUNTIME-CAPABILITY" as const,
runtime,
module,
capability: requirement[1],
message: `Module '${module}' requires ${requirement[1]}, which is unavailable in the ${runtime} runtime.`,
},
];
});
}
@@ -0,0 +1,278 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`compiler output remains snapshot-compatible for the canonical component contract 1`] = `
{
"code":
"// compiled from .wrn
import Button from "@wrnexus/ui/components/Button.wrn";
import { Buffer as __WrnexusBuffer } from "node:buffer";
export const __wrnexusComponent = "Counter";
export const __wrnexusRuntime = "universal";
export const __wrnexusRender = "hybrid";
export const __wrnexusHydrate = "load";
export const __wrnexusHydrationId = "Counter:1skggk6";
export const __wrnexusBehavior = {
"functions": "function increment(){\\n count = count + 1\\n output.change(count)\\n }",
"outputs": [
{
"name": "change",
"payload": {
"name": "value",
"valueType": "number",
"optional": false
}
}
],
"computed": [],
"effects": [],
"lifecycle": {},
"watches": []
};
export interface CounterProps {
[attribute: string]: unknown;
"label"?: string;
}
export interface CounterOutputs {
"change"(value: number): void;
}
function __coerce(v: any, def: any, declared: string = "unknown"): any {
if (v === undefined || v === null) {
return def;
}
if (declared === "number" || typeof def === "number") {
const parsed = Number(v);
if (!Number.isFinite(parsed)) throw new TypeError("Expected a finite number prop");
return parsed;
}
if (declared === "boolean" || typeof def === "boolean") {
if (v === true || v === "" || v === "true" || v === 1 || v === "1") return true;
if (v === false || v === "false" || v === 0 || v === "0") return false;
throw new TypeError("Expected a boolean prop");
}
if (declared === "array" || Array.isArray(def)) {
if (Array.isArray(v)) {
return v;
}
if (typeof v === "string") {
try {
const parsed = JSON.parse(v);
return Array.isArray(parsed) ? parsed : def;
} catch {
if (declared === "array") throw new TypeError("Expected an array prop");
return def;
}
}
return def;
}
if (declared === "object" || (def !== null && typeof def === "object")) {
if (
v !== null &&
typeof v === "object" &&
!Array.isArray(v)
) {
return v;
}
if (typeof v === "string") {
try {
const parsed = JSON.parse(v);
return (
parsed !== null &&
typeof parsed === "object" &&
!Array.isArray(parsed)
)
? parsed
: def;
} catch {
if (declared === "object") throw new TypeError("Expected an object prop");
return def;
}
}
return def;
}
if (declared === "bigint") return BigInt(v);
if (declared === "function" && typeof v !== "function") {
throw new TypeError("Expected a function prop");
}
return declared === "unknown" && def === undefined ? v : String(v);
}
function __restProps(
props: Record<string, any>,
declared: Set<string>,
): Record<string, any> {
return Object.fromEntries(
Object.entries(props).filter(([name]) => !declared.has(name)),
);
}
function __wireHtml(v: any): string {
return String(v == null ? "" : v).replace(
/[&<>]/g,
(c) =>
c === "&"
? "&amp;"
: c === "<"
? "&lt;"
: "&gt;",
);
}
function __wireAttr(v: any): string {
return String(v == null ? "" : v).replace(
/[&<>"]/g,
(c) =>
c === "&"
? "&amp;"
: c === "<"
? "&lt;"
: c === ">"
? "&gt;"
: "&quot;",
);
}
function __wireBooleanAttr(name: string, value: any): string {
return value === true ||
value === "true" ||
value === "" ||
value === 1 ||
value === "1" ||
value === name
? " " + name
: "";
}
function __wireSpreadAttrs(value: any): string {
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
const booleanAttributes = new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);
const attributes: string[] = [];
for (const [name, raw] of Object.entries(value)) {
const lowerName = name.toLowerCase();
if (
!/^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name) ||
lowerName.startsWith("on") ||
lowerName === "style" ||
lowerName === "slot" ||
lowerName === "data-component" ||
lowerName.startsWith("data-wrn")
) {
continue;
}
if (booleanAttributes.has(lowerName)) {
attributes.push(__wireBooleanAttr(name, raw));
continue;
}
if (raw === false || raw === null || raw === undefined) continue;
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
}
return attributes.join("");
}
function __wireProp(v: any): string {
const value =
v !== null && typeof v === "object"
? JSON.stringify(v)
: String(v == null ? "" : v);
return __wireAttr(value);
}
function __wireRaw(v: any): string {
return String(v == null ? "" : v);
}
function __wrnexusSerializeScopeValue(value: any): string {
if (value === undefined) {
return "undefined";
}
if (value === null) {
return "null";
}
if (typeof value === "number") {
return Number.isFinite(value)
? String(value)
: "null";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "string") {
return JSON.stringify(value);
}
try {
const serialized = JSON.stringify(value);
return serialized === undefined
? "undefined"
: serialized;
} catch {
return "null";
}
}
function __wrnexusScopeDecl(obj: Record<string, any>): string {
return Object.keys(obj)
.map(
(key) =>
key +
": " +
__wrnexusSerializeScopeValue(
obj[key],
),
)
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
export function render(props: CounterProps = {} as CounterProps): string {
const __p = props || {};
const label: string = __coerce(__p["label"], ("Count"), "string");
const __attrs = __restProps(__p, new Set(["label"]));
let count = (0);
const __scopeState = { "label": label, "count": count };
const __scope = __wrnexusScopeDecl(__scopeState);
const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");
return \`<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}" data-wrn-behavior="eyJmdW5jdGlvbnMiOiJmdW5jdGlvbiBpbmNyZW1lbnQoKXtcbiAgICAgIGNvdW50ID0gY291bnQgKyAxXG4gICAgICBvdXRwdXQuY2hhbmdlKGNvdW50KVxuICAgIH0iLCJvdXRwdXRzIjpbeyJuYW1lIjoiY2hhbmdlIiwicGF5bG9hZCI6eyJuYW1lIjoidmFsdWUiLCJ2YWx1ZVR5cGUiOiJudW1iZXIiLCJvcHRpb25hbCI6ZmFsc2V9fV0sImNvbXB1dGVkIjpbXSwiZWZmZWN0cyI6W10sImxpZmVjeWNsZSI6e30sIndhdGNoZXMiOltdfQ==" data-wrn-hydration="Counter:1skggk6" data-wrn-hydrate="load" data-wrn-runtime="universal" data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__">
<div data-component="Button"\${__wireSpreadAttrs(__attrs)} on:click="\${__wireProp(increment)}">\${__wireHtml(label)}: <span data-text="count">\${__wireHtml(count)}</span></div>
</div>\`;
}
export default { name: "Counter", kind: "component", render };
"
,
"diagnostics": [],
}
`;
+21
View File
@@ -0,0 +1,21 @@
import { expect, test } from "bun:test";
import { compile } from "../src/index.ts";
test("compiles schema-backed actions and progressively enhanced forms", () => {
const output = compile(
`import { CreateUserSchema } from "./schema";
page Users {
action createUser using CreateUserSchema { invalidate("users"); return { id: input.name } }
view { <form @submit='createUser'><input name='name' /></form> }
}`,
"Users.wrn",
).code;
expect(output).toContain('data-wrn-action="createUser"');
expect(output).toContain('name="_wrnexus_action" value="createUser"');
expect(output).toContain("schema: CreateUserSchema");
expect(output).toContain("__wrnexusInvalidatedTags");
expect(output).toContain(
"createActionClient<InferSchema<typeof CreateUserSchema>, Awaited<ReturnType<typeof createUser>>>",
);
expect(output).not.toContain("data-on-submit");
});
@@ -0,0 +1,70 @@
import { expect, test } from "bun:test";
import { generate, parse } from "../src/index.ts";
test("Async syntax compiles loading, success and error branches into inert templates", () => {
const code = generate(
parse(`page Users {
load client users { return [{ name: "Ada" }] }
view {
<Async source="users" retries="3">
<Loading><p>Loading users</p></Loading>
<Success data="users"><p>{users.name}</p></Success>
<Error error="error"><p>{error.message}</p></Error>
</Async>
}
}`),
);
expect(code).toContain('data-wrn-async="users"');
expect(code).toContain('data-wrn-async-retries="3"');
expect(code).toContain("data-wrn-async-loading");
expect(code).toContain("data-wrn-async-success");
expect(code).toContain("data-wrn-async-error");
expect(code).toContain("__wrnexusClientLoad");
});
test("server named loads render Async success content during SSR", () => {
const code = generate(
parse(`page Users {
load server users { return { name: "Ada" } }
view {
<Async source="users">
<Loading><p>Loading</p></Loading>
<Success><p>{users.name}</p></Success>
<Error><p>Failed</p></Error>
</Async>
}
}`),
);
expect(code).toContain('const users = ctx["users"]');
expect(code).toContain('data-wrn-async-resolved="true"');
expect(code).toContain('ctx["users"] !== undefined');
});
test("named loads support memoized dependencies and deferred execution", () => {
const code = generate(
parse(`page Data {
load server account { return { id: 7 } }
load server projects after account { return [account.id] }
load server audit after projects defer { return { project: projects[0] } }
view { <Async source="audit"><Loading>Wait</Loading><Success>Ready</Success></Async> }
}`),
);
expect(code).toContain("const account = await __load_account()");
expect(code).toContain("const projects = await __load_projects()");
expect(code).toContain("__promise_projects ??=");
expect(code).toContain("export async function __wrnexusClientLoad");
expect(code).toContain('return { "audit": __values[0] }');
});
test("load dependency cycles and cross-phase server dependencies fail compilation", () => {
expect(() =>
parse(
`page Cycle { load server first after second { return 1 } load server second after first { return 2 } view { <p>x</p> } }`,
),
).toThrow("cycle");
expect(() =>
parse(
`page Phase { load client browser { return 1 } load server invalid after browser { return 2 } view { <p>x</p> } }`,
),
).toThrow("cannot depend");
});
@@ -0,0 +1,11 @@
import { expect, test } from "bun:test";
import { generate, parse } from "../src/index.ts";
test("client-rendered pages emit an inert template and browser mount anchor", () => {
const code = generate(
parse('page ClientOnly { render = "client" view { <main><h1>Browser only</h1></main> } }'),
);
expect(code).toContain('data-wrn-client-root="');
expect(code).toContain('data-wrn-client-template="');
expect(code.indexOf("Browser only")).toBeGreaterThan(code.indexOf("<template"));
});
+21
View File
@@ -7,6 +7,27 @@ import { generate, parse } from "../src/index.ts";
import { compileWireFile } from "../src/index.ts";
import { mountHtml } from "@wrnexus/test";
test("explicit static rendering disables hydration metadata", () => {
const output = compileWireFile(`page StaticPage {
render = "static"
state count = 0
view { <button @click="count++">{count}</button> }
}`);
expect(output).toContain('export const __wrnexusRender = "static"');
expect(output).toContain('data-wrn-hydrate="none"');
expect(output).toContain('export const __wrnexusHydrate = "none"');
});
test("named data loads compile as parallel typed data entries", () => {
const output = compileWireFile(`page Users {
load users { return ["Ada"] }
load server teams { return ["Core"] }
view { <p>Users</p> }
}`);
expect(output).toContain("await Promise.all");
expect(output).toContain('return { "users": __values[0], "teams": __values[1] }');
});
let seq = 0;
/** Compile a `.wrn` source and import the resulting module. */
async function compileAndImport(src: string): Promise<Record<string, unknown>> {
@@ -0,0 +1,15 @@
import { expect, test } from "bun:test";
import { compile } from "../src/index.ts";
test("compiles declarative portals, transitions and dynamic component cases", () => {
const result = compile(`page Ui {
view {
<Portal to="#modal"><p>Modal</p></Portal>
<Transition name="fade"><p>Animated</p></Transition>
<Component is="Admin"><section data-component-case="Admin">Admin</section><section data-component-case="Guest">Guest</section></Component>
}
}`);
expect(result.code).toContain('data-wrn-portal="#modal"');
expect(result.code).toContain('data-wrn-transition="fade"');
expect(result.code).toContain('data-wrn-dynamic-component="Admin"');
});
+12
View File
@@ -0,0 +1,12 @@
import { expect, test } from "bun:test";
import { generate, parse } from "../src/index.ts";
test("KeepAlive compiles to a keyed live-instance preservation boundary", () => {
const output = generate(
parse(
`page Dashboard { navigation { preserve = ["component"] } view { <KeepAlive key="filters"><DashboardFilters /></KeepAlive> } }`,
),
);
expect(output).toContain('data-wrn-keepalive="filters"');
expect(output).not.toContain('data-component="KeepAlive"');
});
@@ -0,0 +1,23 @@
import { expect, test } from "bun:test";
import { analyzeOptimizations, generate, optimizeAst, parse } from "../src/index.ts";
test("compiler folds literal branches and reports optimization opportunities", () => {
const ast = parse(`component Optimized {
props { title: string = "Hello" }
state count = 0
state unused = 1
functions {
client function increment(): void { count++ }
client function orphan(): void { unused++ }
}
style { .used { color: red } .unused-css { color: blue } }
view { <section class="used"><h1>{title}</h1>{#if false}<p>dead</p>{:else}<button @click="increment">{count}</button>{/if}</section> }
}`);
const report = analyzeOptimizations(ast);
expect(report.eliminatedBranches).toBeGreaterThan(0);
expect(report.unusedHandlers).toContain("orphan");
expect(report.unusedLocalCssClasses).toContain("unused-css");
expect(report.constantProps).toContain("title");
expect(optimizeAst(ast).ast.view).not.toEqual(ast.view);
expect(generate(ast)).not.toContain("dead");
});
@@ -0,0 +1,15 @@
import { expect, test } from "bun:test";
import { analyzeRuntimeRequirements, generate, parse } from "../src/index.ts";
test("partial-static pages compile transparent static and streamed dynamic boundaries", () => {
const ast = parse(
`page Dashboard { render = "partial-static" view { <Static><header>Docs</header></Static><Dynamic><p>User</p></Dynamic> } }`,
);
expect(ast.renderMode).toBe("partial-static");
expect(analyzeRuntimeRequirements(ast).kind).toBe("streaming-ssr");
const output = generate(ast);
expect(output).toContain("wrn-dynamic-region");
expect(output).toContain("__wrnexusBuildStaticShell");
expect(output).toContain('<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>');
expect(output).not.toContain('data-component="Static"');
});
+44
View File
@@ -0,0 +1,44 @@
import { expect, test } from "bun:test";
import { compile, diagnose } from "../src/index.ts";
test("compiler output remains snapshot-compatible for the canonical component contract", () => {
const source = `import Button from "@wrnexus/ui/components/Button.wrn";
component Counter {
props {
label: string = "Count"
}
state {
count = 0
}
outputs {
change(value: number)
}
functions {
client function increment(): void {
count = count + 1
output.change(count)
}
}
view {
<Button on:click={increment}>{label}: {count}</Button>
}
}
`;
const result = compile(source, "Counter.wrn");
expect({ code: result.code, diagnostics: result.richDiagnostics }).toMatchSnapshot();
});
test("diagnostics tolerate deterministic malformed-source fuzz cases", () => {
let state = 0x8f3a21;
const alphabet = "{}[]()<>=:/@#$'\"` abcdefghijklmnopqrstuvwxyz0123456789\n\t";
for (let sample = 0; sample < 500; sample++) {
let source = "";
const length = 1 + (state % 180);
for (let index = 0; index < length; index++) {
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
source += alphabet[state % alphabet.length];
}
expect(() => diagnose(source, { file: `fuzz-${sample}.wrn` })).not.toThrow();
}
});
@@ -0,0 +1,12 @@
import { expect, test } from "bun:test";
import { analyzeRuntimeImports, runtimeCapabilities } from "../src/index.ts";
test("edge and workers reject Node capabilities with stable diagnostics", () => {
const source = `import fs from "node:fs";\nimport { connect } from "node:net";`;
expect(analyzeRuntimeImports(source, "edge").map((item) => item.capability)).toEqual([
"filesystem",
"tcp",
]);
expect(analyzeRuntimeImports(source, "bun")).toEqual([]);
expect(runtimeCapabilities("service-worker").has("filesystem")).toBe(false);
});
+20
View File
@@ -0,0 +1,20 @@
# @wrnexus/content
Typed content collections for Markdown/MDX-like documents and remote CMS records. Collections
validate frontmatter through any `{ parse(input) }` schema, render escaped HTML, and expose draft
preview, versions, references, headings, search indexes, pagination, RSS and sitemaps.
```ts
const posts = defineCollection({
name: "posts",
schema: PostSchema,
loader: localContentLoader("content/posts"),
previewToken: process.env.CONTENT_PREVIEW_TOKEN,
});
const published = await posts.load();
const preview = await posts.load({ previewToken: request.headers.get("x-preview-token") ?? "" });
```
Remote systems implement `CmsAdapter`, use `cmsContentLoader`, or return JSON records through
`remoteContentLoader`. Markdown HTML is escaped by default; raw executable HTML is never trusted.

Some files were not shown because too many files have changed in this diff Show More