import type { Plugin } from "vite"; // A lazily imported page, rail, dialog or player must stay out of the chunks loaded at boot. A page // is imported with import()/lazy() so it is fetched only when it is opened, and Rollup honours that // only while nothing on the boot path imports the same module statically: one static import — a // module's index.ts re-exporting it is the easy way — and the module joins a boot chunk, the import() // becomes a no-op, and nothing says so. That rule used to be a hope; this makes it a build failure, // naming each module and the boot chunk it landed in. 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. const SHELL = "src/shell/App.tsx"; 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) 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, shell: string): string[] { const byName = new Map(chunks.map((c) => [c.fileName, c])); const isShell = (id: string) => posix(id).endsWith(`/${shell}`); const boot = new Set(); const visit = (c: ChunkLike | undefined) => { if (!c || boot.has(c)) return; boot.add(c); c.imports.forEach((name) => visit(byName.get(name))); }; chunks.filter((c) => c.isEntry || c.moduleIds.some(isShell)).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(); } export function chunkGuard(): Plugin { return { name: "siftlode-chunk-guard", apply: "build", generateBundle(_options, bundle) { const lazy = new Set( [...this.getModuleIds()].filter( (id) => (this.getModuleInfo(id)?.dynamicImporters.length ?? 0) > 0, ), ); const chunks: ChunkLike[] = Object.values(bundle).flatMap((o) => o.type === "chunk" ? [ { fileName: o.fileName, isEntry: o.isEntry, imports: o.imports, moduleIds: o.moduleIds, }, ] : [], ); const leaks = bootLeaks(chunks, lazy, SHELL); if (leaks.length) this.error( `lazily imported, yet loaded at boot: ${leaks.join(", ")}. ` + "Something on the boot path imports it statically (an index.ts re-export?): import it lazily there too, or not at all.", ); }, }; }