Repository navigation
test(guard): widen the client-bundle guard to every "use client" entry point (#10692) - #10700
Conversation
…y point (#10692) The guard shipped with #10695 watched two hand-picked modules. It now walks the static import graph from all 753 "use client" files in src/ (plus the two originally pinned entries), so the invariant is verified across the repo instead of where someone remembered to look. Full sweep runs in ~750ms. Two exclusions make that practical: - `import type` is not an edge — TypeScript erases it before the bundler sees it. Counting type imports turns 3 real findings into 29; a guard that cries wolf gets switched off. - Dynamic `import()` is still not followed. It does not break a bundle edge (that was tried for #10692 and failed) but it does move the module into a chunk the browser fetches on demand, which is a legitimate boundary. The widened sweep immediately found what the narrow one could not: five value-form imports of `db/batches` / `db/files` across three files under dashboard/batch, each reaching db/core → the SQLite driver. All five bind only interfaces (BatchRecord, FileRecord) used in type position, so the compiler was eliding them and the build stayed green — the same latent shape as #10692 before #10647 removed the toolchain's tolerance. Marking them `import type` makes the elision explicit instead of incidental. Refs #10692
Found while validating on Windows: this guard is a silent no-op there
Proof it was broken: with the The fix —
|
|
The 7 red checks are inherited base-red, not this PR — proven, not assumed. Both this PR and # fail on the exact same 7 jobs despite having no file in common. Reproduced on the pure base tip The failures are in areas this diff does not touch: Antigravity cloudcode envelope + public models, GLM provider import (4 tests), free-model catalog qwen-web ids, i18n key drift (#6695), Tracked under #9985. |
Required follow-up: the merged guard is a no-op on Windows — 2 line-item changesThe widened guard merged via this PR walks the import graph correctly on POSIX, but on Windows All changes are in ITEM 1 — Before (line 79): return path.relative(REPO_ROOT, candidate);After (lines 79–82): // Normalize to forward slashes: `SERVER_ONLY` is written with `/`, but on Windows
// `path.relative` returns `\` separators — an unnormalized match would silently
// pass there, turning this guard into a no-op on Windows.
return path.relative(REPO_ROOT, candidate).replace(/\/g, "/");ITEM 2 — Before (line 158): acc.push(path.relative(REPO_ROOT, full));After (line 161 — line 158 plus the 3 comment lines added by ITEM 1): acc.push(path.relative(REPO_ROOT, full).replace(/\/g, "/"));Keep both: ITEM 1 fixes the leak detection; ITEM 2 keeps Verification required (both platforms):
Ready-to-merge commit on |
…y point (diegosouzapw#10692) (diegosouzapw#10700) The guard shipped with diegosouzapw#10695 watched two hand-picked modules. It now walks the static import graph from all 753 "use client" files in src/ (plus the two originally pinned entries), so the invariant is verified across the repo instead of where someone remembered to look. Full sweep runs in ~750ms. Two exclusions make that practical: - `import type` is not an edge — TypeScript erases it before the bundler sees it. Counting type imports turns 3 real findings into 29; a guard that cries wolf gets switched off. - Dynamic `import()` is still not followed. It does not break a bundle edge (that was tried for diegosouzapw#10692 and failed) but it does move the module into a chunk the browser fetches on demand, which is a legitimate boundary. The widened sweep immediately found what the narrow one could not: five value-form imports of `db/batches` / `db/files` across three files under dashboard/batch, each reaching db/core → the SQLite driver. All five bind only interfaces (BatchRecord, FileRecord) used in type position, so the compiler was eliding them and the build stayed green — the same latent shape as diegosouzapw#10692 before diegosouzapw#10647 removed the toolchain's tolerance. Marking them `import type` makes the elision explicit instead of incidental. Refs diegosouzapw#10692 Co-authored-by: Xiangzhe <bakryun0718@proton.me>
…y point (diegosouzapw#10692) (diegosouzapw#10700) The guard shipped with diegosouzapw#10695 watched two hand-picked modules. It now walks the static import graph from all 753 "use client" files in src/ (plus the two originally pinned entries), so the invariant is verified across the repo instead of where someone remembered to look. Full sweep runs in ~750ms. Two exclusions make that practical: - `import type` is not an edge — TypeScript erases it before the bundler sees it. Counting type imports turns 3 real findings into 29; a guard that cries wolf gets switched off. - Dynamic `import()` is still not followed. It does not break a bundle edge (that was tried for diegosouzapw#10692 and failed) but it does move the module into a chunk the browser fetches on demand, which is a legitimate boundary. The widened sweep immediately found what the narrow one could not: five value-form imports of `db/batches` / `db/files` across three files under dashboard/batch, each reaching db/core → the SQLite driver. All five bind only interfaces (BatchRecord, FileRecord) used in type position, so the compiler was eliding them and the build stayed green — the same latent shape as diegosouzapw#10692 before diegosouzapw#10647 removed the toolchain's tolerance. Marking them `import type` makes the elision explicit instead of incidental. Refs diegosouzapw#10692 Co-authored-by: Xiangzhe <bakryun0718@proton.me>
…y point (diegosouzapw#10692) (diegosouzapw#10700) The guard shipped with diegosouzapw#10695 watched two hand-picked modules. It now walks the static import graph from all 753 "use client" files in src/ (plus the two originally pinned entries), so the invariant is verified across the repo instead of where someone remembered to look. Full sweep runs in ~750ms. Two exclusions make that practical: - `import type` is not an edge — TypeScript erases it before the bundler sees it. Counting type imports turns 3 real findings into 29; a guard that cries wolf gets switched off. - Dynamic `import()` is still not followed. It does not break a bundle edge (that was tried for diegosouzapw#10692 and failed) but it does move the module into a chunk the browser fetches on demand, which is a legitimate boundary. The widened sweep immediately found what the narrow one could not: five value-form imports of `db/batches` / `db/files` across three files under dashboard/batch, each reaching db/core → the SQLite driver. All five bind only interfaces (BatchRecord, FileRecord) used in type position, so the compiler was eliding them and the build stayed green — the same latent shape as diegosouzapw#10692 before diegosouzapw#10647 removed the toolchain's tolerance. Marking them `import type` makes the elision explicit instead of incidental. Refs diegosouzapw#10692 Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Refs #10692. Follow-up to #10695, which fixed the leak; this makes the invariant checkable
everywhere instead of at two hand-picked entry points.
What changed in the guard
It now walks the static import graph from every
"use client"file insrc/(753 of them)plus the two modules #10695 pinned by hand. Full sweep: ~750ms.
Two exclusions, both load-bearing:
import typeis not an edge. TypeScript erases it before the bundler sees it. Countingtype imports turns 3 real findings into 29 — and a guard that cries wolf gets switched off.
import()is still not followed. It does not actually break a bundle edge (triedfor base-red: production build fails — 28 Node-builtin resolution errors from server-only code reaching the client bundle (dashboard/providers page → db/core) #10692, did not work), but it does move the module into a chunk the browser fetches on
demand, which is a legitimate boundary.
What the widened sweep found
Five value-form imports across three files under
dashboard/batch, each reachingdb/core→ the SQLite driver:batch/page.tsx{ FileRecord },{ BatchRecord }batch/files/page.tsx{ FileRecord },{ BatchRecord }batch/batch-utils.ts{ BatchRecord },{ FileRecord }All five bind only interfaces (
BatchRecord,FileRecord) used in type position, so thecompiler was eliding them and the build stayed green. That is precisely the shape #10692 had
before #10647 removed the toolchain's tolerance: one value-carrying binding added to any of
those
import {}clauses and the SQLite driver lands in the browser bundle again.Marking them
import typemakes the elision explicit rather than incidental — no runtimechange.
Worth noting for anyone reading the earlier investigation: my report said 2 findings in 2
files. That undercounted — the graph walk reports only the first chain per entry point, so
fixing one import exposed the next. The real number was 5 across 3 files, each found only after
the previous was fixed.
Validation
The widened guard was run before the fixes and failed naming the exact chains; it passes
after.
typecheck:coreclean,eslintclean, and the sibling#10674import guard still green.Not done here
verbatimModuleSyntax/ theconsistent-type-importsESLint rule would make this classimpossible by construction rather than by vigilance. That is a repo-wide change and deserves its
own evaluation — flagged, not attempted.