import { describe, expect, it } from "vitest"; import { existsSync, readFileSync, statSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import ts from "typescript"; // A shared /watch link has its own entry (watch.html → watchMain.tsx) so the friend it was sent to // loads the player page and nothing of the app. That holds only while nothing the page imports // reaches back into the app — and the first build of the entry showed how easily it does: one // file-size formatter imported the app's i18n, and with it every locale of every area (≈ 120 KB gzip, // half the page). This walks the entry's imports as the bundler does — the static ones and the lazy // `import("…")` chunks it would fetch later — and names the path to any of what the app alone may load. const SRC = resolve(__dirname); // Stands for an `import()` whose argument is not a string literal. const DYNAMIC = ""; const FORBIDDEN: Record = { "src/i18n/index.ts": "the app's i18n (every locale) — the page has its own strings (watchStrings)", i18next: "the app's i18n runtime", "react-i18next": "the app's i18n runtime", "@tanstack/react-query": "the app's data layer", "src/App.tsx": "the app shell", "src/main.tsx": "the app's entry", "src/lib/notifications.ts": "the app's notification center", "src/lib/logHooks.ts": "the app's client-log wiring", "flag-icons": "the app's flag sprites", [DYNAMIC]: "an import() of a computed path — no walk can say what it loads", }; function resolveSpec(from: string, spec: string): string | null { if (spec === DYNAMIC) return DYNAMIC; if (!spec.startsWith(".")) { const parts = spec.split("/"); return parts.slice(0, spec.startsWith("@") ? 2 : 1).join("/"); } const base = resolve(dirname(from), spec); for (const ext of ["", ".ts", ".tsx", "/index.ts", "/index.tsx"]) { const p = base + ext; if (existsSync(p) && statSync(p).isFile()) return p; } return null; } /** Specifiers a file loads at run time: value imports and re-exports (never `import type`), and * every `import()` anywhere in it — a lazy chunk is loaded by the page as surely as a static one. */ function runtimeImports(file: string): string[] { const sf = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true); const specs: string[] = []; for (const st of sf.statements) { if ( ts.isImportDeclaration(st) && st.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword ) { specs.push((st.moduleSpecifier as ts.StringLiteral).text); } else if (ts.isExportDeclaration(st) && st.moduleSpecifier && !st.isTypeOnly) { specs.push((st.moduleSpecifier as ts.StringLiteral).text); } } const visit = (node: ts.Node) => { if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) { const arg = node.arguments[0]; specs.push(arg && ts.isStringLiteralLike(arg) ? arg.text : DYNAMIC); } ts.forEachChild(node, visit); }; visit(sf); return specs; } const key = (p: string) => p.startsWith("/") || /^[A-Za-z]:/.test(p) ? relative(join(SRC, ".."), p).split("\\").join("/") : p; /** Every module the entry reaches, each with the importer that first reached it. */ function reach(entry: string): Map { const seen = new Map([[key(entry), null]]); const queue = [entry]; for (let file = queue.shift(); file; file = queue.shift()) { if (!/\.tsx?$/.test(file)) continue; for (const spec of runtimeImports(file)) { const target = resolveSpec(file, spec); if (target === null || seen.has(key(target))) continue; seen.set(key(target), key(file)); if (!target.startsWith("@") && /\.tsx?$/.test(target)) queue.push(target); } } return seen; } function chain(seen: Map, mod: string): string { const path = [mod]; for (let up = seen.get(mod); up; up = seen.get(up)) path.unshift(up); return path.join(" → "); } describe("the /watch entry", () => { it("loads nothing the app alone may load", () => { const seen = reach(join(SRC, "watchMain.tsx")); expect(seen.has("src/components/WatchPage.tsx")).toBe(true); // the walk is reading the entry const leaks = Object.keys(FORBIDDEN) .filter((mod) => seen.has(mod)) .map((mod) => `${FORBIDDEN[mod]}: ${chain(seen, mod)}`); expect(leaks).toEqual([]); }); it("catches a leak one import deep (the walk is not vacuous)", () => { // main.tsx is the app: walking it must find the app's i18n, and a page it only loads lazily. const app = reach(join(SRC, "main.tsx")); expect(app.has("src/i18n/index.ts")).toBe(true); expect(app.has("src/components/legal/Terms.tsx")).toBe(true); }); it("serves the same head as the app's page, so the server's OG block lands the same way", () => { const page = (name: string) => readFileSync(join(SRC, "..", name), "utf8"); // Comments aside (the OG markers are kept: the server replaces between them), and the script. const norm = (html: string) => html .replace(//g, "") .replace(/\s+/g, " ") .replace(/src="\/src\/[^"]+"/, 'src="ENTRY"'); expect(norm(page("watch.html"))).toBe(norm(page("index.html"))); expect(page("watch.html")).toContain('src="/src/watchMain.tsx"'); }); });