mirror of
https://github.com/vitejs/vite.git
synced 2024-11-21 14:48:41 +00:00
fix(css): avoid generating empty JS files when JS files becomes empty but has CSS files imported (#16078)
This commit is contained in:
parent
c9aa06a0f1
commit
95fe5a79c4
@ -549,6 +549,8 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
||||
|
||||
async renderChunk(code, chunk, opts) {
|
||||
let chunkCSS = ''
|
||||
// the chunk is empty if it's a dynamic entry chunk that only contains a CSS import
|
||||
const isJsChunkEmpty = code === '' && !chunk.isEntry
|
||||
let isPureCssChunk = true
|
||||
const ids = Object.keys(chunk.modules)
|
||||
for (const id of ids) {
|
||||
@ -561,7 +563,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
||||
isPureCssChunk = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (!isJsChunkEmpty) {
|
||||
// if the module does not have a style, then it's not a pure css chunk.
|
||||
// this is true because in the `transform` hook above, only modules
|
||||
// that are css gets added to the `styles` map.
|
||||
@ -723,13 +725,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
||||
}
|
||||
|
||||
if (chunkCSS) {
|
||||
if (isPureCssChunk && (opts.format === 'es' || opts.format === 'cjs')) {
|
||||
// this is a shared CSS-only chunk that is empty.
|
||||
pureCssChunks.add(chunk)
|
||||
}
|
||||
|
||||
if (config.build.cssCodeSplit) {
|
||||
if (opts.format === 'es' || opts.format === 'cjs') {
|
||||
if (isPureCssChunk) {
|
||||
// this is a shared CSS-only chunk that is empty.
|
||||
pureCssChunks.add(chunk)
|
||||
}
|
||||
|
||||
const isEntry = chunk.isEntry && isPureCssChunk
|
||||
const cssFullAssetName = ensureFileExt(chunk.name, '.css')
|
||||
// if facadeModuleId doesn't exist or doesn't have a CSS extension,
|
||||
@ -837,6 +839,40 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
||||
return
|
||||
}
|
||||
|
||||
function extractCss() {
|
||||
let css = ''
|
||||
const collected = new Set<OutputChunk>()
|
||||
const prelimaryNameToChunkMap = new Map(
|
||||
Object.values(bundle)
|
||||
.filter((chunk): chunk is OutputChunk => chunk.type === 'chunk')
|
||||
.map((chunk) => [chunk.preliminaryFileName, chunk]),
|
||||
)
|
||||
|
||||
function collect(fileName: string) {
|
||||
const chunk = bundle[fileName]
|
||||
if (!chunk || chunk.type !== 'chunk' || collected.has(chunk)) return
|
||||
collected.add(chunk)
|
||||
|
||||
chunk.imports.forEach(collect)
|
||||
css += chunkCSSMap.get(chunk.preliminaryFileName) ?? ''
|
||||
}
|
||||
|
||||
for (const chunkName of chunkCSSMap.keys())
|
||||
collect(prelimaryNameToChunkMap.get(chunkName)?.fileName ?? '')
|
||||
|
||||
return css
|
||||
}
|
||||
let extractedCss = !hasEmitted && extractCss()
|
||||
if (extractedCss) {
|
||||
hasEmitted = true
|
||||
extractedCss = await finalizeCss(extractedCss, true, config)
|
||||
this.emitFile({
|
||||
name: cssBundleName,
|
||||
type: 'asset',
|
||||
source: extractedCss,
|
||||
})
|
||||
}
|
||||
|
||||
// remove empty css chunks and their imports
|
||||
if (pureCssChunks.size) {
|
||||
// map each pure css chunk (rendered chunk) to it's corresponding bundle
|
||||
@ -893,40 +929,6 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
||||
delete bundle[`${fileName}.map`]
|
||||
})
|
||||
}
|
||||
|
||||
function extractCss() {
|
||||
let css = ''
|
||||
const collected = new Set<OutputChunk>()
|
||||
const prelimaryNameToChunkMap = new Map(
|
||||
Object.values(bundle)
|
||||
.filter((chunk): chunk is OutputChunk => chunk.type === 'chunk')
|
||||
.map((chunk) => [chunk.preliminaryFileName, chunk]),
|
||||
)
|
||||
|
||||
function collect(fileName: string) {
|
||||
const chunk = bundle[fileName]
|
||||
if (!chunk || chunk.type !== 'chunk' || collected.has(chunk)) return
|
||||
collected.add(chunk)
|
||||
|
||||
chunk.imports.forEach(collect)
|
||||
css += chunkCSSMap.get(chunk.preliminaryFileName) ?? ''
|
||||
}
|
||||
|
||||
for (const chunkName of chunkCSSMap.keys())
|
||||
collect(prelimaryNameToChunkMap.get(chunkName)?.fileName ?? '')
|
||||
|
||||
return css
|
||||
}
|
||||
let extractedCss = !hasEmitted && extractCss()
|
||||
if (extractedCss) {
|
||||
hasEmitted = true
|
||||
extractedCss = await finalizeCss(extractedCss, true, config)
|
||||
this.emitFile({
|
||||
name: cssBundleName,
|
||||
type: 'asset',
|
||||
source: extractedCss,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
findAssetFile,
|
||||
getColor,
|
||||
isBuild,
|
||||
listAssets,
|
||||
page,
|
||||
readManifest,
|
||||
untilUpdated,
|
||||
@ -12,6 +13,7 @@ test('should load all stylesheets', async () => {
|
||||
expect(await getColor('h1')).toBe('red')
|
||||
expect(await getColor('h2')).toBe('blue')
|
||||
expect(await getColor('.dynamic')).toBe('green')
|
||||
expect(await getColor('.async-js')).toBe('blue')
|
||||
expect(await getColor('.chunk')).toBe('magenta')
|
||||
})
|
||||
|
||||
@ -40,7 +42,12 @@ describe.runIf(isBuild)('build', () => {
|
||||
expect(findAssetFile(/style-.*\.js$/)).toBe('')
|
||||
expect(findAssetFile('main.*.js$')).toMatch(`/* empty css`)
|
||||
expect(findAssetFile('other.*.js$')).toMatch(`/* empty css`)
|
||||
expect(findAssetFile(/async.*\.js$/)).toBe('')
|
||||
expect(findAssetFile(/async-[-\w]{8}\.js$/)).toBe('')
|
||||
|
||||
const assets = listAssets()
|
||||
expect(assets).not.toContainEqual(
|
||||
expect.stringMatching(/async-js-[-\w]{8}\.js$/),
|
||||
)
|
||||
})
|
||||
|
||||
test('should remove empty chunk, HTML without JS', async () => {
|
||||
|
3
playground/css-codesplit/async-js.css
Normal file
3
playground/css-codesplit/async-js.css
Normal file
@ -0,0 +1,3 @@
|
||||
.async-js {
|
||||
color: blue;
|
||||
}
|
2
playground/css-codesplit/async-js.js
Normal file
2
playground/css-codesplit/async-js.js
Normal file
@ -0,0 +1,2 @@
|
||||
// a JS file that becomes an empty file but imports CSS files
|
||||
import './async-js.css'
|
@ -2,6 +2,7 @@
|
||||
<h2>This should be blue</h2>
|
||||
|
||||
<p class="dynamic">This should be green</p>
|
||||
<p class="async-js">This should be blue</p>
|
||||
<p class="inline">This should not be yellow</p>
|
||||
<p class="dynamic-inline"></p>
|
||||
<p class="mod">This should be yellow</p>
|
||||
|
@ -9,6 +9,7 @@ import chunkCssUrl from './chunk.css?url'
|
||||
globalThis.__test_chunkCssUrl = chunkCssUrl
|
||||
|
||||
import('./async.css')
|
||||
import('./async-js')
|
||||
|
||||
import('./inline.css?inline').then((css) => {
|
||||
document.querySelector('.dynamic-inline').textContent = css.default
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { expectWithRetry, getColor, isBuild, listAssets } from '~utils'
|
||||
|
||||
test('should load all stylesheets', async () => {
|
||||
expect(await getColor('.shared-linked')).toBe('blue')
|
||||
await expectWithRetry(() => getColor('.async-js')).toBe('blue')
|
||||
})
|
||||
|
||||
describe.runIf(isBuild)('build', () => {
|
||||
test('should remove empty chunk', async () => {
|
||||
const assets = listAssets()
|
||||
expect(assets).not.toContainEqual(
|
||||
expect.stringMatching(/shared-linked-.*\.js$/),
|
||||
)
|
||||
expect(assets).not.toContainEqual(expect.stringMatching(/async-js-.*\.js$/))
|
||||
})
|
||||
})
|
3
playground/css-no-codesplit/async-js.css
Normal file
3
playground/css-no-codesplit/async-js.css
Normal file
@ -0,0 +1,3 @@
|
||||
.async-js {
|
||||
color: blue;
|
||||
}
|
2
playground/css-no-codesplit/async-js.js
Normal file
2
playground/css-no-codesplit/async-js.js
Normal file
@ -0,0 +1,2 @@
|
||||
// a JS file that becomes an empty file but imports CSS files
|
||||
import './async-js.css'
|
5
playground/css-no-codesplit/index.html
Normal file
5
playground/css-no-codesplit/index.html
Normal file
@ -0,0 +1,5 @@
|
||||
<link rel="stylesheet" href="./shared-linked.css" />
|
||||
<script type="module" src="./index.js"></script>
|
||||
|
||||
<p class="shared-linked">shared linked: this should be blue</p>
|
||||
<p class="async-js">async JS importing CSS: this should be blue</p>
|
1
playground/css-no-codesplit/index.js
Normal file
1
playground/css-no-codesplit/index.js
Normal file
@ -0,0 +1 @@
|
||||
import('./async-js')
|
12
playground/css-no-codesplit/package.json
Normal file
12
playground/css-no-codesplit/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@vitejs/test-css-no-codesplit",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"debug": "node --inspect-brk ../../packages/vite/bin/vite",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
3
playground/css-no-codesplit/shared-linked.css
Normal file
3
playground/css-no-codesplit/shared-linked.css
Normal file
@ -0,0 +1,3 @@
|
||||
.shared-linked {
|
||||
color: blue;
|
||||
}
|
1
playground/css-no-codesplit/sub.html
Normal file
1
playground/css-no-codesplit/sub.html
Normal file
@ -0,0 +1 @@
|
||||
<link rel="stylesheet" href="./shared-linked.css" />
|
14
playground/css-no-codesplit/vite.config.js
Normal file
14
playground/css-no-codesplit/vite.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
cssCodeSplit: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, './index.html'),
|
||||
sub: resolve(__dirname, './sub.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
@ -613,6 +613,8 @@ importers:
|
||||
specifier: ^1.24.1
|
||||
version: 1.24.1
|
||||
|
||||
playground/css-no-codesplit: {}
|
||||
|
||||
playground/css-sourcemap:
|
||||
devDependencies:
|
||||
less:
|
||||
|
Loading…
Reference in New Issue
Block a user