release: WRNexusJS 0.8.3
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parse, resolveWrnImports } from "../src/index.ts";
|
||||
|
||||
test("configured import aliases resolve application client modules", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-import-alias-"));
|
||||
const app = join(root, "app");
|
||||
const page = join(app, "settings.wrn");
|
||||
const helper = join(app, "client", "storage.ts");
|
||||
const clientIndex = join(app, "client", "index.ts");
|
||||
mkdirSync(join(app, "client"), { recursive: true });
|
||||
writeFileSync(helper, "export function persist(value: unknown) { return value; }\n");
|
||||
writeFileSync(clientIndex, "export const storageReady = true;\n");
|
||||
|
||||
try {
|
||||
const ast = parse(`import { persist } from "~/client/storage.ts"
|
||||
page Settings { view { <button>Save</button> } }`);
|
||||
const [resolved] = resolveWrnImports(ast.structuredImports, page, {
|
||||
appRoot: root,
|
||||
mode: "explicit",
|
||||
aliases: { "~": "./app" },
|
||||
});
|
||||
expect(resolved?.diagnostic).toBeUndefined();
|
||||
expect(resolved?.resolved).toBe(helper);
|
||||
|
||||
const directoryAst = parse(`import { storageReady } from "~client"
|
||||
page ClientIndex { view { <p>Ready</p> } }`);
|
||||
const [directoryResolved] = resolveWrnImports(directoryAst.structuredImports, page, {
|
||||
appRoot: root,
|
||||
mode: "explicit",
|
||||
aliases: { "~client": "./app/client" },
|
||||
});
|
||||
expect(directoryResolved?.diagnostic).toBeUndefined();
|
||||
expect(directoryResolved?.resolved).toBe(clientIndex);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { generate, generateTargets, parse } from "../src/index.ts";
|
||||
|
||||
test("page computed values derived from request state are declared during SSR", () => {
|
||||
const code = generate(
|
||||
parse(`import UserCard from "./UserCard.wrn"
|
||||
page Chat {
|
||||
state {
|
||||
currentUserId = ctx.url.searchParams.get("as") ?? "asha"
|
||||
users = [{ id: "asha", name: "Asha" }, { id: "rohan", name: "Rohan" }]
|
||||
}
|
||||
computed {
|
||||
currentUser = users.find((user) => user.id === currentUserId) ?? users[0]
|
||||
}
|
||||
view {
|
||||
<h1>{currentUser.name}</h1>
|
||||
{#each users as user}
|
||||
<UserCard user='{user}' />
|
||||
<div data-user='{user.id}'></div>
|
||||
{/each}
|
||||
}
|
||||
}`),
|
||||
);
|
||||
|
||||
expect(code).toContain("const currentUser =");
|
||||
expect(code).toContain("__wrnexusEscapeHtml(currentUser.name)");
|
||||
expect(code).toContain("__wrnexusPropAttr(user)");
|
||||
expect(code).toContain("__wrnexusEscapeHtml(user.id)");
|
||||
expect(code).not.toContain("__wrnexusPropAttr(user.id)");
|
||||
expect(code).toContain('encoded = value === undefined ? "undefined" : JSON.stringify(value)');
|
||||
expect(code).not.toContain("String((__state as any)[key])");
|
||||
});
|
||||
|
||||
test("computed values that reference ctx directly stay dynamic during SSR", () => {
|
||||
const code = generate(
|
||||
parse(`page DirectRequestComputed {
|
||||
computed { identity = ctx.url.searchParams.get("as") ?? "asha" }
|
||||
view { <p>{identity}</p> }
|
||||
}`),
|
||||
);
|
||||
expect(code).toContain("const identity =");
|
||||
expect(code).toContain("__wrnexusEscapeHtml(identity)");
|
||||
});
|
||||
|
||||
test("computed values derived from server loads stay dynamic during SSR", () => {
|
||||
const code = generate(
|
||||
parse(`page LoadedComputed {
|
||||
load server overview { return { count: 4 } }
|
||||
computed { total = overview.count + 1 }
|
||||
view { <p>{total}</p> }
|
||||
}`),
|
||||
);
|
||||
expect(code).toContain('const overview = ctx["overview"]');
|
||||
expect(code).toContain("__wrnexusEscapeHtml(total)");
|
||||
});
|
||||
|
||||
test("request-derived state dependencies stay dynamic during SSR", async () => {
|
||||
const code = generate(
|
||||
parse(`page Identity {
|
||||
state {
|
||||
userId = ctx.url.searchParams.get("as") ?? "asha"
|
||||
label = "User: " + userId
|
||||
}
|
||||
view { <p>{label}</p> }
|
||||
}`),
|
||||
);
|
||||
expect(code).toContain("__wrnexusEscapeHtml(label)");
|
||||
expect(code).not.toContain('<span data-text="label"></span>');
|
||||
const javascript = new Bun.Transpiler({ loader: "ts" }).transformSync(code);
|
||||
const moduleUrl = `data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}`;
|
||||
const rendered = await (
|
||||
await import(moduleUrl)
|
||||
).default({
|
||||
url: new URL("http://localhost/?as=rohan"),
|
||||
});
|
||||
expect(rendered).toContain("User: rohan");
|
||||
});
|
||||
|
||||
test("Async aliases and invalidation tags are scoped in generated templates", () => {
|
||||
const code = generate(
|
||||
parse(`page Uploads {
|
||||
load client uploads { return { name: "report.pdf" } }
|
||||
view {
|
||||
<Async source="uploads" tags="uploads,files">
|
||||
<Loading>Loading</Loading>
|
||||
<Success data="file"><a href="/files/{file.name}">{file.name}</a></Success>
|
||||
<Error error="problem"><p>{problem.message}</p></Error>
|
||||
</Async>
|
||||
}
|
||||
}`),
|
||||
);
|
||||
|
||||
expect(code).toContain('data-wrn-async-tags="uploads,files"');
|
||||
expect(code).toContain('data-wrn-async-alias="file"');
|
||||
expect(code).toContain('data-wrn-async-alias="problem"');
|
||||
expect(code).toContain("const file =");
|
||||
expect(code).toContain("const problem =");
|
||||
});
|
||||
|
||||
test("browser codegen prunes SSR-only UI imports and keeps client dependencies", () => {
|
||||
const browser = generateTargets(
|
||||
parse(`import { Button } from "@wrnexus/ui"
|
||||
import { save } from "./helper.ts"
|
||||
component Settings {
|
||||
state { enabled = true }
|
||||
functions {
|
||||
client function persist(): void { save(enabled) }
|
||||
}
|
||||
view {
|
||||
<Button label="Save" />
|
||||
<button @click='persist()'>Save</button>
|
||||
}
|
||||
}`),
|
||||
).browser;
|
||||
|
||||
expect(browser).not.toContain("@wrnexus/ui");
|
||||
expect(browser).toContain('import { save } from "./helper.ts"');
|
||||
expect(browser).toContain('"persist": function');
|
||||
});
|
||||
|
||||
test("reactive view imports trigger a browser module without hydrating static SSR helpers", () => {
|
||||
const reactiveAst = parse(`import { formatName } from "./format.ts"
|
||||
page Profile {
|
||||
state { name = "Asha" }
|
||||
view { <p>{formatName(name)}</p> }
|
||||
}`);
|
||||
const staticAst = parse(`import { readFileSync } from "node:fs"
|
||||
page StaticProfile {
|
||||
runtime = "server"
|
||||
view { <p>{readFileSync("profile.txt", "utf8")}</p> }
|
||||
}`);
|
||||
|
||||
expect(generate(reactiveAst)).toContain('data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"');
|
||||
expect(generateTargets(reactiveAst).browser).toContain(
|
||||
'import { formatName } from "./format.ts"',
|
||||
);
|
||||
expect(generate(staticAst)).not.toContain("data-wrn-client-module");
|
||||
});
|
||||
|
||||
test("state dependency cycles fail code generation", () => {
|
||||
expect(() =>
|
||||
generate(
|
||||
parse(`page CyclicState {
|
||||
state { first = second + 1 second = first + 1 }
|
||||
view { <p>{first}</p> }
|
||||
}`),
|
||||
),
|
||||
).toThrow("WRN-STATE-CYCLE");
|
||||
});
|
||||
|
||||
test("computed dependency cycles fail code generation", () => {
|
||||
expect(() =>
|
||||
generate(
|
||||
parse(`page Cyclic {
|
||||
computed { first = second + 1 second = first + 1 }
|
||||
view { <p>{first}</p> }
|
||||
}`),
|
||||
),
|
||||
).toThrow("WRN-COMPUTED-CYCLE");
|
||||
});
|
||||
Reference in New Issue
Block a user