Files
WRNexusJS/packages/compiler/test/browser-transpile.test.ts
ClintchizandClaude Opus 5 55fed2177a
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
fix(compiler): strip TypeScript from client function bodies
`wrnexus build` failed on any client function whose body used TypeScript:

    const requestBody: Record<string, unknown> = {}
    error: Expected ";" but found ":"

Codegen copies a client function's body into the browser module verbatim.
It removes the types from the function's *signature*, which is what made
this easy to miss -- the emitted module looked transpiled, and only bodies
carried types through. The artifact is written as .mjs and read back as
plain JavaScript, so the failure surfaced as a syntax error in generated
code rather than at the .wrn line responsible.

Browser modules are now transpiled before they are written, at all three
sites that emit one (the production build and both dev-server paths).

Reproduced end to end: a page with an annotated body failed the build with
the reported errors, and after the fix builds, ships valid minified JS, and
runs -- the handler sets its state correctly in a browser.

Note: the same body is also embedded as a string for the CSP-safe fallback
interpreter, which still receives it untranspiled. The compiled module
shadows the fallback, so this is only reachable in the window before that
module loads. Left alone here because stripping it lives in codegen, which
also runs under Node in the editor bundle where the Bun transpiler is
unavailable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:03:14 +05:30

94 lines
2.9 KiB
TypeScript

import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generateTargets } from "../src/targets.ts";
import { stripBrowserTypes } from "../src/browser-transpile.ts";
/** Build the browser module for a page whose client function body is TypeScript. */
function browserModuleFor(body: string): string {
const source = `page Repro {
functions {
client async function run(): Promise<void> {
${body}
}
}
view {
<main><button @click="run()">go</button></main>
}
}
`;
return generateTargets(parse(source)).browser;
}
/** The artifact is written as .mjs, so this is how the runtime reads it back. */
function parsesAsJavaScript(code: string): boolean {
try {
new Function(code.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, ""));
return true;
} catch {
return false;
}
}
test("a client function body keeps its TypeScript in the generated module", () => {
// Codegen strips the signature's types but copies the body verbatim, which is
// what made this easy to miss. Guarding the premise the fix rests on.
const generated = browserModuleFor(` const requestBody: Record<string, unknown> = {}`);
expect(generated).toContain("const requestBody: Record<string, unknown>");
expect(parsesAsJavaScript(generated)).toBe(false);
});
test("stripping types makes an annotated client function body valid JavaScript", () => {
const stripped = stripBrowserTypes(
browserModuleFor(` const requestBody: Record<string, unknown> = {}
requestBody.q = "x"`),
);
expect(parsesAsJavaScript(stripped)).toBe(true);
expect(stripped).not.toContain("Record<string, unknown>");
expect(stripped).toContain("requestBody.q");
});
test("casts, generics and local interfaces survive stripping", () => {
const stripped = stripBrowserTypes(
browserModuleFor(` interface Local { a: string }
const names: string[] = ["a"]
const typed = { a: "x" } as Local
const total = (1 as number) + names.length
console.log(typed.a, total)`),
);
expect(parsesAsJavaScript(stripped)).toBe(true);
expect(stripped).toContain("console.log");
expect(stripped).not.toContain("interface Local");
});
test("the module's exported bindings are preserved", () => {
// A transpile that dropped one of these would break hydration silently.
const stripped = stripBrowserTypes(
browserModuleFor(` const value: number = 1
console.log(value)`),
);
for (const binding of [
"__wrnexusClientFunctions",
"__wrnexusClientState",
"__wrnexusOutputs",
"__wrnexusImportedBindings",
"bindClientScope",
]) {
expect(stripped).toContain(binding);
}
});
test("a body with no TypeScript is left working", () => {
const stripped = stripBrowserTypes(
browserModuleFor(` const plain = { a: 1 }
console.log(plain.a)`),
);
expect(parsesAsJavaScript(stripped)).toBe(true);
expect(stripped).toContain("console.log");
});