@depup/msw 3.0.2-depup.0 → 3.0.3-depup.0

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.
@@ -7,7 +7,7 @@
7
7
  * - Please do NOT modify this file.
8
8
  */
9
9
 
10
- const PACKAGE_VERSION = '3.0.2'
10
+ const PACKAGE_VERSION = '3.0.3'
11
11
  const INTEGRITY_CHECKSUM = '5cd5cf8b54c3a90f82960cedcd637772'
12
12
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@depup/msw",
3
- "version": "3.0.2-depup.0",
3
+ "version": "3.0.3-depup.0",
4
4
  "description": "The industry standard for API mocking in JavaScript. (with updated dependencies)",
5
5
  "type": "module",
6
6
  "main": "./lib/core/index.js",
@@ -94,7 +94,6 @@
94
94
  "funding": "https://github.com/sponsors/mswjs",
95
95
  "files": [
96
96
  "config/constants.js",
97
- "config/scripts/postinstall.js",
98
97
  "cli",
99
98
  "lib",
100
99
  "src",
@@ -123,9 +122,9 @@
123
122
  ],
124
123
  "sideEffects": false,
125
124
  "dependencies": {
126
- "@inquirer/confirm": "^6.3.2",
125
+ "@inquirer/confirm": "^6.3.3",
127
126
  "@msw/url": "^0.1.2",
128
- "@mswjs/interceptors": "^0.45.6",
127
+ "@mswjs/interceptors": "^0.45.7",
129
128
  "cookie": "^2.0.1",
130
129
  "headers-polyfill": "^5.0.1",
131
130
  "is-node-process": "^1.2.0",
@@ -167,7 +166,7 @@
167
166
  "json-bigint": "^1.0.0",
168
167
  "knip": "^6.34.0",
169
168
  "lint-staged": "^17.1.1",
170
- "msw": "^3.0.2",
169
+ "msw": "^3.0.3",
171
170
  "oxlint": "^1.75.0",
172
171
  "page-with": "^0.6.1",
173
172
  "prettier": "^3.9.6",
@@ -229,17 +228,21 @@
229
228
  "changes": {
230
229
  "@inquirer/confirm": {
231
230
  "from": "^6.0.11",
232
- "to": "^6.3.2"
231
+ "to": "^6.3.3"
232
+ },
233
+ "@mswjs/interceptors": {
234
+ "from": "^0.45.6",
235
+ "to": "^0.45.7"
233
236
  },
234
237
  "type-fest": {
235
238
  "from": "^5.5.0",
236
239
  "to": "^5.10.0"
237
240
  }
238
241
  },
239
- "depsUpdated": 2,
242
+ "depsUpdated": 3,
240
243
  "originalPackage": "msw",
241
- "originalVersion": "3.0.2",
242
- "processedAt": "2026-10-03T08:12:25.616Z",
244
+ "originalVersion": "3.0.3",
245
+ "processedAt": "2026-10-10T04:42:39.550Z",
243
246
  "smokeTest": "passed"
244
247
  }
245
248
  }
@@ -17,7 +17,7 @@ export function createDefaultNetworkOptions(
17
17
  sources: [
18
18
  supportsServiceWorker()
19
19
  ? new ServiceWorkerSource({ serviceWorker: { url: workerUrl } })
20
- : new FallbackHttpSource({}),
20
+ : new FallbackHttpSource(),
21
21
  new InterceptorSource({
22
22
  interceptors: [
23
23
  new WebSocketInterceptor() as InterceptorSourceOptions['interceptors'][number],
@@ -56,11 +56,8 @@ export function setupWorker(...handlers: Array<AnyHandler>): SetupWorker {
56
56
  options: options?.serviceWorker?.options,
57
57
  },
58
58
  findWorker: options?.findWorker,
59
- quiet: options?.quiet,
60
- })
61
- : new FallbackHttpSource({
62
- quiet: options?.quiet,
63
59
  })
60
+ : new FallbackHttpSource()
64
61
 
65
62
  network.configure({
66
63
  sources: [
@@ -1,23 +1,26 @@
1
1
  import { FetchInterceptor } from '@mswjs/interceptors/fetch'
2
2
  import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
3
3
  import { InterceptorSource } from '#core/experimental/sources/interceptor-source'
4
+ import type { NetworkFrameResolutionContext } from '#core/experimental/frames/network-frame'
4
5
  import { devUtils } from '#core/utils/internal/dev-utils'
5
6
 
6
- interface FallbackHttpSourceOptions {
7
- quiet?: boolean
8
- }
9
-
10
7
  export class FallbackHttpSource extends InterceptorSource {
11
- constructor(private readonly options: FallbackHttpSourceOptions) {
8
+ /**
9
+ * The resolved context of the network this source was last enabled with.
10
+ */
11
+ #context?: NetworkFrameResolutionContext
12
+
13
+ constructor() {
12
14
  super({
13
15
  interceptors: [new XMLHttpRequestInterceptor(), new FetchInterceptor()],
14
16
  })
15
17
  }
16
18
 
17
- public enable(): void {
19
+ public enable(context?: NetworkFrameResolutionContext): void {
20
+ this.#context = context
18
21
  super.enable()
19
22
 
20
- if (!this.options.quiet) {
23
+ if (!this.#context?.quiet) {
21
24
  this.#printStartMessage()
22
25
  }
23
26
  }
@@ -25,7 +28,7 @@ export class FallbackHttpSource extends InterceptorSource {
25
28
  public disable(): void {
26
29
  super.disable()
27
30
 
28
- if (!this.options.quiet) {
31
+ if (!this.#context?.quiet) {
29
32
  this.#printStopMessage()
30
33
  }
31
34
  }
@@ -2,6 +2,7 @@ import { invariant } from 'outvariant'
2
2
  import type { Emitter } from 'rettime'
3
3
  import { FetchResponse } from '@mswjs/interceptors'
4
4
  import { NetworkSource } from '#core/experimental/sources/network-source'
5
+ import type { NetworkFrameResolutionContext } from '#core/experimental/frames/network-frame'
5
6
  import { RequestHandler } from '#core/handlers/request-handler'
6
7
  import {
7
8
  HttpNetworkFrame,
@@ -23,7 +24,6 @@ import { validateWorkerScope } from '../utils/validate-worker-scope'
23
24
  import { shouldInvalidateWorker } from '../utils/should-invalidate-worker'
24
25
 
25
26
  export interface ServiceWorkerSourceOptions {
26
- quiet?: boolean
27
27
  serviceWorker: {
28
28
  url: string
29
29
  options?: RegistrationOptions
@@ -75,6 +75,10 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
75
75
  }
76
76
 
77
77
  #options: ServiceWorkerSourceOptions
78
+ /**
79
+ * The resolved context of the network this source was last enabled with.
80
+ */
81
+ #context?: NetworkFrameResolutionContext
78
82
  /**
79
83
  * @note We cannot use `WeakMap` here as request/response
80
84
  * identity cannot be preserved through the client-worker channel.
@@ -109,7 +113,10 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
109
113
  })
110
114
  }
111
115
 
112
- public async enable(): Promise<ServiceWorkerRegistration> {
116
+ public async enable(
117
+ context?: NetworkFrameResolutionContext,
118
+ ): Promise<ServiceWorkerRegistration> {
119
+ this.#context = context
113
120
  this.#channel.removeAllListeners()
114
121
  this.#frames.clear()
115
122
 
@@ -147,7 +154,7 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
147
154
  })
148
155
  await clientConfirmationPromise.promise
149
156
 
150
- if (!this.#options.quiet) {
157
+ if (!this.#context?.quiet) {
151
158
  this.#printStartMessage()
152
159
  }
153
160
 
@@ -178,7 +185,7 @@ export class ServiceWorkerSource extends NetworkSource<ServiceWorkerHttpNetworkF
178
185
  * `#handleRequest`. `#startWorker` swaps in a fresh deferred on re-enable.
179
186
  */
180
187
 
181
- if (!this.#options.quiet) {
188
+ if (!this.#context?.quiet) {
182
189
  this.#printStopMessage()
183
190
  }
184
191
  }
@@ -294,7 +301,7 @@ Please consider using a custom "serviceWorker.url" option to point to the actual
294
301
  this.#channel.postMessage('KEEPALIVE_REQUEST')
295
302
  }, 5000)
296
303
 
297
- if (!this.#options.quiet) {
304
+ if (!this.#context?.quiet) {
298
305
  validateWorkerScope(registration)
299
306
  }
300
307
 
@@ -104,19 +104,3 @@ test('returns false when findWorker is the same reference', () => {
104
104
  ),
105
105
  ).toBe(false)
106
106
  })
107
-
108
- test('returns false regardless of the "quiet" option', () => {
109
- expect(
110
- shouldInvalidateWorker(
111
- createOptions({ quiet: true }),
112
- createOptions({ quiet: true }),
113
- ),
114
- ).toBe(false)
115
-
116
- expect(
117
- shouldInvalidateWorker(
118
- createOptions({ quiet: false }),
119
- createOptions({ quiet: true }),
120
- ),
121
- ).toBe(false)
122
- })
@@ -203,7 +203,7 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
203
203
  )
204
204
  })
205
205
 
206
- return source.enable()
206
+ return source.enable(resolvedOptions.context)
207
207
  })
208
208
 
209
209
  return colorlessPromiseAll(result) as MaybePromise<
@@ -308,6 +308,67 @@ describe(InMemoryHandlersController.prototype.reset, () => {
308
308
  expect(controller.getHandlersByKind('websocket')).toEqual([wsOne, wsTwo])
309
309
  expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
310
310
  })
311
+
312
+ test('resets the kept handlers and disposes of the removed ones', () => {
313
+ const initialHandler = http.get('/initial', () => {})
314
+ initialHandler.reset = vi.fn()
315
+ initialHandler.dispose = vi.fn()
316
+ const runtimeHandler = http.get('/runtime', () => {})
317
+ runtimeHandler.reset = vi.fn()
318
+ runtimeHandler.dispose = vi.fn()
319
+
320
+ const controller = new InMemoryHandlersController([initialHandler])
321
+ controller.use([runtimeHandler])
322
+ controller.reset([])
323
+
324
+ expect.soft(initialHandler.reset).toHaveBeenCalledOnce()
325
+ expect.soft(initialHandler.dispose).not.toHaveBeenCalled()
326
+ expect.soft(runtimeHandler.reset).not.toHaveBeenCalled()
327
+ expect(runtimeHandler.dispose).toHaveBeenCalledOnce()
328
+ })
329
+
330
+ test('reports a rejected disposal of a removed handler', async () => {
331
+ vi.spyOn(console, 'error').mockImplementation(() => {})
332
+ onTestFinished(() => vi.restoreAllMocks())
333
+
334
+ const runtimeHandler = http.get('/runtime', () => {})
335
+ runtimeHandler.dispose = () =>
336
+ Promise.reject(new Error('Handler disposal error'))
337
+
338
+ const controller = new InMemoryHandlersController([])
339
+ controller.use([runtimeHandler])
340
+ controller.reset([])
341
+
342
+ await expect
343
+ .poll(() => console.error)
344
+ .toHaveBeenCalledWith(
345
+ `[MSW] Failed to dispose of "request" handler removed during reset. Please see the original error below.\nError: Handler disposal error`,
346
+ )
347
+ })
348
+
349
+ test('removes the remaining handlers when a disposal throws', () => {
350
+ vi.spyOn(console, 'error').mockImplementation(() => {})
351
+ onTestFinished(() => vi.restoreAllMocks())
352
+
353
+ const throwingHandler = http.get('/throwing', () => {})
354
+ throwingHandler.dispose = () => {
355
+ throw new Error('Handler disposal error')
356
+ }
357
+ const runtimeHandler = http.get('/runtime', () => {})
358
+ runtimeHandler.dispose = vi.fn()
359
+
360
+ const controller = new InMemoryHandlersController([])
361
+ controller.use([throwingHandler, runtimeHandler])
362
+ controller.reset([])
363
+
364
+ expect
365
+ .soft(console.error)
366
+ .toHaveBeenCalledWith(
367
+ `[MSW] Failed to dispose of "request" handler removed during reset. Please see the original error below.\nError: Handler disposal error`,
368
+ )
369
+ expect.soft(runtimeHandler.dispose).toHaveBeenCalledOnce()
370
+ expect(controller.currentHandlers()).toEqual([])
371
+ })
311
372
  })
312
373
 
313
374
  describe(InMemoryHandlersController.prototype.listHandlers, () => {
@@ -168,11 +168,38 @@ export abstract class HandlersController {
168
168
  ),
169
169
  )
170
170
 
171
- for (const handler of this.currentHandlers()) {
171
+ const { initialHandlers } = this.getState()
172
+ const nextInitialHandlers =
173
+ nextHandlers.length > 0
174
+ ? groupHandlersByKind(nextHandlers)
175
+ : initialHandlers
176
+ const currentHandlers = new Set(this.currentHandlers())
177
+ const nextHandlersSet = new Set(
178
+ Object.values(nextInitialHandlers).flat().filter(Boolean),
179
+ )
180
+
181
+ for (const handler of currentHandlers.intersection(nextHandlersSet)) {
172
182
  handler.reset()
173
183
  }
174
184
 
175
- const { initialHandlers } = this.getState()
185
+ // The handlers that leave the network with this reset release
186
+ // whatever they hold (e.g. live connection bindings).
187
+ // Resetting is synchronous, so their disposal is not awaited.
188
+ for (const handler of currentHandlers.difference(nextHandlersSet)) {
189
+ try {
190
+ const disposal = handler.dispose()
191
+
192
+ if (disposal instanceof Promise) {
193
+ disposal.catch((error) => {
194
+ this.#printDisposalError(handler, error)
195
+ })
196
+ }
197
+ } catch (error) {
198
+ // A failing disposal must not prevent the remaining handlers
199
+ // from being disposed of, nor the removed handlers from being cleared.
200
+ this.#printDisposalError(handler, error)
201
+ }
202
+ }
176
203
 
177
204
  if (nextHandlers.length === 0) {
178
205
  this.setState({
@@ -182,11 +209,9 @@ export abstract class HandlersController {
182
209
  return
183
210
  }
184
211
 
185
- const normalizedNextHandlers = groupHandlersByKind(nextHandlers)
186
-
187
212
  this.setState({
188
- initialHandlers: normalizedNextHandlers,
189
- handlers: { ...normalizedNextHandlers },
213
+ initialHandlers: nextInitialHandlers,
214
+ handlers: { ...nextInitialHandlers },
190
215
  })
191
216
  }
192
217
 
@@ -217,6 +242,14 @@ export abstract class HandlersController {
217
242
  #validateHandlers(handlers: Array<AnyHandler>): boolean {
218
243
  return handlers.every((handler) => !Array.isArray(handler))
219
244
  }
245
+
246
+ #printDisposalError(handler: AnyHandler, error: unknown): void {
247
+ devUtils.error(
248
+ 'Failed to dispose of "%s" handler removed during reset. Please see the original error below.\n%s',
249
+ handler.kind,
250
+ error,
251
+ )
252
+ }
220
253
  }
221
254
 
222
255
  export class InMemoryHandlersController extends HandlersController {
@@ -2,6 +2,7 @@ import { Emitter, TypedEvent, type TypedListenerOptions } from 'rettime'
2
2
  import type {
3
3
  AnyNetworkFrame,
4
4
  ExtractFrameEvents,
5
+ NetworkFrameResolutionContext,
5
6
  } from '../frames/network-frame'
6
7
 
7
8
  class NetworkFrameEvent<
@@ -33,7 +34,13 @@ export abstract class NetworkSource<
33
34
  this.emitter = new Emitter()
34
35
  }
35
36
 
36
- public abstract enable(): unknown | Promise<unknown>
37
+ /**
38
+ * Enable this network source.
39
+ * @param context The resolved context of the network this source belongs to.
40
+ */
41
+ public abstract enable(
42
+ context?: NetworkFrameResolutionContext,
43
+ ): unknown | Promise<unknown>
37
44
 
38
45
  public async queue(frame: Frame): Promise<void> {
39
46
  await this.emitter.emitAsPromise(
@@ -32,8 +32,9 @@ export abstract class Handler {
32
32
  /**
33
33
  * Release the resources held by this handler.
34
34
  *
35
- * @note This method is invoked automatically when the network is
36
- * disabled (e.g. `server.close()`). Override it in the handlers that
35
+ * @note This method is invoked automatically when the handler leaves
36
+ * the network: when the network is disabled (e.g. `server.close()`)
37
+ * or when a reset removes it (e.g. `server.resetHandlers()`). Override it in the handlers that
37
38
  * hold onto anything beyond a single frame, like timers, connections,
38
39
  * or event listeners. Returning a promise makes the network await
39
40
  * this handler's disposal before it tears itself down.
@@ -585,24 +585,32 @@ export class GraphQLSubscriptionTransportHandler extends GraphQLWebSocketHandler
585
585
  }
586
586
 
587
587
  /**
588
- * Drop this transport's subscribers and active subscriptions from the
589
- * sessions it participates in. The sessions themselves are left intact:
590
- * they are shared with the other transports of the same connection and
591
- * own the protocol listeners for as long as the client stays connected.
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
- let ownsConnection = false
599
-
600
- for (const [handler, entry] of connection.subscribers) {
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