Skip to content

Commit 7a97a04

Browse files
authored
fix(build): invalidate chunk hash when css changed (#11475)
1 parent 3d346c0 commit 7a97a04

File tree

3 files changed

+131
-2
lines changed

3 files changed

+131
-2
lines changed

packages/vite/src/node/__tests__/build.spec.ts

+118-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,107 @@ import { fileURLToPath } from 'node:url'
33
import colors from 'picocolors'
44
import type { Logger } from 'vite'
55
import { describe, expect, test, vi } from 'vitest'
6-
import type { OutputOptions } from 'rollup'
6+
import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup'
77
import type { LibraryFormats, LibraryOptions } from '../build'
8-
import { resolveBuildOutputs, resolveLibFilename } from '../build'
8+
import { build, resolveBuildOutputs, resolveLibFilename } from '../build'
99
import { createLogger } from '../logger'
1010

1111
const __dirname = resolve(fileURLToPath(import.meta.url), '..')
1212

1313
type FormatsToFileNames = [LibraryFormats, string][]
14+
15+
describe('build', () => {
16+
test('file hash should change when css changes for dynamic entries', async () => {
17+
const buildProject = async (cssColor: string) => {
18+
return (await build({
19+
root: resolve(__dirname, 'packages/build-project'),
20+
logLevel: 'silent',
21+
build: {
22+
write: false,
23+
},
24+
plugins: [
25+
{
26+
name: 'test',
27+
resolveId(id) {
28+
if (
29+
id === 'entry.js' ||
30+
id === 'subentry.js' ||
31+
id === 'foo.css'
32+
) {
33+
return '\0' + id
34+
}
35+
},
36+
load(id) {
37+
if (id === '\0entry.js') {
38+
return `window.addEventListener('click', () => { import('subentry.js') });`
39+
}
40+
if (id === '\0subentry.js') {
41+
return `import 'foo.css'`
42+
}
43+
if (id === '\0foo.css') {
44+
return `.foo { color: ${cssColor} }`
45+
}
46+
},
47+
},
48+
],
49+
})) as RollupOutput
50+
}
51+
const result = await Promise.all([
52+
buildProject('red'),
53+
buildProject('blue'),
54+
])
55+
assertOutputHashContentChange(result[0], result[1])
56+
})
57+
58+
test('file hash should change when pure css chunk changes', async () => {
59+
const buildProject = async (cssColor: string) => {
60+
return (await build({
61+
root: resolve(__dirname, 'packages/build-project'),
62+
logLevel: 'silent',
63+
build: {
64+
write: false,
65+
},
66+
plugins: [
67+
{
68+
name: 'test',
69+
resolveId(id) {
70+
if (
71+
id === 'entry.js' ||
72+
id === 'foo.js' ||
73+
id === 'bar.js' ||
74+
id === 'baz.js' ||
75+
id === 'foo.css' ||
76+
id === 'bar.css' ||
77+
id === 'baz.css'
78+
) {
79+
return '\0' + id
80+
}
81+
},
82+
load(id) {
83+
if (id === '\0entry.js') {
84+
return `
85+
window.addEventListener('click', () => { import('foo.js') });
86+
window.addEventListener('click', () => { import('bar.js') });`
87+
}
88+
if (id === '\0foo.js') return `import 'foo.css'; import 'baz.js'`
89+
if (id === '\0bar.js') return `import 'bar.css'; import 'baz.js'`
90+
if (id === '\0baz.js') return `import 'baz.css'`
91+
if (id === '\0foo.css') return `.foo { color: red }`
92+
if (id === '\0bar.css') return `.foo { color: green }`
93+
if (id === '\0baz.css') return `.foo { color: ${cssColor} }`
94+
},
95+
},
96+
],
97+
})) as RollupOutput
98+
}
99+
const result = await Promise.all([
100+
buildProject('yellow'),
101+
buildProject('blue'),
102+
])
103+
assertOutputHashContentChange(result[0], result[1])
104+
})
105+
})
106+
14107
const baseLibOptions: LibraryOptions = {
15108
fileName: 'my-lib',
16109
entry: 'mylib.js',
@@ -439,3 +532,26 @@ describe('resolveBuildOutputs', () => {
439532
)
440533
})
441534
})
535+
536+
/**
537+
* for each chunks in output1, if there's a chunk in output2 with the same fileName,
538+
* ensure that the chunk code is the same. if not, the chunk hash should have changed.
539+
*/
540+
function assertOutputHashContentChange(
541+
output1: RollupOutput,
542+
output2: RollupOutput,
543+
) {
544+
for (const chunk of output1.output) {
545+
if (chunk.type === 'chunk') {
546+
const chunk2 = output2.output.find(
547+
(c) => c.type === 'chunk' && c.fileName === chunk.fileName,
548+
) as OutputChunk | undefined
549+
if (chunk2) {
550+
expect(
551+
chunk.code,
552+
`the ${chunk.fileName} chunk has the same hash but different contents between builds`,
553+
).toEqual(chunk2.code)
554+
}
555+
}
556+
}
557+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<h1>Hello world</h1>
2+
3+
<script type="module" src="entry.js"></script>

packages/vite/src/node/plugins/css.ts

+10
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,16 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
622622
return null
623623
},
624624

625+
augmentChunkHash(chunk) {
626+
if (chunk.viteMetadata?.importedCss.size) {
627+
let hash = ''
628+
for (const id of chunk.viteMetadata.importedCss) {
629+
hash += id
630+
}
631+
return hash
632+
}
633+
},
634+
625635
async generateBundle(opts, bundle) {
626636
// @ts-expect-error asset emits are skipped in legacy bundle
627637
if (opts.__vite_skip_asset_emit__) {

0 commit comments

Comments
 (0)