Skip to content

Commit e2e9c64

Browse files
authored
fix(browser): detect shared chunks via the optimizer (#11661)
1 parent 0c4c74e commit e2e9c64

5 files changed

Lines changed: 113 additions & 3 deletions

File tree

‎packages/browser/src/node/index.ts‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { ServerResponse } from 'node:http'
2-
import type { HtmlTagDescriptor, UserConfig, UserConfig as ViteUserConfig } from 'vite'
2+
import type {
3+
HtmlTagDescriptor,
4+
UserConfig,
5+
UserConfig as ViteUserConfig,
6+
ViteDevServer,
7+
} from 'vite'
38
import type {
49
BrowserCommand,
510
BrowserProviderOption,
@@ -15,7 +20,7 @@ import { MockerRegistry } from '@vitest/mocker'
1520
import { interceptorPlugin, manualMockPlugin } from '@vitest/mocker/node'
1621
import { distClientRoot as uiClientRoot } from '@vitest/ui'
1722
import { cleanUrl, toArray } from '@vitest/utils/helpers'
18-
import { join, resolve } from 'pathe'
23+
import { basename, join, resolve } from 'pathe'
1924
import sirv from 'sirv'
2025
import c from 'tinyrainbow'
2126
import {
@@ -56,6 +61,21 @@ export {
5661

5762
const versionRegexp = /(?:\?|&)v=\w{8}/
5863

64+
const optimizedChunkFiles = new WeakMap<object, Set<string>>()
65+
66+
function isOptimizedChunk(server: ViteDevServer, url: string): boolean {
67+
const chunks = server.environments.client.depsOptimizer?.metadata.chunks
68+
if (!chunks) {
69+
return false
70+
}
71+
let files = optimizedChunkFiles.get(chunks)
72+
if (!files) {
73+
files = new Set(Object.values(chunks).map((chunk) => basename(chunk.file)))
74+
optimizedChunkFiles.set(chunks, files)
75+
}
76+
return files.has(basename(cleanUrl(url)))
77+
}
78+
5979
// pin a Cache-Control value and stop Vite's later middleware from overriding it
6080
function pinCacheControl(res: ServerResponse, value: string): void {
6181
res.setHeader('Cache-Control', value)
@@ -276,7 +296,7 @@ body {
276296
// them in _some_ tests, while keeping original modules in others;
277297
// there is no way to configure that in Vite, so we pin no-cache
278298
// and ignore the cache-control Vite sets in the next middleware
279-
else if (versionRegexp.test(url) && !url.includes('chunk-') && interceptsMocks()) {
299+
else if (versionRegexp.test(url) && !isOptimizedChunk(server, url) && interceptsMocks()) {
280300
pinCacheControl(res, 'no-cache')
281301
}
282302
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('passes', () => {
4+
expect(1).toBe(1)
5+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('passes', () => {
4+
expect(1).toBe(1)
5+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { defineConfig } from 'vitest/config'
3+
import { instances, provider } from '../../settings'
4+
5+
export default defineConfig({
6+
cacheDir: fileURLToPath(new URL('./node_modules/.vite', import.meta.url)),
7+
test: {
8+
browser: {
9+
enabled: true,
10+
provider,
11+
instances,
12+
},
13+
},
14+
})
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Plugin } from 'vite'
2+
import { basename } from 'pathe'
3+
import { expect, test } from 'vitest'
4+
import { runBrowserTests } from './utils'
5+
6+
const versionedRequestRegexp = /[?&]v=\w{8}/
7+
8+
function recordDependencyResponses() {
9+
const responses: { kind: 'chunk' | 'entry'; cacheControl: string }[] = []
10+
const plugin: Plugin = {
11+
name: 'record-dependency-responses',
12+
configureServer(server) {
13+
server.middlewares.use((req, res, next) => {
14+
res.on('finish', () => {
15+
if (!req.url || !versionedRequestRegexp.test(req.url)) {
16+
return
17+
}
18+
const chunks = server.environments.client.depsOptimizer?.metadata.chunks ?? {}
19+
const chunkFiles = new Set(Object.values(chunks).map((chunk) => basename(chunk.file)))
20+
const file = basename(new URL(req.url, 'http://localhost').pathname)
21+
responses.push({
22+
kind: chunkFiles.has(file) ? 'chunk' : 'entry',
23+
cacheControl: String(res.getHeader('cache-control')),
24+
})
25+
})
26+
next()
27+
})
28+
},
29+
}
30+
return { plugin, responses }
31+
}
32+
33+
function summarize(responses: { kind: string; cacheControl: string }[]) {
34+
const summary: Record<string, string[]> = {}
35+
for (const { kind, cacheControl } of responses) {
36+
const values = (summary[kind] ??= [])
37+
if (!values.includes(cacheControl)) {
38+
values.push(cacheControl)
39+
}
40+
}
41+
return summary
42+
}
43+
44+
test('shared optimizer chunks keep immutable caching when mocks are served by request interception', async () => {
45+
const { plugin, responses } = recordDependencyResponses()
46+
const { stderr } = await runBrowserTests(
47+
{
48+
root: './fixtures/optimized-chunks-cache',
49+
browser: { importMapMocks: false },
50+
},
51+
undefined,
52+
{ plugins: [plugin] },
53+
)
54+
55+
expect(stderr).toReportNoErrors()
56+
expect(summarize(responses)).toMatchInlineSnapshot(`
57+
{
58+
"chunk": [
59+
"max-age=31536000,immutable",
60+
],
61+
"entry": [
62+
"no-cache",
63+
],
64+
}
65+
`)
66+
})

0 commit comments

Comments
 (0)