release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { buildRouter } from "@wrnexus/router";
|
||||
import { v } from "@wrnexus/validation";
|
||||
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
||||
const server = { upgrade: () => false };
|
||||
|
||||
test("server actions validate, enforce CSRF, invalidate, and progressively enhance forms", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-action-runtime-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages", "users.ts"), "export default () => '';");
|
||||
const router = buildRouter(app);
|
||||
const schema = v.object({ name: v.string().min(2) });
|
||||
const security: Record<string, string> = {};
|
||||
let authenticated = false;
|
||||
let granted = false;
|
||||
const handlers = createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router,
|
||||
loadModule: async () => ({
|
||||
default: () => `<form method="post" data-wrn-action="createUser"><input name="name"></form>`,
|
||||
__wrnexusActions: {
|
||||
createUser: {
|
||||
schema,
|
||||
run: (input: { name: string }, ctx: { locals: Record<string, unknown> }) => {
|
||||
ctx.locals.__wrnexusInvalidatedTags = ["users", "users"];
|
||||
return { id: `user-${input.name}` };
|
||||
},
|
||||
},
|
||||
},
|
||||
__wrnexusSecurity: security,
|
||||
}),
|
||||
getMiddleware: async () => [
|
||||
(ctx, next) => {
|
||||
if (authenticated) ctx.user = { id: "operator" };
|
||||
ctx.locals.permissions = granted ? ["users.create"] : [];
|
||||
return next();
|
||||
},
|
||||
],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
|
||||
const page = await handlers.fetch(new Request("https://example.test/users"), server);
|
||||
const html = await page!.text();
|
||||
expect(html).toContain('name="_csrf"');
|
||||
expect(html).toContain("/__wrnexus/actions.js");
|
||||
const cookie = page!.headers.get("set-cookie")!;
|
||||
const token = /wire-csrf=([^;]+)/.exec(cookie)?.[1];
|
||||
if (!token) throw new Error("expected CSRF cookie");
|
||||
|
||||
const invalid = await handlers.fetch(
|
||||
new Request("https://example.test/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
cookie,
|
||||
"x-wrnexus-action": "createUser",
|
||||
"x-csrf-token": token,
|
||||
},
|
||||
body: JSON.stringify({ name: "x" }),
|
||||
}),
|
||||
server,
|
||||
);
|
||||
expect(invalid?.status).toBe(422);
|
||||
expect(await invalid?.json()).toMatchObject({ errors: { name: expect.any(String) } });
|
||||
|
||||
const noCsrf = await handlers.fetch(
|
||||
new Request("https://example.test/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
cookie,
|
||||
"x-wrnexus-action": "createUser",
|
||||
},
|
||||
body: JSON.stringify({ name: "Ada" }),
|
||||
}),
|
||||
server,
|
||||
);
|
||||
expect(noCsrf?.status).toBe(403);
|
||||
|
||||
const success = await handlers.fetch(
|
||||
new Request("https://example.test/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
cookie,
|
||||
"x-wrnexus-action": "createUser",
|
||||
"x-csrf-token": token,
|
||||
},
|
||||
body: JSON.stringify({ name: "Ada" }),
|
||||
}),
|
||||
server,
|
||||
);
|
||||
expect(await success?.json()).toEqual({
|
||||
ok: true,
|
||||
data: { id: "user-Ada" },
|
||||
invalidated: ["users"],
|
||||
});
|
||||
|
||||
security.auth = "required";
|
||||
expect(
|
||||
(
|
||||
await handlers.fetch(
|
||||
new Request("https://example.test/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
cookie,
|
||||
"x-wrnexus-action": "createUser",
|
||||
"x-csrf-token": token,
|
||||
},
|
||||
body: JSON.stringify({ name: "Ada" }),
|
||||
}),
|
||||
server,
|
||||
)
|
||||
)?.status,
|
||||
).toBe(401);
|
||||
authenticated = true;
|
||||
security.permission = "users.create";
|
||||
expect(
|
||||
(
|
||||
await handlers.fetch(
|
||||
new Request("https://example.test/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
cookie,
|
||||
"x-wrnexus-action": "createUser",
|
||||
"x-csrf-token": token,
|
||||
},
|
||||
body: JSON.stringify({ name: "Ada" }),
|
||||
}),
|
||||
server,
|
||||
)
|
||||
)?.status,
|
||||
).toBe(403);
|
||||
granted = true;
|
||||
|
||||
const form = new FormData();
|
||||
form.set("_wrnexus_action", "createUser");
|
||||
form.set("_csrf", token);
|
||||
form.set("name", "Grace");
|
||||
const progressive = await handlers.fetch(
|
||||
new Request("https://example.test/users", {
|
||||
method: "POST",
|
||||
headers: { cookie, origin: "https://example.test" },
|
||||
body: form,
|
||||
}),
|
||||
server,
|
||||
);
|
||||
expect(progressive?.status).toBe(303);
|
||||
expect(progressive?.headers.get("location")).toBe("/users");
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { CacheCoordinator } from "@wrnexus/cache";
|
||||
import { buildRouter } from "@wrnexus/router";
|
||||
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
||||
|
||||
test("declarative route policies cache loader data and expose inspection", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-cache-runtime-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/dashboard.ts"), "export default () => '';\n");
|
||||
let loads = 0;
|
||||
const cache = new CacheCoordinator();
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
cache,
|
||||
loadModule: async () => ({
|
||||
__wrnexusCache: {
|
||||
strategy: "stale-while-revalidate",
|
||||
ttl: "5m",
|
||||
tags: '["dashboard"]',
|
||||
vary: '["language"]',
|
||||
},
|
||||
__wrnexusLoad: async () => ({ count: ++loads }),
|
||||
default: (ctx: { count: number }) => `<p>${ctx.count}</p>`,
|
||||
}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
const server = { upgrade: () => false };
|
||||
const first = await handlers.fetch(new Request("https://example.test/dashboard"), server);
|
||||
const second = await handlers.fetch(new Request("https://example.test/dashboard"), server);
|
||||
expect(first?.headers.get("x-wrnexus-data-cache")).toBe("MISS");
|
||||
expect(second?.headers.get("x-wrnexus-data-cache")).toBe("HIT");
|
||||
expect(loads).toBe(1);
|
||||
const inspection = await handlers.fetch(
|
||||
new Request("https://example.test/__wrnexus/cache"),
|
||||
server,
|
||||
);
|
||||
expect((await inspection?.json())?.layers.data).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("safe full-page policies reuse static documents", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-page-cache-runtime-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/marketing.ts"), "export default () => '';\n");
|
||||
let renders = 0;
|
||||
const pageCache = new CacheCoordinator();
|
||||
const handlers = createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
cache: pageCache,
|
||||
loadModule: async () => ({
|
||||
__wrnexusCache: { scope: "page", strategy: "fresh", ttl: "5m", tags: '["marketing"]' },
|
||||
default: () => `<h1>Render ${++renders}</h1>`,
|
||||
}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
const server = { upgrade: () => false };
|
||||
const first = await handlers.fetch(new Request("https://example.test/marketing"), server);
|
||||
const second = await handlers.fetch(new Request("https://example.test/marketing"), server);
|
||||
expect(first?.headers.get("x-wrnexus-page-cache")).toBe("MISS");
|
||||
expect(pageCache.inspect().layers.page).toHaveLength(1);
|
||||
expect(second?.headers.get("x-wrnexus-page-cache")).toBe("HIT");
|
||||
const firstHtml = await first!.text();
|
||||
const secondHtml = await second!.text();
|
||||
expect(secondHtml).toContain("Render 1");
|
||||
expect(/nonce="([^"]+)"/.exec(firstHtml)?.[1]).not.toBe(/nonce="([^"]+)"/.exec(secondHtml)?.[1]);
|
||||
expect(renders).toBe(1);
|
||||
});
|
||||
|
||||
test("full-page cache refuses CSRF-bearing documents", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-page-cache-csrf-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/account.ts"), "export default () => '';\n");
|
||||
let renders = 0;
|
||||
const cache = new CacheCoordinator();
|
||||
const handlers = createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
cache,
|
||||
loadModule: async () => ({
|
||||
__wrnexusCache: { scope: "page", strategy: "fresh", ttl: "5m" },
|
||||
default: () =>
|
||||
`<form method="post" data-wrn-action="save"><button>${++renders}</button></form>`,
|
||||
}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
const server = { upgrade: () => false };
|
||||
await handlers.fetch(new Request("https://example.test/account"), server);
|
||||
await handlers.fetch(new Request("https://example.test/account"), server);
|
||||
expect(renders).toBe(2);
|
||||
expect(cache.inspect().layers.page).toEqual([]);
|
||||
});
|
||||
@@ -53,3 +53,20 @@ test("legacy, compatible, and explicit import modes are enforced from app config
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
});
|
||||
|
||||
test("compiler-native reactive elements do not require application imports", () => {
|
||||
const { root, page } = fixture();
|
||||
writeFileSync(
|
||||
page,
|
||||
`page Home {
|
||||
state active = "Admin"
|
||||
view {
|
||||
<Transition name="fade"><Component is={active}><div data-component-case="Admin">Admin</div></Component></Transition>
|
||||
<Portal to="body"><p>Notice</p></Portal>
|
||||
<Async source="profile"><Loading>Loading</Loading><Success data="profile">Ready</Success><Error error="error">Failed</Error></Async>
|
||||
}
|
||||
}`,
|
||||
);
|
||||
setCompileImportOptions(root, { mode: "explicit" });
|
||||
expect(() => compileWireArtifacts(page, 5)).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { buildRouter } from "@wrnexus/router";
|
||||
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
||||
|
||||
test("server loader data is available to page rendering", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-load-runtime-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/users.ts"), "export default () => '';\n");
|
||||
const handlers = createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async () => ({
|
||||
__wrnexusLoad: async () => ({ users: ["Ada", "Lin"] }),
|
||||
default: (ctx: { users: string[]; data: { users: string[] } }) =>
|
||||
`<p>${ctx.users.join(",")} / ${ctx.data.users.length}</p>`,
|
||||
}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
const response = await handlers.fetch(new Request("https://example.test/users"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
expect(await response!.text()).toContain("Ada,Lin / 2");
|
||||
});
|
||||
|
||||
test("HMR page synchronization initializes request-scoped loader caching", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-load-runtime-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/async.ts"), "export default () => '';\n");
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: true,
|
||||
router: buildRouter(app),
|
||||
loadModule: async () => ({
|
||||
__wrnexusLoad: async () => ({ message: "Loaded through HMR" }),
|
||||
default: (ctx: { message: string }) => `<p>${ctx.message}</p>`,
|
||||
}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
|
||||
const html = await new Promise<string>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error("HMR response timed out")), 1_000);
|
||||
handlers.websocket.message(
|
||||
{
|
||||
data: { kind: "hmr", baseUrl: "http://localhost", headers: [] },
|
||||
send(value) {
|
||||
clearTimeout(timeout);
|
||||
resolve(String(value));
|
||||
},
|
||||
close() {},
|
||||
},
|
||||
JSON.stringify({ type: "sync", path: "/async" }),
|
||||
);
|
||||
});
|
||||
|
||||
const message = JSON.parse(html) as { type: string; html?: string; message?: string };
|
||||
expect(message.type).toBe("html");
|
||||
expect(message.message).toBeUndefined();
|
||||
expect(message.html).toContain("Loaded through HMR");
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { HealthRegistry } from "@wrnexus/core";
|
||||
import type { Router } from "@wrnexus/router";
|
||||
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||
|
||||
function runtime(health: HealthRegistry) {
|
||||
const router: Router = {
|
||||
pages: [],
|
||||
api: [],
|
||||
realtime: [],
|
||||
middlewareFiles: [],
|
||||
components: [],
|
||||
layouts: [],
|
||||
stores: [],
|
||||
schemas: [],
|
||||
matchPage: () => null,
|
||||
matchApi: () => null,
|
||||
matchRealtime: () => null,
|
||||
};
|
||||
return createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router,
|
||||
loadModule: async () => ({}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
health,
|
||||
observability: { enabled: true, sampleRate: 1, exporter: "none" },
|
||||
} satisfies RuntimeDeps);
|
||||
}
|
||||
|
||||
const server = { upgrade: () => false };
|
||||
|
||||
test("runtime exposes separate liveness and dependency readiness probes", async () => {
|
||||
const health = new HealthRegistry();
|
||||
health.register("database", () => ({ status: "down", message: "offline" }));
|
||||
const handlers = runtime(health);
|
||||
|
||||
const live = await handlers.fetch(new Request("https://example.test/healthz"), server);
|
||||
const ready = await handlers.fetch(new Request("https://example.test/readyz"), server);
|
||||
|
||||
expect(live?.status).toBe(200);
|
||||
expect(await live?.json()).toEqual({ status: "up" });
|
||||
expect(ready?.status).toBe(503);
|
||||
expect(await ready?.json()).toEqual({ status: "down" });
|
||||
});
|
||||
|
||||
test("built production responses carry the framework security-header baseline", async () => {
|
||||
const handlers = runtime(new HealthRegistry());
|
||||
const response = await handlers.fetch(new Request("https://example.test/healthz"), server);
|
||||
expect(response?.headers.get("strict-transport-security")).toContain("max-age=");
|
||||
expect(response?.headers.get("content-security-policy")).toContain("default-src 'self'");
|
||||
expect(response?.headers.get("x-content-type-options")).toBe("nosniff");
|
||||
expect(response?.headers.get("referrer-policy")).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { precomputePartialStaticShell } from "../src/partial-build.ts";
|
||||
|
||||
test("precomputes nested page components while erasing dynamic region bodies", async () => {
|
||||
const result = await precomputePartialStaticShell(
|
||||
{
|
||||
__wrnexusBuildStaticShell: () =>
|
||||
'<div data-component="Card" title="Docs"></div><wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>',
|
||||
},
|
||||
[
|
||||
{
|
||||
name: "Card",
|
||||
mod: { render: (props) => `<article>${props?.title}</article>` },
|
||||
},
|
||||
],
|
||||
);
|
||||
expect(result.regions).toBe(1);
|
||||
expect(result.shell).toContain("<article>Docs</article>");
|
||||
expect(result.shell).toContain('data-wrn-dynamic-placeholder="wrn-region-0"');
|
||||
expect(result.shell).not.toContain("wrn-dynamic-region");
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { PWA_CLIENT, usesMobileRuntime } from "../src/runtime.ts";
|
||||
import { PWA_CLIENT, PWA_DEV_CLEANUP_CLIENT, usesMobileRuntime } from "../src/runtime.ts";
|
||||
|
||||
test("PWA registration is valid JavaScript and Trusted Types compatible", () => {
|
||||
expect(() => new Bun.Transpiler({ loader: "js" }).transformSync(PWA_CLIENT)).not.toThrow();
|
||||
@@ -7,6 +7,14 @@ test("PWA registration is valid JavaScript and Trusted Types compatible", () =>
|
||||
expect(PWA_CLIENT).toContain("createScriptURL(swUrl)");
|
||||
});
|
||||
|
||||
test("development PWA cleanup removes stale WRNexus service workers and caches", () => {
|
||||
expect(() =>
|
||||
new Bun.Transpiler({ loader: "js" }).transformSync(PWA_DEV_CLEANUP_CLIENT),
|
||||
).not.toThrow();
|
||||
expect(PWA_DEV_CLEANUP_CLIENT).toContain("registration.unregister()");
|
||||
expect(PWA_DEV_CLEANUP_CLIENT).toContain("wrnexus-pwa-");
|
||||
});
|
||||
|
||||
test("mobile runtime is shipped only for pages using mobile or native directives", () => {
|
||||
expect(usesMobileRuntime('<main class="page">Docs</main>')).toBe(false);
|
||||
expect(usesMobileRuntime('<button data-native-mobile="share">Share</button>')).toBe(true);
|
||||
|
||||
@@ -2,7 +2,67 @@ import { expect, test } from "bun:test";
|
||||
import { mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { invalidateModule, loadModule, setCompileCacheDir } from "../src/pipeline.ts";
|
||||
import {
|
||||
compileWireArtifacts,
|
||||
compileWireArtifactsAsync,
|
||||
getWrnCompileMetrics,
|
||||
invalidateModule,
|
||||
loadModule,
|
||||
resetWrnCompileMetrics,
|
||||
setCompileCacheDir,
|
||||
setDevCompilerPipeline,
|
||||
} from "../src/pipeline.ts";
|
||||
|
||||
test("development compilation awaits plugin AST and code transforms", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-pipeline-"));
|
||||
const file = join(root, "page.wrn");
|
||||
let astTransformed = false;
|
||||
setCompileCacheDir(join(root, ".wrnexus"));
|
||||
setDevCompilerPipeline({
|
||||
async transformAst(ast) {
|
||||
await Promise.resolve();
|
||||
astTransformed = true;
|
||||
return ast;
|
||||
},
|
||||
async transformCode(code) {
|
||||
await Promise.resolve();
|
||||
return `${code}\nexport const pluginTransformed = true;\n`;
|
||||
},
|
||||
virtualModules: new Map(),
|
||||
});
|
||||
try {
|
||||
writeFileSync(file, "page Home { view { <h1>Plugin</h1> } }\n");
|
||||
const artifact = await compileWireArtifactsAsync(file);
|
||||
expect(astTransformed).toBeTrue();
|
||||
expect((await import(artifact.main)).pluginTransformed).toBeTrue();
|
||||
} finally {
|
||||
setDevCompilerPipeline(null);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("WRN compilation exposes cache hit, miss, timing, and error metrics", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-cache-metrics-"));
|
||||
const file = join(root, "page.wrn");
|
||||
setCompileCacheDir(join(root, ".wrnexus"));
|
||||
resetWrnCompileMetrics();
|
||||
try {
|
||||
writeFileSync(file, "page Home { view { <h1>Metrics</h1> } }\n");
|
||||
compileWireArtifacts(file);
|
||||
compileWireArtifacts(file);
|
||||
writeFileSync(file, "page Broken { view { <h1> } }\n");
|
||||
expect(() => compileWireArtifacts(file)).toThrow();
|
||||
expect(getWrnCompileMetrics()).toMatchObject({
|
||||
hits: 1,
|
||||
misses: 2,
|
||||
compilations: 1,
|
||||
errors: 1,
|
||||
});
|
||||
expect(getWrnCompileMetrics().totalDurationMs).toBeGreaterThanOrEqual(0);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("invalidateModule loads changed server modules without restarting the process", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-"));
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { buildRouter } from "@wrnexus/router";
|
||||
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
||||
|
||||
test("plugin render lifecycle transforms final documents", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-render-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async () => ({ default: () => "<h1>Home</h1>" }),
|
||||
renderHtml: (html) => html.replace("</body>", "<!-- plugin-render --></body>"),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
const response = await handlers.fetch(new Request("https://example.test/"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
expect(await response?.text()).toContain("<!-- plugin-render -->");
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createProductionHandlers, type ProdManifest } from "../src/prod.ts";
|
||||
|
||||
const manifest: ProdManifest = {
|
||||
pages: [
|
||||
{
|
||||
raw: "/",
|
||||
mod: { default: () => "<main>Production artifact</main>", meta: { title: "Prod" } },
|
||||
},
|
||||
{
|
||||
raw: "/partial",
|
||||
staticShell:
|
||||
'<main>Build shell<template data-wrn-dynamic-placeholder="wrn-region-0"></template></main>',
|
||||
mod: {
|
||||
default: () =>
|
||||
'<main>Request shell<wrn-dynamic-region data-wrn-dynamic="true"><strong>User 42</strong></wrn-dynamic-region></main>',
|
||||
meta: { title: "Partial" },
|
||||
__wrnexusRender: "partial-static",
|
||||
},
|
||||
},
|
||||
{
|
||||
raw: "/async",
|
||||
mod: {
|
||||
default: () => "<main>Async page</main>",
|
||||
meta: { title: "Async" },
|
||||
__wrnexusClientLoad: async () => ({ users: [{ id: 1, name: "Ada" }] }),
|
||||
},
|
||||
},
|
||||
],
|
||||
api: [],
|
||||
realtime: [],
|
||||
middleware: [],
|
||||
components: [],
|
||||
layouts: [],
|
||||
};
|
||||
|
||||
const server = { upgrade: () => false } as never;
|
||||
|
||||
test("supervised production runtime injects reconnecting DOM-morph support", async () => {
|
||||
const handlers = createProductionHandlers(manifest, { developmentRuntime: true });
|
||||
const response = await handlers.fetch(new Request("http://localhost/"), server);
|
||||
expect(response).toBeInstanceOf(Response);
|
||||
expect(await (response as Response).text()).toContain("/__wrnexus/hmr");
|
||||
});
|
||||
|
||||
test("production streams request regions into the build-time static shell", async () => {
|
||||
const handlers = createProductionHandlers(manifest, {});
|
||||
const response = (await handlers.fetch(
|
||||
new Request("http://localhost/partial"),
|
||||
server,
|
||||
)) as Response;
|
||||
const html = await response.text();
|
||||
expect(response.headers.get("x-wrnexus-static-shell")).toBe("build");
|
||||
expect(html).toContain("Build shell");
|
||||
expect(html).not.toContain("Request shell");
|
||||
expect(html).toContain("User 42");
|
||||
});
|
||||
|
||||
test("normal production output remains free of development HMR", async () => {
|
||||
const handlers = createProductionHandlers(manifest, {});
|
||||
const response = await handlers.fetch(new Request("http://localhost/"), server);
|
||||
expect(await (response as Response).text()).not.toContain("/__wrnexus/hmr");
|
||||
});
|
||||
|
||||
test("production client-load endpoint returns only the requested named result", async () => {
|
||||
const handlers = createProductionHandlers(manifest, {});
|
||||
const response = (await handlers.fetch(
|
||||
new Request("http://localhost/__wrnexus/client-load?route=%2Fasync&name=users"),
|
||||
server,
|
||||
)) as Response;
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("cache-control")).toBe("private, no-store");
|
||||
expect(await response.json()).toEqual({ data: [{ id: 1, name: "Ada" }] });
|
||||
});
|
||||
Reference in New Issue
Block a user