Skip to content

Commit d04500f

Browse files
committed
fix: invalidate CSS hashes on renderBuiltUrl change (fix #13996)
1 parent a8e28f9 commit d04500f

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,58 @@ describe('build', () => {
189189
expect(jsChunks1).not.toEqual(jsChunks2)
190190
})
191191

192+
test('file hash should change when renderBuiltUrl changes for CSS chunks', async () => {
193+
const buildProject = async (renderBuiltUrl: any) => {
194+
return (await build({
195+
root: resolve(dirname, 'packages/build-project'),
196+
logLevel: 'silent',
197+
build: {
198+
write: false,
199+
},
200+
experimental: {
201+
renderBuiltUrl,
202+
},
203+
plugins: [
204+
{
205+
name: 'test',
206+
resolveId(id) {
207+
if (
208+
id === 'entry.js' ||
209+
id === 'subentry.js' ||
210+
id === 'foo.css'
211+
) {
212+
return '\0' + id
213+
}
214+
},
215+
load(id) {
216+
if (id === '\0entry.js') {
217+
return `window.addEventListener('click', () => { import('subentry.js') });`
218+
}
219+
if (id === '\0subentry.js') {
220+
return `import 'foo.css'`
221+
}
222+
if (id === '\0foo.css') {
223+
return `.foo { color: red }`
224+
}
225+
},
226+
},
227+
],
228+
})) as RolldownOutput
229+
}
230+
const result = await Promise.all([
231+
buildProject((filename: string) => `/cdn-a/${filename}`),
232+
buildProject((filename: string) => `/cdn-b/${filename}`),
233+
])
234+
const cssAssets1 = result[0].output
235+
.filter((o) => o.type === 'asset' && o.fileName.endsWith('.css'))
236+
.map((o) => o.fileName)
237+
const cssAssets2 = result[1].output
238+
.filter((o) => o.type === 'asset' && o.fileName.endsWith('.css'))
239+
.map((o) => o.fileName)
240+
expect(cssAssets1.length).toBeGreaterThan(0)
241+
expect(cssAssets1).not.toEqual(cssAssets2)
242+
})
243+
192244
test.for([
193245
[true, true],
194246
[true, false],

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'node:fs'
22
import fsp from 'node:fs/promises'
33
import path from 'node:path'
4+
import { createHash } from 'node:crypto'
45
import { fileURLToPath, pathToFileURL } from 'node:url'
56
import postcssrc from 'postcss-load-config'
67
import type {
@@ -1133,7 +1134,9 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin {
11331134
)
11341135
for (const cssAsset of cssAssets) {
11351136
if (typeof cssAsset.source === 'string') {
1136-
cssAsset.source = cssAsset.source.replace(viteHashUpdateMarkerRE, '')
1137+
cssAsset.source = cssAsset.source
1138+
.replace(viteHashUpdateMarkerRE, '')
1139+
.replace(viteConfigHashMarkerRE, '')
11371140
}
11381141
}
11391142
},
@@ -1900,6 +1903,7 @@ function combineSourcemapsIfExists(
19001903

19011904
const viteHashUpdateMarker = '/*$vite$:1*/'
19021905
const viteHashUpdateMarkerRE = /\/\*\$vite\$:\d+\*\//
1906+
const viteConfigHashMarkerRE = /\/\*\$vite-config-hash\$:[0-9a-f]+\*\//
19031907

19041908
async function finalizeCss(css: string, config: ResolvedConfig) {
19051909
// hoist external @imports and @charset to the top of the CSS chunk per spec (#1845 and #6333)
@@ -1919,6 +1923,15 @@ async function finalizeCss(css: string, config: ResolvedConfig) {
19191923
// to avoid that happening, we inject an additional string so that a different hash is generated
19201924
// for the same CSS content
19211925
css += viteHashUpdateMarker
1926+
// Include renderBuiltUrl in the CSS content hash so that changing
1927+
// the function produces different asset filenames
1928+
if (config.experimental?.renderBuiltUrl) {
1929+
const hash = createHash('sha256')
1930+
.update(config.experimental.renderBuiltUrl.toString())
1931+
.digest('hex')
1932+
.substring(0, 8)
1933+
css += `/*$vite-config-hash$:${hash}*/`
1934+
}
19221935
return css
19231936
}
19241937

0 commit comments

Comments
 (0)