#!/usr/bin/env node // Phase 2 Plan 02-03 — PIPE-02 structural verification. // // After `npm run build`, Vite emits dynamic imports as separate chunks // when the lazy import.meta.glob target is not also imported eagerly. // Phase 2 currently does both — the eager `fragments` export keeps // Phase-1 loader.test.ts green while the lazy `loadSeasonFragments` // surface is in place for Phase 4+. This script verifies that Season-1 // fragment content reaches dist/ via the build output regardless of // which import path is responsible — proving the structural plumbing // exists for Plan 02-04+ to switch consumers to the lazy path without // a build-system rework. // // Three structural checks (any one passing is sufficient): // 1. dist/assets/ contains a chunk filename mentioning 'fragments', // 'season1', or '01-soil' (Vite default chunk-naming for dynamic // imports preserves a path slug — production builds may hash it). // 2. Some chunk's contents reference the source path // `/content/seasons/01-soil/` (via the ?raw inline) or a known // Season-1 fragment id like `season1.soil.first-bloom` or // `season1.soil._exhaustion` (the exhaustion sentinel). // 3. The dist/index.html's preloader manifest references at least one // chunk we believe to be Season-1 content (not currently used; left // as future-extension hook). // // On failure, prints the dist/assets listing for the dev to inspect and // exits non-zero with guidance pointing at RESEARCH Pattern 8 / Plan 02-03 // SUMMARY.md. // // Refactor note (Plan 02-03 Task 3): the script body lives in `runCheck()` // so the Vitest test can import it without triggering process.exit at // module-eval. CLI invocation gates on import.meta.url === argv[1]. import { readdirSync, existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; /** * @typedef {{ * ok: boolean, * message: string, * chunkNameMatch: boolean, * chunkContentMatch: boolean, * files: string[], * }} CheckResult */ /** * Run the PIPE-02 structural check against the on-disk dist/. * * @returns {CheckResult} */ export function runCheck() { const distAssets = resolve(process.cwd(), 'dist/assets'); if (!existsSync(distAssets)) { return { ok: false, message: '[check-bundle-split] dist/assets/ not found — run `npm run build` first', chunkNameMatch: false, chunkContentMatch: false, files: [], }; } const files = readdirSync(distAssets); const jsFiles = files.filter((f) => f.endsWith('.js')); // Check 1 — chunk filename slug match. const chunkNameMatch = jsFiles.some( (f) => f.includes('fragments') || f.includes('season1') || f.includes('01-soil'), ); // Check 2 — chunk contents reference Season-1 source path or a known id. let chunkContentMatch = false; for (const f of jsFiles) { const contents = readFileSync(resolve(distAssets, f), 'utf8'); if ( contents.includes('/content/seasons/01-soil/') || contents.includes('season1.soil.first-bloom') || contents.includes('season1.soil._exhaustion') ) { chunkContentMatch = true; break; } } if (chunkNameMatch || chunkContentMatch) { return { ok: true, message: `[check-bundle-split] PIPE-02 OK — Season-1 content reachable via build output\n` + ` chunkNameMatch=${chunkNameMatch}, chunkContentMatch=${chunkContentMatch}\n` + ` files: ${jsFiles.join(', ')}`, chunkNameMatch, chunkContentMatch, files: jsFiles, }; } return { ok: false, message: `[check-bundle-split] FAIL — no chunk references /content/seasons/01-soil/\n` + ` dist/assets contained: ${files.join(', ')}\n` + ` Expected: a chunk filename or content containing "fragments" / "season1" / "01-soil"\n` + ` See RESEARCH.md Pattern 8 (Per-Season Lazy Loading) and the Plan 02-03 SUMMARY for context.`, chunkNameMatch, chunkContentMatch, files: jsFiles, }; } // CLI invocation guard. Comparing import.meta.url to a file:// URL of // process.argv[1] (the script path) tells us whether we're running as // `node scripts/check-bundle-split.mjs` (yes, run the check) vs being // imported by Vitest (no, just expose runCheck and stay quiet). const isCli = (() => { try { return import.meta.url === new URL(`file://${process.argv[1]}`).href; } catch { return false; } })(); if (isCli) { const result = runCheck(); if (result.ok) { console.log(result.message); process.exit(0); } else { console.error(result.message); process.exit(1); } }