From e2e9c64b0575b5b073bb76f447d016304e697758 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 20 Oct 2026 14:53:57 +0200 Subject: [PATCH] fix(browser): detect shared chunks via the optimizer (#11661) --- packages/browser/src/node/index.ts | 26 +++++++- .../optimized-chunks-cache/first.test.ts | 5 ++ .../optimized-chunks-cache/second.test.ts | 5 ++ .../optimized-chunks-cache/vitest.config.ts | 14 ++++ .../specs/optimized-chunks-cache.test.ts | 66 +++++++++++++++++++ 5 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 test/browser/fixtures/optimized-chunks-cache/first.test.ts create mode 100644 test/browser/fixtures/optimized-chunks-cache/second.test.ts create mode 100644 test/browser/fixtures/optimized-chunks-cache/vitest.config.ts create mode 100644 test/browser/specs/optimized-chunks-cache.test.ts diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index e1cfc47ba..027314641 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -1,5 +1,10 @@ import type { ServerResponse } from 'node:http' -import type { HtmlTagDescriptor, UserConfig, UserConfig as ViteUserConfig } from 'vite' +import type { + HtmlTagDescriptor, + UserConfig, + UserConfig as ViteUserConfig, + ViteDevServer, +} from 'vite' import type { BrowserCommand, BrowserProviderOption, @@ -15,7 +20,7 @@ import { MockerRegistry } from '@vitest/mocker' import { interceptorPlugin, manualMockPlugin } from '@vitest/mocker/node' import { distClientRoot as uiClientRoot } from '@vitest/ui' import { cleanUrl, toArray } from '@vitest/utils/helpers' -import { join, resolve } from 'pathe' +import { basename, join, resolve } from 'pathe' import sirv from 'sirv' import c from 'tinyrainbow' import { @@ -56,6 +61,21 @@ export { const versionRegexp = /(?:\?|&)v=\w{8}/ +const optimizedChunkFiles = new WeakMap>() + +function isOptimizedChunk(server: ViteDevServer, url: string): boolean { + const chunks = server.environments.client.depsOptimizer?.metadata.chunks + if (!chunks) { + return false + } + let files = optimizedChunkFiles.get(chunks) + if (!files) { + files = new Set(Object.values(chunks).map((chunk) => basename(chunk.file))) + optimizedChunkFiles.set(chunks, files) + } + return files.has(basename(cleanUrl(url))) +} + // pin a Cache-Control value and stop Vite's later middleware from overriding it function pinCacheControl(res: ServerResponse, value: string): void { res.setHeader('Cache-Control', value) @@ -276,7 +296,7 @@ body { // them in _some_ tests, while keeping original modules in others; // there is no way to configure that in Vite, so we pin no-cache // and ignore the cache-control Vite sets in the next middleware - else if (versionRegexp.test(url) && !url.includes('chunk-') && interceptsMocks()) { + else if (versionRegexp.test(url) && !isOptimizedChunk(server, url) && interceptsMocks()) { pinCacheControl(res, 'no-cache') } } diff --git a/test/browser/fixtures/optimized-chunks-cache/first.test.ts b/test/browser/fixtures/optimized-chunks-cache/first.test.ts new file mode 100644 index 000000000..6cc21c324 --- /dev/null +++ b/test/browser/fixtures/optimized-chunks-cache/first.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('passes', () => { + expect(1).toBe(1) +}) diff --git a/test/browser/fixtures/optimized-chunks-cache/second.test.ts b/test/browser/fixtures/optimized-chunks-cache/second.test.ts new file mode 100644 index 000000000..6cc21c324 --- /dev/null +++ b/test/browser/fixtures/optimized-chunks-cache/second.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('passes', () => { + expect(1).toBe(1) +}) diff --git a/test/browser/fixtures/optimized-chunks-cache/vitest.config.ts b/test/browser/fixtures/optimized-chunks-cache/vitest.config.ts new file mode 100644 index 000000000..83682318a --- /dev/null +++ b/test/browser/fixtures/optimized-chunks-cache/vitest.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import { instances, provider } from '../../settings' + +export default defineConfig({ + cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)), + test: { + browser: { + enabled: true, + provider, + instances, + }, + }, +}) diff --git a/test/browser/specs/optimized-chunks-cache.test.ts b/test/browser/specs/optimized-chunks-cache.test.ts new file mode 100644 index 000000000..798296fe5 --- /dev/null +++ b/test/browser/specs/optimized-chunks-cache.test.ts @@ -0,0 +1,66 @@ +import type { Plugin } from 'vite' +import { basename } from 'pathe' +import { expect, test } from 'vitest' +import { runBrowserTests } from './utils' + +const versionedRequestRegexp = /[?&]v=\w{8}/ + +function recordDependencyResponses() { + const responses: { kind: 'chunk' | 'entry'; cacheControl: string }[] = [] + const plugin: Plugin = { + name: 'record-dependency-responses', + configureServer(server) { + server.middlewares.use((req, res, next) => { + res.on('finish', () => { + if (!req.url || !versionedRequestRegexp.test(req.url)) { + return + } + const chunks = server.environments.client.depsOptimizer?.metadata.chunks ?? {} + const chunkFiles = new Set(Object.values(chunks).map((chunk) => basename(chunk.file))) + const file = basename(new URL(req.url, 'http://localhost').pathname) + responses.push({ + kind: chunkFiles.has(file) ? 'chunk' : 'entry', + cacheControl: String(res.getHeader('cache-control')), + }) + }) + next() + }) + }, + } + return { plugin, responses } +} + +function summarize(responses: { kind: string; cacheControl: string }[]) { + const summary: Record = {} + for (const { kind, cacheControl } of responses) { + const values = (summary[kind] ??= []) + if (!values.includes(cacheControl)) { + values.push(cacheControl) + } + } + return summary +} + +test('shared optimizer chunks keep immutable caching when mocks are served by request interception', async () => { + const { plugin, responses } = recordDependencyResponses() + const { stderr } = await runBrowserTests( + { + root: './fixtures/optimized-chunks-cache', + browser: { importMapMocks: false }, + }, + undefined, + { plugins: [plugin] }, + ) + + expect(stderr).toReportNoErrors() + expect(summarize(responses)).toMatchInlineSnapshot(` + { + "chunk": [ + "max-age=31536000,immutable", + ], + "entry": [ + "no-cache", + ], + } + `) +}) -- 2.54.0