fix: preload css for nested dynamic imports#22759
Conversation
f7f90fa to
4b943fb
Compare
sapphi-red
left a comment
There was a problem hiding this comment.
This was already latent in Vite 7 on Rollup. Rollup hoisted the CSS of a nested chunk up to its parent, so the affected import was never trapped inside the nested
.then. Rolldown keeps the CSS on the original chunk, which is what exposes the problem.
Why does this happen?
Sorry, that paragraph was wrong. It was AI-written and I didn't catch it. I've fixed the description. The real reason it only shows up on Rolldown is the wrapping shape, not CSS hoisting. Rollup-era Vite wrapped each __vitePreload(() => import("a"), Ma).then(() => __vitePreload(() => import("b"), Mb))The markers stay interleaved ( Rolldown's native plugin wraps the whole __vitePreload(() => import("a").then(() => __vitePreload(() => import("b"), Mb)), Ma)Now both imports come first and This wrapping change came in with rolldown/rolldown#8328 (tree-shaking for |
|
ok. Is it possible to change the Rolldown plugin to output __vitePreload(() => import("a"), Ma).then(() => __vitePreload(() => import("b"), Mb))? That feels less fragile. Also, have you considered other ways to solve this? |
If this wasn't an intentional change in rolldown/rolldown#8328, I'll probably need to adjust it on the Rolldown side. |
|
On second thought, __vitePreload(() => import("a"), Ma).then(() => __vitePreload(() => import("b"), Mb))isn't possible, because Rolldown requires |
Yes, that's what I thought |
Description
Fixes #22700 and closes #22721
In a nested dynamic import like
import('a').then(() => import('b'))whereahas a CSS dependency, the production build set the outer import's preload deps tovoid 0. The stylesheet forawas emitted to disk and listed in the__vite__mapDepstable, but nothing referenced its index, so no<link>was created and the CSS never loaded in production. Dev is unaffected because CSS is injected by JS there.Root cause
generateBundleinimportAnalysisBuild.tsmatched each dynamicimport()with the next__VITE_PRELOAD__marker after it. Rolldown wraps the wholeimport().then()expression, so the wrappers nest and the inner marker comes before the outer one in the text:Matching by the next marker gave the inner marker to both imports, where the second write overwrote the first, and left the outer marker unclaimed, so the fallback set it to
void 0and a's deps, including its CSS, were lost.This only surfaces with Rolldown because the dynamic-import wrapping shape changed. Rollup-era Vite wrapped each
import()on its own and left the.then()outside, like__vitePreload(() => import('a'), Ma).then(() => __vitePreload(() => import('b'), Mb)), so the markers stayed interleaved (import a < Ma < import b < Mb) and the next-marker pairing landed correctly. Rolldown's native plugin wraps the wholeimport().then()for a non-destructuring.then, which nests the inner marker before the outer one. That whole-.thenwrapping landed in rolldown#8328 to tree-shakeimport().then((m) => m.prop)(refs #21121), and a side effect is that.then(() => import())nests the markers too, which is what trips the proximity-based pairing.Fix
The imports and their markers nest like balanced brackets, so they are matched with a stack in a single pass, where each marker closes the innermost import still open. Every import keeps its own deps for any nesting depth, for ternary arms like
cond ? import(a) : import(b), and for sibling imports inside a.then. It also removes a quadratic repeated scan that the old approach did once per import on deeply nested chunks.Tests
A build regression was added under
playground/dynamic-import/nested/then-css/. It uses a nestedimport().then(() => import())where the outer module has a CSS side effect, and asserts that the outer module's stylesheet actually applies. It fails onmainand passes with this change.pnpm --filter vite buildandpnpm test-build dynamic-importboth pass, serve mode passes, and eslint and oxfmt are clean.Thanks to @mturac for digging into this in #22721.
Co-authored-by: mehmet turac 345446+mturac@users.noreply.github.com