Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,14 +1100,23 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
// remove pure css chunk from other chunk's imports,
// and also register the emitted CSS files under the importer
// chunks instead.
//
// We need to preserve the original import order: CSS from pure
// CSS chunks should appear at the same position they had in the
// imports list, not after the chunk's own CSS.
// Save the chunk's own CSS before merging.
const ownImportedCss = [...chunk.viteMetadata!.importedCss]

// Collect CSS from pure CSS chunks in their original import
// order position.
const pureCssFromImports: string[] = []

chunk.imports = chunk.imports.filter((file) => {
if (pureCssChunkNames.includes(file)) {
const { importedCss, importedAssets } = (
bundle[file] as OutputChunk
).viteMetadata!
importedCss.forEach((file) =>
chunk.viteMetadata!.importedCss.add(file),
)
importedCss.forEach((file) => pureCssFromImports.push(file))
importedAssets.forEach((file) =>
chunk.viteMetadata!.importedAssets.add(file),
)
Expand All @@ -1117,6 +1126,12 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
return true
})
if (chunkImportsPureCssChunk) {
// Rebuild importedCss: pure CSS chunk CSS first (preserving
// import order), then the chunk's own CSS.
chunk.viteMetadata!.importedCss = new Set([
...pureCssFromImports,
...ownImportedCss,
])
chunk.code = replaceEmptyChunk(chunk.code)
}
}
Expand Down
55 changes: 51 additions & 4 deletions playground/css-dynamic-import/__tests__/css-dynamic-import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@ const baseOptions = [
{ base: '/', label: 'absolute' },
]

const getConfig = (base: string): InlineConfig => ({
const getConfig = (
base: string,
extra?: Partial<InlineConfig>,
): InlineConfig => ({
base,
root: rootDir,
logLevel: 'silent',
server: { port: ports['css/dynamic-import'] },
preview: { port: ports['css/dynamic-import'] },
build: { assetsInlineLimit: 0 },
build: { assetsInlineLimit: 0, ...extra?.build },
...extra,
})

async function withBuild(base: string, fn: () => Promise<void>) {
const config = getConfig(base)
async function withBuild(
base: string,
fn: () => Promise<void>,
extra?: Partial<InlineConfig>,
) {
const config = getConfig(base, extra)
await build(config)
const server = await preview(config)

Expand Down Expand Up @@ -91,3 +99,42 @@ baseOptions.forEach(({ base, label }) => {
},
)
})

// Test: CSS from pure CSS chunks (via manualChunks) should preserve
// the original import order. When base.css is in a separate pure CSS
// chunk that was imported BEFORE page.css, base.css should appear
// before page.css in the cascade. This means page.css (green) should
// override base.css (red). (#3924, #6375)
test.runIf(isBuild)(
'css order is preserved when pure css chunks are created via manualChunks',
async () => {
const path = await import('node:path')
await withBuild(
'/',
async () => {
await page.waitForSelector('.async-order-el', { state: 'attached' })
// page.css (green) should override base.css (red) because
// page.js imports base.js first (which brings in base.css),
// then imports page.css — so page.css should come later in cascade
expect(await getColor('.async-order-el')).toBe('green')
},
{
build: {
assetsInlineLimit: 0,
rollupOptions: {
output: {
// Split base.css into its own chunk, creating a pure CSS chunk.
// This reproduces the bug where CSS from pure CSS chunks ends up
// after the importing chunk's own CSS in the cascade.
manualChunks(id) {
if (id.includes('async-order/base.css')) {
return 'async-base'
}
},
},
},
},
},
)
},
)
3 changes: 3 additions & 0 deletions playground/css-dynamic-import/async-order/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.async-order-el {
color: red;
}
5 changes: 5 additions & 0 deletions playground/css-dynamic-import/async-order/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import './base.css'

export function base() {
return 'base'
}
4 changes: 4 additions & 0 deletions playground/css-dynamic-import/async-order/page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* Same specificity as base.css - later in cascade should win */
.async-order-el {
color: green;
}
13 changes: 13 additions & 0 deletions playground/css-dynamic-import/async-order/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// base.css is imported as a standalone CSS import.
// When manualChunks splits it into a pure CSS chunk,
// the CSS ordering must still be preserved:
// base.css should come BEFORE page.css in the cascade.
import './base.css'
import './page.css'

export function render() {
const el = document.createElement('div')
el.className = 'async-order-el'
el.textContent = 'async order test'
document.body.appendChild(el)
}
5 changes: 5 additions & 0 deletions playground/css-dynamic-import/index.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<p class="css-dynamic-import">This should be green</p>

<script type="module" src="./index.js"></script>

<!-- CSS ordering test for pure CSS chunks (#3924, #6375) -->
<script type="module">
import('./async-order/page.js').then(({ render }) => render())
</script>
Loading