// What can be checked about the client half without a browser. // // Not much, and being honest about that is the point: the client half's real // failures are timing and resolution, and neither has a shape a DOM-less test // runner can see. MODULE_API.md §7.7's four-step browser smoke is what actually // proves this half works, and it is re-run whenever this seam changes. // // What IS testable here is the configuration that decides resolution — and one // of these tests exists because the trap it guards cost the Phase 1 spike real // time: Vite's object-form `resolve.alias` does PREFIX matching, so a `react` // key silently also rewrites `react/jsx-runtime`. An anchored regexp in the // array form cannot. That is a property of the config, and a test can hold it. import test from 'node:test' import assert from 'node:assert' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const HERE = path.dirname(fileURLToPath(import.meta.url)) const CLIENT = path.resolve(HERE, '..') const { bareImports, problemsWith } = await import('../scripts/checkExternals.js') const configModule = await import('../vite.config.js') const config = configModule.default const { SHARED, SHARED_PACKAGES: guardedPackages } = configModule test('every alias is an anchored regexp, never a bare prefix string', () => { const aliases = config.resolve.alias assert.ok(Array.isArray(aliases), 'alias must use the ARRAY form — the object form prefix-matches') for (const { find } of aliases) { assert.ok(find instanceof RegExp, `alias "${find}" is a string; a string prefix-matches`) assert.ok(find.source.startsWith('^') && find.source.endsWith('$'), `alias ${find} is not anchored`) } }) test('react and react/jsx-runtime resolve to different shims', () => { // The exact collision the object form causes. Asserted on the outcome rather // than on the config's shape, so it keeps holding however the config is // rewritten. const resolve = (specifier) => config.resolve.alias.find(({ find }) => find.test(specifier))?.replacement assert.ok(resolve('react')) assert.ok(resolve('react/jsx-runtime')) assert.notStrictEqual(resolve('react'), resolve('react/jsx-runtime')) }) test('every shared dependency is aliased', () => { for (const specifier of ['react', 'react/jsx-runtime', 'react-dom', 'react-dom/client', 'react-router-dom']) { assert.ok( config.resolve.alias.some(({ find }) => find.test(specifier)), `${specifier} is not aliased — it would be bundled, giving the page a second copy`, ) } }) test('rollup external stays empty — it preempts the aliases rather than backing them up', () => { // Rollup asks `external` BEFORE Vite's alias resolver runs, so a specifier // listed in both is marked external and never aliased. The chunk then ships // bare `import 'react'`, which no browser can resolve without an import map // and CSP forbids one. §3.6 shows both; they do not compose. assert.deepStrictEqual(config.build.rollupOptions.external, []) }) test('the not-bundled guard covers every shared specifier and is not derived from them', () => { // The direction of this dependency is the finding. Deriving the forbidden // package list FROM the alias list means deleting an alias also deletes the // guard against what that alias prevented — which is precisely when the guard // is needed. So the guard states the contract, and this asserts the aliases // stay inside it. const packages = new Set(guardedPackages) for (const { specifier } of SHARED) { const pkg = specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0] assert.ok(packages.has(pkg), `${pkg} is aliased but not guarded against being bundled`) } }) test('every alias points at a shim file that exists', () => { for (const { find, replacement } of config.resolve.alias) { assert.ok(fs.existsSync(replacement), `alias ${find} points at a missing file: ${replacement}`) } }) test('the build emits one unhashed entry.js, which is what module.json names', () => { assert.deepStrictEqual(config.build.lib.formats, ['es']) assert.strictEqual(config.build.lib.fileName(), 'entry.js') const manifest = JSON.parse(fs.readFileSync(path.resolve(CLIENT, '..', 'module.json'), 'utf8')) assert.strictEqual(manifest.client.entry, 'client/dist/entry.js') assert.strictEqual(config.build.outDir, 'dist') }) test('modulePreload polyfilling stays off — an inline bootstrap is refused under CSP', () => { assert.strictEqual(config.build.modulePreload.polyfill, false) }) test('exactly one file reads window.__rg, and every shim goes through it', () => { // `shim/rg.js` is the single reader, and that is not tidiness: it is what // makes the "core did not publish its dependencies" message reachable. The // shims touch the global before anything else in the chunk does, so a check // placed in the first-imported file is a guarantee that lasts until someone // sorts the imports. const dir = path.join(CLIENT, 'src', 'shim') const shims = fs.readdirSync(dir) assert.ok(shims.length >= 5) for (const file of shims) { const source = fs.readFileSync(path.join(dir, file), 'utf8') const code = source.replace(/^\s*\/\/.*$/gm, '') // the comments discuss the global if (file === 'rg.js') { assert.match(code, /window\.__rg/, 'rg.js must be the one that reads the global') assert.doesNotMatch(code, /^\s*import\s/m, 'rg.js imports something') continue } assert.doesNotMatch(code, /window\.__rg/, `${file} reads the global directly instead of via rg()`) assert.match(code, /rg\(\)/, `${file} does not resolve through rg()`) // A shim may import its sibling helper and nothing else — anything further // would be a shim with a dependency to resolve, the problem it exists to remove. for (const [, spec] of code.matchAll(/^\s*import\s[^'"]*['"]([^'"]+)['"]/gm)) { assert.strictEqual(spec, './rg.js', `${file} imports ${spec}`) } } }) test('the built chunk has no bare imports and bundles no shared dependency', () => { // The artifact check itself, over the artifact that ships. Skipped rather than // failed when there is no build: `npm test` must be runnable before `npm run // build`, and CI runs them in order. const chunk = path.join(CLIENT, 'dist', 'entry.js') if (!fs.existsSync(chunk)) return assert.deepStrictEqual(problemsWith(fs.readFileSync(chunk, 'utf8')), []) }) test('an import inside a string is not an import — the check reads code, not text', () => { // The regression that made this necessary: slice 3's chunk was the first with // any content in it, and a button labelled "Approve and import" put the token // immediately before a quote. The check rejected the whole build, naming a // fragment of minified JSX as the offending specifier. const uiCopy = 'const a=n("button",{children:"Approve and import"}),b=1;' assert.deepStrictEqual(bareImports(uiCopy), []) // Neither is one in a comment, or in a template literal. assert.deepStrictEqual(bareImports('// import "react" would be wrong here\nconst a=1'), []) assert.deepStrictEqual(bareImports('/* import "react" */ const a=1'), []) assert.deepStrictEqual(bareImports('const s=`import "react"`'), []) // And a real one still is, in each form the build could emit. assert.deepStrictEqual(bareImports('import"react";'), ['react']) assert.deepStrictEqual(bareImports('import{useState}from"react";'), ['react']) assert.deepStrictEqual(bareImports('const m=await import("react-dom/client")'), ['react-dom/client']) // A relative specifier is a split chunk, not a shared dependency: not our concern. assert.deepStrictEqual(bareImports('import"./other.js";'), []) // The case that proves the mask tracks escapes: a quote escaped INSIDE a // string must not end it early and leave the tail looking like code. assert.deepStrictEqual(bareImports('const s="he said \\"import\\" loudly";'), []) })