Uh oh!
There was an error while loading. Please reload this page.
fix(console): name the unresolved import when the eager-closure walk hits a non-chunk - #6095
Conversation
…hits a non-chunk `emit-eager-closure-report`'s `writeBundle` hook read every member of the eager closure off disk with an unguarded `fs.readFileSync`. The walk seeds itself from `chunk.imports`, and rolldown lists a chunk's EXTERNAL imports in that array beside the file names of real chunks, so a bare specifier vite could not resolve joins the closure under its own name and is then read as a path. Measured: the specifier is NOT a `type: 'chunk'` entry in the output bundle map (`Object.hasOwn(bundle, '@objectstack/core/logger') === false`, 515 bundle keys, 508 chunks) — it only ever appears inside `assets/framework-*.js`'s `imports` array. No `advancedChunks.groups` test matches it either: the id carries no `/node_modules/` and no `/@objectstack+` segment. Guard the read with an existence check and `this.error()` with the missing name, the chunk that imports it, and the diagnosis. The failure stays loud — that direction is correct, and skipping the member instead would make this report under-count, the one direction the two counter-probes above exist to refuse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
yinlianghui-tw
commented
Aug 24, 2026
PM: ACCEPT — shape 1 landed, shape 2's stated mechanism falsified, and you found something worse than the cardThe card's own diagnosis was wrong, and you disproved it rather than inheriting itThe card says The real mechanism is different and simpler: it appears only inside And the card's follow-on suspicion — that an Reporting a falsified hypothesis as the deliverable is the right outcome for an investigation leg.
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#5996
Shape 1 of the card, as ruled in the dispatch order. Shape 2 was investigated and is reported below — no chunking configuration was touched.
The premise was partly spent — re-derived first
b0de7a85c(PR #5995, card #5391 — the commit that makes the spec-dist override fail fast when its own declared deps cannot be found) is onorigin/main, so the route the card was found through is closed:resolveSpecDistInjectionnow rejects anOBJECTSTACK_SPEC_DISToverride whose declareddependenciesdo not resolve, before the build starts.The crash is still reachable through the
OBJECTSTACK_CLIENT_DISTsibling hook, which the card named as a candidate and which #5995 did not touch. That hook is a plain Vite string alias (workspaceAliases['@objectstack/client'] = resolved) with no validation of the override's own dependencies. Reproduction: copy the installed@objectstack/client@17.2.0dist/to a directory outside the workspace and point the hook at it. Its three bare imports —@objectstack/core/logger,@objectstack/spec/api,@objectstack/spec/data— have nonode_modulesto resolve from at that location.Before / after, same reproduction
Both runs use the same override, the same tree, and configs that differ by exactly this PR's diff (each generated from its side by one identical
sedthat removesviteIneffectiveDynamicImports()— see "A second defect" below for why that removal is needed to see either error at all).Before (
vite.config.tsat7c96c9420):After (this branch), exact text:
Two spots are rendered with square brackets above where the real message uses angle brackets (
assets/[name]-[hash].js, and the compiled-config path in the next section): GitHub's body sanitizer deletes short angle-bracket fragments from a PR body, code fence or not.apps/console/vite.config.tsin this diff carries the angle-bracket spelling, which is what a developer actually sees in the terminal.It names the missing file, the chunk that imports it, and the assumption. The failure stays a build failure — only its message changed.
Which artifact was under test
Vite compiles the config through
configLoader: 'native'intoapps/console/node_modules/.vite-temp/[config].timestamp-[ms]-[rand].mjs, freshly per invocation — the two paired runs name different files in their stack traces (…before.ts.timestamp-1787588787207-3d31ebadb622b.mjsvs…after.ts.timestamp-1787588797390-b421aa65b219f.mjs), so neither read a cached compile. Independently, the "after" output contains sentence text that exists nowhere in this repository except this PR's diff, which no stale artifact could have produced. Every build below was run from a dedicated worktree at the commit under test; the final verification union was run onb3e278420, this branch's head.Non-vacuity — the guard does not fire on a normal build
Real
vite.config.ts(this branch), no override, onb3e278420:Byte-identical to the pre-fix baseline measured on
7c96c9420(52/508 chunks, 3298242 bytes), and the guard's message appears zero times in the log. The reason it is silent is measured, not assumed: on a normal build the only externals in the bundle aremodule,fsandpath, reached from@objectstack/lint, which sits behind the lazy boundary — so they never enter the eager set.Shape 2 — investigation only, no configuration changed
The card's stated mechanism is not what happens, and
advancedChunksis not implicated. Instrumented the real bundle map insidewriteBundleon the reproduction:{ "bundleKeys": 515, "chunkCount": 508, "eagerCount": 55, "phantomsInEagerSet": ["@objectstack/core/logger", "@objectstack/spec/api", "@objectstack/spec/data"], "phantomIsBundleKey": [{ "fn": "@objectstack/core/logger", "inBundleMap": false, "fileExists": false }, …], "listedInImportsOf": [{ "fn": "@objectstack/core/logger", "chunks": [{ "fileName": "assets/framework-Bwcc-tp7.js", "isEntry": false, "imports": ["assets/rolldown-runtime-C0FnF6B9.js", "assets/vendor-react-CsWFRkED.js", "assets/vendor-objectstack-BGBxHuAB.js", "assets/vendor-i18n-Cpia2wlg.js", "@objectstack/spec/data", "@objectstack/spec/api", "@objectstack/core/logger"] }] }] }inBundleMap: false; 515 bundle keys = 508 chunks + 7 assets, none of them the specifier). A first pass that listed bundle entries with no file on disk returnedmissing: []while the crash still occurred — that is what pointed at the real path.OutputChunk.imports, which rolldown populates with a chunk's external imports beside the file names of real chunks.importsonto its queue and doeseager.add(fileName)without askingchunks.has(fileName), so externals join the eager set and are then read as paths. That is the whole mechanism.advancedChunks.groupstest matches these ids. An unresolved specifier keeps its bare form (@objectstack/core/logger, and the card'spg-connection-string), which carries neither a/node_modules/segment nor a/@objectstack+segment, soVENDOR_OBJECTSTACK_TESTand every other group test miss it. The suspicion that a group test matches bare external specifiers is not supported; nothing here argues for touching chunking configuration.The obvious alternative — teach the walk to skip names that are not in
chunks— is deliberately not taken. It would silence a genuinely broken bundle (a bare specifier no browser can load) and make this report under-count, which is the exact direction the module's two counter-probes exist to refuse.A second defect this uncovered — filed, not fixed here
Under the real console plugin stack, neither the old
ENOENTnor the new message reaches the developer.viteIneffectiveDynamicImports()'scloseBundlecounter-probe fires whenever the build died before the pinned warnings were emitted, and its error replaces the reported build error. On the reproduction with the unmodified plugin list, the only thing printed is43 pinned ineffective dynamic import(s) did NOT fire— the eager-closure error appears zero times, before and after this PR. That masking is why the paired runs above remove that one plugin, and it is a separate defect in a different plugin (#5325's ledger), out of this card's scope. Filed as objectui#6093. This PR's improvement is real but will only be visible to developers once that masking is fixed.Also filed: objectui#6094 —
OBJECTSTACK_CLIENT_DISThas no equivalent of the fail-fast dependency validation #5995 gaveOBJECTSTACK_SPEC_DIST, which is the asymmetry this reproduction exploits.Verification run on
b3e278420pnpm exec vite build(apps/console, no override)52/508 chunks, 3298242 bytes gzipped, guard silentpnpm exec vite build(apps/console, reproduction)pnpm --filter @object-ui/console type-checktsc --noEmit && tsc -b tsconfig.node.json --force;tsconfig.node.jsonis the project that containsvite.config.ts)pnpm --filter @object-ui/console lint✖ 203 problems (0 errors, 203 warnings), all pre-existingpnpm exec vitest run(repo root) on the 6 config-adjacent suitesTest Files 6 passed (6)·Tests 157 passed (157)node scripts/check-changeset-presence.mjs✅ No source of a released package changed in this range, so no changeset is owed.The vitest selection is
scripts/__tests__/{check-eager-closure-budget,vite-ineffective-dynamic-imports,vite-objectstack-spec-dist,scripts-type-check,side-effects-declaration-consistency,check-changeset-presence}.test.ts— the suites that reachapps/console/vite.config.tsor the plugins it imports. Run from the repo root, never package-scoped. This is a declared narrowing of the repo-wide suite; CI runs the full farm and owns that verdict.The first
type-checkrun reported 352TS2882errors insrc/**andpackages/app-shell/**. That was the unbuilt-dependency-closure trap, not this change: none of the 352 namedvite.config.ts, and afterpnpm --workspace-concurrency=2 --filter '@object-ui/console^...' buildthe same command exits 0.A changeset with empty frontmatter is included:
apps/console/vite.config.tsis not published source —@object-ui/console'sfileslist is["dist", "plugin.ts", "plugin.js", "plugin.d.ts", "README.md"]— so this ships nothing, and the presence gate agrees.Generated by Claude Code