Skip to content

[Bug]: chunk-optimization dedupe emits literal import(null) when a sibling-dynamic-import target chunk is removed #9406

@IWANABETHATGUY

Description

@IWANABETHATGUY

Filed as a follow-up to #9350 (being closed as fixed by #9398). Original report by @arvinxx at #9350 (comment) — opening a dedicated issue so the import(null) codegen variant is tracked separately and can be verified against the fix.

Reproduction link or steps

Hitting the same dedupe pass (#9305) in production with a different and more severe codegen symptom: rolldown 1.0.1 emits literal import(null) for some import() call sites whose target chunk was deduped away.

Bisect

Using Vite + rolldown through rolldownOptions. Two consecutive Vercel builds, same source commit family (canary), differ only in patch versions:

Vite rolldown Result
8.0.12 1.0.0 OK
8.0.13 1.0.1 broken

Vite 8.0.13's only chunk-affecting change is vitejs/vite#22444 — the rolldown 1.0.0 → 1.0.1 bump, which contains #9305.

Concrete diff between the two builds

Same source (src/libs/trpc/client/lambda.ts):

headers: async () => {
  const { createHeaderWithAuth } = await import('@/services/_auth');
  // ...
}

rolldown 1.0.0 (working) — client-DYN1hRzS.js:

let{createHeaderWithAuth:e}=await o(async()=>{
  let{createHeaderWithAuth:e}=await import(`./_auth-enwjeLpB.js`).then(e=>(e.i(),e.t));
  return{createHeaderWithAuth:e}
}, __vite__mapDeps([45,1,8,46,12,3,13,14,4,5,6,7,9,10,11,15,16,4,...]))

rolldown 1.0.1 (broken) — client-DPywsu0Y.js:

let{createHeaderWithAuth:e}=await o(async()=>{
  let{createHeaderWithAuth:e}=await import(null);   // ← literal null
  return{createHeaderWithAuth:e}
}, [])                                              // ← deps emptied too

A grep over the entire broken client-*.js for _auth-*.js returns zero matches — rolldown didn't just empty the deps list, it eliminated every reference to the _auth chunk filename, but left the import() call site behind with a null argument. Two distinct call sites in the bundle exhibit this (both are lambda.ts / tools.ts headers callbacks that share the import('@/services/_auth') pattern).

Likely shape that triggers it

The file containing the broken import has 6 sibling dynamic imports inside one tRPC client setup:

// inside errorHandlingLink + httpBatchLink headers, all in the same module:
await import('@/services/_auth');
await import('@/layout/AuthProvider/MarketAuth/events');
await import('@/store/user/store');
await import('@/components/Error/loginRequiredNotification');
await import('@/store/image');
await import('@/store/image/slices/generationConfig/selectors');

These targets share several transitive deps, so the "already-loaded by every importer" analysis from #9305 has many candidates to merge — consistent with the unsound-sibling-merge failure mode in #9350, just escalated all the way to "the chunk itself disappears, the call site stays."

If a separate minimal repro for the import(null) variant would help, happy to put one together.

What is expected?

import() call sites should emit a valid chunk specifier (or be removed entirely along with the call site). They should never resolve to literal null at runtime.

What is actually happening?

Production runtime throws on every tRPC call:

TypeError: Failed to resolve module specifier 'null'

…because the headers callback contains await import('@/services/_auth') and rolldown emitted import(null) for it. The headers callback is on the hot path for every request, so the whole app is down.

The chunk_optimization dedupe pass appears to reduce the _auth chunk's dependent-entry bits to empty for some sibling-dynamic-import shapes; the rewriting step then has nothing to point the import() at and writes import(null). Believed to be the same root cause as #9350 — under-approximating "guaranteed already loaded" when sibling dynamic entries can be reached independently — but with a much more severe symptom (the chunk filename is gone, not just an extra side effect). Worth confirming whether #9398 already covers this variant.

System Info

rolldown 1.0.1 (regression vs 1.0.0)
Vite 8.0.13 (regression vs 8.0.12)
Build environment: Vercel
Repro shape: 6 sibling `await import()` calls in the same module sharing transitive deps

Any additional comments?

Real-world impact: For LobeHub the only mitigation was pinning Vite back to 8.0.12 (lobehub/lobehub#14804). The repo installs with lockfile=false + resolution-mode=highest, so the broken 1.0.1 was picked up automatically on the next deploy after 8.0.13 published and took the whole app offline until the pin landed.

Reported by @arvinxx in #9350 (comment).

Metadata

Metadata

Type

Priority

None yet

Effort

None yet

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions