Skip to content

Build: nested import().then(() => import()) drops outer __vitePreload deps to void 0, orphaning CSS #22700

Description

@charlsantony

Describe the bug

With a nested dynamic import of the form import('./a').then(() => import('./b')) where ./a has any CSS side-effect dep, the outer dynamic import in the built entry chunk is wrapped as __vitePreload(() => import('./a-…js'), void 0) instead of __vitePreload(..., __vite__mapDeps([…])). The inner import's deps array is correct.

The CSS file is emitted to disk and even appears in the __vite__mapDeps lookup table, but nothing references its index — no preload call, no <link> in the generated HTML. The stylesheet never loads in production. Dev mode (vite) works fine because the dev server injects CSS via JS.

Expected

import('./a.js') should be wrapped as __vitePreload(() => import('./a-…js'), __vite__mapDeps([…, 'a-….css'])) so the CSS dep is preloaded as a <link rel="stylesheet"> when the chunk loads.

Actual

The built entry chunk ends with:

js r(()=>import(`./a-….js`).then(e=>{...,r(()=>import(`./b-….js`),[])}), void 0); // ↑ outer call: deps lost ​

__vite__mapDeps table contains both a-….js and a-….css, but no code path references them.

Root cause

packages/vite/src/node/plugins/importAnalysisBuild.ts, generateBundle hook, walks dynamic import() calls in source-text order and pairs each with the next __VITE_PRELOAD__ marker after its end position.

After the transform pass, the chunk source has the shape:

js __vitePreload(() => import('./a'), __VITE_PRELOAD__ /* B */).then(() => { __vitePreload(() => import('./b'), __VITE_PRELOAD__ /* A */); }); ​

Inner marker A textually precedes outer marker B because it sits inside the .then(...) callback. The pairing loop walks imports in order [a, b]:

  1. ./a finds marker A as the next marker after end_a → writes ./a's deps to A; adds posA to rewroteMarkerStartPos.
  2. ./b finds marker A again as the next marker after end_boverwrites A with ./b's deps.
  3. Fallback pass finds marker B not in rewroteMarkerStartPos → replaces with "void 0".

./a's preload deps (which would have included its CSS) are silently lost.

Suggested fix

Skip past markers already claimed by a previous import in the same iteration:

ts let markerStartPos = findPreloadMarker(code, end); while (markerStartPos !== -1 && rewroteMarkerStartPos.has(markerStartPos)) { markerStartPos = findPreloadMarker(code, markerStartPos + preloadMarker.length); } ​

Or, more robustly, pair markers to imports by AST relationship rather than source-text proximity.

Why this only surfaces on Vite 8

The latent bug exists in Vite 7 too. With Rollup, CSS attached to nested chunks was hoisted to the nearest entry/parent chunk, so the CSS dep ended up on a chunk whose dynamic import wasn't trapped in a nested-then. Vite 8 ships Rolldown, which keeps CSS on the original chunk — exposing the bug. Same chunk-shape change documented in laravel/framework#59662.

Related but distinct

Reproduction

https://github.com/charlsantony/vite-css-preload-bug

Steps to reproduce

  1. Clone the repro repo
  2. npm install
  3. npm run build
  4. Open dist/assets/index-*.js — the trailing __vitePreload call has void 0 as its second argument
  5. grep -rn "a-.*\.css" dist/ — the CSS filename only appears inside the __vite__mapDeps string table, never in index.html or any preload call
  6. npm run preview — body background should be hotpink, renders white

Bug is build-only. vite (dev mode) is unaffected.

System Info

System:
    OS: macOS 26.5
    CPU: (12) arm64 Apple M3 Pro
    Shell: zsh
  Binaries:
    Node: 22.20.0
    npm: 10.9.3
  npmPackages:
    vite: 8.0.16 => 8.0.16

Used Package Manager

pnpm

Logs

No response

Validations

Metadata

Metadata

Assignees

Labels

feat: buildp3-minor-bugAn edge case that only affects very specific usage (priority)regressionThe issue only appears after a new release

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions