From 7a97a04b2a39e2c50aff8fe4ef3ca2e82fca6184 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Fri, 23 Dec 2022 22:51:41 +0800 Subject: [PATCH] fix(build): invalidate chunk hash when css changed (#11475) --- .../vite/src/node/__tests__/build.spec.ts | 120 +++++++++++++++++- .../packages/build-project/index.html | 3 + packages/vite/src/node/plugins/css.ts | 10 ++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 packages/vite/src/node/__tests__/packages/build-project/index.html diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index 4e65a2848602d3..94ebb685b7b7cb 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -3,14 +3,107 @@ import { fileURLToPath } from 'node:url' import colors from 'picocolors' import type { Logger } from 'vite' import { describe, expect, test, vi } from 'vitest' -import type { OutputOptions } from 'rollup' +import type { OutputChunk, OutputOptions, RollupOutput } from 'rollup' import type { LibraryFormats, LibraryOptions } from '../build' -import { resolveBuildOutputs, resolveLibFilename } from '../build' +import { build, resolveBuildOutputs, resolveLibFilename } from '../build' import { createLogger } from '../logger' const __dirname = resolve(fileURLToPath(import.meta.url), '..') type FormatsToFileNames = [LibraryFormats, string][] + +describe('build', () => { + test('file hash should change when css changes for dynamic entries', async () => { + const buildProject = async (cssColor: string) => { + return (await build({ + root: resolve(__dirname, 'packages/build-project'), + logLevel: 'silent', + build: { + write: false, + }, + plugins: [ + { + name: 'test', + resolveId(id) { + if ( + id === 'entry.js' || + id === 'subentry.js' || + id === 'foo.css' + ) { + return '\0' + id + } + }, + load(id) { + if (id === '\0entry.js') { + return `window.addEventListener('click', () => { import('subentry.js') });` + } + if (id === '\0subentry.js') { + return `import 'foo.css'` + } + if (id === '\0foo.css') { + return `.foo { color: ${cssColor} }` + } + }, + }, + ], + })) as RollupOutput + } + const result = await Promise.all([ + buildProject('red'), + buildProject('blue'), + ]) + assertOutputHashContentChange(result[0], result[1]) + }) + + test('file hash should change when pure css chunk changes', async () => { + const buildProject = async (cssColor: string) => { + return (await build({ + root: resolve(__dirname, 'packages/build-project'), + logLevel: 'silent', + build: { + write: false, + }, + plugins: [ + { + name: 'test', + resolveId(id) { + if ( + id === 'entry.js' || + id === 'foo.js' || + id === 'bar.js' || + id === 'baz.js' || + id === 'foo.css' || + id === 'bar.css' || + id === 'baz.css' + ) { + return '\0' + id + } + }, + load(id) { + if (id === '\0entry.js') { + return ` + window.addEventListener('click', () => { import('foo.js') }); + window.addEventListener('click', () => { import('bar.js') });` + } + if (id === '\0foo.js') return `import 'foo.css'; import 'baz.js'` + if (id === '\0bar.js') return `import 'bar.css'; import 'baz.js'` + if (id === '\0baz.js') return `import 'baz.css'` + if (id === '\0foo.css') return `.foo { color: red }` + if (id === '\0bar.css') return `.foo { color: green }` + if (id === '\0baz.css') return `.foo { color: ${cssColor} }` + }, + }, + ], + })) as RollupOutput + } + const result = await Promise.all([ + buildProject('yellow'), + buildProject('blue'), + ]) + assertOutputHashContentChange(result[0], result[1]) + }) +}) + const baseLibOptions: LibraryOptions = { fileName: 'my-lib', entry: 'mylib.js', @@ -439,3 +532,26 @@ describe('resolveBuildOutputs', () => { ) }) }) + +/** + * for each chunks in output1, if there's a chunk in output2 with the same fileName, + * ensure that the chunk code is the same. if not, the chunk hash should have changed. + */ +function assertOutputHashContentChange( + output1: RollupOutput, + output2: RollupOutput, +) { + for (const chunk of output1.output) { + if (chunk.type === 'chunk') { + const chunk2 = output2.output.find( + (c) => c.type === 'chunk' && c.fileName === chunk.fileName, + ) as OutputChunk | undefined + if (chunk2) { + expect( + chunk.code, + `the ${chunk.fileName} chunk has the same hash but different contents between builds`, + ).toEqual(chunk2.code) + } + } + } +} diff --git a/packages/vite/src/node/__tests__/packages/build-project/index.html b/packages/vite/src/node/__tests__/packages/build-project/index.html new file mode 100644 index 00000000000000..f05e0c91dbb125 --- /dev/null +++ b/packages/vite/src/node/__tests__/packages/build-project/index.html @@ -0,0 +1,3 @@ +

Hello world

+ + diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 9834858bf1a878..142a696662f5e3 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -622,6 +622,16 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { return null }, + augmentChunkHash(chunk) { + if (chunk.viteMetadata?.importedCss.size) { + let hash = '' + for (const id of chunk.viteMetadata.importedCss) { + hash += id + } + return hash + } + }, + async generateBundle(opts, bundle) { // @ts-expect-error asset emits are skipped in legacy bundle if (opts.__vite_skip_asset_emit__) {