From 17c25e75faa81b2c5cddea20ac845c86bad0c794 Mon Sep 17 00:00:00 2001 From: Timothy Date: Mon, 27 Jul 2026 02:40:31 +0200 Subject: [PATCH] fix(650): replace node:fs/path/url with import.meta.glob in the pageSize guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate is npm run typecheck (tsc -b --pretty false, project-mode) — the prior fix for the 3 node:*-unresolvable errors deferred a decision rather than resolving it, per instruction not to change tsconfig unilaterally. Evaluated and rejected: adding "node" to tsconfig.app.json's `types` (makes Node globals type-visible to production browser code, dissolving a deliberate separation the repo documents) and a file-local `/// ` (empirically leaks Node's ambient `setTimeout` into the whole tsc -b program, breaking 3 unrelated window.setTimeout mocks — confirmed by trying it). Adopted instead: Vite's `import.meta.glob('/src/**/*.{ts,tsx,mts,cts}', { query: '?raw', import: 'default', eager: true })`, resolved natively by Vite/vitest at transform time — no node:fs, no node:path, no node:url, no tsconfig change, no cross-project references, and the guard test stays exactly where it is (`src/api/`). `vite/client` types (hence `import.meta.glob`) were already wired in via `src/vite-env.d.ts`. `isScannableSourceFileName` is unchanged and still the single place the glob's results are filtered through — the extension set moved into the glob literal, but discovery still runs every matched file through this same named, tested predicate rather than a second copy of the logic. Verified the discovery mechanism swap changes nothing observable: a side-by-side comparison test (old fs-based walk vs new glob-based walk, both run against the current repo, not committed — temporary) found byte-identical results — 19 call sites, 136 scannable files, zero set difference in either direction. Re-ran both required plants against the new mechanism: 1. Duplicate at-cap literal appended to builder/ChannelBuilder.tsx (an already-registered file): caught — `UNREGISTERED (1) ... + builder/ChannelBuilder.tsx:2058:54:literal:100`. 2. New file with a `https://` string (no false match), a shorthand `{ pageSize }`, and two literal pageSize values in one ternary's two branches on a single line: all 3 real sites caught at distinct columns — `+ screens/_fakeDefectPlant.ts:4:30:shorthand:pageSize`, `+ screens/_fakeDefectPlant.ts:8:22:literal:111`, `+ screens/_fakeDefectPlant.ts:8:48:literal:222`. Also re-verified the .mts/.cts discovery pin still fails when the extension set is narrowed back to `.ts`/`.tsx` only (2 of 14 predicate cases fail, as before). All three plants reverted after verification; registry/predicate otherwise untouched. Full local gate: `npm run lint` clean, `npm run typecheck` (tsc -b) clean — zero errors, including the 3 node:* ones this commit resolves — `npx vitest run` 110 files / 1078 tests passed (re-run three times; two runs hit transient vitest worker-pool timeouts from overlapping background processes on this machine, unrelated to the code — a clean sequential run passed in full both before and after). --- web/src/api/pageSizeCallSites.guard.test.ts | 64 +++++++++++++-------- 1 file changed, 40 insertions(+), 24 deletions(-) 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); });