Files
WRNexusJS/packages/compiler/test/apis-client-emit.test.ts
T
Clintchiz a08dfa5322 fix(compiler): reject dynamic api access in client functions
Usage-driven emission can only see api.<name> calls. api["name"]()
or passing api to a helper is invisible to it, silently drops the
block from the browser bundle, and fails at runtime instead of build
time. Detect that dynamic/indirect use (masking strings and comments
first, reusing the tokenizer's skipLiteralOrComment) and refuse to
compile instead, naming the offending function.
2026-08-20 07:25:02 +05:30

101 lines
3.0 KiB
TypeScript

import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generateTargets } from "../src/targets.ts";
const withCalls = (calls: string) => `page Probe {
apis {
used POST /api/used {
request { body { name?: string } }
response { return data.users }
}
unused GET /api/unused {
response { return data.secretShape }
}
}
functions {
client async function go(): Promise<void> {
${calls}
}
}
view { <main><button @click="go()">x</button></main> }
}
`;
test("a block the client calls is emitted into the browser module", () => {
const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser;
expect(browser).toContain("used");
expect(browser).toContain('"/api/used"');
});
test("a block the client never calls is NOT emitted into the browser module", () => {
// Server-only transforms must not ship. This is the point of usage-driven emission.
const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser;
expect(browser).not.toContain("secretShape");
expect(browser).not.toContain('"/api/unused"');
});
test("no api object at all when the client calls none", () => {
const browser = generateTargets(parse(withCalls(` console.log("nothing")`))).browser;
expect(browser).not.toContain("const api =");
});
test("the emitted browser module is valid JavaScript", () => {
const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser;
expect(() => {
new Function(browser.replace(/^\s*import[^\n]*$/gm, "").replace(/\bexport\s+/g, ""));
}).not.toThrow();
});
test("declared field types never reach the browser module", () => {
const browser = generateTargets(parse(withCalls(` await api.used({ name: "a" })`))).browser;
expect(browser).not.toContain("name?: string");
});
test("dynamic bracket access on api is a compile error naming the construct", () => {
expect(() => generateTargets(parse(withCalls(` await api["used"]({ name: "a" })`)))).toThrow(
/api/,
);
});
test("passing api to a helper is a compile error", () => {
expect(() => generateTargets(parse(withCalls(` callHelper(api)`)))).toThrow(/api/);
});
test("a plain api.name(...) call still compiles", () => {
expect(() =>
generateTargets(parse(withCalls(` await api.used({ name: "a" })`))),
).not.toThrow();
});
test("an identifier that merely contains 'api' does not trigger the dynamic-access error", () => {
expect(() =>
generateTargets(
parse(
withCalls(
` const rapidCheck = 1; this.apiary = rapidCheck; await api.used({ name: "a" })`,
),
),
),
).not.toThrow();
});
test("the word 'api' inside a comment or string literal does not trigger the dynamic-access error", () => {
expect(() =>
generateTargets(
parse(
withCalls(
` // this mentions api in a comment\n const note = "the api is great";\n await api.used({ name: "a" })`,
),
),
),
).not.toThrow();
});