// Fails the build if any chunk reachable from the MV3 service worker still // contains a runtime `import()`. // // Chrome throws `TypeError: import() is disallowed on ServiceWorkerGlobalScope` // for dynamic import inside a service worker, and Vite's module-preload helper // reports that failure via `window.dispatchEvent` — which also does not exist // in a worker. The original error is therefore replaced by "window is not // defined" and surfaces nowhere useful. The whole sync and pairing surface was // dead this way, silently, because Rollup happened to split those modules into // their own chunks while inlining everything else. // // Whether a given `await import()` survives to runtime depends on Rollup's // chunking, so source review cannot catch this — only the built output can. import { readFileSync, existsSync, readdirSync } from "fs"; import { join, dirname } from "path"; // Chrome only. The Firefox build declares `background.scripts`, an event page // with a real `window` and `document`, where dynamic import() is legal. const DISTS = ["dist-chrome"]; const STATIC_IMPORT = /(?:^|[;\s})])(?:import|export)\s*(?:[^`"'()]*?from\s*)?[`"']([^`"']+)[`"']/g; const DYNAMIC_IMPORT = /import\(\s*[`"']([^`"']+)[`"']\s*\)/g; function matches(source, re) { return [...source.matchAll(re)].map((m) => m[1]); } function resolveFrom(fromFile, spec) { if (!spec.startsWith(".")) return null; return join(dirname(fromFile), spec); } /** The worker entry, via the loader crxjs emits. */ function serviceWorkerEntry(dist) { const loader = join(dist, "service-worker-loader.js"); if (existsSync(loader)) { const [spec] = matches(readFileSync(loader, "utf8"), STATIC_IMPORT); if (spec) return resolveFrom(loader, spec); } const manifest = JSON.parse(readFileSync(join(dist, "manifest.json"), "utf8")); const direct = manifest.background?.service_worker; return direct ? join(dist, direct) : null; } function walk(entry) { const seen = new Set(); const violations = []; const stack = [entry]; while (stack.length) { const file = stack.pop(); if (seen.has(file) || !existsSync(file)) continue; seen.add(file); const source = readFileSync(file, "utf8"); for (const spec of matches(source, STATIC_IMPORT)) { const next = resolveFrom(file, spec); if (next) stack.push(next); } for (const spec of matches(source, DYNAMIC_IMPORT)) { violations.push({ file, spec }); } } return { chunks: seen.size, violations }; } let failed = false; for (const dist of DISTS) { if (!existsSync(dist)) { console.error(`FAIL: ${dist} not built — nothing to check`); failed = true; continue; } const entry = serviceWorkerEntry(dist); if (!entry || !existsSync(entry)) { console.error(`FAIL ${dist}: could not locate the service worker entry`); failed = true; continue; } const { chunks, violations } = walk(entry); if (violations.length) { failed = true; console.error(`FAIL ${dist}: runtime import() in the service worker graph`); for (const v of violations) console.error(` ${v.file} -> import("${v.spec}")`); console.error( " Fix: make these static top-level imports. Dynamic import() cannot run in a service worker.", ); } else { console.log(`ok ${dist}: ${chunks} worker chunks, no runtime import()`); } } process.exit(failed ? 1 : 0);