msw 3.0.2 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -35
- package/lib/_chunks/cookie-store.js +3 -2
- package/lib/_chunks/cookie-store.js.map +1 -1
- package/lib/_chunks/interceptor-source.d.ts +5 -1
- package/lib/_chunks/interceptor-source.js +18 -5
- package/lib/_chunks/interceptor-source.js.map +1 -1
- package/lib/_chunks/request-handler.d.ts +3 -2
- package/lib/browser/index.d.ts +4 -9
- package/lib/browser/index.js +22 -15
- package/lib/browser/index.js.map +1 -1
- package/lib/graphql/index.d.ts +19 -4
- package/lib/graphql/index.js +24 -12
- package/lib/graphql/index.js.map +1 -1
- package/lib/iife/index.js +68 -35
- package/lib/iife/index.js.map +1 -1
- package/lib/mockServiceWorker.js +152 -12
- package/lib/utils/is-common-asset-request.js +1 -1
- package/lib/utils/is-common-asset-request.js.map +1 -1
- package/package.json +2 -3
- package/src/browser/default-network-options.ts +1 -1
- package/src/browser/setup-worker.ts +1 -4
- package/src/browser/sources/fallback-http-source.ts +11 -8
- package/src/browser/sources/service-worker-source.ts +12 -5
- package/src/browser/utils/should-invalidate-worker.test.ts +0 -16
- package/src/core/experimental/define-network.ts +1 -1
- package/src/core/experimental/handlers-controller.test.ts +61 -0
- package/src/core/experimental/handlers-controller.ts +39 -6
- package/src/core/experimental/sources/network-source.ts +8 -1
- package/src/core/handlers/handler.ts +3 -2
- package/src/graphql/graphql-subscription.ts +30 -12
- package/src/mockServiceWorker.js +150 -10
- package/src/utils/is-common-asset-request.test.ts +371 -0
- package/src/utils/is-common-asset-request.ts +1 -1
|
@@ -585,24 +585,32 @@ export class GraphQLSubscriptionTransportHandler extends GraphQLWebSocketHandler
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
/**
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
|
|
588
|
+
* Remove the given handler from the subscribers of every session
|
|
589
|
+
* this transport participates in. The handler no longer matches
|
|
590
|
+
* subscriptions on the connections opened while it was registered.
|
|
591
|
+
*/
|
|
592
|
+
public unsubscribe(handler: WebSocketHandler): void {
|
|
593
|
+
for (const connection of connections.values()) {
|
|
594
|
+
connection.subscribers.delete(handler)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* End the active subscriptions of the sessions this transport
|
|
600
|
+
* participates in. The sessions and their subscribers are left intact:
|
|
601
|
+
* the handlers that survive the reset keep serving the connections
|
|
602
|
+
* opened before it, while the removed handlers unsubscribe themselves
|
|
603
|
+
* on disposal. The sessions own the protocol listeners for as long as
|
|
604
|
+
* the client stays connected.
|
|
592
605
|
*
|
|
593
606
|
* @note This method is invoked automatically when the handlers
|
|
594
607
|
* controller resets the handlers (e.g. `server.resetHandlers()`).
|
|
595
608
|
*/
|
|
596
609
|
public reset(): void {
|
|
597
610
|
for (const connection of connections.values()) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (entry.transport === this) {
|
|
602
|
-
connection.subscribers.delete(handler)
|
|
603
|
-
ownsConnection = true
|
|
604
|
-
}
|
|
605
|
-
}
|
|
611
|
+
const ownsConnection = Array.from(connection.subscribers.values()).some(
|
|
612
|
+
(entry) => entry.transport === this,
|
|
613
|
+
)
|
|
606
614
|
|
|
607
615
|
// Resetting the handlers detaches the resolvers from their
|
|
608
616
|
// subscriptions, so run their cleanups instead of dropping them.
|
|
@@ -935,6 +943,16 @@ export class GraphQLSubscriptionHandler<
|
|
|
935
943
|
this.isUsed = false
|
|
936
944
|
}
|
|
937
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Stop serving the connections this handler was bound to.
|
|
948
|
+
* @note This method is invoked automatically when the handler
|
|
949
|
+
* is removed from the network, either by resetting the handlers
|
|
950
|
+
* (e.g. `server.resetHandlers()`) or by disabling the network.
|
|
951
|
+
*/
|
|
952
|
+
public dispose(): void {
|
|
953
|
+
this.#transport.unsubscribe(this)
|
|
954
|
+
}
|
|
955
|
+
|
|
938
956
|
/**
|
|
939
957
|
* @note Individual subscription handlers stay silent. The subscription
|
|
940
958
|
* transport owns the GraphQL-aware logging for the entire connection
|
package/src/mockServiceWorker.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
const PACKAGE_VERSION = '<PACKAGE_VERSION>'
|
|
11
11
|
const INTEGRITY_CHECKSUM = '<INTEGRITY_CHECKSUM>'
|
|
12
12
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
|
13
|
+
// Cache Storage is shared by the whole origin. Scope the cache name
|
|
14
|
+
// to this registration so multiple workers do not overwrite each other.
|
|
15
|
+
const ACTIVE_CLIENTS_CACHE_NAME = `msw:active-clients:${self.registration.scope}`
|
|
16
|
+
const ACTIVE_CLIENTS_CACHE_KEY = '/__msw__/active-clients'
|
|
13
17
|
|
|
14
18
|
const activeClientIds = new Set()
|
|
15
19
|
/**
|
|
@@ -17,6 +21,23 @@ const activeClientIds = new Set()
|
|
|
17
21
|
*/
|
|
18
22
|
const pendingRequests = new Map()
|
|
19
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The browser may terminate this worker at any point (idle timeout,
|
|
26
|
+
* hidden page throttling, event timeout) and evaluate it again on the
|
|
27
|
+
* next event, losing all in-memory state. Restore the active client IDs
|
|
28
|
+
* from the storage so the worker keeps handling requests after that.
|
|
29
|
+
*/
|
|
30
|
+
let hasRestoredActiveClientIds = false
|
|
31
|
+
let isStoredActiveClientIdsOutdated = false
|
|
32
|
+
let activeClientIdsStorePromise = Promise.resolve()
|
|
33
|
+
const activeClientIdsRestorePromise = restoreActiveClientIds()
|
|
34
|
+
.catch((error) => {
|
|
35
|
+
console.error('[MSW] Failed to restore the active clients:', error)
|
|
36
|
+
})
|
|
37
|
+
.finally(() => {
|
|
38
|
+
hasRestoredActiveClientIds = true
|
|
39
|
+
})
|
|
40
|
+
|
|
20
41
|
addEventListener('install', function () {
|
|
21
42
|
self.skipWaiting()
|
|
22
43
|
})
|
|
@@ -34,12 +55,17 @@ addEventListener('message', function (event) {
|
|
|
34
55
|
|
|
35
56
|
event.waitUntil(
|
|
36
57
|
(async () => {
|
|
58
|
+
// Never mutate the active clients before they are restored,
|
|
59
|
+
// otherwise storing them would persist an incomplete set.
|
|
60
|
+
await activeClientIdsRestorePromise
|
|
61
|
+
|
|
37
62
|
if (event.data === 'CLIENT_CLOSE') {
|
|
38
63
|
const allClients = await self.clients.matchAll({
|
|
39
64
|
type: 'window',
|
|
40
65
|
})
|
|
41
66
|
|
|
42
67
|
activeClientIds.delete(clientId)
|
|
68
|
+
await storeActiveClientIds()
|
|
43
69
|
|
|
44
70
|
// Await any pending requests from the closing client.
|
|
45
71
|
// This makes sure that those requests are handled and not passthrough.
|
|
@@ -55,13 +81,14 @@ addEventListener('message', function (event) {
|
|
|
55
81
|
|
|
56
82
|
// Unregister itself when there are no more clients
|
|
57
83
|
if (remainingClients.length === 0) {
|
|
84
|
+
await clearStoredActiveClientIds()
|
|
58
85
|
await self.registration.unregister()
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
const client = await self.clients.get(clientId)
|
|
62
89
|
|
|
63
90
|
if (client != null) {
|
|
64
|
-
|
|
91
|
+
notifyClient(client, {
|
|
65
92
|
type: 'CLIENT_CLOSED',
|
|
66
93
|
})
|
|
67
94
|
}
|
|
@@ -83,14 +110,20 @@ addEventListener('message', function (event) {
|
|
|
83
110
|
|
|
84
111
|
switch (event.data) {
|
|
85
112
|
case 'KEEPALIVE_REQUEST': {
|
|
86
|
-
|
|
113
|
+
// Retry a failed store on the next keepalive so the storage
|
|
114
|
+
// catches up with the in-memory state before the worker restarts.
|
|
115
|
+
if (isStoredActiveClientIdsOutdated) {
|
|
116
|
+
await storeActiveClientIds()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
notifyClient(client, {
|
|
87
120
|
type: 'KEEPALIVE_RESPONSE',
|
|
88
121
|
})
|
|
89
122
|
break
|
|
90
123
|
}
|
|
91
124
|
|
|
92
125
|
case 'INTEGRITY_CHECK_REQUEST': {
|
|
93
|
-
|
|
126
|
+
notifyClient(client, {
|
|
94
127
|
type: 'INTEGRITY_CHECK_RESPONSE',
|
|
95
128
|
payload: {
|
|
96
129
|
packageVersion: PACKAGE_VERSION,
|
|
@@ -102,8 +135,9 @@ addEventListener('message', function (event) {
|
|
|
102
135
|
|
|
103
136
|
case 'MOCK_ACTIVATE': {
|
|
104
137
|
activeClientIds.add(clientId)
|
|
138
|
+
await storeActiveClientIds()
|
|
105
139
|
|
|
106
|
-
|
|
140
|
+
notifyClient(client, {
|
|
107
141
|
type: 'MOCKING_ENABLED',
|
|
108
142
|
payload: {
|
|
109
143
|
client: {
|
|
@@ -129,6 +163,22 @@ addEventListener('fetch', function (event) {
|
|
|
129
163
|
return
|
|
130
164
|
}
|
|
131
165
|
|
|
166
|
+
// This request may be the one waking the worker up after the browser
|
|
167
|
+
// has terminated it. Wait for the active clients to be restored
|
|
168
|
+
// before deciding whether to handle the request.
|
|
169
|
+
if (!hasRestoredActiveClientIds) {
|
|
170
|
+
event.respondWith(
|
|
171
|
+
activeClientIdsRestorePromise.then(() => {
|
|
172
|
+
if (activeClientIds.size === 0) {
|
|
173
|
+
return fetch(event.request)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return handleRequest(event)
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
132
182
|
// Bypass all requests when there are no active clients.
|
|
133
183
|
// Prevents the self-unregistered worked from handling requests
|
|
134
184
|
// after it's been terminated (still remains active until the next reload).
|
|
@@ -136,15 +186,85 @@ addEventListener('fetch', function (event) {
|
|
|
136
186
|
return
|
|
137
187
|
}
|
|
138
188
|
|
|
139
|
-
|
|
140
|
-
event.respondWith(handleRequest(event, requestId))
|
|
189
|
+
event.respondWith(handleRequest(event))
|
|
141
190
|
})
|
|
142
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Populate the active client IDs from the storage,
|
|
194
|
+
* ignoring the clients that no longer exist.
|
|
195
|
+
*/
|
|
196
|
+
async function restoreActiveClientIds() {
|
|
197
|
+
const storedClientIds = await readStoredActiveClientIds()
|
|
198
|
+
|
|
199
|
+
if (storedClientIds.length === 0) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const windowClients = await self.clients.matchAll({
|
|
204
|
+
type: 'window',
|
|
205
|
+
})
|
|
206
|
+
const windowClientIds = new Set(windowClients.map((client) => client.id))
|
|
207
|
+
|
|
208
|
+
for (const clientId of storedClientIds) {
|
|
209
|
+
if (windowClientIds.has(clientId)) {
|
|
210
|
+
activeClientIds.add(clientId)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @returns {Promise<Array<string>>}
|
|
217
|
+
*/
|
|
218
|
+
async function readStoredActiveClientIds() {
|
|
219
|
+
const cache = await caches.open(ACTIVE_CLIENTS_CACHE_NAME)
|
|
220
|
+
const response = await cache.match(ACTIVE_CLIENTS_CACHE_KEY)
|
|
221
|
+
|
|
222
|
+
if (!response) {
|
|
223
|
+
return []
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return response.json()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Storing is best-effort: a failure must never prevent the client
|
|
231
|
+
* from being activated or closed, in-memory state is still correct.
|
|
232
|
+
* Stores are serialized so each write persists the latest state
|
|
233
|
+
* and an earlier success cannot clear the flag of a later failure.
|
|
234
|
+
*/
|
|
235
|
+
function storeActiveClientIds() {
|
|
236
|
+
activeClientIdsStorePromise =
|
|
237
|
+
activeClientIdsStorePromise.then(writeActiveClientIds)
|
|
238
|
+
return activeClientIdsStorePromise
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function writeActiveClientIds() {
|
|
242
|
+
try {
|
|
243
|
+
const cache = await caches.open(ACTIVE_CLIENTS_CACHE_NAME)
|
|
244
|
+
await cache.put(
|
|
245
|
+
ACTIVE_CLIENTS_CACHE_KEY,
|
|
246
|
+
new Response(JSON.stringify(Array.from(activeClientIds))),
|
|
247
|
+
)
|
|
248
|
+
isStoredActiveClientIdsOutdated = false
|
|
249
|
+
} catch (error) {
|
|
250
|
+
isStoredActiveClientIdsOutdated = true
|
|
251
|
+
console.error('[MSW] Failed to store the active clients:', error)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function clearStoredActiveClientIds() {
|
|
256
|
+
try {
|
|
257
|
+
await caches.delete(ACTIVE_CLIENTS_CACHE_NAME)
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('[MSW] Failed to clear the stored active clients:', error)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
143
263
|
/**
|
|
144
264
|
* @param {FetchEvent} event
|
|
145
|
-
* @param {string} requestId
|
|
146
265
|
*/
|
|
147
|
-
async function handleRequest(event
|
|
266
|
+
async function handleRequest(event) {
|
|
267
|
+
const requestId = crypto.randomUUID()
|
|
148
268
|
const client = await resolveMainClient(event)
|
|
149
269
|
const requestCloneForEvents = event.request.clone()
|
|
150
270
|
|
|
@@ -172,7 +292,7 @@ async function handleRequest(event, requestId) {
|
|
|
172
292
|
// request failed with a network error). Notify the client so it
|
|
173
293
|
// can release the resources associated with this request.
|
|
174
294
|
if (client && activeClientIds.has(client.id)) {
|
|
175
|
-
|
|
295
|
+
notifyClient(client, {
|
|
176
296
|
type: 'REQUEST_ERROR',
|
|
177
297
|
payload: {
|
|
178
298
|
request: {
|
|
@@ -205,7 +325,7 @@ async function handleRequest(event, requestId) {
|
|
|
205
325
|
// Clone the response so both the client and the library could consume it.
|
|
206
326
|
const responseClone = isEventStreamResponse ? null : response.clone()
|
|
207
327
|
|
|
208
|
-
|
|
328
|
+
notifyClient(
|
|
209
329
|
client,
|
|
210
330
|
{
|
|
211
331
|
type: 'RESPONSE',
|
|
@@ -402,6 +522,26 @@ function sendToClient(client, message, transferrables = []) {
|
|
|
402
522
|
})
|
|
403
523
|
}
|
|
404
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Post a one-way message to the client.
|
|
527
|
+
* Unlike `sendToClient`, this does not open a MessageChannel or wait
|
|
528
|
+
* for a reply. The client never acknowledges these messages, so awaiting
|
|
529
|
+
* them keeps the extended event pending until the browser terminates
|
|
530
|
+
* the worker on its event timeout (5 minutes in Chromium), wiping its state.
|
|
531
|
+
* @param {Client} client
|
|
532
|
+
* @param {any} message
|
|
533
|
+
* @param {Array<Transferable>} transferrables
|
|
534
|
+
*/
|
|
535
|
+
function notifyClient(client, message, transferrables = []) {
|
|
536
|
+
try {
|
|
537
|
+
client.postMessage(message, transferrables.filter(Boolean))
|
|
538
|
+
} catch (error) {
|
|
539
|
+
// A one-way message must never fail its caller (e.g. a transferable
|
|
540
|
+
// that has already been detached must not reject a resolved request).
|
|
541
|
+
console.error('[MSW] Failed to notify the client:', error)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
405
545
|
/**
|
|
406
546
|
* @param {Response} response
|
|
407
547
|
* @param {FetchEvent} event
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { isCommonAssetRequest } from './is-common-asset-request'
|
|
3
|
+
|
|
4
|
+
test('treats requests over the "file:" protocol as common assets', () => {
|
|
5
|
+
expect(
|
|
6
|
+
isCommonAssetRequest(new Request('file:///Users/me/app/index.js')),
|
|
7
|
+
).toBe(true)
|
|
8
|
+
expect(isCommonAssetRequest(new Request('file:///Users/me/app/data'))).toBe(
|
|
9
|
+
true,
|
|
10
|
+
)
|
|
11
|
+
expect(isCommonAssetRequest(new Request('file:///etc/hosts'))).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('does not treat requests over other protocols as common assets by default', () => {
|
|
15
|
+
expect(isCommonAssetRequest(new Request('http://localhost/api/user'))).toBe(
|
|
16
|
+
false,
|
|
17
|
+
)
|
|
18
|
+
expect(isCommonAssetRequest(new Request('https://localhost/api/user'))).toBe(
|
|
19
|
+
false,
|
|
20
|
+
)
|
|
21
|
+
expect(isCommonAssetRequest(new Request('ftp://localhost/api/user'))).toBe(
|
|
22
|
+
false,
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('treats requests to Google Fonts as common assets', () => {
|
|
27
|
+
expect(
|
|
28
|
+
isCommonAssetRequest(
|
|
29
|
+
new Request('https://fonts.googleapis.com/css2?family=Inter'),
|
|
30
|
+
),
|
|
31
|
+
).toBe(true)
|
|
32
|
+
expect(
|
|
33
|
+
isCommonAssetRequest(new Request('https://fonts.googleapis.com/')),
|
|
34
|
+
).toBe(true)
|
|
35
|
+
expect(
|
|
36
|
+
isCommonAssetRequest(new Request('http://fonts.googleapis.com/icon')),
|
|
37
|
+
).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('does not treat requests to other Google hosts as common assets', () => {
|
|
41
|
+
expect(
|
|
42
|
+
isCommonAssetRequest(new Request('https://www.googleapis.com/oauth2/v1')),
|
|
43
|
+
).toBe(false)
|
|
44
|
+
expect(
|
|
45
|
+
isCommonAssetRequest(new Request('https://fonts.gstatic.com/resource')),
|
|
46
|
+
).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('treats requests to node modules served over HTTP as common assets', () => {
|
|
50
|
+
expect(
|
|
51
|
+
isCommonAssetRequest(
|
|
52
|
+
new Request('http://localhost:3000/node_modules/react/index.js'),
|
|
53
|
+
),
|
|
54
|
+
).toBe(true)
|
|
55
|
+
expect(
|
|
56
|
+
isCommonAssetRequest(
|
|
57
|
+
new Request('http://localhost:3000/node_modules/.vite/deps/react.js'),
|
|
58
|
+
),
|
|
59
|
+
).toBe(true)
|
|
60
|
+
expect(
|
|
61
|
+
isCommonAssetRequest(
|
|
62
|
+
new Request('http://localhost:3000/packages/app/node_modules/lodash'),
|
|
63
|
+
),
|
|
64
|
+
).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('does not treat node modules in the hostname as common assets', () => {
|
|
68
|
+
expect(
|
|
69
|
+
isCommonAssetRequest(new Request('http://node_modules.example.com/api')),
|
|
70
|
+
).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('does not treat node modules in the query as common assets', () => {
|
|
74
|
+
expect(
|
|
75
|
+
isCommonAssetRequest(
|
|
76
|
+
new Request('http://localhost/api?path=node_modules/react'),
|
|
77
|
+
),
|
|
78
|
+
).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('treats internal Vite requests as common assets', () => {
|
|
82
|
+
expect(
|
|
83
|
+
isCommonAssetRequest(new Request('http://localhost:5173/@vite/client')),
|
|
84
|
+
).toBe(true)
|
|
85
|
+
expect(
|
|
86
|
+
isCommonAssetRequest(new Request('http://localhost:5173/@vite/env')),
|
|
87
|
+
).toBe(true)
|
|
88
|
+
expect(
|
|
89
|
+
isCommonAssetRequest(
|
|
90
|
+
new Request('http://localhost:5173/@vite-plugin-react/preamble'),
|
|
91
|
+
),
|
|
92
|
+
).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('does not treat Vite mentions in the query as common assets', () => {
|
|
96
|
+
expect(
|
|
97
|
+
isCommonAssetRequest(new Request('http://localhost/api?source=@vite')),
|
|
98
|
+
).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('treats stylesheet requests as common assets', () => {
|
|
102
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.css'))).toBe(
|
|
103
|
+
true,
|
|
104
|
+
)
|
|
105
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.scss'))).toBe(
|
|
106
|
+
true,
|
|
107
|
+
)
|
|
108
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.less'))).toBe(
|
|
109
|
+
true,
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('treats script requests as common assets', () => {
|
|
114
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.js'))).toBe(
|
|
115
|
+
true,
|
|
116
|
+
)
|
|
117
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.mjs'))).toBe(
|
|
118
|
+
true,
|
|
119
|
+
)
|
|
120
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.jsx'))).toBe(
|
|
121
|
+
true,
|
|
122
|
+
)
|
|
123
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.ts'))).toBe(
|
|
124
|
+
true,
|
|
125
|
+
)
|
|
126
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.mts'))).toBe(
|
|
127
|
+
true,
|
|
128
|
+
)
|
|
129
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.tsx'))).toBe(
|
|
130
|
+
true,
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('treats HTML document requests as common assets', () => {
|
|
135
|
+
expect(isCommonAssetRequest(new Request('http://localhost/index.html'))).toBe(
|
|
136
|
+
true,
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('treats font requests as common assets', () => {
|
|
141
|
+
expect(isCommonAssetRequest(new Request('http://localhost/font.ttf'))).toBe(
|
|
142
|
+
true,
|
|
143
|
+
)
|
|
144
|
+
expect(isCommonAssetRequest(new Request('http://localhost/font.otf'))).toBe(
|
|
145
|
+
true,
|
|
146
|
+
)
|
|
147
|
+
expect(isCommonAssetRequest(new Request('http://localhost/font.woff'))).toBe(
|
|
148
|
+
true,
|
|
149
|
+
)
|
|
150
|
+
expect(isCommonAssetRequest(new Request('http://localhost/font.woff2'))).toBe(
|
|
151
|
+
true,
|
|
152
|
+
)
|
|
153
|
+
expect(isCommonAssetRequest(new Request('http://localhost/font.eot'))).toBe(
|
|
154
|
+
true,
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('treats image requests as common assets', () => {
|
|
159
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.gif'))).toBe(
|
|
160
|
+
true,
|
|
161
|
+
)
|
|
162
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.jpg'))).toBe(
|
|
163
|
+
true,
|
|
164
|
+
)
|
|
165
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.jpeg'))).toBe(
|
|
166
|
+
true,
|
|
167
|
+
)
|
|
168
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.png'))).toBe(
|
|
169
|
+
true,
|
|
170
|
+
)
|
|
171
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.avif'))).toBe(
|
|
172
|
+
true,
|
|
173
|
+
)
|
|
174
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.webp'))).toBe(
|
|
175
|
+
true,
|
|
176
|
+
)
|
|
177
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.svg'))).toBe(
|
|
178
|
+
true,
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('treats favicon requests as common assets', () => {
|
|
183
|
+
expect(
|
|
184
|
+
isCommonAssetRequest(new Request('http://localhost/favicon.ico')),
|
|
185
|
+
).toBe(true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('treats video requests as common assets', () => {
|
|
189
|
+
expect(isCommonAssetRequest(new Request('http://localhost/video.mp4'))).toBe(
|
|
190
|
+
true,
|
|
191
|
+
)
|
|
192
|
+
expect(isCommonAssetRequest(new Request('http://localhost/video.webm'))).toBe(
|
|
193
|
+
true,
|
|
194
|
+
)
|
|
195
|
+
expect(isCommonAssetRequest(new Request('http://localhost/video.ogg'))).toBe(
|
|
196
|
+
true,
|
|
197
|
+
)
|
|
198
|
+
expect(isCommonAssetRequest(new Request('http://localhost/video.mov'))).toBe(
|
|
199
|
+
true,
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('treats audio requests as common assets', () => {
|
|
204
|
+
expect(isCommonAssetRequest(new Request('http://localhost/audio.mp3'))).toBe(
|
|
205
|
+
true,
|
|
206
|
+
)
|
|
207
|
+
expect(isCommonAssetRequest(new Request('http://localhost/audio.wav'))).toBe(
|
|
208
|
+
true,
|
|
209
|
+
)
|
|
210
|
+
expect(isCommonAssetRequest(new Request('http://localhost/audio.flac'))).toBe(
|
|
211
|
+
true,
|
|
212
|
+
)
|
|
213
|
+
expect(isCommonAssetRequest(new Request('http://localhost/audio.aac'))).toBe(
|
|
214
|
+
true,
|
|
215
|
+
)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('treats document requests as common assets', () => {
|
|
219
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.pdf'))).toBe(
|
|
220
|
+
true,
|
|
221
|
+
)
|
|
222
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.txt'))).toBe(
|
|
223
|
+
true,
|
|
224
|
+
)
|
|
225
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.csv'))).toBe(
|
|
226
|
+
true,
|
|
227
|
+
)
|
|
228
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.json'))).toBe(
|
|
229
|
+
true,
|
|
230
|
+
)
|
|
231
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.xml'))).toBe(
|
|
232
|
+
true,
|
|
233
|
+
)
|
|
234
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.md'))).toBe(
|
|
235
|
+
true,
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('treats archive requests as common assets', () => {
|
|
240
|
+
expect(
|
|
241
|
+
isCommonAssetRequest(new Request('http://localhost/archive.zip')),
|
|
242
|
+
).toBe(true)
|
|
243
|
+
expect(
|
|
244
|
+
isCommonAssetRequest(new Request('http://localhost/archive.tar')),
|
|
245
|
+
).toBe(true)
|
|
246
|
+
expect(isCommonAssetRequest(new Request('http://localhost/archive.gz'))).toBe(
|
|
247
|
+
true,
|
|
248
|
+
)
|
|
249
|
+
expect(
|
|
250
|
+
isCommonAssetRequest(new Request('http://localhost/archive.tar.gz')),
|
|
251
|
+
).toBe(true)
|
|
252
|
+
expect(
|
|
253
|
+
isCommonAssetRequest(new Request('http://localhost/archive.rar')),
|
|
254
|
+
).toBe(true)
|
|
255
|
+
expect(isCommonAssetRequest(new Request('http://localhost/archive.7z'))).toBe(
|
|
256
|
+
true,
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('matches the asset extension regardless of its case', () => {
|
|
261
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.PNG'))).toBe(
|
|
262
|
+
true,
|
|
263
|
+
)
|
|
264
|
+
expect(isCommonAssetRequest(new Request('http://localhost/main.Js'))).toBe(
|
|
265
|
+
true,
|
|
266
|
+
)
|
|
267
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.JSON'))).toBe(
|
|
268
|
+
true,
|
|
269
|
+
)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('treats nested asset paths as common assets', () => {
|
|
273
|
+
expect(
|
|
274
|
+
isCommonAssetRequest(
|
|
275
|
+
new Request('http://localhost/static/assets/images/logo.png'),
|
|
276
|
+
),
|
|
277
|
+
).toBe(true)
|
|
278
|
+
expect(
|
|
279
|
+
isCommonAssetRequest(new Request('https://cdn.example.com/v1/bundle.js')),
|
|
280
|
+
).toBe(true)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test('ignores the query string when matching the asset extension', () => {
|
|
284
|
+
expect(
|
|
285
|
+
isCommonAssetRequest(new Request('http://localhost/main.js?v=123')),
|
|
286
|
+
).toBe(true)
|
|
287
|
+
expect(
|
|
288
|
+
isCommonAssetRequest(new Request('http://localhost/image.png?w=100&h=100')),
|
|
289
|
+
).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('ignores the hash when matching the asset extension', () => {
|
|
293
|
+
expect(
|
|
294
|
+
isCommonAssetRequest(new Request('http://localhost/main.js#section')),
|
|
295
|
+
).toBe(true)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('does not treat asset-like extensions in the query as common assets', () => {
|
|
299
|
+
expect(
|
|
300
|
+
isCommonAssetRequest(new Request('http://localhost/api?file=image.png')),
|
|
301
|
+
).toBe(false)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('does not treat asset-like extensions in the hostname as common assets', () => {
|
|
305
|
+
expect(isCommonAssetRequest(new Request('http://example.png/api'))).toBe(
|
|
306
|
+
false,
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('does not treat an extension followed by more path as a common asset', () => {
|
|
311
|
+
expect(
|
|
312
|
+
isCommonAssetRequest(new Request('http://localhost/main.js/metadata')),
|
|
313
|
+
).toBe(false)
|
|
314
|
+
expect(isCommonAssetRequest(new Request('http://localhost/image.png/'))).toBe(
|
|
315
|
+
false,
|
|
316
|
+
)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test('does not treat unknown extensions as common assets', () => {
|
|
320
|
+
expect(isCommonAssetRequest(new Request('http://localhost/data.bin'))).toBe(
|
|
321
|
+
false,
|
|
322
|
+
)
|
|
323
|
+
expect(isCommonAssetRequest(new Request('http://localhost/map.wasm'))).toBe(
|
|
324
|
+
false,
|
|
325
|
+
)
|
|
326
|
+
expect(isCommonAssetRequest(new Request('http://localhost/file.docx'))).toBe(
|
|
327
|
+
false,
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('does not treat an extension without a leading dot as a common asset', () => {
|
|
332
|
+
expect(isCommonAssetRequest(new Request('http://localhost/png'))).toBe(false)
|
|
333
|
+
expect(isCommonAssetRequest(new Request('http://localhost/api/json'))).toBe(
|
|
334
|
+
false,
|
|
335
|
+
)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('does not treat API requests as common assets', () => {
|
|
339
|
+
expect(isCommonAssetRequest(new Request('http://localhost/api/user'))).toBe(
|
|
340
|
+
false,
|
|
341
|
+
)
|
|
342
|
+
expect(
|
|
343
|
+
isCommonAssetRequest(new Request('https://api.example.com/v1/users/123')),
|
|
344
|
+
).toBe(false)
|
|
345
|
+
expect(isCommonAssetRequest(new Request('http://localhost/graphql'))).toBe(
|
|
346
|
+
false,
|
|
347
|
+
)
|
|
348
|
+
expect(isCommonAssetRequest(new Request('http://localhost/'))).toBe(false)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('does not treat API requests with dotted segments as common assets', () => {
|
|
352
|
+
expect(
|
|
353
|
+
isCommonAssetRequest(new Request('http://localhost/api/v1.2/users')),
|
|
354
|
+
).toBe(false)
|
|
355
|
+
expect(
|
|
356
|
+
isCommonAssetRequest(new Request('http://localhost/users/john.doe')),
|
|
357
|
+
).toBe(false)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
test('ignores the request method when matching common assets', () => {
|
|
361
|
+
expect(
|
|
362
|
+
isCommonAssetRequest(
|
|
363
|
+
new Request('http://localhost/main.js', { method: 'POST' }),
|
|
364
|
+
),
|
|
365
|
+
).toBe(true)
|
|
366
|
+
expect(
|
|
367
|
+
isCommonAssetRequest(
|
|
368
|
+
new Request('http://localhost/api/user', { method: 'GET' }),
|
|
369
|
+
),
|
|
370
|
+
).toBe(false)
|
|
371
|
+
})
|
|
@@ -40,7 +40,7 @@ export function isCommonAssetRequest(request: Request): boolean {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Ignore common static assets.
|
|
43
|
-
return /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(
|
|
43
|
+
return /\.(s?css|less|m?jsx?|m?tsx?|html|ttf|otf|woff|woff2|eot|gif|jpe?g|png|avif|webp|ico|svg|mp4|webm|ogg|mov|mp3|wav|ogg|flac|aac|pdf|txt|csv|json|xml|md|zip|tar|gz|rar|7z)$/i.test(
|
|
44
44
|
url.pathname,
|
|
45
45
|
)
|
|
46
46
|
}
|