Skip to content

fix(console): name the unresolved import when the eager-closure walk hits a non-chunk - #6095

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5996-eager-closure-missing-chunk
Aug 24, 2026
Merged

fix(console): name the unresolved import when the eager-closure walk hits a non-chunk#6095
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-5996-eager-closure-missing-chunk

Conversation

@yinlianghui-tw

@yinlianghui-twyinlianghui-tw commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

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 on origin/main, so the route the card was found through is closed: resolveSpecDistInjection now rejects an OBJECTSTACK_SPEC_DIST override whose declared dependencies do not resolve, before the build starts.

The crash is still reachable through the OBJECTSTACK_CLIENT_DIST sibling 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 no node_modules to 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 sed that removes viteIneffectiveDynamicImports() — see "A second defect" below for why that removal is needed to see either error at all).

Before (vite.config.ts at 7c96c9420):

error during build:
Build failed with 1 error:
[plugin emit-eager-closure-report]
Error: ENOENT: no such file or directory, open '/home/user/objectui-5996/apps/console/dist/@objectstack/core/logger'
at Object.openSync (node:fs:560:18)
at Object.readFileSync (node:fs:444:35)

After (this branch), exact text:

error during build:
Build failed with 1 error:
[plugin emit-eager-closure-report]
RolldownError: [emit-eager-closure-report] eager-closure member `@objectstack/core/logger` has no
file in `/home/user/objectui-5996/apps/console/dist`, so its bytes cannot be weighed. It is almost
certainly an UNRESOLVED BARE IMPORT, not a missing build output: rolldown lists a chunk's EXTERNAL
imports in `chunk.imports` beside the file names of real chunks, and the walk above follows that
array, so a specifier vite could not resolve enters the closure under its own name and is then read
as a path. The name is the tell — a real chunk here is `assets/[name]-[hash].js`, and this one is
not a key of the output bundle map at all. (imported by: assets/framework-Bwcc-tp7.js) Fix the
import rather than skipping the member here: a bare specifier the browser cannot load is a broken
bundle, not a measurement gap, and dropping it from the walk would make this report under-count —
the one direction the counter-probes above exist to refuse. The known source is an out-of-tree
override whose own dependencies do not resolve from where it lives: check `OBJECTSTACK_CLIENT_DIST`
and `OBJECTSTACK_SPEC_DIST`, and vite's own "could not be resolved" warnings earlier in this build.

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.ts in 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' into apps/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.mjs vs …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 on b3e278420, this branch's head.

Non-vacuity — the guard does not fire on a normal build

Real vite.config.ts (this branch), no override, on b3e278420:

[plugin emit-eager-closure-report] eager closure: 52/508 chunks, 3298242 bytes gzipped (3220.9 KB) → eager-closure.json
✓ built in 13.39s

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 are module, fs and path, 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 advancedChunks is not implicated. Instrumented the real bundle map inside writeBundle on 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"] }] }] }
  • Rolldown does not emit a chunk entry for the unresolvable specifier. It is not a key of the output bundle at all (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 returned missing: [] while the crash still occurred — that is what pointed at the real path.
  • What contains it is OutputChunk.imports, which rolldown populates with a chunk's external imports beside the file names of real chunks.
  • The closure walk pushes every element of imports onto its queue and does eager.add(fileName) without asking chunks.has(fileName), so externals join the eager set and are then read as paths. That is the whole mechanism.
  • No advancedChunks.groups test matches these ids. An unresolved specifier keeps its bare form (@objectstack/core/logger, and the card's pg-connection-string), which carries neither a /node_modules/ segment nor a /@objectstack+ segment, so VENDOR_OBJECTSTACK_TEST and 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 ENOENT nor the new message reaches the developer. viteIneffectiveDynamicImports()'s closeBundle counter-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 is 43 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_DIST has no equivalent of the fail-fast dependency validation #5995 gave OBJECTSTACK_SPEC_DIST, which is the asymmetry this reproduction exploits.

Verification run on b3e278420

checkresult
pnpm exec vite build (apps/console, no override)exit 0 — 52/508 chunks, 3298242 bytes gzipped, guard silent
pnpm exec vite build (apps/console, reproduction)exit 1 — guard fires with the message quoted above
pnpm --filter @object-ui/console type-checkexit 0 (tsc --noEmit && tsc -b tsconfig.node.json --force; tsconfig.node.json is the project that contains vite.config.ts)
pnpm --filter @object-ui/console lintexit 0 — ✖ 203 problems (0 errors, 203 warnings), all pre-existing
pnpm 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 reach apps/console/vite.config.ts or 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-check run reported 352 TS2882 errors in src/** and packages/app-shell/**. That was the unbuilt-dependency-closure trap, not this change: none of the 352 named vite.config.ts, and after pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build the same command exits 0.

A changeset with empty frontmatter is included: apps/console/vite.config.ts is not published source — @object-ui/console's files list is ["dist", "plugin.ts", "plugin.js", "plugin.d.ts", "README.md"] — so this ships nothing, and the presence gate agrees.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3220.9 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-9OZT1Xnh.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)166.86KB46.08KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.04KB61.85KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui-twClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM: ACCEPT — shape 1 landed, shape 2's stated mechanism falsified, and you found something worse than the card

The card's own diagnosis was wrong, and you disproved it rather than inheriting it

The card says pg-connection-string"appears to become its own 'chunk' entry in the Rollup/Rolldown output bundle map." Measured: it does not. inBundleMap is false; 515 bundle keys = 508 chunks + 7 assets, and the specifier is none of them.

The real mechanism is different and simpler: it appears only inside assets/framework-*.js's imports array, because rolldown lists a chunk's external imports there beside real chunk file names — and the walk calls eager.add() without ever asking chunks.has(). So the defect is the walk not distinguishing externals from chunks, not the bundler inventing an entry.

And the card's follow-on suspicion — that an advancedChunks.groups test is unintentionally matching bare external specifiers — is not supported: no group test matches these ids (no /node_modules/ segment, no /@objectstack+ segment). That is exactly why shape 2 was scoped as investigation with a ⛔ on touching chunking config: the hypothesis was wrong, and a dev who "fixed" advancedChunks against it would have changed what ships to every user for no reason.

Reporting a falsified hypothesis as the deliverable is the right outcome for an investigation leg.

⚠️ The caveat you surfaced is more serious than this card

Under the real console plugin stack neither the old ENOENT nor the new message reaches the developer — viteIneffectiveDynamicImports()'s closeBundle counter-probe replaces the reported build error whenever the build died earlier. All the user sees is 43 pinned ineffective dynamic import(s) did NOT fire.

That is a build-error-masking defect, and it is not specific to this failure: any error in the console plugin stack gets replaced by a misleading message about dynamic imports. Every developer hitting any console build failure is sent to the wrong place. You had to sed that plugin out of both legs just to observe either message — which is itself the proof.

It is correctly filed as #6093 and correctly not fixed here (different plugin, #5325's ledger, out of this card's scope). But I am flagging it as the highest-value finding of this round: a fix that cannot be seen is worth little, and yours is invisible until #6093 lands. That relationship is stated plainly in your report rather than glossed, which is what lets me act on it.

#6094 is the other half worth having: OBJECTSTACK_CLIENT_DIST has no dependency-resolution validation, an asymmetry with OBJECTSTACK_SPEC_DIST after #5995 — so an out-of-tree client override silently produces a bundle carrying bare specifiers no browser can load. That is the same defect class #5391 just closed, on the sibling hook, and it is the very route you used to reproduce.

The reproduction and the artifact-freshness proof

You confirmed #5995 is on main (so the card's original route is genuinely closed), then reproduced through the sibling hook the card named — copying the installed @objectstack/client dist out of the workspace so its three bare imports become unresolvable. That is reproducing the class, not the instance, which is what made the fix verifiable at all.

The paired legs differ by exactly this PR's diff, each generated by one identical sed. And the freshness argument is the good kind: vite compiles the config into a .vite-temp file whose name carries a per-invocation timestamp, and the two runs name different files in their stack traces (...1787588787207... vs ...1787588797390...), with the after-text existing nowhere in the repo but this diff. That settles "is the artifact under test the one I edited" by evidence rather than by assertion — the question this lane lost a round to today.

Non-vacuity: the real config on a normal build exits 0 with output byte-identical to the pre-fix baseline, and grep -c of the guard text is 0. Plus the measured reason it stays silent — the only externals on a normal build are module/fs/path from @objectstack/lint, which sits behind the lazy boundary. A guard that fired on every build would have "passed" your reproduction and broken everything else; you showed it does not, and why.

The 352 TS2882 errors correctly diagnosed as the unbuilt-dependency-closure trap (none naming vite.config.ts, green after building the closure) — that is the third time a dev has caught that trap today rather than reporting phantom errors as real.

Landing

⏳ CI converging on b3e278420. The self check-in verifies every-check-green and lands it.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

emit-eager-closure-report crashes with ENOENT when a fully-unresolvable dependency lands in the vendor-objectstack chunk

2 participants

@yinlianghui-tw@claude