build(web): fail vite build on a browser-externalized Node built-in (#1769)#1777
Conversation
…1769) Vite 8 (rolldown) externalizes a node:* / Node built-in that reaches the browser graph with a warning and ships a `module.exports = {}` stub — the build still succeeds, shipping a broken bundle. The runtime browser smoke (smoke:web:browser, #1615) only catches the subset where that stub is *called* at module init; a stub imported but never called ships silently and nothing gates it. Add a Vite build plugin that turns that specific browser-externalization warning into a hard `vite build` error, moving detection upstream into `npm run build` / `validate` and additionally catching the never-called case the runtime smoke can't see. - clients/web/server/browser-externalized-builtin-gate.ts: Vite-agnostic detection + error + per-build state machine (unit-tested to the ≥90 gate). - vite.config.ts: thin Vite wiring. A throw inside rolldown's onLog is swallowed (the one hook where it doesn't abort — verified against vite@8.0.0), so the gate records the warning in onLog and re-throws in buildEnd. Scoped to `vite build` (apply: 'build'); the Node runner build (tsup) is unaffected. - scripts/verify-build-gate.mjs + verify:build-gate (in npm run ci and the GitHub workflow): runs a real build with a node:fs probe and asserts it fails via the gate — the only check that catches the warning phrasing drifting in a future Vite bump and silently disabling the message-keyed gate. There is no stable log `code` on the rolldown warning (only message + plugin), so the gate keys off the documented message phrasing; the drift guard above is what keeps that safe across Vite versions. smoke:web:browser stays as the runtime backstop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
@claude review |
|
Claude finished @cliffhall's task in 3m 51s —— View job Code review — PR #1777 (build gate for browser-externalized Node built-ins)
SummaryGood shape overall. The split — Vite-agnostic detection in Findings below, most significant first. Nothing here is a blocker for the gate's primary purpose. 1. Gate state is per plugin instance, not per build — sticky failure across rebuilds
Today's scripts don't use buildStart() { gate.reset(); }, // plus a reset() on the gate interface2.
|
…1769) Review follow-ups on PR #1777: - Per-build state: add gate.reset() called from the plugin's buildStart, so a `vite build --watch` rebuild doesn't inherit a prior build's recorded warning (the plugin instance is reused across rebuilds). Makes the docblock true. - buildEnd(error) now only asserts on the success path — rolldown also calls buildEnd(error) when the build already failed for another reason, and throwing then would mask that real error with the #1769 message. - verify-build-gate.mjs: drop the shell `rm -rf` cache-clear entirely (verified the gate fires reliably without it — the earlier "caching" symptom was the onLog-swallow behavior, not a stale cache); add a result.error spawn-failure check so a missing `npx` isn't misdiagnosed as "not via the gate"; add SIGINT/SIGTERM handlers so an interrupt during the multi-minute build still restores the mutated entry; add a progress log before the spawn. - Match the double-quote style of the sibling server/ files. - Docs: list browser-externalized-builtin-gate.ts in the AGENTS.md server/ block and the clients/web/README server/ list (with why a build helper lives under server/), and add verify-build-gate to the root README scripts/ line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks — thorough review. Addressed all of 1–5 plus the nits in 1. Per-build state / sticky failure under watch — fixed. Added 2. 3. 4. Spawn-failure misdiagnosis — fixed. Added an explicit 5. Docs — fixed. Added 6. Nits:
Good catch on the
|
|
@claude review |
|
Claude finished @cliffhall's task in 3m 49s —— View job Code review — PR #1777, round 2 (after
|
- Scope the gate to the browser (`client`) environment via applyToEnvironment, so a future SSR/node environment built from this config isn't failed for a legitimate node:* import — browser-only intent is now structural. - Report ALL externalized built-ins in one build, deduped, instead of only the first offender (N leaks no longer means N fix-and-rebuild cycles). browserExternalizedBuiltinError now takes string[]; gate uses a Set. - verify-build-gate.mjs: pin Vite with `npx --no-install` so the drift check can never silently run a registry-fetched Vite instead of the repo-pinned one (the whole point is version fidelity); append the probe instead of prepending (ES imports hoist, and appending won't demote a future leading directive); clarify the signal-handler comment (handlers are a backstop — a blocked spawnSync tears down via the process group first). - AGENTS.md: update both `ci` pipeline enumerations to include verify:build-gate and drop the now-false "Storybook is the only CI step left out" claim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-2 addressed in 1. Stale 2. 3. Environment scoping — fixed. Added 4. Report all offenders — done. The gate now collects every matched warning (deduped, via a 5. Nits:
On the two "what I checked and found fine" notes: yes — the
|
|
@claude review |
|
Claude finished @cliffhall's task in 4m 39s —— View job Code review — PR #1777, round 3 (after
|
- Error message: name the transitive-dependency remedy (a resolve.alias to a browser shim) — "remove the import / gate behind the backend" doesn't apply when the node:* import lives in node_modules. - isBrowserExternalizedBuiltinLog is now a `message is string` type guard, so recordLog drops its redundant `!== undefined` clause while still typing the Set.add() — a real one-liner, not a coverage-only removal. - verify-build-gate.mjs: docblock now states WHY it builds the real config + entry (catches config-level regressions — plugin removed, onwarn suppression added — that a temp config would miss), so the minute-long build isn't later "optimized" into a temp entry and silently lose that coverage. Guard the entry read so a renamed main.tsx fails through fail() with an actionable message. On a botched restore, save the captured original to a sidecar .bak and point there instead of recommending `git checkout --`, which would discard uncommitted edits. - AGENTS.md: fix the muddled CI-steps sentence (GitHub CI runs the whole chain as separate steps, Storybook last) and note the applyToEnvironment client-only scoping in the gate prose. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-3 addressed in 1. Muddled 2. Why the real build, not a temp entry — documented. Agreed the fidelity is the point (a temp config keeps passing when someone deletes the plugin or adds an 3. Transitive-dependency escape hatch — error message extended. The remedy text now covers the case where the 4. Verify-script robustness — both fixed.
5. Nits:
Thanks for the three passes — the transitive-dep remedy and the
|
|
@claude review |
|
Claude finished @cliffhall's task in 3m 43s —— View job Code review — PR #1777, round 4 (after
|
output contains |
actual cause |
|---|---|
| the known phrase | warning fired as expected but the build passed → plugin not applied (deleted from plugins, applyToEnvironment env-name mismatch, onwarn suppression) |
node:fs but not the phrase |
phrasing drift → update BROWSER_EXTERNALIZED_BUILTIN_PHRASE |
| neither | the probe never reached the browser graph (entry moved, probe tree-shaken) |
Vite prints the externalization warning at default log level and nothing in these scripts passes --logLevel, so the phrase-presence check is reliable. This is the same class of fix as the round-2 result.error one: a failure that misdiagnoses itself sends someone at the wrong file.
2. No spawn timeout, and output is captured — a hung vite build gives zero diagnostics for 6 hours
scripts/verify-build-gate.mjs:98. Output is deliberately captured (needed for the assertions), which is a fair trade — but it means a build that hangs prints nothing, and the build job in .github/workflows/main.yml has no timeout-minutes, so it burns to GitHub's 360-minute default and then dies with an empty log for this step.
spawnSync takes timeout directly, and on timeout it sets result.error (ETIMEDOUT) — which the round-2 result.error branch at :123 already reports. So this is one option away from a bounded failure with a real message:
result = spawnSync("npx", ["--no-install", "vite", "build"], {
cwd: webDir, encoding: "utf8", timeout: 10 * 60_000, killSignal: "SIGKILL",
});3. The probe write is the one path not routed through fail()
scripts/verify-build-gate.mjs:89. Round 3 guarded the entry read (:57-64) so a renamed main.tsx fails actionably — but the write immediately after is unguarded. On a read-only checkout or an EACCES, it throws a raw stack out of the try/finally (the finally restores, then the exception escapes), which is exactly the raw-ENOENT-stack shape you just removed from the read path. Wrapping it to fail("could not write the probe into <entry> (…)") makes the script's diagnostics uniform.
4. Nits
- The gate's tests don't run in the fast loop.
browser-externalized-builtin-gate.test.tssits undersrc/test/integration/, so theintegrationGlobexcludes it from theunitproject (vite.config.ts:29,:260) —npm run testand thereforevalidatenever run it; it only executes undercoverage. That matches the seven siblingserver/*.test.tsfiles, so convention wins, but note those are there because they're Node-only I/O tests, whereas this module is pure and would run fine inunit. Fine as-is; just don't expectvalidateto catch a gate-logic regression. SIGTERMexits 130 (:83) — 130 is SIGINT's conventional code; SIGTERM's is 143. Cosmetic, and nothing reads it.browserExternalizedBuiltinError([])produces an error with an empty "Original Vite warning(s):" list. Unreachable through the gate (size > 0guards it at:87), but the function is exported and unguarded.
What I checked and found fine
- All five round-3 items verified in the code: the
AGENTS.mdCI-steps sentence now plainly says GitHub CI runs the same chain as separate steps with Storybook last; the docblock's "why the REAL config + entry … Do NOT optimize it into a temp entry" paragraph at:18-26is exactly the guard that was missing; the transitive-dependencyresolve.aliasremedy is in the error text (gate.ts:53-56); the guarded read +.bakrestore are in (:57-64,:110-118); the type-predicate refactor removes the redundant clause without weakening typing;AGENTS.md's gate bullet now documents theapplyToEnvironmentclient-only narrowing. distis still safe across the intentionally-failing build. The throw is inbuildEnd, which precedesrenderStart(where Vite'semptyOutDirruns), soclients/web/distfromvalidatesurvives intosmoke:web. Worth knowing the safety net cuts both ways:ensure-web-build.tswould silently rebuild a wipeddist, so CI green wouldn't prove this — the ordering argument is what does.- The
#1769marker is a sound assertion key: it's in the thrown message, Vite prints plugin errors, and nothing else in the build output emits it. - No
--logLevelanywhere in the build scripts, so nothing starvesonLog— still true after this round's changes. - Coverage:
clients/web/server/**is in theinclude(vite.config.ts:154) and not excluded; all four exports plusreset()are exercised, both sides of?? falseandsize > 0hit, and the new multi-offender/dedupe/reset tests cover the round-2 additions. Nov8 ignoreneeded. apply: 'build'+applyToEnvironmentare proven live by the CIverify:build-gatestep passing ona8a368a7/f4db356a— the env filter demonstrably didn't switch the gate off, which is the one thing that couldn't be established by reading.- Docs land in all four places, and
package.json'scichain matches the workflow step order.
· branchv2/fix-1769-build-gate-externalized-builtin
…tics (#1769) All in scripts/verify-build-gate.mjs's failure paths: - Split the "build SUCCEEDED" failure into three diagnoses keyed off the captured output, so it names the right file: warning present but build passed → plugin not applying (removed from `plugins`, `applyToEnvironment` env-name mismatch, or an `onwarn` suppression); node:fs present but phrase absent → phrasing drift, update the constant; neither → probe never reached the graph. The old message hard-coded "phrasing drifted", which misdirects for the config-regression class this script exists to catch. Added applyToEnvironment to the docblock's config-regression list too. - Bound the build with a 10-min spawnSync timeout (+ SIGKILL); a timeout sets result.error (ETIMEDOUT) and reports via the existing branch, instead of burning to the GitHub job's 360-min default with no output. - Guard the probe write so a read-only/EACCES checkout fails through fail() with an actionable message instead of escaping as a raw stack. - SIGTERM now exits 143 (was 130, which is SIGINT's code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-4 addressed in 1.
Added a 2. No spawn timeout — fixed. Added 3. Unguarded probe write — fixed. Moved the probe write out of the restore- Nits:
Thanks again for the four rounds — the three-way diagnosis and the timeout are genuine robustness wins.
|
|
@claude review |
|
Claude finished @cliffhall's task in 3m 0s —— View job Code review — PR #1777, round 5 (after
|
…#1769) All in scripts/verify-build-gate.mjs: - Drift guard: assert the gate source file still contains the mirrored KNOWN_PHRASE literal, failing fast if the two drift. Without it, a stale KNOWN_PHRASE after a phrasing update would make the three-way diagnosis misreport a plugin-not-applying regression as a phrasing drift — the exact misdirection the diagnosis exists to avoid. - Make the .bak safety net reachable on the path that needs it most: restoreEntry now catches its own write failure and routes to a shared saveBackupAndFail helper, instead of letting the write escape the finally as a raw stack (which skipped the .bak fallback and left the probe injected in the entry). - Add the fourth "plugin isn't applying" cause to the diagnosis: a future Vite emitting the warning before the client env's buildStart reset clears it (same symptom as the other three). - Flag once, on the failing-check path, that clients/web/dist holds a probe build so a local debugger doesn't serve it unaware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-5 addressed in 1. 2. Failed restore escaping the 3. Nits:
Really appreciate the five passes — the drift guard in particular turns a "keep these in sync by hand" comment into something enforced.
|
|
@claude review |
|
Claude finished @cliffhall's task in 2m 33s —— View job Code review — PR #1777, round 6 (after
|
…ts (#1769) All in scripts/verify-build-gate.mjs; one theme — the literals the script asserts on can each go stale independently of what they mirror: - Success key: assert on the gate's distinctive thrown-error prefix "Build failed (#1769)", not a bare "#1769". The issue number also appears in vite.config.ts comments, so a code frame from an unrelated build error there could otherwise make the check report OK with the gate dead — the one direction it must never get wrong (a false pass hiding a dead gate). - Drift guard now anchors on the BROWSER_EXTERNALIZED_BUILTIN_PHRASE assignment (regex) instead of a whole-file substring match, so the old wording lingering in a comment can't mask a changed constant; and its readFileSync of the gate module is guarded so a moved file fails through fail() actionably. - Extract PROBE_MODULE ("node:fs") used to build both the PROBE and the diagnosis check + messages, so changing the probe module can't silently misroute a phrasing-drift failure into "probe never reached the graph". - Comment the load-bearing diagnosis order (phrase before module-name). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-6 addressed in 1. 2. Drift guard checked the file, not the constant — fixed. Anchored it on the 3. Probe module duplicated — fixed. Extracted Nits:
Six passes in and the findings have converged on "make the script's literal contracts self-enforcing," which they now are. Thanks for the thoroughness.
|
|
Follow-up issue for the |
|
@claude review |
|
Claude finished @cliffhall's task in 2m 28s —— View job Code review — PR #1777, round 7 (after
|
#1769) - Extract ERROR_PREFIX ("Build failed (#1769)") and drift-guard it against the gate module too, and use it as the success key. Previously only KNOWN_PHRASE was guarded; the prefix is mirrored across the same TS/.mjs boundary for the same reason, so rewording the gate's thrown error would make a correctly-firing gate report "the build broke for another reason" (benign direction, but the same misdirection the other guards close). - Write the restore-failure .bak under the OS temp dir, not next to the entry: a sidecar inside src/ is untracked and a `git add -A` (likely while recovering) would sweep it into a commit. - Branch-(b) remediation now says update BOTH the gate constant AND KNOWN_PHRASE, so a phrasing-drift fix lands in one pass instead of tripping the drift guard on the next run. - Note next to BROWSER_EXTERNALIZED_BUILTIN_PHRASE that it must stay a single string literal (the script's drift guard anchors a regex on the assignment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-7 addressed in 1. Second mirrored literal now drift-guarded too. Extracted Nits:
Agreed the findings have converged — the script's two literal contracts with the gate module are now both self-enforcing, and I think this is the natural stopping point. Thanks for the seven passes; the gate is materially more robust for them.
|
|
@claude review |
|
Claude finished @cliffhall's task in 3m 33s —— View job Code review — PR #1777, round 8 (after
|
…nLog order (#1769) - saveBackupAndFail no longer claims a backup it may not have written: it tracks whether the write succeeded and only points the user at the .bak in that case (otherwise it says the probe is still injected and to restore from VCS). Uses mkdtempSync for a unique dir instead of a fixed tmp filename, so a pre-existing/foreign-owned /tmp file can't make the recovery message point at someone else's content. - Guard the restore-verification readFileSync too, routing a failure through saveBackupAndFail so the whole restore path is uniformly safe (a raw throw there would skip the backup net). - vite.config.ts: enforce: 'pre' on the gate plugin so it runs ahead of the normal-plugin group in the onLog chain — a plugin whose onLog returns false filters the log for later plugins, so trailing the array would let a future log-filtering plugin silently blind the gate. Makes "the gate sees every log" structural rather than array-position-dependent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-8 addressed in 1. 2. 3. Restore-verification read guarded — fixed. Routed it through Nits:
That's eight rounds; the recovery path is now uniformly guarded and the
|
|
@claude review |
|
Claude finished @cliffhall's task in 2m 29s —— View job Code review — PR #1777, round 9 (after
|
…#1769) The success key confirmed the #1769 gate fired, but not that OUR probe tripped it. If the repo already leaked a Node built-in, the gate would fire on that and the script would report OK even if the probe was tree-shaken or entryPath went stale — branch (c)'s failure silently inverted into a pass. Add an assertion that PROBE_MODULE appears among the gate's embedded offender warnings. Unreachable inside `npm run ci` (validate's build:web fails first on a pre-existing leak), but this script is a documented standalone command — run exactly when debugging a leak, the case where this mattered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-9 addressed in Success key now proves the probe fired the gate, not just a gate firing. Added an assertion that Nits — all left as-is with agreement:
Nine rounds in, every read/write on the mutation path is guarded, both mirrored literals are drift-guarded, the
|
|
@claude review |
|
Claude finished @cliffhall's task in 2m 28s —— View job Code review — PR #1777, round 10 (after
|
…1769) The probe writeFileSync uses the default 'w' flag (truncates at open), so a mid-write ENOSPC/EIO could leave clients/web/src/main.tsx empty or half-written — and its catch called fail() directly, the one path on this script that could destroy the entry rather than merely dirty it (every other read/write is already routed into the recovery net). Call restoreEntry() in that catch to roll the partial write back (falling through to the .bak net if the restore itself can't write), and correct the comment that claimed a write failure happens "before any mutation". Also widen the enforce:'pre' comment to note it orders ahead of Vite's core plugins too (harmless — the gate defines no resolve/load/transform). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-10 addressed in Partial probe write can no longer destroy Nits:
Ten rounds in, every read and write on the mutation path — including a truncating partial write — is now routed into the recovery net, both mirrored literals are drift-guarded, the
|
|
@claude review |
|
Claude finished @cliffhall's task in 2m 13s —— View job Code review — PR #1777, round 11 (after
|
| trigger | probe write | rollback write | what the developer sees |
|---|---|---|---|
| read-only checkout / EACCES / EROFS | fails at open |
fails at open |
"failed to restore main.tsx … pre-run contents were saved to /tmp/…; restore from there" |
| ENOSPC / EDQUOT | fails mid-write | fails (disk still full) | same |
| transient EIO | fails mid-write | succeeds | correct message ✓ |
In the top row nothing was ever written — the entry is byte-identical to what the developer had — yet they're told the restore failed and pointed at a /tmp backup to recover from, which is the recovery-misdirection class rounds 8 and 10 each closed one instance of, reintroduced from the other side. It also writes a mkdtemp backup of an unmodified file as a side effect.
Keeping the rollback but not letting it swallow the diagnosis:
} catch (err) {
// Roll back a possible partial write ('w' truncates at open) — but don't let
// a failing rollback swallow the primary error, which is the actual diagnosis.
let rolledBack = false;
try {
writeFileSync(entryPath, original);
restored = rolledBack = true;
} catch { /* reported in the message below */ }
fail(
`could not write the probe into ${path.relative(repoRoot, entryPath)} (${err.message})` +
(rolledBack
? ""
: " — the entry may be truncated or partially written; restore it from version control"),
);
}2. Nits
ERROR_PREFIXisn't pinned as contiguous the wayKNOWN_PHRASEis. The drift guard at:145is a plaingateSource.includes(ERROR_PREFIX), and in the gate module the prefix sits inside the first fragment of a concatenated message (gate.ts:53).BROWSER_EXTERNALIZED_BUILTIN_PHRASEcarries a "keep it a single string literal" pin (gate.ts:30-32) protecting its regex; the prefix has no equivalent, so a reflow like"Build failed " + "(#1769): …"would trip the guard with a false drift. Prettier doesn't split string literals, so this is theoretical — a half-line comment next to the error message would close it symmetrically.- Signal handlers are live before the probe write (
:175vs:190). A SIGINT in that window runsrestoreEntry(), writingoriginalover an unmodified file — a harmless no-op on a writable tree, but on a read-only one it takes thesaveBackupAndFailpath for a file that was never touched. Same root cause as Progress notifications #1; fixing Progress notifications #1 doesn't cover this one, and it's rare enough not to be worth its own guard. browserExternalizedBuiltinError([])— still agree with the round-4 call to leave it (size > 0guards the only caller; a guard would be dead code needing its own coverage).
What I checked and found fine
- Round 10's item verified in the code:
restoreEntry()in the probe-write catch (:192), and the comment corrected to name the'w'-truncates-at-open mechanism and the rollback (:183-188). Theenforce: 'pre'comment was also widened to note it orders the gate ahead of Vite's core plugins including the emittingrolldown:vite-resolve, and why that's harmless (vite.config.ts:56-67). - Both drift guards still hold against the live source — re-ran their logic: the
BROWSER_EXTERNALIZED_BUILTIN_PHRASEassignment regex matches,ERROR_PREFIXis present in the gate module, and the phrase occurs exactly once in the file, so no comment copy could mask a changed constant. - The round-9 probe assertion is sound in practice: Vite's warning is
Module "node:fs" has been externalized…, the gate embeds every original warning verbatim (gate.ts:59-60), andoutput.includes(PROBE_MODULE)therefore matches — confirmed by CI'sverify:build-gatestep passing oncbe2ab5band50eb49f4, which is the only way that assertion could be validated. - The gate module is complete: 98 lines, four exports, zero Vite import, the
message is stringpredicate keepingrecordLoga guard-free one-liner withSet<string>typed. Eight tests cover both branches of?? falseandsize > 0plus dedupe / multi-offender /reset()/ per-instance;clients/web/server/**is in the coverageincludeand unexcluded — ≥90 on all four dimensions with nov8 ignore. The test's third copy of the phrase self-checks against the exported constant (:31), so it can't drift silently. distordering is safe. The expected path throws inbuildEnd, before output generation andemptyOutDir, so the real bundle fromvalidatesurvives intosmoke; the threestatus === 0paths that did complete a build flag the probe bundle once (:258-260).onLogreturns nothing, so Vite's original warning still prints alongside the gate's error — the developer sees the offending module path and the remediation, including the transitive-dependencyresolve.aliasadvice.- No
--logLevelanywhere in the build scripts, so nothing starvesonLogbefore the gate sees the warning. - Docs are consistent across all five places —
AGENTS.mdstructure block, the gate bullet (bothapply: 'build'andapplyToEnvironmentdocumented), both pipeline enumerations;clients/web/README.md:22with the "why it lives underserver/" justification; the root READMEscripts/line and testing table.package.json'scichain matches the workflow's step order, and the workflow step carries its own explanatory comment. - Follow-up chore(web): format-gate the ungated first-party TS/JS (server/, vite.config.ts, scripts/) #1778 remains the right home for the
scripts/+server/+vite.config.tsprettier gap.
· branchv2/fix-1769-build-gate-externalized-builtin
…he write error (#1769) Round 10's restoreEntry() in the probe-write catch swallowed the primary error: restoreEntry() on its own write failure calls saveBackupAndFail → fail → exit, so the "could not write the probe" diagnosis never ran when the rollback also failed — which is exactly the read-only-checkout / ENOSPC case (both writes fail at open). On a read-only checkout nothing was ever written, yet the developer was told the restore failed and pointed at a /tmp backup of an untouched file. Roll back inline instead: attempt the restore without exiting, track whether it succeeded, then always report the PRIMARY write error, appending "restore from version control" only when the rollback didn't take. Also pin the "Build failed (#1769)" error prefix as contiguous in a single string fragment (a comment symmetric with the phrase pin), since verify-build-gate.mjs drift-guards on it as ERROR_PREFIX. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-11 addressed in Rollback no longer swallows the primary write error — fixed. You're right: Nits:
|
|
@claude review |
|
Claude finished @cliffhall's task in 3m 52s —— View job Code review — PR #1777, round 12 (after
|
… its own guard (#1769) The round-11 pin comment quoted "Build failed (#1769)" verbatim, so the prefix appeared twice in the gate module and verify-build-gate.mjs's whole-file includes(ERROR_PREFIX) drift guard passed on the comment alone — rewording the thrown error would have kept the guard green while a correctly-firing gate reported "the build broke for another reason." Reworded the comment to not repeat the full lead (matching how the phrase pin avoids repeating its phrase); the literal now occurs exactly once, restoring the guard's meaning. Also make the probe-write recovery message factual instead of inferred: read the entry back and only warn "restore from version control" when it actually differs from the original. On a read-only checkout both writes fail at open, so the file is untouched and the old hedge needlessly implied damage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Round-12 addressed in 1. Pin comment defeating its own drift guard — fixed. Good catch, and my regression: the round-11 comment quoted 2. Recovery message inferring damage — made factual. Instead of guessing from whether the rollback write succeeded, it now reads the entry back and only appends "restore from version control" when the contents actually differ from the original (read failing → assume the worst). On a read-only checkout both writes fail at open, so the file is untouched and no longer gets a spurious damage warning. Also let me drop the "may be" hedge, since it's now a fact. Nits #3 (TDZ closure over
|
Closes #1769
Problem
Follow-up from #1615 / PR #1768. Vite 8 (rolldown), when a
node:*/ bare Node built-in reaches the browser graph, externalizes it with a warning and still succeeds the build, shipping amodule.exports = {}stub:smoke:web:browser(CI: add a headless-browser boot smoke for the web client #1615), but only there, and only at runtime.{}and is invisible to the runtime smoke by design → nothing gates it.Fix
A Vite build plugin turns that specific browser-externalization warning into a hard
vite builderror, so a Node built-in in the browser graph now failsnpm run build/validate— upstream of the smoke, and covering both cases.clients/web/server/browser-externalized-builtin-gate.ts— the Vite-agnostic logic (phrase match, error, per-build state machine). Unit-tested to the ≥90 gate; no Vite import, so it's trivially testable.clients/web/vite.config.ts— the thin Vite wiring. Key subtlety: throwing inside rolldown'sonLogis swallowed (it's the one hook where a thrown error doesn't abort the build — verified againstvite@8.0.0). So the gate records the warning inonLogand re-throws inbuildEnd, where a throw aborts the build with a non-zero exit. Scoped tovite build(apply: 'build') — nevervite devor the vitest projects; the Node runner build (tsup,build:runner) is a separate config where built-ins are legitimate.Guarding the version-drift risk the issue calls out
There is no stable log
codeon the rolldown warning (it carries onlymessage+plugin: "rolldown:vite-resolve"— captured againstvite@8.0.0), so the gate keys off the documented message phrasing (…has been externalized for browser compatibility). The issue notes this phrasing differs across Vite releases and could silently disable a message-keyed gate.scripts/verify-build-gate.mjs(npm run verify:build-gate, wired intonpm run ciand the GitHub workflow) closes that: it forces anode:fsimport intosrc/main.tsx, runs a realvite build, and asserts the build fails via the #1769 gate — then restores the entry. The unit tests cover the detection logic against a captured string; only this real build catches a live-Vite phrasing drift.smoke:web:browserstays as the runtime backstop for crashes the build can't reason about.Verification
vite buildwith a Node built-in in the browser graph → fails with the actionable Build gate: fail vite build on a browser-externalized Node built-in (SEP of #1615) #1769 error (exit 1). ✓vite buildof the real app → succeeds, other warnings (chunk-size) still print. ✓npm run verify:build-gate→ OK, and leavessrc/main.tsxclean. ✓npm run cigreen locally (validate, coverage gate incl. the new module,verify:build-gate, smoke, Storybook).Docs
Updated
AGENTS.md(build-gate section next tosmoke:web:browser), the rootREADME.mdtesting table, and the CI workflow.🤖 Generated with Claude Code
https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5