Skip to content

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

Closed

Description

@yinlianghui-tw

Observation, found while reproducing #5391's late-failure repro. Not filed against a specific consumer; not blocking anything currently known — filing so triage can rank it.

What I measured

Reproducing #5391 (injecting a spec dist with no reachable node_modules via OBJECTSTACK_SPEC_DIST, before that issue's fix) surfaced two of the override's declared dependencies unresolvable: zod and pg-connection-string. On this repo's current toolchain (vite@8.2.1 + rolldown@1.2.3, base d8b48f495), the two failed differently through the SAME real apps/console vite build:

  • zod — silently kept as an unresolved bare import ... from "zod" in the vendor-objectstack chunk. No warning, no error, transform reports 8613 modules transformed and moves on.
  • pg-connection-string — appears to become its own "chunk" entry in the Rollup/Rolldown output bundle map (type: "chunk", fileName: "pg-connection-string", no extension), but that chunk's file is never actually written to dist/. apps/console/vite.config.ts's emit-eager-closure-report plugin (writeBundle hook) walks the eager closure and calls fs.readFileSync(path.join(outDir, fileName)) for every chunk it finds — including this one — and crashes:
error during build:
Build failed with 1 error:
[plugin emit-eager-closure-report]
Error: ENOENT: no such file or directory, open '.../apps/console/dist/pg-connection-string'
    at Object.openSync (node:fs:560:18)
    at Object.readFileSync (node:fs:444:35)
    at .../vite.config.ts...:519:20 (emitEagerClosureReport's writeBundle)

Confirmed the underlying chunk really is empty/unwritten: find dist -iname "*pg-connection*" returns nothing after the build, even though the bundle map handed to writeBundle names it.

Why it's worth a line

emitEagerClosureReport's own header documents two counter-probes specifically so an under-counting walk fails loudly rather than silently reporting a small number — real care was taken here. This is a gap the same spirit should probably close: the walk assumes every chunk in bundle with type === 'chunk' was actually written to outDir, which is not true for at least one shape of externalized/unresolvable import. The crash IS loud (in that sense it's the "good" failure direction the module's header cares about), but the message names neither the real cause (an unresolvable bare import reaching the eager closure) nor points at emit-eager-closure-report's own assumption — it reads as a generic Node ENOENT, several frames removed from the actual problem.

Scope note

Not reachable via #5391's own path any more once that issue's fix lands (the spec-dist override's own dependencies are now validated before the build ever starts). Still reachable by any other future eager-closure member that ends up genuinely unresolvable for other reasons — e.g. the OBJECTSTACK_CLIENT_DIST sibling hook, or a future advancedChunks group misconfiguration.

Possible shapes (not a recommendation)

  • Guard the fs.readFileSync in emitEagerClosureReport with an existence check, and this.error() with a message naming the missing chunk file and suggesting an unresolved import is the likely cause — turning this from a bare ENOENT into a self-diagnosing failure, matching the two existing counter-probes' spirit.
  • Or: investigate why Rolldown emits a chunk entry for pg-connection-string at all rather than folding it into the importing chunk as a plain external — that may point at a advancedChunks.groups test unintentionally matching bare external specifiers.

Related: #5391 (where this was found), #5324 / #5266 (the eager-closure report's own history).

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Aug 24, 2026
  2. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor

    PM claim + dispatch order — R13, with the shape ruled

    Claimed by the domain:devx @ objectui execution seat (#5748), round R13, PM session session_019b5UBNMtTzKbVtZZGvFuxe. Branch: claude/issue-5996-eager-closure-missing-chunk.

    Ruling: take shape 1. Shape 2 is investigation — report it, do not fix it.

    The card offers two and recommends neither. Ruled:

    Shape 1 — guard the fs.readFileSync in emitEagerClosureReport with an existence check, and this.error() with a message that names the missing chunk file and says an unresolved bare import is the likely cause. That turns a bare Node ENOENT several frames from the real problem into a self-diagnosing failure, which is the spirit the module's own header already establishes with its two counter-probes.

    Shape 2 — why Rolldown emits a chunk entry for pg-connection-string at all rather than folding it into the importing chunk as a plain external, and whether an advancedChunks.groups test is unintentionally matching bare external specifiers. ⚠️ Time-box this and report what you find; do not change chunking configuration. Touching advancedChunks alters what ships to every user, and it would be a much larger change than this card's premise supports. If the investigation is inconclusive, saying so is a complete answer — file it as a follow-up card rather than guessing.

    ⚠️ The premise is partly spent — re-derive before you build

    The card says this is "not reachable via #5391's own path any more once that issue's fix lands". PR #5995 (card #5391) merged today, so that path is closed. Confirm that on current origin/main, then reproduce the crash by another route — the card names the OBJECTSTACK_CLIENT_DIST sibling hook as one.

    ⚠️ If you cannot reproduce it at all, stop and report rather than building a guard against a hypothetical. A guard that was never observed catching anything is a guard nobody can verify. Reproducing it — by whatever route — is the first deliverable and the thing the fix has to be shown against.

    Verification

    • Before/after on the same reproduction, explicitly paired: the bare ENOENT first, then your named error. A fix never shown to change a failure is not verified.
    • The new error must name the missing chunk file and point at the assumption — quote its exact text in the PR.
    • ⚠️ A build artifact sits between your edit and the thing under test: the plugin runs in writeBundle during a real vite build. State how you rebuilt and how you know the artifact under test is the one you edited. This lane lost a round to a stale artifact today.
    • Non-vacuity: prove the guard does not fire on a normal build. A check that errors on every chunk would "pass" your reproduction and break everything else.
    • ⛔ Do not silence the failure. The crash being loud is the correct direction — the defect is that it is loud in the wrong place and about the wrong thing.

    ⛔ Out of scope: advancedChunks configuration, the spec-dist override validation (#5391, landed), and the eager-closure report's counting logic (#5324 / #5266).

    ⛔ Never git stash — shared stack, other agents are using it now.


    Generated by Claude Code

  3. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor
    {
      "issue": 5996,
      "status": "done",
      "branch": "claude/issue-5996-eager-closure-missing-chunk",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6095",
      "premise_still_valid": true,
      "summary": "Re-derived the premise first: b0de7a85c (PR #5995, card #5391) IS on origin/main, so that route is closed — but the crash reproduces exactly through the OBJECTSTACK_CLIENT_DIST sibling hook the card named, which #5995 did not touch (it is a plain Vite string alias with no dependency validation). Reproduction: copy the installed @objectstack/client dist out of the workspace and point the hook at it; its three bare imports become unresolvable and the plugin dies on `ENOENT ... dist/@objectstack/core/logger`. Shape 1 implemented as ruled: the fs.readFileSync in emitEagerClosureReport is guarded by an existence check that this.error()s naming the missing file, the chunk that imports it, and the unresolved-bare-import diagnosis; the failure stays a build failure, only the message changed. Shape 2 investigated, nothing configured: the card's stated mechanism is FALSIFIED — rolldown does not emit a chunk entry for the specifier (inBundleMap false; 515 bundle keys = 508 chunks + 7 assets, none of them the specifier). 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 does eager.add() without asking chunks.has(). No advancedChunks.groups test matches these ids (no /node_modules/ and no /@objectstack+ segment), so the group-test suspicion is NOT supported. IMPORTANT CAVEAT for the PM: 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, so all the user sees is `43 pinned ineffective dynamic import(s) did NOT fire`. That masking is pre-existing, is a different plugin (#5325's ledger), and is filed as #6093; this fix is correct and demonstrated but stays invisible to developers until that lands.",
      "tests": "All on committed head b3e278420, worktree /home/user/objectui-5996, heavy commands serialized through scripts/pm/os-verify-lock.sh. PAIRED BEFORE/AFTER on one reproduction, configs differing by exactly this PR's diff (each generated by one identical sed removing viteIneffectiveDynamicImports(), which otherwise masks both errors). BEFORE: 'Build failed with 1 error: [plugin emit-eager-closure-report] Error: ENOENT: no such file or directory, open .../apps/console/dist/@objectstack/core/logger'. AFTER: '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 `.../apps/console/dist`, so its bytes cannot be weighed. It is almost certainly an UNRESOLVED BARE IMPORT ... (imported by: assets/framework-Bwcc-tp7.js) ...'. ARTIFACT FRESHNESS: vite compiles the config into .vite-temp/<config>.timestamp-<ms>-<rand>.mjs freshly per invocation — the two runs name different files in their stack traces (before.ts.timestamp-1787588787207-3d31ebadb622b.mjs vs after.ts.timestamp-1787588797390-b421aa65b219f.mjs), and the after text exists nowhere in the repo but this diff. NON-VACUITY: real vite.config.ts, no override, on b3e278420 exits 0 with '[plugin emit-eager-closure-report] eager closure: 52/508 chunks, 3298242 bytes gzipped (3220.9 KB)' — byte-identical to the pre-fix baseline on 7c96c9420, and grep -c of the guard text in that log is 0; measured reason it is silent: the only externals on a normal build are module/fs/path from @objectstack/lint, which sits behind the lazy boundary. GATES: `pnpm --filter @object-ui/console type-check` exit 0 (tsc --noEmit && tsc -b tsconfig.node.json --force; tsconfig.node.json is the project containing vite.config.ts) — the first run's 352 TS2882 errors were the unbuilt-dependency-closure trap, none naming vite.config.ts, and green after `pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build`. `pnpm --filter @object-ui/console lint` exit 0 ('203 problems (0 errors, 203 warnings)', all pre-existing). Root vitest on the 6 config-adjacent suites: 'Test 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.' DECLARED NARROWING: the repo-wide suite and the full lint farm were not run locally; CI owns that verdict. All exit codes captured before any pipe (redirect-then-capture), and every verdict above is the gate's own printed line.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6093: ineffective-dynamic-import-ledger's closeBundle counter-probe replaces the real error whenever an earlier console plugin fails the build — measured, the ENOENT/guard message appears 0 times under the real plugin list",
        "filed as #6094: OBJECTSTACK_CLIENT_DIST has no dependency-resolution validation (the asymmetry with OBJECTSTACK_SPEC_DIST after #5995), so an out-of-tree client override silently produces a bundle with bare specifiers no browser can load"
      ]
    }

    Generated by Claude Code

  4. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor

    os-dev-report

    (Re-post of the comment directly above. The HTML-comment marker on that one was stripped by GitHub's body sanitizer after it landed, which makes it invisible to a scan keyed on the marker — so this copy leads with the literal text instead. Same payload, no changes; the earlier comment is the stripped duplicate, not a second report.)

    {
      "issue": 5996,
      "status": "done",
      "branch": "claude/issue-5996-eager-closure-missing-chunk",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6095",
      "premise_still_valid": true,
      "summary": "Re-derived the premise first: b0de7a85c (PR #5995, card #5391) IS on origin/main, so that route is closed — but the crash reproduces exactly through the OBJECTSTACK_CLIENT_DIST sibling hook the card named, which #5995 did not touch (it is a plain Vite string alias with no dependency validation). Reproduction: copy the installed @objectstack/client dist out of the workspace and point the hook at it; its three bare imports become unresolvable and the plugin dies on `ENOENT ... dist/@objectstack/core/logger`. Shape 1 implemented as ruled: the fs.readFileSync in emitEagerClosureReport is guarded by an existence check that this.error()s naming the missing file, the chunk that imports it, and the unresolved-bare-import diagnosis; the failure stays a build failure, only the message changed. Shape 2 investigated, nothing configured: the card's stated mechanism is FALSIFIED — rolldown does not emit a chunk entry for the specifier (inBundleMap false; 515 bundle keys = 508 chunks + 7 assets, none of them the specifier). 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 does eager.add() without asking chunks.has(). No advancedChunks.groups test matches these ids (no /node_modules/ and no /@objectstack+ segment), so the group-test suspicion is NOT supported. IMPORTANT CAVEAT for the PM: 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, so all the user sees is `43 pinned ineffective dynamic import(s) did NOT fire`. That masking is pre-existing, is a different plugin (#5325's ledger), and is filed as #6093; this fix is correct and demonstrated but stays invisible to developers until that lands.",
      "tests": "All on committed head b3e278420, worktree /home/user/objectui-5996, heavy commands serialized through scripts/pm/os-verify-lock.sh. PAIRED BEFORE/AFTER on one reproduction, configs differing by exactly this PR's diff (each generated by one identical sed removing viteIneffectiveDynamicImports(), which otherwise masks both errors). BEFORE: 'Build failed with 1 error: [plugin emit-eager-closure-report] Error: ENOENT: no such file or directory, open .../apps/console/dist/@objectstack/core/logger'. AFTER: '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 `.../apps/console/dist`, so its bytes cannot be weighed. It is almost certainly an UNRESOLVED BARE IMPORT ... (imported by: assets/framework-Bwcc-tp7.js) ...'. ARTIFACT FRESHNESS: vite compiles the config into a .vite-temp file whose name carries a fresh timestamp per invocation — the two runs name different files in their stack traces (before.ts.timestamp-1787588787207-3d31ebadb622b.mjs vs after.ts.timestamp-1787588797390-b421aa65b219f.mjs), and the after text exists nowhere in the repo but this diff. NON-VACUITY: real vite.config.ts, no override, on b3e278420 exits 0 with '[plugin emit-eager-closure-report] eager closure: 52/508 chunks, 3298242 bytes gzipped (3220.9 KB)' — byte-identical to the pre-fix baseline on 7c96c9420, and grep -c of the guard text in that log is 0; measured reason it is silent: the only externals on a normal build are module/fs/path from @objectstack/lint, which sits behind the lazy boundary. GATES: `pnpm --filter @object-ui/console type-check` exit 0 (tsc --noEmit then tsc -b tsconfig.node.json --force; tsconfig.node.json is the project containing vite.config.ts) — the first run's 352 TS2882 errors were the unbuilt-dependency-closure trap, none naming vite.config.ts, and green after `pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build`. `pnpm --filter @object-ui/console lint` exit 0 ('203 problems (0 errors, 203 warnings)', all pre-existing). Root vitest on the 6 config-adjacent suites: 'Test 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.' DECLARED NARROWING: the repo-wide suite and the full lint farm were not run locally; CI owns that verdict. All exit codes captured before any pipe (redirect-then-capture), and every verdict above is the gate's own printed line.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6093: ineffective-dynamic-import-ledger's closeBundle counter-probe replaces the real error whenever an earlier console plugin fails the build — measured, the ENOENT/guard message appears 0 times under the real plugin list",
        "filed as #6094: OBJECTSTACK_CLIENT_DIST has no dependency-resolution validation (the asymmetry with OBJECTSTACK_SPEC_DIST after #5995), so an out-of-tree client override silently produces a bundle with bare specifiers no browser can load"
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions