Skip to content

fix(css): preserve source import order in bundle#22374

Draft
maruthang wants to merge 1 commit into
vitejs:mainfrom
maruthang:fix/issue-22301-css-bundle-order
Draft

fix(css): preserve source import order in bundle#22374
maruthang wants to merge 1 commit into
vitejs:mainfrom
maruthang:fix/issue-22301-css-bundle-order

Conversation

@maruthang
Copy link
Copy Markdown

Description

Fixes the CSS bundle ordering when cssCodeSplit: false. The previous chunk-level walk visited chunk.imports (cross-chunk CSS) first and appended the chunk's own CSS last. Because a chunk's "own CSS" can come from modules imported before other modules whose CSS lives in another chunk, this inverted the source-import order whenever a force-split dependency was involved.

Concrete repro from the issue: an entry that does

import './style.css'
import './counter.js' // force-split into its own chunk

emitted counter-style.css before style.css in the bundled stylesheet — the opposite of source order — so later overrides in counter.js's CSS lost to earlier base styles.

Root cause

In packages/vite/src/node/plugins/css.ts, the old collect(chunkName) recursion walked chunk.imports first and only then appended chunkCSSMap.get(chunkName). That ordering is fine when a chunk has no own CSS interleaved with imports, but breaks the moment a module's CSS comes between other imports in the source.

Fix

Replaced the chunk-level walk with a per-module walk, scoped strictly to the cssCodeSplit: false branch:

  • During renderChunk, record CSS keyed by source module id (per chunk) in addition to the existing per-chunk map.
  • In generateBundle, walk modules via getModuleInfo(id).importedIds, which preserves source-import order. When the walk encounters an import that lives in a different chunk, recurse into that chunk at exactly that source position, so cross-chunk CSS is interleaved correctly.
  • Iterate multi-entry builds in two passes: entries that are NOT imported by another entry first, so an importing entry's walk gets the chance to recurse into its dependency entries at the correct source position.

cssCodeSplit: true and the rest of the pipeline are untouched.

fixes #22301

Alternatives explored

This is a non-trivial change. Earlier reporters and patches tried more localized fixes — e.g. swapping the order of "own CSS" vs "imported CSS" inside collect(), or sorting chunks by entry order — and each one broke a different ordering case (CSS overrides from a dependency vs. CSS overrides from the entry, multi-entry shared chunks, async imports). The only ordering that's actually well-defined is the source import order of the modules, which Rollup already exposes via getModuleInfo().importedIds. Walking that graph directly avoids having to reconstruct module order from chunk metadata.

Testing

Added a regression scenario to playground/css-no-codesplit:

  • New entries order-static.html / order-static.js import a base stylesheet and then a force-split dependency (order-static-dep.js) whose CSS overrides the base.
  • vite.config.js adds the new entry and force-splits the dep into its own chunk to reproduce the original bug shape.
  • New assertion in playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts verifies the source-imported override (green) wins over the earlier-imported base (red) in the built bundle.

Verified failing-before / passing-after by reverting only the css.ts change.

Validation run locally:

  • pnpm run build — pass
  • pnpm run test-unit — 779 passed, 4 skipped (all 44 css unit tests pass)
  • pnpm run typecheck — pass
  • pnpm run lint — pass (only a pre-existing parse error in playground/preserve-symlinks/module-a/linked.js, unrelated)
  • playground/css-codesplit (cssCodeSplit: true) and playground/css builds still succeed — fix is scoped to cssCodeSplit: false
  • playground/css-no-codesplit build output verified manually; full e2e on Windows is blocked by an unrelated symlink fixture setup limitation.

What type of PR is this?

  • Bug fix

When cssCodeSplit is false, the previous chunk-level walk visited
chunk.imports first and appended the chunk's own CSS last, inverting
source-import order whenever a force-split dependency's CSS belonged
to a module imported after the importing module's own CSS.

Replace the chunk-level collect() walk with a per-module walk keyed by
source module id during renderChunk. In generateBundle we walk modules
via getModuleInfo(id).importedIds (source order) and recurse into other
chunks at the exact source position of the cross-chunk import. Multi-
entry iteration runs in two passes so an importing entry recurses into
its dependency entries at the correct source position. Scope is limited
to the cssCodeSplit:false branch.

fixes vitejs#22301
@sapphi-red
Copy link
Copy Markdown
Member

converting to draft as the tests are failing

@sapphi-red sapphi-red marked this pull request as draft May 21, 2026 06:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CSS bundle order is incorrect

2 participants