Files
WRNexusJS/packages/compiler/test/v060-targets.test.ts
T

228 lines
8.3 KiB
TypeScript

import { expect, test } from "bun:test";
import { gzipSync } from "bun";
import { generateTargets, parse } from "../src/index.ts";
const ast = parse(`component ConfirmDialog {
props { title: string open: boolean = false }
state { loading: boolean = false }
server state { internalId: string = "secret" }
outputs { confirm(payload: string) close() }
functions {
client async function confirm(value: string): Promise<void> {
const saved = await server.confirm(value)
output.confirm(saved)
}
server async function confirm(value: string): Promise<string> { return value }
shared function normalize(value: string): string { return value.trim() }
}
view { <button>{title}</button> }
}`);
test("emits separate browser and server function targets", () => {
const targets = generateTargets(ast);
expect(targets.browser).toContain('"confirm": async function');
expect(targets.browser).toContain('"normalize": function');
expect(targets.server).toContain("async function confirm");
expect(targets.rpc).toEqual([
expect.objectContaining({ component: "ConfirmDialog", function: "confirm" }),
]);
expect(targets.declarations).toContain("interface ConfirmDialogOutputs");
expect(targets.declarations).toContain("interface ConfirmDialogServerCalls");
});
test("component contracts retain declared union options", () => {
const targets = generateTargets(
parse(`component SizeBox {
props { size: "small" | "medium" | "large" = "medium" }
view { <div></div> }
}`),
);
expect(targets.contract.props[0]?.options).toEqual(["small", "medium", "large"]);
});
test("generates a standalone browser module for imported stores", () => {
const targets = generateTargets(
parse(`page store SearchStore {
state { query: string = "" }
client state { focused: boolean = false }
server state { secret: string = "hidden" }
computed { empty: boolean = query.length === 0 }
functions { client function setQuery(value: string): void { query = value } }
persist { storage = "session" include = ["query"] version = 1 }
}`),
);
expect(targets.browser).toContain("__wrnexusStoreRegistry");
expect(targets.browser).toContain('name: "SearchStore"');
expect(targets.browser).toContain('"query"');
expect(targets.browser).not.toContain('"secret": ("hidden")');
expect(targets.declarations).toContain("interface SearchStoreInstance");
});
test("browser codegen avoids reserved prop bindings and parameter collisions", () => {
const targets = generateTargets(
parse(`component ReservedBindings {
props { class: string = "" output: string = "" }
state { value: string = "" }
outputs { change(payload: { value: string }) }
functions {
client function update(output: string): void {
value = output
}
client function notify(): void {
output.change({ value: value })
}
}
view { <button>{class}</button> }
}`),
);
expect(targets.browser).not.toContain("const { class }");
expect(targets.browser).not.toContain("const output = context.output;\n const output");
expect(() => new Function(targets.browser.replace(/^export\s+/gm, ""))).not.toThrow();
});
test("RPC manifests expose only server functions referenced through server.name", () => {
const targets = generateTargets(
parse(`component SecureActions {
functions {
client async function saveClient(): Promise<void> { await server.save("ok") }
server async function save(value: string): Promise<string> { return value }
server function internalSecret(): string { return "secret" }
}
view { <button @click='saveClient()'>Save</button> }
}`),
);
expect(targets.rpc.map((entry) => entry.function)).toEqual(["save"]);
expect(targets.server).toContain("internalSecret");
});
test("generated browser stores bind typed RPC, persistence validation, and HMR", () => {
const targets = generateTargets(
parse(`global store UserStore {
state { user: string | null = null count: number = 0 }
functions {
client async function refresh(): Promise<void> { user = await server.loadCurrentUser() }
server async function loadCurrentUser(): Promise<string | null> { return "Ajay" }
server function internalOnly(): string { return "secret" }
}
persist {
storage = "local"
include = ["count"]
version = 2
migrations { function migrate(value, fromVersion, toVersion) { return value } }
validate { function validate(value) { return value && typeof value === "object" ? value : null } }
}
}`),
);
expect(targets.browser).toContain("const server = context.server");
expect(targets.browser).toContain("currentDefinition.persist.migrate");
expect(targets.browser).toContain("currentDefinition.persist.validate");
expect(targets.browser).toContain("__wrnexusApplyStoreHotUpdate");
expect(targets.rpc.map((entry) => entry.function)).toEqual(["loadCurrentUser"]);
});
test("browser codegen binds peer client functions through the scoped function table", () => {
const targets = generateTargets(
parse(`component PeerCalls {
state { value: number = 0 }
functions {
client function increment(): void { value += 1 }
client function run(): void { increment() }
}
view { <button @click='run()'>{value}</button> }
}`),
);
expect(targets.browser).toContain('implementations["increment"]');
const executable = new Function(
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
)() as { bindClientScope: (context: Record<string, unknown>) => Record<string, () => void> };
const state = { value: 0 };
const functions = executable.bindClientScope({
state,
props: {},
output: {},
server: {},
refs: {},
});
functions.run?.();
expect(state.value).toBe(1);
});
test("deferred timer state writes commit after the client function returns", async () => {
const targets = generateTargets(
parse(`component DeferredWrite {
state { value: number = 0 }
functions {
client function later(): void { setTimeout(() => { value = 2 }, 0) }
}
view { <button @click='later()'>{value}</button> }
}`),
);
const executable = new Function(
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
)() as { bindClientScope: (context: Record<string, unknown>) => Record<string, () => void> };
let rendered = "0";
const state = new Proxy(
{ value: 0 },
{
set(target, property, value) {
Reflect.set(target, property, value);
rendered = String(value);
return true;
},
},
);
const functions = executable.bindClientScope({
state,
props: {},
output: {},
server: {},
refs: {},
});
functions.later?.();
expect(state.value).toBe(0);
await new Promise((resolve) => setTimeout(resolve, 10));
expect(state.value).toBe(2);
expect(rendered).toBe("2");
});
test("the deferred commit helper does not shadow an authored commit function", () => {
const targets = generateTargets(
parse(`component AuthoredCommit {
state { value: number = 0 }
functions {
client function commit(): void { value = 1 }
client function save(): void { commit() }
}
view { <button @click='save()'>{value}</button> }
}`),
);
expect(() => new Function(targets.browser.replace(/^export\s+/gm, ""))).not.toThrow();
expect(targets.browser).not.toContain("const commit = __wrnexusCommit");
});
test("generated client modules stay below decoded-size and duplication ratchets", () => {
const states = Array.from({ length: 12 }, (_, index) => `s${index}: number = ${index}`).join(" ");
const functions = Array.from(
{ length: 20 },
(_, index) =>
`client function f${index}(): void { s${index % 12} += 1 ${Array.from(
{ length: 20 },
(_unused, peer) => (peer === index ? "" : `if (false) f${peer}()`),
).join(" ")} }`,
).join("\n");
const browser = generateTargets(
parse(`component ClientSizeStress {
state { ${states} }
functions { ${functions} }
view { <button></button> }
}`),
).browser;
const decoded = new TextEncoder().encode(browser).length;
const compressed = gzipSync(new TextEncoder().encode(browser)).length;
expect(decoded).toBeLessThanOrEqual(13_000);
expect(decoded / compressed).toBeLessThanOrEqual(10);
});