#!/usr/bin/env node // ── §5.1's client half — what stayed a bare import in the built chunk ────── // // The server half's boundary check reads source. The client half's has to read // the BUILD OUTPUT, because the failure it exists to catch is invisible in // source: `import { useState } from 'react'` is correct in every file, and // whether it ends up as core's React or as a second copy welded into the chunk // is decided by vite.config.js's aliases. A missed alias changes nothing you can // see until a hook throws in the browser. // // So: build, then ask the artifact two questions. // // 1. **Is there a bare import left?** There must not be. Aliased shims are // bundled, so a surviving bare specifier means an alias missed and // `external` caught it — the loud failure the config prefers, but still a // failure, and better found here than by a browser refusing to load. // 2. **Did a shared dependency get bundled?** React's own source has // fingerprints that no module of ours would contain by accident. Finding // one means the chunk carries a second React, which is the silent version // of the same mistake and the one worth the fingerprint check. // // Run after `npm run build`, in CI, on the artifact that ships. import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const CHUNK = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'entry.js') if (!fs.existsSync(CHUNK)) { console.error(`No chunk at ${CHUNK} — run \`npm run build\` first.`) process.exit(1) } const chunk = fs.readFileSync(CHUNK, 'utf8') const problems = [] // Static and dynamic imports that survived into the output. A relative or // absolute specifier is a chunk that was split, which this build does not do — // `lib` mode with one entry emits one file — so anything here is a bare name. const IMPORTS = /(?:^|[\s;}])(?:import\s+[^'"]*?from\s*|import\s*|import\()\s*['"]([^'"]+)['"]/g const bare = new Set() for (const [, specifier] of chunk.matchAll(IMPORTS)) { if (!specifier.startsWith('.') && !specifier.startsWith('/')) bare.add(specifier) } if (bare.size) { problems.push( `the chunk still imports ${[...bare].map((s) => `"${s}"`).join(', ')} — ` + 'nothing can resolve a bare specifier in the browser without an import map, ' + 'and CSP forbids one. Alias it to a shim in vite.config.js (MODULE_API.md §3.6).', ) } // Fingerprints from the shared libraries' own source. Each is a string those // packages ship and this module has no other reason to contain. const BUNDLED = [ { what: 'react', probe: 'react.development.js' }, { what: 'react', probe: 'Invalid hook call' }, { what: 'react-dom', probe: 'react-dom.development.js' }, { what: 'react-router-dom', probe: 'useRoutes() may be used only in the context of a component' }, ] for (const { what, probe } of BUNDLED) { if (chunk.includes(probe)) { problems.push( `the chunk appears to BUNDLE ${what} (found ${JSON.stringify(probe)}). ` + 'There is exactly one React in the page and core owns it — a second copy ' + 'loads fine and then fails at the first hook (MODULE_API.md §3.2).', ) } } if (problems.length) { console.error('\nThe built chunk breaks the shared-dependency rule:\n') for (const p of problems) console.error(` - ${p}\n`) process.exit(1) } const kb = (fs.statSync(CHUNK).size / 1024).toFixed(1) console.log(`OK — dist/entry.js (${kb} kB) has no bare imports and bundles no shared dependency.`)