fix(frontend): the chunk guard's logic lives in the guard, so no file sits in two TypeScript projects
The guard's logic was a src/lib file the config project also listed. The app project, which references the config project, then refuses that source on a clean build (TS6305) — while every local run reused the referenced project's cached output and stayed green; only the Docker image build failed. The logic is back inside chunkGuard.ts, its test beside it at the frontend root (vitest now also collects `*.test.ts` there), and a gate contract fails when the config project's include reaches into src/ again. The guard was re-proved on a build with a leaking re-export.
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { bootLeaks, type ChunkLike } from "./chunkLeaks";
|
||||
|
||||
const APP = "/r/src/shell/App.tsx";
|
||||
const chunk = (fileName: string, moduleIds: string[], imports: string[] = [], isEntry = false) =>
|
||||
({ fileName, moduleIds, imports, isEntry }) satisfies ChunkLike;
|
||||
|
||||
describe("a lazily imported module", () => {
|
||||
const lazy = new Set([APP, "/r/src/modules/plex/PlexBrowse.tsx"]);
|
||||
|
||||
it("is a leak in the entry chunk, in a chunk the entry imports, or in the App chunk", () => {
|
||||
const P = "/r/src/modules/plex/PlexBrowse.tsx";
|
||||
for (const chunks of [
|
||||
[chunk("index.js", ["/r/src/main.tsx", P], [], true)],
|
||||
[chunk("index.js", ["/r/src/main.tsx"], ["shared.js"], true), chunk("shared.js", [P])],
|
||||
[chunk("index.js", ["/r/src/main.tsx"], [], true), chunk("App.js", [APP, P])],
|
||||
[
|
||||
chunk("index.js", ["/r/src/main.tsx"], [], true),
|
||||
chunk("App.js", [APP], ["s.js"]),
|
||||
chunk("s.js", [P]),
|
||||
],
|
||||
])
|
||||
expect(bootLeaks(chunks, lazy, "src/shell/App.tsx")).toEqual([
|
||||
`${P} (${chunks[chunks.length - 1]!.fileName})`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("is not a leak in its own chunk, or in one only another lazy chunk loads — and App itself is lazy by design", () => {
|
||||
const chunks = [
|
||||
chunk("index.js", ["/r/src/main.tsx"], ["react.js"], true),
|
||||
chunk("react.js", ["/r/node_modules/react/index.js"]),
|
||||
chunk("App.js", [APP], ["react.js"]),
|
||||
chunk("PlexBrowse.js", ["/r/src/modules/plex/PlexBrowse.tsx"], ["react.js"]),
|
||||
];
|
||||
expect(bootLeaks(chunks, lazy, "src/shell/App.tsx")).toEqual([]);
|
||||
});
|
||||
|
||||
it("is checked on Windows paths too", () => {
|
||||
const win = "D:\\r\\src\\modules\\plex\\PlexBrowse.tsx";
|
||||
const chunks = [chunk("index.js", ["D:\\r\\src\\main.tsx", win], [], true)];
|
||||
expect(bootLeaks(chunks, new Set([win]), "src/shell/App.tsx")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
// Which lazily imported modules a production build loads at boot anyway — the logic behind the
|
||||
// chunkGuard build plugin (frontend/chunkGuard.ts), kept free of Vite so a unit test can drive it.
|
||||
//
|
||||
// A page, a rail, a dialog or a player is imported with import()/lazy() so it is fetched only when it
|
||||
// is opened. Rollup honours that only while nothing on the boot path imports the same module
|
||||
// statically: one static import — a re-export from a module's index.ts is the easy way to add one —
|
||||
// and the module joins a boot chunk, the import() becomes a no-op, and nothing says so. The boot path
|
||||
// is the entry chunks and what they import, plus the shell (App, itself loaded lazily but fetched
|
||||
// at boot alongside the identity probe) and what it imports.
|
||||
|
||||
export interface ChunkLike {
|
||||
fileName: string;
|
||||
isEntry: boolean;
|
||||
/** The chunks this one imports statically. */
|
||||
imports: string[];
|
||||
moduleIds: string[];
|
||||
}
|
||||
|
||||
const posix = (id: string) => id.split("\\").join("/");
|
||||
|
||||
/** Every lazily imported module (`lazy`: module ids) that sits in a boot chunk, as "id (chunk)". The
|
||||
* shell's own module (`shell`, a path under the project, e.g. "src/shell/App.tsx") is exempt: it is
|
||||
* lazy so that the public pages never load it, and its chunk is the shell. */
|
||||
export function bootLeaks(chunks: ChunkLike[], lazy: Set<string>, shell: string): string[] {
|
||||
const byName = new Map(chunks.map((c) => [c.fileName, c]));
|
||||
const isShell = (id: string) => posix(id).endsWith(`/${shell}`);
|
||||
const roots = chunks.filter((c) => c.isEntry || c.moduleIds.some(isShell));
|
||||
const boot = new Set<ChunkLike>();
|
||||
const visit = (c: ChunkLike | undefined) => {
|
||||
if (!c || boot.has(c)) return;
|
||||
boot.add(c);
|
||||
c.imports.forEach((name) => visit(byName.get(name)));
|
||||
};
|
||||
roots.forEach(visit);
|
||||
const leaks: string[] = [];
|
||||
for (const c of boot)
|
||||
for (const id of c.moduleIds)
|
||||
if (lazy.has(id) && !isShell(id)) leaks.push(`${posix(id)} (${c.fileName})`);
|
||||
return leaks.sort();
|
||||
}
|
||||
@@ -472,7 +472,7 @@ describe("the gate register (docs/gates.md)", () => {
|
||||
"reactPlugin.ts",
|
||||
"devOnlyGuard.ts",
|
||||
"chunkGuard.ts",
|
||||
"src/lib/chunkLeaks.ts",
|
||||
"chunkGuard.test.ts",
|
||||
"playwright.config.ts",
|
||||
"postcss.config.js",
|
||||
],
|
||||
@@ -490,6 +490,14 @@ describe("the gate register (docs/gates.md)", () => {
|
||||
}
|
||||
expect(missingFrom(found)).toEqual([]);
|
||||
});
|
||||
it("puts no file in two projects", () => {
|
||||
// A src file the config project also lists builds into that project's output, and the app
|
||||
// project (which references it) then refuses the source (TS6305) — but only on a clean
|
||||
// build: a local run reuses the referenced project's cached output and stays green, so only
|
||||
// the Docker image build said so.
|
||||
const node = jsonc("frontend/tsconfig.node.json").include as string[];
|
||||
expect(node.filter((f) => f === "src" || f.startsWith("src/"))).toEqual([]);
|
||||
});
|
||||
it("type-checks every config file at the frontend root, or lists it", () => {
|
||||
const checked = new Set(INCLUDE["frontend/tsconfig.node.json"]);
|
||||
const roots = readdirSync(FRONTEND).filter((f) => /[.](ts|js|mjs|cjs)$/.test(f));
|
||||
|
||||
@@ -1287,7 +1287,7 @@
|
||||
"export default defineConfig({",
|
||||
" plugins: [reactWithCompiler()],",
|
||||
" test: {",
|
||||
" include: [\"src/**/*.test.{ts,tsx}\"],",
|
||||
" include: [\"src/**/*.test.{ts,tsx}\", \"*.test.ts\"],",
|
||||
" environment: \"node\",",
|
||||
" coverage: {",
|
||||
" provider: \"v8\",",
|
||||
|
||||
Reference in New Issue
Block a user