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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "auth-showcase",
"version": "0.1.0",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
+9
View File
@@ -0,0 +1,9 @@
node_modules
dist
**/.wrnexus
.git
*.log
*.db
*.db-shm
*.db-wal
.DS_Store
@@ -0,0 +1,7 @@
# 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
+3
View File
@@ -1,5 +1,8 @@
node_modules/
dist/
.wrnexus/
.wirefw/
**/.wirefw/
**/*.gen.ts
**/*.generated.d.ts
*.log
+20
View File
@@ -0,0 +1,20 @@
# syntax=docker/dockerfile:1
# --- build stage: install deps + produce dist/server.js ---
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lock* bun.lockb* ./
RUN bun install
COPY . .
RUN bun run build
# --- runtime stage: slim image with only the built server + migrations ---
FROM oven/bun:1-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
COPY --from=build /app/dist ./dist
COPY --from=build /app/app/db/migrations ./app/db/migrations
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD bun -e "fetch('http://localhost:'+(process.env.PORT||3000)+'/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["bun", "dist/server.js"]
+6 -1
View File
@@ -8,6 +8,11 @@ export const GET = async () => {
};
export const POST = async (ctx: Context) => {
const body = await ctx.req.json();
const body = await ctx.req.json().catch(() => undefined);
if (body === undefined)
return Response.json(
{ error: "Request body must be valid JSON." },
{ status: 400, headers: { "cache-control": "no-store" } },
);
return Response.json({ received: body });
};
@@ -0,0 +1,14 @@
import { createGraphqlHandler } from "@wrnexus/graphql";
const graphql = createGraphqlHandler({
maxDepth: 8,
maxAliases: 20,
allowIntrospection: false,
async execute(request) {
return { data: { example: request.operationName ?? "anonymous" } };
},
});
export function POST(ctx: { req: Request }): Promise<Response> {
return graphql(ctx.req);
}
+12
View File
@@ -0,0 +1,12 @@
import { defineEndpoint } from "@wrnexus/core";
import { CreateUserSchema } from "../schemas/create-user.ts";
/** Schema-derived request and response types are emitted into wrnexus.generated.d.ts. */
export const POST = defineEndpoint({
input: CreateUserSchema,
output: CreateUserSchema,
description: "Validate and echo a typed user payload.",
handler(input) {
return input;
},
});
@@ -0,0 +1,11 @@
export const webhook = {
event: "payment.completed",
summary: "Payment completed",
description: "Sent after a payment reaches its settled state.",
payloadSchema: "#/components/schemas/PaymentCompleted",
signatureHeader: "x-payment-signature",
};
export async function POST(): Promise<Response> {
return Response.json({ accepted: true });
}
+38
View File
@@ -73,4 +73,42 @@ describe("full app", () => {
expect(typeof body.message).toBe("string");
expect(body.lang).toBeTruthy();
});
test("language-server playground renders its real interactive output", async () => {
const res = await app.fetch("/language-tools");
expect(res.status).toBe(200);
const html = await res.text();
expect(html).toContain("Language server playground");
expect(html).toContain("computed double:");
expect(html).toContain('data-text="doubled">0</span>');
expect(html).toContain('aria-live="polite"');
});
test("login renders real accessible fields without an accidental Card component", async () => {
const res = await app.fetch("/login?next=/dashboard");
expect(res.status).toBe(200);
const html = await res.text();
expect(html).toContain('name="email"');
expect(html).toContain('name="password"');
expect(html).toContain('autocomplete="current-password"');
expect(html).not.toContain("Card title");
});
test("platform showcase uses the public layout and styled reactive primitives", async () => {
const res = await app.fetch("/platform-showcase");
expect(res.status).toBe(200);
const html = await res.text();
expect(html).toContain("WrNexus");
expect(html).toContain('data-wrn-dynamic-component="Admin"');
expect(html).toContain('data-wrn-portal="#showcase-modal"');
expect(html).toContain("platform-showcase");
});
test("async data page renders its server loader without a cache failure", async () => {
const res = await app.fetch("/async-data");
expect(res.status).toBe(200);
const html = await res.text();
expect(html).toContain("Rendered by the server loader");
expect(html).toContain("Loading the browser profile");
});
});
+20 -2
View File
@@ -3,9 +3,27 @@
component AuthLayout {
view {
<div class="auth-wrap">
<div data-component="card" class="auth-card">
<section class="auth-card" aria-label="Authentication">
<slot></slot>
</div>
</section>
</div>
}
style {
.auth-wrap {
display: grid;
min-height: 100vh;
place-items: center;
padding: 1.5rem;
}
.auth-card {
width: min(100%, 28rem);
padding: 2rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-lg);
background: var(--wire-color-surface);
box-shadow: 0 1.25rem 3rem rgb(0 0 0 / 0.18);
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ component DashboardLayout {
<a href="/ui">Components</a>
<a href="/">Home</a>
</nav>
<div data-component="theme-toggle" label="Toggle theme"></div>
<button type="button" data-wire-theme-toggle aria-label="Toggle color theme">Theme</button>
</aside>
<main class="dash-main">
+1 -1
View File
@@ -11,7 +11,7 @@ component PublicLayout {
<a href="/chat">{t:nav.chat}</a>
<a href="/dashboard">{t:nav.dashboard}</a>
<a href="/about">{t:nav.about}</a>
<div data-component="theme-toggle" label="Theme"></div>
<button type="button" data-wire-theme-toggle aria-label="Toggle color theme">Theme</button>
<button class="wire-btn wire-btn--ghost wire-btn--sm" data-wire-lang-set="en">EN</button>
<button class="wire-btn wire-btn--ghost wire-btn--sm" data-wire-lang-set="es">ES</button>
</nav>
@@ -0,0 +1,29 @@
page AsyncData {
layout = "public"
load server summary {
return { message: "Rendered by the server loader" }
}
load client profile {
return { name: "Ada", role: "Administrator" }
}
view {
<section class="stack">
<h1>Async data boundaries</h1>
<Async source="summary">
<Loading><p>Preparing the server summary…</p></Loading>
<Success data="summary"><p>{summary.message}</p></Success>
<Error error="error"><p>{error.message}</p></Error>
</Async>
<Async source="profile" retries="2">
<Loading><p>Loading the browser profile…</p></Loading>
<Success data="profile"><p>Welcome {profile.name} — {profile.role}</p></Success>
<Error error="error"><p>Profile failed: {error.message}</p></Error>
</Async>
</section>
}
}
@@ -0,0 +1,15 @@
page ClientOnly {
layout = "public"
render = "client"
hydrate = "load"
state count: number = 0
view {
<section class="wire-card stack">
<p class="wire-badge">Mounted in the browser</p>
<h1>Client-only page</h1>
<button type="button" class="wire-btn" @click="count++">Count: {count}</button>
</section>
}
}
+2 -1
View File
@@ -27,10 +27,11 @@ page Home {
restyles instantly when the theme changes. -->
<div class="themed-box">
This box uses theme tokens. Current theme controls its colors.
<button data-wire-theme-toggle class="theme-toggle">Toggle theme</button>
<button type="button" data-wire-theme-toggle class="theme-toggle">Toggle theme</button>
</div>
<p><a class="text-red-600 hover:underline" href="/about">About</a></p>
<p><a class="hover:underline" href="/language-tools">Language server playground</a></p>
}
style {
@@ -0,0 +1,104 @@
// Real page for checking compiler, formatter, LSP navigation, rename, and reactivity.
page LanguageTools {
layout = "public"
state count: number = 0
state message: string = "The shared language server is active."
computed doubled: number = count * 2
seo {
title = "WRN language tools"
description = "Interactive WRN page used to verify editor and compiler features."
canonical = "/language-tools"
}
functions {
function increment() {
count++
message = `Renaming count updates all ${count} references safely.`
}
function reset() {
count = 0
message = "State reset. Try hover, definition, references, and rename in your editor."
}
}
view {
<main class="language-tools">
<header>
<span class="eyebrow">Editor-neutral WRN tooling</span>
<h1>Language server playground</h1>
<p>{message}</p>
</header>
<section aria-labelledby="counter-heading" class="demo-card">
<h2 id="counter-heading">Reactive Data symbol test</h2>
<p aria-live="polite">Count: {count}; computed double: {doubled}</p>
<div class="actions">
<button type="button" @click="increment()">Increment</button>
<button type="button" class="secondary" @click="reset()">Reset</button>
</div>
</section>
<section aria-labelledby="features-heading" class="demo-card">
<h2 id="features-heading">Features exercised by this file</h2>
<ul>
<li>Compiler and accessibility diagnostics</li>
<li>Canonical formatting and completion</li>
<li>Hover, symbols, definition, references, and rename</li>
<li>Typed state, computed values, and browser events</li>
</ul>
</section>
</main>
}
style {
.language-tools {
display: grid;
max-width: 58rem;
gap: 1.25rem;
margin: 3rem auto;
}
.language-tools header,
.demo-card {
display: grid;
gap: 0.75rem;
}
.eyebrow {
color: var(--wire-color-primary);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.demo-card {
padding: 1.25rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-lg);
background: var(--wire-color-surface);
}
.actions {
display: flex;
gap: 0.75rem;
}
.actions button {
padding: 0.65rem 1rem;
border: 1px solid var(--wire-color-primary);
border-radius: var(--wire-radius-md);
background: var(--wire-color-primary);
color: var(--wire-color-primary-contrast);
cursor: pointer;
}
.actions .secondary {
background: transparent;
color: var(--wire-color-primary);
}
}
}
+32 -7
View File
@@ -11,24 +11,49 @@ page Login {
view {
<form data-schema="login" method="post" action="/api/login" data-redirect="/dashboard">
<div data-component="stack" gap="4">
<div class="auth-stack">
<h1>Sign in</h1>
<div class="wire-alert wire-alert--success" data-success="Signed in! The form posted JSON and was validated server-side." hidden></div>
<div>
<div data-component="input" type="email" name="email" placeholder="you@example.com"></div>
<label class="auth-field">
<span>Email address</span>
<input type="email" name="email" autocomplete="email" placeholder="you@example.com" required />
<span class="wire-field-error" data-error="email"></span>
</div>
</label>
<div>
<div data-component="input" type="password" name="password" placeholder="Password"></div>
<label class="auth-field">
<span>Password</span>
<input type="password" name="password" autocomplete="current-password" placeholder="Password" required />
<span class="wire-field-error" data-error="password"></span>
</div>
</label>
<button type="submit" class="wire-btn wire-btn--primary wire-btn--lg">Sign in</button>
<p><a href="/">Back home</a></p>
</div>
</form>
}
style {
form,
.auth-stack,
.auth-field {
display: grid;
gap: 0.5rem;
}
form,
.auth-stack {
gap: 1.25rem;
}
.auth-field input {
min-height: 2.75rem;
padding: 0.7rem 0.8rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface);
color: inherit;
}
}
}
@@ -0,0 +1,27 @@
page PartialStatic {
layout = "public"
render = "partial-static"
seo {
title = "Partial-static streaming"
description = "A build-time WRNexus shell with request-streamed dynamic content."
}
view {
<Static>
<section class="wire-card stack">
<p class="wire-badge">Built once</p>
<h1>Build-time static shell</h1>
<p>This heading and page structure are emitted into dist/partial-shells.json.</p>
</section>
</Static>
<Dynamic>
<section class="wire-card stack" aria-live="polite">
<p class="wire-badge">Streamed per request</p>
<h2>Dynamic account region</h2>
<p>Personalized data belongs inside this boundary.</p>
</section>
</Dynamic>
}
}
@@ -0,0 +1,77 @@
page PlatformShowcase {
layout = "public"
render = "hybrid"
hydrate = "load"
state active = "Admin"
view {
<main class="platform-showcase">
<header>
<span class="eyebrow">Reactive primitives</span>
<h1>Reactive platform showcase</h1>
<p>Switch a dynamic component and verify transition and portal rendering.</p>
</header>
<section class="showcase-card">
<button type="button" @click='active = active === "Admin" ? "Guest" : "Admin"'>Switch component</button>
<Transition name="fade">
<Component is={active}>
<section class="component-case" data-component-case="Admin"><strong>Administrator tools</strong><p>Manage users, permissions, and platform settings.</p></section>
<section class="component-case" data-component-case="Guest"><strong>Guest dashboard</strong><p>Explore the public workspace and available resources.</p></section>
</Component>
</Transition>
</section>
<div id="showcase-modal" class="portal-target" aria-live="polite"></div>
<Portal to="#showcase-modal"><p>Portaled content rendered in its target.</p></Portal>
</main>
}
style {
.platform-showcase {
display: grid;
max-width: 58rem;
gap: 1.25rem;
margin: 3rem auto;
}
.platform-showcase header,
.showcase-card,
.component-case {
display: grid;
gap: 0.75rem;
}
.eyebrow {
color: var(--wire-color-primary);
font-weight: 700;
text-transform: uppercase;
}
.showcase-card,
.portal-target {
padding: 1.25rem;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-lg);
background: var(--wire-color-surface);
}
.showcase-card button {
width: fit-content;
padding: 0.7rem 1rem;
border: 0;
border-radius: var(--wire-radius-md);
background: var(--wire-color-primary);
color: var(--wire-color-primary-contrast);
cursor: pointer;
}
.fade-enter-active {
animation: platform-fade 180ms ease-out;
}
@keyframes platform-fade {
from { opacity: 0; transform: translateY(0.25rem); }
to { opacity: 1; transform: translateY(0); }
}
}
}
@@ -0,0 +1,27 @@
import { CreateUserSchema } from "../schemas/create-user";
page ServerActions {
seo {
title = "Typed server actions"
description = "Schema validation, CSRF, invalidation and progressive enhancement"
}
action createUser using CreateUserSchema {
invalidate("users")
return { id: crypto.randomUUID(), name: input.name, email: input.email }
}
view {
<main>
<h1>Typed server actions</h1>
<p>The form works with or without JavaScript.</p>
<form @submit="createUser">
<label for="action-name">Name</label>
<input id="action-name" name="name" autocomplete="name" required />
<label for="action-email">Email</label>
<input id="action-email" name="email" type="email" autocomplete="email" required />
<button type="submit">Create user</button>
</form>
</main>
}
}
@@ -0,0 +1,13 @@
import { defineJob } from "@wrnexus/queue";
export interface WelcomeEmailPayload {
userId: number;
email: string;
}
export default defineJob<WelcomeEmailPayload>({
name: "welcome-email",
async run(job) {
console.log(`Welcome email queued for ${job.data.email}`);
},
});
+84 -10
View File
@@ -4,16 +4,50 @@
export interface Routes {
"/": Record<string, never>;
"/about": Record<string, never>;
"/async-data": Record<string, never>;
"/chat": Record<string, never>;
"/client-only": Record<string, never>;
"/dashboard": Record<string, never>;
"/hello": Record<string, never>;
"/language-tools": Record<string, never>;
"/login": Record<string, never>;
"/modal": Record<string, never>;
"/partial-static": Record<string, never>;
"/platform-showcase": Record<string, never>;
"/reactive": Record<string, never>;
"/server-actions": Record<string, never>;
"/test": Record<string, never>;
"/ui": Record<string, never>;
}
export interface RouteNames {
"index": "/";
"about": "/about";
"async.data": "/async-data";
"chat": "/chat";
"client.only": "/client-only";
"dashboard": "/dashboard";
"hello": "/hello";
"language.tools": "/language-tools";
"login": "/login";
"modal": "/modal";
"partial.static": "/partial-static";
"platform.showcase": "/platform-showcase";
"reactive": "/reactive";
"server.actions": "/server-actions";
"test": "/test";
"ui": "/ui";
}
export interface RouteQueries {
[path: string]: Record<string, string | number | boolean | null | undefined>;
}
export type RoutePath = keyof Routes;
export type RouteName = keyof RouteNames;
export type RouteQuery<P extends RoutePath> = P extends keyof RouteQueries
? RouteQueries[P]
: Record<string, string | number | boolean | null | undefined>;
type RouteValue = string | readonly string[] | undefined;
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
@@ -22,17 +56,9 @@ function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
return values.map((part) => encodeURIComponent(part)).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, RouteValue>;
function buildHref(path: string, params: Record<string, RouteValue> = {}): string {
const output: string[] = [];
for (const segment of String(path).split("/").filter(Boolean)) {
for (const segment of path.split("/").filter(Boolean)) {
let name: string | undefined;
let optional = false;
let catchAll = false;
@@ -61,3 +87,51 @@ export function href<P extends RoutePath>(
}
return "/" + output.filter(Boolean).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, RouteValue>;
return buildHref(String(path), params);
}
export function route<N extends RouteName>(
name: N,
...args: keyof Routes[RouteNames[N]] extends never
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
: Record<string, never> extends Routes[RouteNames[N]]
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
: [params: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
): string {
const paths: Record<RouteName, RoutePath> = {
"index": "/",
"about": "/about",
"async.data": "/async-data",
"chat": "/chat",
"client.only": "/client-only",
"dashboard": "/dashboard",
"hello": "/hello",
"language.tools": "/language-tools",
"login": "/login",
"modal": "/modal",
"partial.static": "/partial-static",
"platform.showcase": "/platform-showcase",
"reactive": "/reactive",
"server.actions": "/server-actions",
"test": "/test",
"ui": "/ui"
} as Record<RouteName, RoutePath>;
const output = buildHref(paths[name], (args[0] ?? {}) as Record<string, RouteValue>);
const query = args[1];
if (!query) return output;
const search = new URLSearchParams();
for (const [key, value] of Object.entries(query))
if (value !== undefined && value !== null) search.set(key, String(value));
const text = search.toString();
return text ? `${output}?${text}` : output;
}
@@ -0,0 +1,6 @@
import { v } from "@wrnexus/validation";
export const CreateUserSchema = v.object({
name: v.string().trim().min(2),
email: v.string().trim().email(),
});
@@ -0,0 +1,86 @@
import {
afterAll,
beforeAll,
createHarness,
describe,
expect,
test,
type Harness,
} from "@wrnexus/test";
describe("real application security abuse checks", () => {
let app: Harness;
beforeAll(async () => {
app = await createHarness(import.meta.dir + "/..");
});
afterAll(() => app?.close());
test("sets browser hardening headers on rendered pages", async () => {
const response = await app.fetch("/");
expect(response.headers.get("content-security-policy")).toContain("default-src");
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
expect(response.headers.get("x-frame-options")).toBeTruthy();
expect(response.headers.get("referrer-policy")).toBeTruthy();
});
test("does not reflect script payloads into HTML", async () => {
const payload = `<script>globalThis.__attacked=true</script>`;
const response = await app.fetch(`/?search=${encodeURIComponent(payload)}`);
expect(response.status).toBe(200);
expect(await response.text()).not.toContain(payload);
});
test("rejects traversal attempts without exposing source files", async () => {
for (const path of ["/../../package.json", "/%2e%2e/%2e%2e/package.json", "/..%5c..%5c.env"]) {
const response = await app.fetch(path);
expect([400, 404]).toContain(response.status);
const body = await response.text();
expect(body).not.toContain("DATABASE_URL");
expect(body).not.toContain('"workspaces"');
}
});
test("does not grant CORS credentials to an untrusted origin", async () => {
const response = await app.fetch("/api/hello", {
headers: { origin: "https://evil.example" },
});
expect(response.headers.get("access-control-allow-origin")).toBeNull();
expect(response.headers.get("access-control-allow-credentials")).toBeNull();
});
test("requires CSRF for login and avoids credential oracle details", async () => {
const response = await app.fetch("/api/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: "victim@example.com", password: "wrong-password" }),
});
expect(response.status).toBe(403);
expect(await response.text()).not.toContain("passwordHash");
});
test("handles malformed JSON without a stack trace", async () => {
const response = await app.fetch("/api/echo", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{broken",
});
expect(response.status).toBe(400);
const body = await response.text();
expect(body).not.toContain(" at ");
expect(body).not.toContain("node_modules");
});
test("refuses actual request bodies over the configured limit", async () => {
try {
const response = await app.fetch("/api/echo", {
method: "POST",
headers: { "content-type": "application/json" },
body: `"${"x".repeat(10 * 1024 * 1024)}"`,
});
expect(response.status).toBe(413);
} catch (error) {
// Bun rejects the oversized socket before application dispatch on some versions.
expect(String(error)).toMatch(/ECONNRESET|socket connection was closed/i);
}
});
});
@@ -0,0 +1,30 @@
import type { Context } from "@wrnexus/core";
const apiInput: WRNexusGenerated.ApiContracts["/api/typed-user"]["POST"]["input"] = {
name: "Ada",
email: "ada@example.test",
};
const queuePayload: WRNexusGenerated.QueuePayloads["welcome-email"] = {
userId: 42,
email: "ada@example.test",
};
const queryArgs: WRNexusGenerated.DatabaseQueries["GetUserByEmail"]["args"] = {
email: "ada@example.test",
};
const realtimeMessage: WRNexusGenerated.RealtimeMessages["/realtime/chat"] = {
user: "ada",
text: "hello",
};
const middlewareContext: WRNexusGenerated.MiddlewareContexts["auth"] = {} as Context;
void [apiInput, queuePayload, queryArgs, realtimeMessage, middlewareContext];
// @ts-expect-error schema-derived input requires an email.
const invalidApiInput: WRNexusGenerated.ApiContracts["/api/typed-user"]["POST"]["input"] = {
name: "Missing email",
};
void invalidApiInput;
// @ts-expect-error generated SQL query arguments require an email string.
const invalidQuery: WRNexusGenerated.DatabaseQueries["GetUserByEmail"]["args"] = {};
void invalidQuery;
+59
View File
@@ -0,0 +1,59 @@
// 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 = "about" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "language.tools" | "login" | "modal" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "test" | "ui";
type ApiRoute = "/api/echo" | "/api/graphql-example" | "/api/hello" | "/api/login" | "/api/logout" | "/api/me" | "/api/typed-user" | "/api/users/csr" | "/api/users/ssr" | "/api/webhooks/payment";
type RealtimeRoute = "/realtime/chat" | "/realtime/hello";
type EnvironmentKey = "APP_LABEL" | "DATABASE_URL" | "DEMO_SHARED" | "HOST" | "NODE_ENV" | "PORT" | "SESSION_SECRET" | "UAT_ONLY";
type TranslationKey = "api.greeting" | "home.intro" | "home.title" | "nav.about" | "nav.chat" | "nav.dashboard" | "nav.home" | "nav.ui";
type QueueName = "welcome-email";
type CacheKey = "users";
interface Components {
"Modal": { props: Record<string, never>; outputs: Record<string, never> };
"Counter": { props: { "start"?: unknown; "label"?: unknown }; outputs: Record<string, never> };
}
interface ApiContracts {
"/api/webhooks/payment": { POST: ApiContract<typeof import("../api/webhooks/payment.ts")["POST"]> };
"/api/users/csr": { GET: ApiContract<typeof import("../api/users/csr.ts")["GET"]> };
"/api/users/ssr": { GET: ApiContract<typeof import("../api/users/ssr.ts")["GET"]> };
"/api/graphql-example": { POST: ApiContract<typeof import("../api/graphql-example.ts")["POST"]> };
"/api/typed-user": { POST: ApiContract<typeof import("../api/typed-user.ts")["POST"]> };
"/api/logout": { POST: ApiContract<typeof import("../api/logout.ts")["POST"]> };
"/api/hello": { GET: ApiContract<typeof import("../api/hello.ts")["GET"]> };
"/api/login": { POST: ApiContract<typeof import("../api/login.ts")["POST"]> };
"/api/echo": { GET: ApiContract<typeof import("../api/echo.ts")["GET"]>; POST: ApiContract<typeof import("../api/echo.ts")["POST"]> };
"/api/me": { GET: ApiContract<typeof import("../api/me.ts")["GET"]> };
}
interface MiddlewareContexts {
"auth": MiddlewareContext<(typeof import("../middleware/auth.ts"))["default"]>;
"logger": MiddlewareContext<(typeof import("../middleware/logger.ts"))["default"]>;
"ratelimit": MiddlewareContext<(typeof import("../middleware/ratelimit.ts"))["default"]>;
}
interface DatabaseQueries {
"GetUserByEmail": QueryContract<typeof import("../db/queries.gen.ts")["GetUserByEmail"]>;
"ListUsers": QueryContract<typeof import("../db/queries.gen.ts")["ListUsers"]>;
"CountActive": QueryContract<typeof import("../db/queries.gen.ts")["CountActive"]>;
"CreateUser": QueryContract<typeof import("../db/queries.gen.ts")["CreateUser"]>;
"DeactivateUser": QueryContract<typeof import("../db/queries.gen.ts")["DeactivateUser"]>;
}
interface RealtimeMessages {
"/realtime/hello": RealtimeMessage<(typeof import("../pages/hello.wrn"))["default"]>;
"/realtime/chat": RealtimeMessage<(typeof import("../realtime/chat.ts"))["default"]>;
}
interface QueuePayloads {
"welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>;
}
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
}
@@ -0,0 +1 @@
// AUTO-GENERATED plugin type aggregation - do not edit.
+10
View File
@@ -0,0 +1,10 @@
# 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.
+44
View File
@@ -0,0 +1,44 @@
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 } }]
+6
View File
@@ -0,0 +1,6 @@
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; }
}
+22
View File
@@ -0,0 +1,22 @@
[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
+30
View File
@@ -0,0 +1,30 @@
services:
app:
build: .
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: "3000"
DATABASE_URL: postgres://wire:wire@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: wire
POSTGRES_PASSWORD: wire
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U wire -d app"]
interval: 3s
timeout: 3s
retries: 20
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
+19
View File
@@ -0,0 +1,19 @@
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"
@@ -0,0 +1,67 @@
# API examples
## postWebhooksPayment
```bash
curl -X POST "http://localhost:3000/api/webhooks/payment"
```
## getUsersCsr
```bash
curl -X GET "http://localhost:3000/api/users/csr"
```
## getUsersSsr
```bash
curl -X GET "http://localhost:3000/api/users/ssr"
```
## postGraphqlExample
```bash
curl -X POST "http://localhost:3000/api/graphql-example"
```
## postTypedUser
```bash
curl -X POST "http://localhost:3000/api/typed-user"
```
## postLogout
```bash
curl -X POST "http://localhost:3000/api/logout"
```
## getHello
```bash
curl -X GET "http://localhost:3000/api/hello"
```
## postLogin
```bash
curl -X POST "http://localhost:3000/api/login"
```
## getEcho
```bash
curl -X GET "http://localhost:3000/api/echo"
```
## postEcho
```bash
curl -X POST "http://localhost:3000/api/echo"
```
## getMe
```bash
curl -X GET "http://localhost:3000/api/me"
```
@@ -0,0 +1,79 @@
<!doctype html><meta charset="utf-8" /><meta name="viewport" content="width=device-width" /><title>
basic-app API</title
><style>
body {
font: 16px system-ui;
max-width: 960px;
margin: auto;
padding: 2rem;
}
code,
pre {
background: #f4f4f5;
padding: 0.2rem 0.4rem;
}
article {
border-bottom: 1px solid #ddd;
padding: 1rem 0;
}
</style>
<h1>basic-app API</h1>
<p>OpenAPI 3.1 · 11 operations · <a href="openapi.json">specification</a></p>
<article>
<h2><code>POST</code> /api/webhooks/payment</h2>
<p>Payment completed</p>
<p>Sent after a payment reaches its settled state.</p>
<p>Webhook event: <code>payment.completed</code> · signature: <code>x-payment-signature</code></p>
<small>app/api/webhooks/payment.ts</small>
</article>
<article>
<h2><code>GET</code> /api/users/csr</h2>
<p>getUsersCsr</p>
<small>app/api/users/csr.ts</small>
</article>
<article>
<h2><code>GET</code> /api/users/ssr</h2>
<p>getUsersSsr</p>
<small>app/api/users/ssr.ts</small>
</article>
<article>
<h2><code>POST</code> /api/graphql-example</h2>
<p>postGraphqlExample</p>
<small>app/api/graphql-example.ts</small>
</article>
<article>
<h2><code>POST</code> /api/typed-user</h2>
<p>postTypedUser</p>
<p>Validate and echo a typed user payload.</p>
<small>app/api/typed-user.ts</small>
</article>
<article>
<h2><code>POST</code> /api/logout</h2>
<p>postLogout</p>
<small>app/api/logout.ts</small>
</article>
<article>
<h2><code>GET</code> /api/hello</h2>
<p>getHello</p>
<small>app/api/hello.ts</small>
</article>
<article>
<h2><code>POST</code> /api/login</h2>
<p>postLogin</p>
<small>app/api/login.ts</small>
</article>
<article>
<h2><code>GET</code> /api/echo</h2>
<p>getEcho</p>
<small>app/api/echo.ts</small>
</article>
<article>
<h2><code>POST</code> /api/echo</h2>
<p>postEcho</p>
<small>app/api/echo.ts</small>
</article>
<article>
<h2><code>GET</code> /api/me</h2>
<p>getMe</p>
<small>app/api/me.ts</small>
</article>
@@ -0,0 +1,318 @@
{
"openapi": "3.1.0",
"info": {
"title": "basic-app API",
"version": "0.8.0"
},
"paths": {
"/api/webhooks/payment": {
"post": {
"operationId": "postWebhooksPayment",
"summary": "Payment completed",
"description": "Sent after a payment reaches its settled state.",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/webhooks/payment.ts"
}
},
"/api/users/csr": {
"get": {
"operationId": "getUsersCsr",
"summary": "GET /api/users/csr",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/users/csr.ts"
}
},
"/api/users/ssr": {
"get": {
"operationId": "getUsersSsr",
"summary": "GET /api/users/ssr",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/users/ssr.ts"
}
},
"/api/graphql-example": {
"post": {
"operationId": "postGraphqlExample",
"summary": "POST /api/graphql-example",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/graphql-example.ts"
}
},
"/api/typed-user": {
"post": {
"operationId": "postTypedUser",
"summary": "POST /api/typed-user",
"description": "Validate and echo a typed user payload.",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/typed-user.ts"
}
},
"/api/logout": {
"post": {
"operationId": "postLogout",
"summary": "POST /api/logout",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/logout.ts"
}
},
"/api/hello": {
"get": {
"operationId": "getHello",
"summary": "GET /api/hello",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/hello.ts"
}
},
"/api/login": {
"post": {
"operationId": "postLogin",
"summary": "POST /api/login",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/login.ts"
}
},
"/api/echo": {
"get": {
"operationId": "getEcho",
"summary": "GET /api/echo",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/echo.ts"
},
"post": {
"operationId": "postEcho",
"summary": "POST /api/echo",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/echo.ts"
}
},
"/api/me": {
"get": {
"operationId": "getMe",
"summary": "GET /api/me",
"tags": ["API"],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Invalid request"
},
"500": {
"description": "Internal error"
}
},
"x-wrnexus-source": "app/api/me.ts"
}
}
},
"webhooks": {
"payment.completed": {
"post": {
"summary": "Payment completed",
"description": "Sent after a payment reaches its settled state.",
"parameters": [
{
"name": "x-payment-signature",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaymentCompleted"
}
}
}
},
"responses": {
"200": {
"description": "Webhook accepted"
}
},
"x-wrnexus-source": "app/api/webhooks/payment.ts"
}
}
}
}
@@ -0,0 +1,91 @@
{
"info": {
"name": "basic-app API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "postWebhooksPayment",
"request": {
"method": "POST",
"url": "{{baseUrl}}/api/webhooks/payment"
}
},
{
"name": "getUsersCsr",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/users/csr"
}
},
{
"name": "getUsersSsr",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/users/ssr"
}
},
{
"name": "postGraphqlExample",
"request": {
"method": "POST",
"url": "{{baseUrl}}/api/graphql-example"
}
},
{
"name": "postTypedUser",
"request": {
"method": "POST",
"url": "{{baseUrl}}/api/typed-user"
}
},
{
"name": "postLogout",
"request": {
"method": "POST",
"url": "{{baseUrl}}/api/logout"
}
},
{
"name": "getHello",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/hello"
}
},
{
"name": "postLogin",
"request": {
"method": "POST",
"url": "{{baseUrl}}/api/login"
}
},
{
"name": "getEcho",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/echo"
}
},
{
"name": "postEcho",
"request": {
"method": "POST",
"url": "{{baseUrl}}/api/echo"
}
},
{
"name": "getMe",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/me"
}
}
],
"variable": [
{
"key": "baseUrl",
"value": "http://localhost:3000"
}
]
}
@@ -0,0 +1,5 @@
package wrnexussdk
import ("bytes"; "encoding/json"; "fmt"; "net/http")
type Client struct { BaseURL string; HTTP *http.Client }
func (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 }
@@ -0,0 +1,3 @@
package dev.wrnexus.sdk;
import java.net.URI; import java.net.http.*;
public 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();} }
@@ -0,0 +1,58 @@
const request = async (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 = 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;
};
export const postWebhooksPayment = (params = {}, body, options = {}) => {
void params;
return request("POST", `/api/webhooks/payment`, body, options);
};
export const getUsersCsr = (params = {}, body, options = {}) => {
void params;
return request("GET", `/api/users/csr`, body, options);
};
export const getUsersSsr = (params = {}, body, options = {}) => {
void params;
return request("GET", `/api/users/ssr`, body, options);
};
export const postGraphqlExample = (params = {}, body, options = {}) => {
void params;
return request("POST", `/api/graphql-example`, body, options);
};
export const postTypedUser = (params = {}, body, options = {}) => {
void params;
return request("POST", `/api/typed-user`, body, options);
};
export const postLogout = (params = {}, body, options = {}) => {
void params;
return request("POST", `/api/logout`, body, options);
};
export const getHello = (params = {}, body, options = {}) => {
void params;
return request("GET", `/api/hello`, body, options);
};
export const postLogin = (params = {}, body, options = {}) => {
void params;
return request("POST", `/api/login`, body, options);
};
export const getEcho = (params = {}, body, options = {}) => {
void params;
return request("GET", `/api/echo`, body, options);
};
export const postEcho = (params = {}, body, options = {}) => {
void params;
return request("POST", `/api/echo`, body, options);
};
export const getMe = (params = {}, body, options = {}) => {
void params;
return request("GET", `/api/me`, body, options);
};
@@ -0,0 +1,19 @@
import json, urllib.request
class WrnexusApi:
def __init__(self, base_url): self.base_url = base_url.rstrip('/')
def request(self, method, path, body=None):
data = None if body is None else json.dumps(body).encode()
request = urllib.request.Request(self.base_url + path, data=data, method=method, headers={'content-type':'application/json'})
with urllib.request.urlopen(request) as response: return json.load(response)
def postWebhooksPayment(self, path, body=None): return self.request('POST', path, body)
def getUsersCsr(self, path, body=None): return self.request('GET', path, body)
def getUsersSsr(self, path, body=None): return self.request('GET', path, body)
def postGraphqlExample(self, path, body=None): return self.request('POST', path, body)
def postTypedUser(self, path, body=None): return self.request('POST', path, body)
def postLogout(self, path, body=None): return self.request('POST', path, body)
def getHello(self, path, body=None): return self.request('GET', path, body)
def postLogin(self, path, body=None): return self.request('POST', path, body)
def getEcho(self, path, body=None): return self.request('GET', path, body)
def postEcho(self, path, body=None): return self.request('POST', path, body)
def getMe(self, path, body=None): return self.request('GET', path, body)
@@ -0,0 +1,109 @@
export type RequestOptions = { baseUrl?: string; headers?: HeadersInit };
type ApiEnvelope = { data?: unknown; error?: { message?: string } };
const request = async (
method: string,
path: string,
body: unknown,
options: RequestOptions = {},
) => {
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: 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;
};
export const postWebhooksPayment = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("POST", `/api/webhooks/payment`, body, options);
};
export const getUsersCsr = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("GET", `/api/users/csr`, body, options);
};
export const getUsersSsr = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("GET", `/api/users/ssr`, body, options);
};
export const postGraphqlExample = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("POST", `/api/graphql-example`, body, options);
};
export const postTypedUser = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("POST", `/api/typed-user`, body, options);
};
export const postLogout = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("POST", `/api/logout`, body, options);
};
export const getHello = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("GET", `/api/hello`, body, options);
};
export const postLogin = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("POST", `/api/login`, body, options);
};
export const getEcho = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("GET", `/api/echo`, body, options);
};
export const postEcho = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("POST", `/api/echo`, body, options);
};
export const getMe = (
params: Record<string, string> = {},
body: unknown,
options: RequestOptions = {},
) => {
void params;
return request("GET", `/api/me`, body, options);
};
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "basic-app",
"version": "0.1.0",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
@@ -22,13 +22,13 @@
},
"devDependencies": {
"@wrnexus/test": "workspace:*",
"@eslint/js": "latest",
"@eslint/js": "^10.0.1",
"@iconify-json/lucide": "^1.2.118",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.0.0",
"eslint": "latest",
"eslint": "^10.8.0",
"prettier": "^3.9.4",
"tailwindcss": "^4.0.0",
"typescript-eslint": "latest"
"typescript-eslint": "^8.65.0"
}
}
+8
View File
@@ -0,0 +1,8 @@
[build]
builder = "DOCKERFILE"
[deploy]
startCommand = "bun dist/server.js"
healthcheckPath = "/readyz"
restartPolicyType = "ON_FAILURE"
preDeployCommand = ["bunx wrnexus db migrate --profile=production"]
+11
View File
@@ -0,0 +1,11 @@
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
+2
View File
@@ -10,6 +10,8 @@ import type { AppConfig } from "@wrnexus/styles";
* framework-level headers and optional CORS.
*/
const config: AppConfig = {
frameworkBehaviour: 1,
compatibilityDate: "2026-08-02",
head: [
// --- Use a CSS framework via CDN (uncomment one) ---
// Bootstrap:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "captcha-showcase",
"version": "0.1.0",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "component-showcase",
"version": "0.1.0",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
@@ -187,7 +187,7 @@ test("every declared public event is documented and visible in the playground",
expect(source).toContain("data-playground-event-log");
for (const event of component.events) {
expect(source).toContain(`<code>@${event}</code>`);
expect(source).toContain(`@${event}='console.log(event.detail)'`);
expect(source).toContain(`@${event}='console.log(payload)'`);
}
}
});
+14
View File
@@ -0,0 +1,14 @@
# WRNexus i18n showcase
A complete English, Hindi, and Marathi example covering SSR translations, translated attributes,
interpolation, plural rules, numbers, INR currency, percentages, dates, relative time, lists,
fallback chains, browser language negotiation, and cookie-persisted language selection.
```bash
bun install
bun run --cwd examples/i18n-showcase dev
```
Open `http://localhost:3000`, change the language with the packaged `LanguageSwitcher`, and inspect
the document `<html lang>` attribute and `wire-lang` cookie. The `/api/formats` endpoint demonstrates
request-aware `Intl` formatting using the same selected locale.
+22
View File
@@ -0,0 +1,22 @@
import type { Context } from "@wrnexus/core";
import { createLocaleFormatter, formatMessage, plural } from "@wrnexus/i18n";
export const GET = async (ctx: Context) => {
const locale = ctx.lang || "en";
const format = createLocaleFormatter(locale, "Asia/Kolkata");
const count = 3;
return Response.json({
locale,
direction: ["ar", "fa", "he", "ur"].includes(locale.split("-")[0]!) ? "rtl" : "ltr",
translated: ctx.t("api.greeting", { name: "Asha" }),
interpolated: formatMessage(ctx.t("api.inbox"), { name: "Asha", count }, locale),
plural: plural(count, { one: ctx.t("api.itemOne"), other: ctx.t("api.itemOther") }, locale),
number: format.number(1_234_567.89),
currency: format.currency(1_234.5, "INR"),
percent: format.number(0.78, { style: "percent" }),
date: format.date("2026-08-15T09:30:00+05:30", { dateStyle: "full" }),
relativeTime: format.relative(-3, "day", { numeric: "auto" }),
list: format.list(["Mumbai", "Pune", "Nagpur"], { style: "long", type: "conjunction" }),
});
};
@@ -0,0 +1,14 @@
layout Document {
props {
language: string = "en"
}
view {
<html>
<head></head>
<body>
<div id="app"><slot /></div>
</body>
</html>
}
}
@@ -0,0 +1,69 @@
{
"header": {
"eyebrow": "WRNexus i18n",
"title": "Internationalization showcase"
},
"hero": {
"kicker": "Three languages, one page",
"title": "Everything needed for an international product",
"description": "The server chooses a language from the cookie or Accept-Language header and renders translated HTML."
},
"sections": { "content": "Translated content" },
"switcher": {
"label": "Language",
"placeholder": "Choose language",
"helper": "Saved securely in your language cookie",
"variantsTitle": "Responsive switcher variants",
"variantsDescription": "Every variant reads locales from configuration and the active language from the request cookie.",
"compactTitle": "Compact",
"segmentedTitle": "Segmented"
},
"cards": {
"text": {
"title": "Text translation",
"body": "Headings, paragraphs, buttons, and labels are rendered on the server."
},
"attributes": {
"title": "Attribute translation",
"label": "Search label",
"placeholder": "Search in English"
},
"interpolation": {
"title": "Interpolation",
"body": "Parameters such as a person's name are inserted safely.",
"action": "Open the dynamic API"
},
"plural": {
"title": "Plural rules",
"body": "One item and many items follow locale-aware CLDR rules."
},
"numbers": { "title": "Numbers and currency" },
"time": { "title": "Dates and relative time" },
"list": { "title": "List formatting" },
"fallback": {
"title": "Fallback chains",
"body": "Missing regional content falls back to the base language and then English."
},
"document": {
"title": "Document language",
"body": "The same cookie sets the server-rendered html lang and dir attributes."
}
},
"samples": {
"number": "1,234,567.89",
"currency": "₹1,234.50",
"percent": "78%",
"date": "15 August 2026",
"relative": "3 days ago",
"list": "Mumbai, Pune, and Nagpur"
},
"api": {
"title": "Try request-aware formatting",
"description": "The JSON endpoint uses ctx.lang, translations, Intl formatters, interpolation, plurals, and lists.",
"action": "View localized JSON",
"greeting": "Hello, {name}!",
"inbox": "{name}, you have {count, plural, one {# message} other {# messages}}.",
"itemOne": "# item",
"itemOther": "# items"
}
}
@@ -0,0 +1,69 @@
{
"header": {
"eyebrow": "WRNexus अंतर्राष्ट्रीयकरण",
"title": "बहुभाषी उदाहरण"
},
"hero": {
"kicker": "तीन भाषाएँ, एक पृष्ठ",
"title": "एक अंतर्राष्ट्रीय उत्पाद के लिए आवश्यक सब कुछ",
"description": "सर्वर कुकी या Accept-Language हेडर से भाषा चुनता है और अनुवादित HTML प्रस्तुत करता है।"
},
"sections": { "content": "अनुवादित सामग्री" },
"switcher": {
"label": "भाषा",
"placeholder": "भाषा चुनें",
"helper": "आपकी भाषा कुकी में सुरक्षित रूप से सहेजा गया",
"variantsTitle": "रेस्पॉन्सिव स्विचर प्रकार",
"variantsDescription": "हर प्रकार कॉन्फ़िगरेशन से भाषाएँ और अनुरोध कुकी से सक्रिय भाषा पढ़ता है।",
"compactTitle": "कॉम्पैक्ट",
"segmentedTitle": "खंडित"
},
"cards": {
"text": {
"title": "पाठ अनुवाद",
"body": "शीर्षक, अनुच्छेद, बटन और लेबल सर्वर पर प्रस्तुत होते हैं।"
},
"attributes": {
"title": "एट्रिब्यूट अनुवाद",
"label": "खोज लेबल",
"placeholder": "हिन्दी में खोजें"
},
"interpolation": {
"title": "इंटरपोलेशन",
"body": "व्यक्ति के नाम जैसे पैरामीटर सुरक्षित रूप से जोड़े जाते हैं।",
"action": "डायनेमिक API खोलें"
},
"plural": {
"title": "बहुवचन नियम",
"body": "एक और अनेक वस्तुएँ भाषा के CLDR नियमों का पालन करती हैं।"
},
"numbers": { "title": "संख्याएँ और मुद्रा" },
"time": { "title": "तिथियाँ और सापेक्ष समय" },
"list": { "title": "सूची स्वरूपण" },
"fallback": {
"title": "फॉलबैक श्रृंखला",
"body": "अनुपलब्ध क्षेत्रीय सामग्री पहले मूल भाषा और फिर अंग्रेज़ी में मिलती है।"
},
"document": {
"title": "दस्तावेज़ की भाषा",
"body": "यही कुकी सर्वर द्वारा प्रस्तुत html lang और dir एट्रिब्यूट तय करती है।"
}
},
"samples": {
"number": "12,34,567.89",
"currency": "₹1,234.50",
"percent": "78%",
"date": "15 अगस्त 2026",
"relative": "3 दिन पहले",
"list": "मुंबई, पुणे और नागपुर"
},
"api": {
"title": "अनुरोध के अनुसार स्वरूपण आज़माएँ",
"description": "JSON एंडपॉइंट ctx.lang, अनुवाद, Intl फॉर्मैटर, इंटरपोलेशन, बहुवचन और सूचियों का उपयोग करता है।",
"action": "स्थानीयकृत JSON देखें",
"greeting": "नमस्ते, {name}!",
"inbox": "{name}, आपके पास {count, plural, one {# संदेश} other {# संदेश}} हैं।",
"itemOne": "# वस्तु",
"itemOther": "# वस्तुएँ"
}
}
@@ -0,0 +1,69 @@
{
"header": {
"eyebrow": "WRNexus आंतरराष्ट्रीयीकरण",
"title": "बहुभाषिक उदाहरण"
},
"hero": {
"kicker": "तीन भाषा, एक पृष्ठ",
"title": "आंतरराष्ट्रीय उत्पादनासाठी आवश्यक असलेले सर्व काही",
"description": "सर्व्हर कुकी किंवा Accept-Language हेडरवरून भाषा निवडतो आणि भाषांतरित HTML प्रस्तुत करतो."
},
"sections": { "content": "भाषांतरित मजकूर" },
"switcher": {
"label": "भाषा",
"placeholder": "भाषा निवडा",
"helper": "तुमच्या भाषा कुकीमध्ये सुरक्षितपणे जतन केले",
"variantsTitle": "प्रतिसादक्षम स्विचर प्रकार",
"variantsDescription": "प्रत्येक प्रकार कॉन्फिगरेशनमधून भाषा आणि विनंती कुकीमधून सक्रिय भाषा घेतो.",
"compactTitle": "संक्षिप्त",
"segmentedTitle": "विभाजित"
},
"cards": {
"text": {
"title": "मजकूर भाषांतर",
"body": "शीर्षके, परिच्छेद, बटणे आणि लेबले सर्व्हरवर प्रस्तुत होतात."
},
"attributes": {
"title": "गुणधर्म भाषांतर",
"label": "शोध लेबल",
"placeholder": "मराठीत शोधा"
},
"interpolation": {
"title": "इंटरपोलेशन",
"body": "व्यक्तीच्या नावासारखे पॅरामीटर सुरक्षितपणे जोडले जातात.",
"action": "डायनॅमिक API उघडा"
},
"plural": {
"title": "अनेकवचन नियम",
"body": "एक आणि अनेक वस्तू स्थानिक CLDR नियमांचे पालन करतात."
},
"numbers": { "title": "संख्या आणि चलन" },
"time": { "title": "दिनांक आणि सापेक्ष वेळ" },
"list": { "title": "यादी स्वरूपण" },
"fallback": {
"title": "फॉलबॅक साखळी",
"body": "प्रादेशिक मजकूर उपलब्ध नसल्यास मूळ भाषा आणि नंतर इंग्रजी वापरली जाते."
},
"document": {
"title": "दस्तावेजाची भाषा",
"body": "हीच कुकी सर्व्हरने प्रस्तुत केलेले html lang आणि dir गुणधर्म ठरवते."
}
},
"samples": {
"number": "12,34,567.89",
"currency": "₹1,234.50",
"percent": "78%",
"date": "15 ऑगस्ट 2026",
"relative": "3 दिवसांपूर्वी",
"list": "मुंबई, पुणे आणि नागपूर"
},
"api": {
"title": "विनंतीनुसार स्वरूपण वापरून पहा",
"description": "JSON एंडपॉइंट ctx.lang, भाषांतरे, Intl फॉर्मॅटर, इंटरपोलेशन, अनेकवचन आणि याद्या वापरतो.",
"action": "स्थानिकीकरण केलेला JSON पहा",
"greeting": "नमस्कार, {name}!",
"inbox": "{name}, तुमच्याकडे {count, plural, one {# संदेश} other {# संदेश}} आहेत.",
"itemOne": "# वस्तू",
"itemOther": "# वस्तू"
}
}
@@ -0,0 +1,64 @@
import LanguageSwitcher from "@wrnexus/i18n/components/LanguageSwitcher.wrn"
page InternationalizationShowcase {
seo {
title = "Internationalization Showcase"
description = "English, Hindi, and Marathi localization with WRNexusJS."
}
view {
<main class="min-h-screen bg-[var(--wire-color-bg)] text-[var(--wire-color-text)]">
<header class="border-b border-[var(--wire-color-border)] bg-[var(--wire-color-surface)]">
<div class="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-5 px-6 py-5">
<div>
<p class="m-0 text-xs font-semibold uppercase tracking-[0.2em] text-[var(--wire-color-primary)]" data-t="header.eyebrow">WRNexus i18n</p>
<h1 class="m-0 mt-1 text-xl font-bold" data-t="header.title">Internationalization showcase</h1>
</div>
<LanguageSwitcher
label="Language / भाषा"
placeholder="Choose language"
helperText="Saved in your language cookie"
/>
</div>
</header>
<div class="mx-auto grid max-w-6xl gap-8 px-6 py-10">
<section class="rounded-3xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-8 shadow-sm">
<p class="m-0 text-sm font-medium text-[var(--wire-color-primary)]" data-t="hero.kicker">Three languages, one page</p>
<h2 class="mb-3 mt-2 text-4xl font-bold tracking-tight" data-t="hero.title">Everything needed for an international product</h2>
<p class="m-0 max-w-3xl text-lg leading-8 text-[var(--wire-color-muted)]" data-t="hero.description">The server chooses a language from the cookie or Accept-Language header and renders translated HTML.</p>
</section>
<section>
<h2 class="mb-2 text-2xl font-bold" data-t="switcher.variantsTitle">Responsive switcher variants</h2>
<p class="mb-5 mt-0 text-[var(--wire-color-muted)]" data-t="switcher.variantsDescription">Every variant reads locales from configuration and the active language from the request cookie.</p>
<div class="grid items-start gap-5 rounded-3xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 md:grid-cols-2">
<div class="grid gap-2"><strong data-t="switcher.compactTitle">Compact</strong><LanguageSwitcher variant="compact" showHelper="false" label="Language / भाषा" /></div>
<div class="grid gap-2"><strong data-t="switcher.segmentedTitle">Segmented</strong><LanguageSwitcher variant="segmented" showHelper="false" label="Language / भाषा" /></div>
</div>
</section>
<section>
<h2 class="mb-4 text-2xl font-bold" data-t="sections.content">Translated content</h2>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<article class="demo-card"><span class="icon-[lucide--languages] demo-icon"></span><h3 data-t="cards.text.title">Text translation</h3><p data-t="cards.text.body">Headings, paragraphs, buttons, and labels are rendered on the server.</p></article>
<article class="demo-card"><span class="icon-[lucide--text-cursor-input] demo-icon"></span><h3 data-t="cards.attributes.title">Attribute translation</h3><label class="mt-3 grid gap-1 text-sm"><span data-t="cards.attributes.label">Search label</span><input class="rounded-xl border border-[var(--wire-color-border)] bg-transparent px-3 py-2" t:placeholder="cards.attributes.placeholder" /></label></article>
<article class="demo-card"><span class="icon-[lucide--message-square-more] demo-icon"></span><h3 data-t="cards.interpolation.title">Interpolation</h3><p data-t="cards.interpolation.body">Parameters such as a person's name are inserted safely.</p><a class="demo-link" href="/api/formats" data-t="cards.interpolation.action">Open the dynamic API</a></article>
<article class="demo-card"><span class="icon-[lucide--copy-plus] demo-icon"></span><h3 data-t="cards.plural.title">Plural rules</h3><p data-t="cards.plural.body">One item and many items follow locale-aware CLDR rules.</p></article>
<article class="demo-card"><span class="icon-[lucide--indian-rupee] demo-icon"></span><h3 data-t="cards.numbers.title">Numbers and currency</h3><p data-t="samples.number">12,34,567.89</p><p data-t="samples.currency">₹1,234.50</p><p data-t="samples.percent">78%</p></article>
<article class="demo-card"><span class="icon-[lucide--calendar-days] demo-icon"></span><h3 data-t="cards.time.title">Dates and relative time</h3><p data-t="samples.date">15 August 2026</p><p data-t="samples.relative">3 days ago</p></article>
<article class="demo-card"><span class="icon-[lucide--list] demo-icon"></span><h3 data-t="cards.list.title">List formatting</h3><p data-t="samples.list">Mumbai, Pune, and Nagpur</p></article>
<article class="demo-card"><span class="icon-[lucide--route] demo-icon"></span><h3 data-t="cards.fallback.title">Fallback chains</h3><p data-t="cards.fallback.body">Missing regional content falls back to the base language and then English.</p></article>
<article class="demo-card"><span class="icon-[lucide--file-code-2] demo-icon"></span><h3 data-t="cards.document.title">Document language</h3><p data-t="cards.document.body">The same cookie sets the server-rendered html lang and dir attributes.</p></article>
</div>
</section>
<section class="rounded-3xl bg-[var(--wire-color-primary)] p-8 text-[var(--wire-color-primary-contrast)]">
<h2 class="m-0 text-2xl font-bold" data-t="api.title">Try request-aware formatting</h2>
<p class="mb-5 mt-2 opacity-85" data-t="api.description">The JSON endpoint uses ctx.lang, translations, Intl formatters, interpolation, plurals, and lists.</p>
<a href="/api/formats" class="inline-flex rounded-xl bg-white px-4 py-2 font-semibold text-slate-900" data-t="api.action">View localized JSON</a>
</section>
</div>
</main>
}
}
+55
View File
@@ -0,0 +1,55 @@
// AUTO-GENERATED by `wrnexus dev` - do not edit.
// Typed routes support required, optional, and catch-all parameters.
export interface Routes {
"/": Record<string, never>;
}
export type RoutePath = keyof Routes;
type RouteValue = string | readonly string[] | undefined;
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
if (value === undefined) return "";
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
return values.map((part) => encodeURIComponent(part)).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, RouteValue>;
const output: string[] = [];
for (const segment of String(path).split("/").filter(Boolean)) {
let name: string | undefined;
let optional = false;
let catchAll = false;
if (segment.startsWith("[[") && segment.endsWith("]]")) {
optional = true;
name = segment.slice(2, -2);
} else if (segment.startsWith("[") && segment.endsWith("]")) {
name = segment.slice(1, -1);
if (name.endsWith("?")) {
optional = true;
name = name.slice(0, -1);
}
}
if (!name) {
output.push(segment);
continue;
}
if (name.startsWith("...")) {
catchAll = true;
name = name.slice(3);
}
const value = params[name];
if (value === undefined && optional) continue;
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
output.push(encodeRouteValue(value, catchAll));
}
return "/" + output.filter(Boolean).join("/");
}
@@ -0,0 +1,59 @@
@import "tailwindcss";
@plugin "@iconify/tailwind4";
@source "../**/*.wrn";
@source "../../../packages/i18n/components/*.wrn";
html {
color-scheme: light dark;
}
body {
margin: 0;
font-family: "Plus Jakarta Sans", "Noto Sans Devanagari", ui-sans-serif, system-ui, sans-serif;
}
/* The server sets <html lang> from the language cookie, so each script can use
a typography stack designed for its glyph shapes and metrics. */
html:lang(hi) body,
html:lang(mr) body {
font-family: "Noto Sans Devanagari", "Nirmala UI", Mangal, sans-serif;
}
html[dir="rtl"] body {
text-align: start;
}
.demo-card {
display: flex;
min-height: 12rem;
flex-direction: column;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-xl);
background: var(--wire-color-surface);
padding: 1.25rem;
}
.demo-card h3 {
margin: 0.75rem 0 0.35rem;
font-size: 1rem;
}
.demo-card p {
margin: 0.2rem 0;
color: var(--wire-color-muted);
line-height: 1.6;
}
.demo-icon {
width: 1.4rem;
height: 1.4rem;
color: var(--wire-color-primary);
}
.demo-link {
margin-top: auto;
padding-top: 1rem;
color: var(--wire-color-primary);
font-size: 0.875rem;
font-weight: 600;
}
+25
View File
@@ -0,0 +1,25 @@
{
"name": "i18n-showcase",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run ../../packages/cli/src/index.ts dev .",
"build": "bun run ../../packages/cli/src/index.ts build .",
"test": "bun test test",
"typecheck": "tsc --noEmit -p tsconfig.json",
"check": "bun run typecheck && bun run test && bun run build"
},
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/i18n": "workspace:*",
"@wrnexus/ui": "workspace:*"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.118",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.2"
}
}
@@ -0,0 +1,41 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { loadLocales, makeT, resolveI18n, resolveLang, translationCoverage } from "@wrnexus/i18n";
const localeDir = join(import.meta.dir, "../app/locales");
const i18n = resolveI18n(loadLocales(localeDir, { strict: true }), {
default: "en",
locales: ["en", "hi", "mr"],
strict: true,
});
test("ships complete English, Hindi, and Marathi translations", () => {
expect(i18n.langs).toEqual(["en", "hi", "mr"]);
const coverage = translationCoverage(i18n);
expect(coverage.en?.percentage).toBe(100);
expect(coverage.hi?.percentage).toBe(100);
expect(coverage.mr?.percentage).toBe(100);
expect(makeT(i18n, "hi")("header.title")).toBe("बहुभाषी उदाहरण");
expect(makeT(i18n, "mr")("header.title")).toBe("बहुभाषिक उदाहरण");
});
test("resolves browser negotiation and persisted cookie preferences", () => {
expect(resolveLang(i18n, undefined, "mr-IN,hi;q=0.8,en;q=0.5")).toBe("mr");
expect(resolveLang(i18n, "hi", "mr;q=1")).toBe("hi");
});
test("uses the packaged language switcher and document shell", () => {
const page = readFileSync(join(import.meta.dir, "../app/pages/index.wrn"), "utf8");
const document = readFileSync(join(import.meta.dir, "../app/layouts/document.wrn"), "utf8");
expect(page).toContain(
'import LanguageSwitcher from "@wrnexus/i18n/components/LanguageSwitcher.wrn"',
);
expect(page).toContain("<LanguageSwitcher");
expect(page).not.toContain("locales=");
expect(page).not.toContain("current=");
expect(page).toContain('variant="compact"');
expect(page).toContain('variant="segmented"');
expect(page).toContain('t:placeholder="cards.attributes.placeholder"');
expect(document).toContain("<html>");
});
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": { "noEmit": true },
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
}
+41
View File
@@ -0,0 +1,41 @@
import type { AppConfig } from "@wrnexus/styles";
const config = {
seo: {
title: "WRNexus i18n Showcase",
description: "English, Hindi, and Marathi internationalization examples.",
robots: "noindex,nofollow",
},
theme: { palette: "violet", default: "light" },
fonts: {
google: [
{ family: "Plus Jakarta Sans", weights: [400, 500, 600, 700] },
{ family: "Noto Sans Devanagari", weights: [400, 500, 600, 700] },
],
display: "swap",
sans: '"Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif',
},
// Keep localhost development deterministic. A service-worker navigation cache
// is URL-based and cannot distinguish pages rendered from different cookies.
pwa: { serviceWorker: false },
i18n: {
default: "en",
locales: ["en", "hi", "mr"],
labels: { en: "English", hi: "हिन्दी", mr: "मराठी" },
fallbacks: { hi: ["en"], mr: ["en"] },
cookie: { name: "wire-lang", path: "/", maxAge: 31_536_000, sameSite: "Lax" },
strict: true,
},
styles: {
entry: "app/styles/global.css",
failureMode: "throw",
process: async ({ entryPath, appRoot, mode }) => {
if (!entryPath) throw new Error("The i18n showcase stylesheet was not resolved.");
const args = ["@tailwindcss/cli", "-i", entryPath];
if (mode === "production") args.push("--minify");
return await Bun.$.cwd(appRoot)`bunx ${args}`.text();
},
},
} satisfies AppConfig;
export default config;