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.
101 lines
3.0 KiB
TypeScript
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();
|
|
});
|