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) {
|
async renderChunk(code, chunk, opts) {
|
||||||
let chunkCSS = ''
|
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
|
let isPureCssChunk = true
|
||||||
const ids = Object.keys(chunk.modules)
|
const ids = Object.keys(chunk.modules)
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
@ -561,7 +563,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
|||||||
isPureCssChunk = false
|
isPureCssChunk = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!isJsChunkEmpty) {
|
||||||
// if the module does not have a style, then it's not a pure css chunk.
|
// 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
|
// this is true because in the `transform` hook above, only modules
|
||||||
// that are css gets added to the `styles` map.
|
// that are css gets added to the `styles` map.
|
||||||
@ -723,13 +725,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chunkCSS) {
|
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 (config.build.cssCodeSplit) {
|
||||||
if (opts.format === 'es' || opts.format === 'cjs') {
|
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 isEntry = chunk.isEntry && isPureCssChunk
|
||||||
const cssFullAssetName = ensureFileExt(chunk.name, '.css')
|
const cssFullAssetName = ensureFileExt(chunk.name, '.css')
|
||||||
// if facadeModuleId doesn't exist or doesn't have a CSS extension,
|
// if facadeModuleId doesn't exist or doesn't have a CSS extension,
|
||||||
@ -837,6 +839,40 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
|
|||||||
return
|
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
|
// remove empty css chunks and their imports
|
||||||
if (pureCssChunks.size) {
|
if (pureCssChunks.size) {
|
||||||
// map each pure css chunk (rendered chunk) to it's corresponding bundle
|
// 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`]
|
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,
|
findAssetFile,
|
||||||
getColor,
|
getColor,
|
||||||
isBuild,
|
isBuild,
|
||||||
|
listAssets,
|
||||||
page,
|
page,
|
||||||
readManifest,
|
readManifest,
|
||||||
untilUpdated,
|
untilUpdated,
|
||||||
@ -12,6 +13,7 @@ test('should load all stylesheets', async () => {
|
|||||||
expect(await getColor('h1')).toBe('red')
|
expect(await getColor('h1')).toBe('red')
|
||||||
expect(await getColor('h2')).toBe('blue')
|
expect(await getColor('h2')).toBe('blue')
|
||||||
expect(await getColor('.dynamic')).toBe('green')
|
expect(await getColor('.dynamic')).toBe('green')
|
||||||
|
expect(await getColor('.async-js')).toBe('blue')
|
||||||
expect(await getColor('.chunk')).toBe('magenta')
|
expect(await getColor('.chunk')).toBe('magenta')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -40,7 +42,12 @@ describe.runIf(isBuild)('build', () => {
|
|||||||
expect(findAssetFile(/style-.*\.js$/)).toBe('')
|
expect(findAssetFile(/style-.*\.js$/)).toBe('')
|
||||||
expect(findAssetFile('main.*.js$')).toMatch(`/* empty css`)
|
expect(findAssetFile('main.*.js$')).toMatch(`/* empty css`)
|
||||||
expect(findAssetFile('other.*.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 () => {
|
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>
|
<h2>This should be blue</h2>
|
||||||
|
|
||||||
<p class="dynamic">This should be green</p>
|
<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="inline">This should not be yellow</p>
|
||||||
<p class="dynamic-inline"></p>
|
<p class="dynamic-inline"></p>
|
||||||
<p class="mod">This should be yellow</p>
|
<p class="mod">This should be yellow</p>
|
||||||
|
@ -9,6 +9,7 @@ import chunkCssUrl from './chunk.css?url'
|
|||||||
globalThis.__test_chunkCssUrl = chunkCssUrl
|
globalThis.__test_chunkCssUrl = chunkCssUrl
|
||||||
|
|
||||||
import('./async.css')
|
import('./async.css')
|
||||||
|
import('./async-js')
|
||||||
|
|
||||||
import('./inline.css?inline').then((css) => {
|
import('./inline.css?inline').then((css) => {
|
||||||
document.querySelector('.dynamic-inline').textContent = css.default
|
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
|
specifier: ^1.24.1
|
||||||
version: 1.24.1
|
version: 1.24.1
|
||||||
|
|
||||||
|
playground/css-no-codesplit: {}
|
||||||
|
|
||||||
playground/css-sourcemap:
|
playground/css-sourcemap:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
less:
|
less:
|
||||||
|
Loading…
Reference in New Issue
Block a user