fix(650): replace node:fs/path/url with import.meta.glob in the pageSize guard
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 30s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 33s
PR Gates / Docs update reminder (pull_request) Successful in 37s
Review verdict / Set review-verdict status (pull_request) Successful in 36s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m33s
PR Gates / Script tests (pytest) (pull_request) Successful in 1m7s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 21m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 24m42s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 27m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
review-verdict/h10 Review-verdict: MERGEABLE @ 17c25e7 (base: main)
PR Gates / decisions lifecycle (pull_request) Successful in 13s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 30s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 33s
PR Gates / Docs update reminder (pull_request) Successful in 37s
Review verdict / Set review-verdict status (pull_request) Successful in 36s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m33s
PR Gates / Script tests (pytest) (pull_request) Successful in 1m7s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 21m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 24m42s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 27m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
review-verdict/h10 Review-verdict: MERGEABLE @ 17c25e7 (base: main)
PR Gates / decisions lifecycle (pull_request) Successful in 13s
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
`/// <reference types="node" />` (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).
This commit is contained in:
@@ -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 `/// <reference types="node" />` 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<string, string>;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user