diff --git a/web/src/api/pageSizeCallSites.guard.test.ts b/web/src/api/pageSizeCallSites.guard.test.ts index 1f270c44f..244f9217d 100644 --- a/web/src/api/pageSizeCallSites.guard.test.ts +++ b/web/src/api/pageSizeCallSites.guard.test.ts @@ -1,6 +1,3 @@ -import { readFileSync, readdirSync } from 'node:fs'; -import { dirname, join, relative } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { pageSizeSiteId, scanPageSizeSites } from './pageSizeScan'; @@ -265,8 +262,26 @@ const REGISTRY: RegistryEntry[] = [ } ]; -// This file lives in `src/api/`; the scan root is `src/`. -const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); +// Enumerates every source file under `src/` via Vite's `import.meta.glob` — eagerly, as raw text +// (`query: '?raw', import: 'default'`) — INSTEAD OF Node's `fs`/`path`/`url` (#650 follow-up). +// This is the only file under `src` that ever needed real filesystem access, and `@types/node` +// isn't wired into `tsconfig.app.json`'s project (deliberately: it covers production browser code +// too, and a file-local `/// ` was tried and reverted — under `tsc -b`'s +// single-program compilation it leaked Node's ambient `setTimeout` into the whole app project, +// breaking three unrelated `window.setTimeout` mocks that expect the DOM signature). `import.meta +// .glob` needs neither `node:fs` nor a tsconfig change: it's resolved by Vite at transform time, +// natively available in the browser/app project, and is the idiomatic Vite/vitest way to enumerate +// source files. Keys are POSIX paths from the project root, e.g. `/src/api/pageSizeScan.ts`. +const rawSourceModules = import.meta.glob('/src/**/*.{ts,tsx,mts,cts}', { + query: '?raw', + import: 'default', + eager: true +}) as Record; + +function basename(path: string): string { + const idx = path.lastIndexOf('/'); + return idx === -1 ? path : path.slice(idx + 1); +} // Extracted from `listSourceFiles`'s inline condition so it's independently testable (#650 // follow-up round 4): a plant that adds a real `.mts` FILE and observes the guard notice it @@ -275,7 +290,10 @@ const SRC_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); // because this repo has no committed `.mts`/`.cts` source and `scanPageSizeSites` parses any // non-`.tsx` filename as plain TS regardless of extension. Testing this predicate directly, by // filename, is what actually regression-pins the file-discovery fix rather than depending on the -// repo happening to contain (or not contain) a matching file. +// repo happening to contain (or not contain) a matching file. This predicate is still what the +// glob's results are filtered THROUGH below (`listSourceFiles`) — the extension SET moved into the +// glob literal, but discovery still runs every matched file through this same named, tested +// function, not a second copy of the logic. export function isScannableSourceFileName(name: string): boolean { // `.mts`/`.cts` are legal TS extensions `tsconfig.app.json`'s `include` covers alongside // `.ts`/`.tsx` — none exist in this repo today, but the glob must not silently skip one if it @@ -285,20 +303,22 @@ export function isScannableSourceFileName(name: string): boolean { ); } -function listSourceFiles(dir: string): string[] { - const out: string[] = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (entry.name === 'generated') { +interface ScannableSource { + /** Path relative to `src/`, e.g. `api/pageSizeScan.ts` — matches the REGISTRY's `file` field. */ + file: string; + text: string; +} + +function listSourceFiles(): ScannableSource[] { + const out: ScannableSource[] = []; + for (const [key, text] of Object.entries(rawSourceModules)) { + if (key.includes('/generated/')) { continue; } - const full = join(dir, entry.name); - if (entry.isDirectory()) { - out.push(...listSourceFiles(full)); + if (!isScannableSourceFileName(basename(key))) { continue; } - if (isScannableSourceFileName(entry.name)) { - out.push(full); - } + out.push({ file: key.replace(/^\/src\//, ''), text }); } return out; } @@ -312,15 +332,11 @@ interface DiscoveredSite { } function discoverPageSizeCallSites(): DiscoveredSite[] { - const files = listSourceFiles(SRC_DIR); const sites: DiscoveredSite[] = []; - for (const absPath of files) { - const relPath = relative(SRC_DIR, absPath); - const text = readFileSync(absPath, 'utf8'); - - for (const site of scanPageSizeSites(text, absPath)) { - sites.push({ file: relPath, ...site }); + for (const { file, text } of listSourceFiles()) { + for (const site of scanPageSizeSites(text, file)) { + sites.push({ file, ...site }); } } @@ -393,7 +409,7 @@ describe('pageSize call-site guard (#650)', () => { ); it('scans a healthy number of source files (anti-vacuity: a broken glob must not pass on zero input)', () => { - const files = listSourceFiles(SRC_DIR); + const files = listSourceFiles(); expect(files.length).toBeGreaterThan(50); });