From 99202b935ce76df9b673aee41523e0a25badd89e Mon Sep 17 00:00:00 2001 From: toxiapo Date: Wed, 26 Oct 2022 17:42:29 -0400 Subject: [PATCH 01/10] feat: wip setupApi --- src/createSetupApi.ts | 104 ++++++++++++++++++++++++++++ src/index.ts | 3 + src/node/setupServer.ts | 145 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 src/createSetupApi.ts diff --git a/src/createSetupApi.ts b/src/createSetupApi.ts new file mode 100644 index 000000000..2b88fae4c --- /dev/null +++ b/src/createSetupApi.ts @@ -0,0 +1,104 @@ +import { + BatchInterceptor, + HttpRequestEventMap, + Interceptor, +} from '@mswjs/interceptors' +import { EventMapType, StrictEventEmitter } from 'strict-event-emitter' +import { + DefaultBodyType, + RequestHandler, + RequestHandlerDefaultInfo, +} from './handlers/RequestHandler' +import { LifeCycleEventEmitter } from './sharedOptions' +import { pipeEvents } from './utils/internal/pipeEvents' +import { toReadonlyArray } from './utils/internal/toReadonlyArray' +import { MockedRequest } from './utils/request/MockedRequest' + +/** + * Generic class for the mock API setup + */ +export abstract class SetupApi { + private readonly initialHandlers: RequestHandler[] + + protected readonly interceptor: BatchInterceptor< + Interceptor[], + HttpRequestEventMap + > + protected readonly emitter = new StrictEventEmitter() + protected readonly publicEmitter = + new StrictEventEmitter() + protected currentHandlers: RequestHandler[] + + public readonly events: LifeCycleEventEmitter> + + constructor( + interceptors: { + new (): Interceptor + }[], + initialHandlers: RequestHandler[], + ) { + this.interceptor = new BatchInterceptor({ + name: 'setup-api', + interceptors: interceptors.map((Interceptor) => new Interceptor()), + }) + // Clone + this.initialHandlers = [...initialHandlers] + this.currentHandlers = [...initialHandlers] + pipeEvents(this.emitter, this.publicEmitter) + this.events = this.registerEvents() + } + + protected apply(): void { + this.interceptor.apply() + } + + protected dispose(): void { + this.emitter.removeAllListeners() + this.publicEmitter.removeAllListeners() + this.interceptor.dispose() + } + + public use(...runtimeHandlers: RequestHandler[]): void { + this.currentHandlers.unshift(...runtimeHandlers) + } + + public restoreHandlers(): void { + this.currentHandlers.forEach((handler) => { + handler.markAsSkipped(false) + }) + } + + public resetHandlers(...nextHandlers: RequestHandler[]) { + this.currentHandlers = + nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers] + } + + public listHandlers(): ReadonlyArray< + RequestHandler< + RequestHandlerDefaultInfo, + MockedRequest, + any, + MockedRequest + > + > { + return toReadonlyArray(this.currentHandlers) + } + + private registerEvents(): LifeCycleEventEmitter< + Record + > { + return { + on: (evt: any, listener: any) => { + return this.publicEmitter.on(evt, listener) + }, + removeListener: (evt: any, listener: any) => { + return this.publicEmitter.removeListener(evt, listener) + }, + removeAllListeners: (...args: any) => { + return this.publicEmitter.removeAllListeners(...args) + }, + } + } + + abstract printHandlers(): void +} diff --git a/src/index.ts b/src/index.ts index b126e0f15..7719845ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,9 @@ import * as context from './context' export { context } export { setupWorker } from './setupWorker/setupWorker' + +export { SetupApi } from './createSetupApi' + export { response, defaultResponse, diff --git a/src/node/setupServer.ts b/src/node/setupServer.ts index b8de95938..12e5281ea 100644 --- a/src/node/setupServer.ts +++ b/src/node/setupServer.ts @@ -1,15 +1,144 @@ import { ClientRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/ClientRequest' import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest' -import { createSetupServer } from './createSetupServer' - +import { SetupApi } from '../createSetupApi' +import { RequestHandler } from '../handlers/RequestHandler' +import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' +import { RequiredDeep } from '../typeUtils' +import { mergeRight } from '../utils/internal/mergeRight' +import { bold } from 'chalk' +import { MockedRequest } from '../utils/request/MockedRequest' +import { handleRequest } from '../utils/handleRequest' +import { + IsomorphicResponse, + MockedResponse as MockedInterceptedResponse, +} from '@mswjs/interceptors' +import { devUtils } from '../utils/internal/devUtils' /** * Sets up a requests interception in Node.js with the given request handlers. * @param {RequestHandler[]} requestHandlers List of request handlers. * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} */ -export const setupServer = createSetupServer( - // List each interceptor separately instead of using the "node" preset - // so that MSW wouldn't bundle the unnecessary classes (i.e. "SocketPolyfill"). - ClientRequestInterceptor, - XMLHttpRequestInterceptor, -) +// export const setupServer = createSetupServer( +// // List each interceptor separately instead of using the "node" preset +// // so that MSW wouldn't bundle the unnecessary classes (i.e. "SocketPolyfill"). +// ClientRequestInterceptor, +// XMLHttpRequestInterceptor, +// ) + +export type ServerLifecycleEventsMap = LifeCycleEventsMap + +const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { + onUnhandledRequest: 'warn', +} + +export class SetupServerApi extends SetupApi { + private resolvedOptions: RequiredDeep + + constructor(handlers: RequestHandler[]) { + super([ClientRequestInterceptor, XMLHttpRequestInterceptor], handlers) + + this.resolvedOptions = {} as RequiredDeep + + // TODO: Re-think this + this.init() + } + + public async init(): Promise { + const _self = this + + this.interceptor.on('request', async function setupServerListener(request) { + const mockedRequest = new MockedRequest(request.url, { + ...request, + body: await request.arrayBuffer(), + }) + + const response = await handleRequest< + MockedInterceptedResponse & { delay?: number } + >( + mockedRequest, + _self.currentHandlers, + _self.resolvedOptions, + _self.emitter, + { + transformResponse(response) { + return { + status: response.status, + statusText: response.statusText, + headers: response.headers.all(), + body: response.body, + delay: response.delay, + } + }, + }, + ) + + if (response) { + // Delay Node.js responses in the listener so that + // the response lookup logic is not concerned with responding + // in any way. The same delay is implemented in the worker. + if (response.delay) { + await new Promise((resolve) => { + setTimeout(resolve, response.delay) + }) + } + + request.respondWith(response) + } + + return + }) + + this.interceptor.on('response', (request, response) => { + if (!request.id) { + return + } + + if (response.headers.get('x-powered-by') === 'msw') { + _self.emitter.emit('response:mocked', response, request.id) + } else { + _self.emitter.emit('response:bypass', response, request.id) + } + }) + } + + public listen(options: Record = {}): void { + this.resolvedOptions = mergeRight( + DEFAULT_LISTEN_OPTIONS, + options, + ) as RequiredDeep + super.apply() + } + + public printHandlers() { + const handlers = this.listHandlers() + + handlers.forEach((handler) => { + const { header, callFrame } = handler.info + + const pragma = handler.info.hasOwnProperty('operationType') + ? '[graphql]' + : '[rest]' + + console.log(`\ +${bold(`${pragma} ${header}`)} +Declaration: ${callFrame} +`) + }) + } + + public close(): void { + super.dispose() + } +} + +export const setupServer = (...handlers: RequestHandler[]) => { + handlers.forEach((handler) => { + if (Array.isArray(handler)) + throw new Error( + devUtils.formatMessage( + 'Failed to call "setupServer" given an Array of request handlers (setupServer([a, b])), expected to receive each handler individually: setupServer(a, b).', + ), + ) + }) + return new SetupServerApi(handlers) +} From 1bedfe5f1d5f858c70e121244d3351dc27986319 Mon Sep 17 00:00:00 2001 From: toxiapo Date: Fri, 28 Oct 2022 17:23:38 -0400 Subject: [PATCH 02/10] feat: concret class for setupWorker --- src/createSetupApi.ts | 17 +- src/node/index.ts | 2 +- src/node/setupServer.ts | 30 +- src/setupWorker/setupWorker.ts | 365 +++++++++--------- .../setup-server/input-validation.test.ts | 2 +- .../setup-worker/input-validation.test.ts | 2 +- 6 files changed, 203 insertions(+), 215 deletions(-) diff --git a/src/createSetupApi.ts b/src/createSetupApi.ts index 2b88fae4c..8ae00318b 100644 --- a/src/createSetupApi.ts +++ b/src/createSetupApi.ts @@ -10,6 +10,7 @@ import { RequestHandlerDefaultInfo, } from './handlers/RequestHandler' import { LifeCycleEventEmitter } from './sharedOptions' +import { devUtils } from './utils/internal/devUtils' import { pipeEvents } from './utils/internal/pipeEvents' import { toReadonlyArray } from './utils/internal/toReadonlyArray' import { MockedRequest } from './utils/request/MockedRequest' @@ -18,8 +19,7 @@ import { MockedRequest } from './utils/request/MockedRequest' * Generic class for the mock API setup */ export abstract class SetupApi { - private readonly initialHandlers: RequestHandler[] - + protected readonly initialHandlers: RequestHandler[] protected readonly interceptor: BatchInterceptor< Interceptor[], HttpRequestEventMap @@ -35,13 +35,22 @@ export abstract class SetupApi { interceptors: { new (): Interceptor }[], + readonly interceptorName: string, initialHandlers: RequestHandler[], ) { + initialHandlers.forEach((handler) => { + if (Array.isArray(handler)) + throw new Error( + devUtils.formatMessage( + `Failed to call "${this.constructor.name}" given an Array of request handlers (${this.constructor.name}([a, b])), expected to receive each handler individually: ${this.constructor.name}(a, b).`, + ), + ) + }) + this.interceptor = new BatchInterceptor({ - name: 'setup-api', + name: interceptorName, interceptors: interceptors.map((Interceptor) => new Interceptor()), }) - // Clone this.initialHandlers = [...initialHandlers] this.currentHandlers = [...initialHandlers] pipeEvents(this.emitter, this.publicEmitter) diff --git a/src/node/index.ts b/src/node/index.ts index 45a8cde32..66c422152 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -1,2 +1,2 @@ -export { setupServer } from './setupServer' +export { setupServer, ServerLifecycleEventsMap } from './setupServer' export type { SetupServerApi } from './glossary' diff --git a/src/node/setupServer.ts b/src/node/setupServer.ts index 12e5281ea..8383b614d 100644 --- a/src/node/setupServer.ts +++ b/src/node/setupServer.ts @@ -12,7 +12,7 @@ import { IsomorphicResponse, MockedResponse as MockedInterceptedResponse, } from '@mswjs/interceptors' -import { devUtils } from '../utils/internal/devUtils' + /** * Sets up a requests interception in Node.js with the given request handlers. * @param {RequestHandler[]} requestHandlers List of request handlers. @@ -35,7 +35,11 @@ export class SetupServerApi extends SetupApi { private resolvedOptions: RequiredDeep constructor(handlers: RequestHandler[]) { - super([ClientRequestInterceptor, XMLHttpRequestInterceptor], handlers) + super( + [ClientRequestInterceptor, XMLHttpRequestInterceptor], + 'server-setup', + handlers, + ) this.resolvedOptions = {} as RequiredDeep @@ -44,9 +48,7 @@ export class SetupServerApi extends SetupApi { } public async init(): Promise { - const _self = this - - this.interceptor.on('request', async function setupServerListener(request) { + this.interceptor.on('request', async (request) => { const mockedRequest = new MockedRequest(request.url, { ...request, body: await request.arrayBuffer(), @@ -56,9 +58,9 @@ export class SetupServerApi extends SetupApi { MockedInterceptedResponse & { delay?: number } >( mockedRequest, - _self.currentHandlers, - _self.resolvedOptions, - _self.emitter, + this.currentHandlers, + this.resolvedOptions, + this.emitter, { transformResponse(response) { return { @@ -94,9 +96,9 @@ export class SetupServerApi extends SetupApi { } if (response.headers.get('x-powered-by') === 'msw') { - _self.emitter.emit('response:mocked', response, request.id) + this.emitter.emit('response:mocked', response, request.id) } else { - _self.emitter.emit('response:bypass', response, request.id) + this.emitter.emit('response:bypass', response, request.id) } }) } @@ -132,13 +134,5 @@ Declaration: ${callFrame} } export const setupServer = (...handlers: RequestHandler[]) => { - handlers.forEach((handler) => { - if (Array.isArray(handler)) - throw new Error( - devUtils.formatMessage( - 'Failed to call "setupServer" given an Array of request handlers (setupServer([a, b])), expected to receive each handler individually: setupServer(a, b).', - ), - ) - }) return new SetupServerApi(handlers) } diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index 4770ea60f..12ce0cc8f 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -1,23 +1,21 @@ import { isNodeProcess } from 'is-node-process' -import { StrictEventEmitter } from 'strict-event-emitter' import { SetupWorkerInternalContext, - SetupWorkerApi, ServiceWorkerIncomingEventsMap, WorkerLifecycleEventsMap, + StartReturnType, } from './glossary' import { createStartHandler } from './start/createStartHandler' import { createStop } from './stop/createStop' -import * as requestHandlerUtils from '../utils/internal/requestHandlerUtils' import { ServiceWorkerMessage } from './start/utils/createMessageChannel' import { RequestHandler } from '../handlers/RequestHandler' import { RestHandler } from '../handlers/RestHandler' -import { prepareStartHandler } from './start/utils/prepareStartHandler' +import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler' import { createFallbackStart } from './start/createFallbackStart' import { createFallbackStop } from './stop/createFallbackStop' import { devUtils } from '../utils/internal/devUtils' -import { pipeEvents } from '../utils/internal/pipeEvents' -import { toReadonlyArray } from '../utils/internal/toReadonlyArray' +import { SetupApi } from '../createSetupApi' +import { mergeRight } from '../utils/internal/mergeRight' interface Listener { target: EventTarget @@ -25,214 +23,201 @@ interface Listener { callback: EventListener } -// Declare the list of event handlers on the module's scope -// so it persists between Fash refreshes of the application's code. -let listeners: Listener[] = [] - -/** - * Creates a new mock Service Worker registration - * with the given request handlers. - * @param {RequestHandler[]} requestHandlers List of request handlers - * @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker`} - */ -export function setupWorker( - ...requestHandlers: RequestHandler[] -): SetupWorkerApi { - requestHandlers.forEach((handler) => { - if (Array.isArray(handler)) +export class SetupWorkerApi extends SetupApi { + private context: SetupWorkerInternalContext + private startHandler: any + private stopHandler: any + private listeners: Listener[] = [] + + constructor(handlers: RequestHandler[]) { + super([], 'worker-setup', handlers) + + if (isNodeProcess()) { throw new Error( devUtils.formatMessage( - 'Failed to call "setupWorker" given an Array of request handlers (setupWorker([a, b])), expected to receive each handler individually: setupWorker(a, b).', + 'Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.', ), ) - }) - - // Error when attempting to run this function in a Node.js environment. - if (isNodeProcess()) { - throw new Error( - devUtils.formatMessage( - 'Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.', - ), - ) - } + } - const emitter = new StrictEventEmitter() - const publicEmitter = new StrictEventEmitter() - pipeEvents(emitter, publicEmitter) - - const context: SetupWorkerInternalContext = { - // Mocking is not considered enabled until the worker - // signals back the successful activation event. - isMockingEnabled: false, - startOptions: undefined, - worker: null, - registration: null, - requestHandlers: [...requestHandlers], - emitter, - workerChannel: { - on(eventType, callback) { - context.events.addListener( - navigator.serviceWorker, - 'message', - (event: MessageEvent) => { - // Avoid messages broadcasted from unrelated workers. - if (event.source !== context.worker) { - return - } + this.context = {} as SetupWorkerInternalContext - const message = event.data as ServiceWorkerMessage< - typeof eventType, - any - > + this.initializeWorkerContext() + } - if (!message) { - return - } + public initializeWorkerContext(): void { + this.context = { + // Mocking is not considered enabled until the worker + // signals back the successful activation event. + isMockingEnabled: false, + startOptions: undefined, + worker: null, + registration: null, + requestHandlers: this.currentHandlers, + emitter: this.emitter, + workerChannel: { + on: ( + eventType: EventType, + callback: ( + event: MessageEvent, + message: ServiceWorkerMessage< + EventType, + ServiceWorkerIncomingEventsMap[EventType] + >, + ) => void, + ) => { + this.context.events.addListener( + navigator.serviceWorker, + 'message', + (event: MessageEvent) => { + // Avoid messages broadcasted from unrelated workers. + if (event.source !== this.context.worker) { + return + } - if (message.type === eventType) { - callback(event, message) - } - }, - ) - }, - send(type) { - context.worker?.postMessage(type) - }, - }, - events: { - addListener( - target: EventTarget, - eventType: string, - callback: EventListener, - ) { - target.addEventListener(eventType, callback) - listeners.push({ eventType, target, callback }) - - return () => { - target.removeEventListener(eventType, callback) - } - }, - removeAllListeners() { - for (const { target, eventType, callback } of listeners) { - target.removeEventListener(eventType, callback) - } - listeners = [] - }, - once(eventType) { - const bindings: Array<() => void> = [] - - return new Promise< - ServiceWorkerMessage< - typeof eventType, - ServiceWorkerIncomingEventsMap[typeof eventType] - > - >((resolve, reject) => { - const handleIncomingMessage = (event: MessageEvent) => { - try { - const message = event.data + const message = event.data as ServiceWorkerMessage< + typeof eventType, + any + > + + if (!message) { + return + } if (message.type === eventType) { - resolve(message) + callback(event, message) } - } catch (error) { - reject(error) - } + }, + ) + }, + send: (type: any) => { + this.context.worker?.postMessage(type) + }, + }, + events: { + addListener: ( + target: EventTarget, + eventType: string, + callback: EventListener, + ) => { + target.addEventListener(eventType, callback) + this.listeners.push({ eventType, target, callback }) + + return () => { + target.removeEventListener(eventType, callback) } + }, + removeAllListeners: () => { + for (const { target, eventType, callback } of this.listeners) { + target.removeEventListener(eventType, callback) + } + this.listeners = [] + }, + once: ( + eventType: EventType, + ) => { + const bindings: Array<() => void> = [] + + return new Promise< + ServiceWorkerMessage< + typeof eventType, + ServiceWorkerIncomingEventsMap[typeof eventType] + > + >((resolve, reject) => { + const handleIncomingMessage = (event: MessageEvent) => { + try { + const message = event.data + + if (message.type === eventType) { + resolve(message) + } + } catch (error) { + reject(error) + } + } - bindings.push( - context.events.addListener( - navigator.serviceWorker, - 'message', - handleIncomingMessage, - ), - context.events.addListener( - navigator.serviceWorker, - 'messageerror', - reject, - ), - ) - }).finally(() => { - bindings.forEach((unbind) => unbind()) - }) + bindings.push( + this.context.events.addListener( + navigator.serviceWorker, + 'message', + handleIncomingMessage, + ), + this.context.events.addListener( + navigator.serviceWorker, + 'messageerror', + reject, + ), + ) + }).finally(() => { + bindings.forEach((unbind) => unbind()) + }) + }, }, - }, - useFallbackMode: - !('serviceWorker' in navigator) || location.protocol === 'file:', + useFallbackMode: + !('serviceWorker' in navigator) || location.protocol === 'file:', + } + + this.startHandler = this.context.useFallbackMode + ? createFallbackStart(this.context) + : createStartHandler(this.context) + this.stopHandler = this.context.useFallbackMode + ? createFallbackStop(this.context) + : createStop(this.context) } - const startHandler = context.useFallbackMode - ? createFallbackStart(context) - : createStartHandler(context) - const stopHandler = context.useFallbackMode - ? createFallbackStop(context) - : createStop(context) - - return { - start: prepareStartHandler(startHandler, context), - stop() { - context.events.removeAllListeners() - context.emitter.removeAllListeners() - publicEmitter.removeAllListeners() - stopHandler() - }, - - use(...handlers) { - requestHandlerUtils.use(context.requestHandlers, ...handlers) - }, - - restoreHandlers() { - requestHandlerUtils.restoreHandlers(context.requestHandlers) - }, - - resetHandlers(...nextHandlers) { - context.requestHandlers = requestHandlerUtils.resetHandlers( - requestHandlers, - ...nextHandlers, - ) - }, + public async start(options: Record = {}): StartReturnType { + this.context.startOptions = mergeRight( + DEFAULT_START_OPTIONS, + options, + ) as SetupWorkerInternalContext['startOptions'] - listHandlers() { - return toReadonlyArray(context.requestHandlers) - }, + return await this.startHandler(this.context.startOptions, options) + } - printHandlers() { - const handlers = this.listHandlers() + public restoreHandlers(): void { + this.context.requestHandlers.forEach((handler) => { + handler.markAsSkipped(false) + }) + } - handlers.forEach((handler) => { - const { header, callFrame } = handler.info - const pragma = handler.info.hasOwnProperty('operationType') - ? '[graphql]' - : '[rest]' + public resetHandlers(...nextHandlers: RequestHandler[]) { + this.context.requestHandlers = + nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers] + } - console.groupCollapsed(`${pragma} ${header}`) + public printHandlers() { + const handlers = this.listHandlers() - if (callFrame) { - console.log(`Declaration: ${callFrame}`) - } + handlers.forEach((handler) => { + const { header, callFrame } = handler.info + const pragma = handler.info.hasOwnProperty('operationType') + ? '[graphql]' + : '[rest]' - console.log('Handler:', handler) + console.groupCollapsed(`${pragma} ${header}`) - if (handler instanceof RestHandler) { - console.log( - 'Match:', - `https://mswjs.io/repl?path=${handler.info.path}`, - ) - } + if (callFrame) { + console.log(`Declaration: ${callFrame}`) + } - console.groupEnd() - }) - }, + console.log('Handler:', handler) - events: { - on(...args) { - return publicEmitter.on(...args) - }, - removeListener(...args) { - return publicEmitter.removeListener(...args) - }, - removeAllListeners(...args) { - return publicEmitter.removeAllListeners(...args) - }, - }, + if (handler instanceof RestHandler) { + console.log('Match:', `https://mswjs.io/repl?path=${handler.info.path}`) + } + + console.groupEnd() + }) } + + public stop(): void { + this.context.events.removeAllListeners() + this.context.emitter.removeAllListeners() + this.publicEmitter.removeAllListeners() + this.stopHandler() + } +} + +export function setupWorker(...handlers: RequestHandler[]) { + return new SetupWorkerApi(handlers) } diff --git a/test/msw-api/setup-server/input-validation.test.ts b/test/msw-api/setup-server/input-validation.test.ts index a5223f986..afbb58154 100644 --- a/test/msw-api/setup-server/input-validation.test.ts +++ b/test/msw-api/setup-server/input-validation.test.ts @@ -14,6 +14,6 @@ test('throws an error given an Array of request handlers to setupServer', async } expect(createServer).toThrow( - `[MSW] Failed to call "setupServer" given an Array of request handlers (setupServer([a, b])), expected to receive each handler individually: setupServer(a, b).`, + `[MSW] Failed to call "SetupServerApi" given an Array of request handlers (SetupServerApi([a, b])), expected to receive each handler individually: SetupServerApi(a, b).`, ) }) diff --git a/test/msw-api/setup-worker/input-validation.test.ts b/test/msw-api/setup-worker/input-validation.test.ts index 2106d7799..391c3d96d 100644 --- a/test/msw-api/setup-worker/input-validation.test.ts +++ b/test/msw-api/setup-worker/input-validation.test.ts @@ -16,7 +16,7 @@ test('throws an error given an Array of request handlers to "setupWorker"', asyn expect(exceptions).toEqual( expect.arrayContaining([ expect.stringContaining( - '[MSW] Failed to call "setupWorker" given an Array of request handlers (setupWorker([a, b])), expected to receive each handler individually: setupWorker(a, b).', + '[MSW] Failed to call "SetupWorkerApi" given an Array of request handlers (SetupWorkerApi([a, b])), expected to receive each handler individually: SetupWorkerApi(a, b).', ), ]), ) From 2e4b492cac571c7b2eefd12763d7632b9eb2cfc6 Mon Sep 17 00:00:00 2001 From: toxiapo Date: Mon, 31 Oct 2022 12:09:23 -0400 Subject: [PATCH 03/10] chore: fix types and clean up --- src/createSetupApi.ts | 12 ++++----- src/node/setupServer.ts | 25 +++++++++---------- src/setupWorker/setupWorker.ts | 3 +++ .../setup-server/printHandlers.test.ts | 1 + 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/createSetupApi.ts b/src/createSetupApi.ts index 8ae00318b..5b1c959ec 100644 --- a/src/createSetupApi.ts +++ b/src/createSetupApi.ts @@ -93,15 +93,13 @@ export abstract class SetupApi { return toReadonlyArray(this.currentHandlers) } - private registerEvents(): LifeCycleEventEmitter< - Record - > { + private registerEvents(): LifeCycleEventEmitter { return { - on: (evt: any, listener: any) => { - return this.publicEmitter.on(evt, listener) + on: (...args) => { + return this.publicEmitter.on(...args) }, - removeListener: (evt: any, listener: any) => { - return this.publicEmitter.removeListener(evt, listener) + removeListener: (...args) => { + return this.publicEmitter.removeListener(...args) }, removeAllListeners: (...args: any) => { return this.publicEmitter.removeAllListeners(...args) diff --git a/src/node/setupServer.ts b/src/node/setupServer.ts index 8383b614d..b28a5d453 100644 --- a/src/node/setupServer.ts +++ b/src/node/setupServer.ts @@ -13,24 +13,16 @@ import { MockedResponse as MockedInterceptedResponse, } from '@mswjs/interceptors' -/** - * Sets up a requests interception in Node.js with the given request handlers. - * @param {RequestHandler[]} requestHandlers List of request handlers. - * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} - */ -// export const setupServer = createSetupServer( -// // List each interceptor separately instead of using the "node" preset -// // so that MSW wouldn't bundle the unnecessary classes (i.e. "SocketPolyfill"). -// ClientRequestInterceptor, -// XMLHttpRequestInterceptor, -// ) - export type ServerLifecycleEventsMap = LifeCycleEventsMap const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { onUnhandledRequest: 'warn', } +/** + * Concrete class to implement the SetupApi for the node server environment, uses both the ClientRequestInterceptor + * and XMLHttpRequestInterceptor. + */ export class SetupServerApi extends SetupApi { private resolvedOptions: RequiredDeep @@ -43,10 +35,12 @@ export class SetupServerApi extends SetupApi { this.resolvedOptions = {} as RequiredDeep - // TODO: Re-think this this.init() } + /** + * Subscribe to all requests that are using the interceptor object + */ public async init(): Promise { this.interceptor.on('request', async (request) => { const mockedRequest = new MockedRequest(request.url, { @@ -133,6 +127,11 @@ Declaration: ${callFrame} } } +/** + * Sets up a requests interception in Node.js with the given request handlers. + * @param {RequestHandler[]} requestHandlers List of request handlers. + * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} + */ export const setupServer = (...handlers: RequestHandler[]) => { return new SetupServerApi(handlers) } diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index 12ce0cc8f..c13cc9a3c 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -23,6 +23,9 @@ interface Listener { callback: EventListener } +/** + * Concrete class to implement the SetupApi for the browser environment, uses the ServerWorker setup. + */ export class SetupWorkerApi extends SetupApi { private context: SetupWorkerInternalContext private startHandler: any diff --git a/test/msw-api/setup-server/printHandlers.test.ts b/test/msw-api/setup-server/printHandlers.test.ts index c4e5836ff..a7e589590 100644 --- a/test/msw-api/setup-server/printHandlers.test.ts +++ b/test/msw-api/setup-server/printHandlers.test.ts @@ -36,6 +36,7 @@ afterAll(() => { test('lists all current request handlers', () => { server.printHandlers() + // Test failed here, commenting so it shows up in the PR expect(console.log).toBeCalledTimes(6) expect(console.log).toBeCalledWith(`\ From a297f884a6f4659044f229a0bc51b3c86ee9dc2d Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Fri, 4 Nov 2022 00:08:36 +0100 Subject: [PATCH 04/10] chore: rely on parent class reset/restore methods --- src/{createSetupApi.ts => SetupApi.ts} | 34 ++++---- src/index.ts | 2 +- src/node/setupServer.ts | 25 +++--- src/setupWorker/setupWorker.ts | 84 ++++++++++--------- .../setup-server/printHandlers.test.ts | 4 +- .../setup-worker/printHandlers.test.ts | 14 ---- 6 files changed, 80 insertions(+), 83 deletions(-) rename src/{createSetupApi.ts => SetupApi.ts} (74%) diff --git a/src/createSetupApi.ts b/src/SetupApi.ts similarity index 74% rename from src/createSetupApi.ts rename to src/SetupApi.ts index 5b1c959ec..4faa2098f 100644 --- a/src/createSetupApi.ts +++ b/src/SetupApi.ts @@ -18,25 +18,24 @@ import { MockedRequest } from './utils/request/MockedRequest' /** * Generic class for the mock API setup */ -export abstract class SetupApi { - protected readonly initialHandlers: RequestHandler[] +export abstract class SetupApi { protected readonly interceptor: BatchInterceptor< - Interceptor[], + Array>, HttpRequestEventMap > - protected readonly emitter = new StrictEventEmitter() - protected readonly publicEmitter = - new StrictEventEmitter() - protected currentHandlers: RequestHandler[] + protected readonly initialHandlers: Array + protected currentHandlers: Array + protected readonly emitter = new StrictEventEmitter() + protected readonly publicEmitter = new StrictEventEmitter() - public readonly events: LifeCycleEventEmitter> + public readonly events: LifeCycleEventEmitter constructor( - interceptors: { + interceptors: Array<{ new (): Interceptor - }[], - readonly interceptorName: string, - initialHandlers: RequestHandler[], + }>, + protected readonly interceptorName: string, + initialHandlers: Array, ) { initialHandlers.forEach((handler) => { if (Array.isArray(handler)) @@ -47,12 +46,17 @@ export abstract class SetupApi { ) }) + /** + * @todo Not all "setup*" APIs rely on interceptors. + * Consider moving this away to the child class. + */ this.interceptor = new BatchInterceptor({ name: interceptorName, interceptors: interceptors.map((Interceptor) => new Interceptor()), }) this.initialHandlers = [...initialHandlers] this.currentHandlers = [...initialHandlers] + pipeEvents(this.emitter, this.publicEmitter) this.events = this.registerEvents() } @@ -67,7 +71,7 @@ export abstract class SetupApi { this.interceptor.dispose() } - public use(...runtimeHandlers: RequestHandler[]): void { + public use(...runtimeHandlers: Array): void { this.currentHandlers.unshift(...runtimeHandlers) } @@ -77,7 +81,7 @@ export abstract class SetupApi { }) } - public resetHandlers(...nextHandlers: RequestHandler[]) { + public resetHandlers(...nextHandlers: Array): void { this.currentHandlers = nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers] } @@ -93,7 +97,7 @@ export abstract class SetupApi { return toReadonlyArray(this.currentHandlers) } - private registerEvents(): LifeCycleEventEmitter { + private registerEvents(): LifeCycleEventEmitter { return { on: (...args) => { return this.publicEmitter.on(...args) diff --git a/src/index.ts b/src/index.ts index 7719845ce..f4cc2e26c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export { context } export { setupWorker } from './setupWorker/setupWorker' -export { SetupApi } from './createSetupApi' +export { SetupApi } from './SetupApi' export { response, diff --git a/src/node/setupServer.ts b/src/node/setupServer.ts index b28a5d453..42e6d093f 100644 --- a/src/node/setupServer.ts +++ b/src/node/setupServer.ts @@ -1,6 +1,10 @@ +import { + IsomorphicResponse, + MockedResponse as MockedInterceptedResponse, +} from '@mswjs/interceptors' import { ClientRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/ClientRequest' import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest' -import { SetupApi } from '../createSetupApi' +import { SetupApi } from '../SetupApi' import { RequestHandler } from '../handlers/RequestHandler' import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' import { RequiredDeep } from '../typeUtils' @@ -8,10 +12,6 @@ import { mergeRight } from '../utils/internal/mergeRight' import { bold } from 'chalk' import { MockedRequest } from '../utils/request/MockedRequest' import { handleRequest } from '../utils/handleRequest' -import { - IsomorphicResponse, - MockedResponse as MockedInterceptedResponse, -} from '@mswjs/interceptors' export type ServerLifecycleEventsMap = LifeCycleEventsMap @@ -26,10 +26,10 @@ const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { export class SetupServerApi extends SetupApi { private resolvedOptions: RequiredDeep - constructor(handlers: RequestHandler[]) { + constructor(handlers: Array) { super( [ClientRequestInterceptor, XMLHttpRequestInterceptor], - 'server-setup', + 'setup-server', handlers, ) @@ -102,10 +102,11 @@ export class SetupServerApi extends SetupApi { DEFAULT_LISTEN_OPTIONS, options, ) as RequiredDeep + super.apply() } - public printHandlers() { + public printHandlers(): void { const handlers = this.listHandlers() handlers.forEach((handler) => { @@ -117,7 +118,7 @@ export class SetupServerApi extends SetupApi { console.log(`\ ${bold(`${pragma} ${header}`)} -Declaration: ${callFrame} + Declaration: ${callFrame} `) }) } @@ -129,9 +130,11 @@ Declaration: ${callFrame} /** * Sets up a requests interception in Node.js with the given request handlers. - * @param {RequestHandler[]} requestHandlers List of request handlers. + * @param {RequestHandler[]} handlers List of request handlers. * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} */ -export const setupServer = (...handlers: RequestHandler[]) => { +export const setupServer = ( + ...handlers: Array +): SetupServerApi => { return new SetupServerApi(handlers) } diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index c13cc9a3c..57b2b20a7 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -4,17 +4,18 @@ import { ServiceWorkerIncomingEventsMap, WorkerLifecycleEventsMap, StartReturnType, + StopHandler, + StartHandler, } from './glossary' import { createStartHandler } from './start/createStartHandler' import { createStop } from './stop/createStop' import { ServiceWorkerMessage } from './start/utils/createMessageChannel' import { RequestHandler } from '../handlers/RequestHandler' -import { RestHandler } from '../handlers/RestHandler' import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler' import { createFallbackStart } from './start/createFallbackStart' import { createFallbackStop } from './stop/createFallbackStop' import { devUtils } from '../utils/internal/devUtils' -import { SetupApi } from '../createSetupApi' +import { SetupApi } from '../SetupApi' import { mergeRight } from '../utils/internal/mergeRight' interface Listener { @@ -23,17 +24,14 @@ interface Listener { callback: EventListener } -/** - * Concrete class to implement the SetupApi for the browser environment, uses the ServerWorker setup. - */ export class SetupWorkerApi extends SetupApi { private context: SetupWorkerInternalContext - private startHandler: any - private stopHandler: any - private listeners: Listener[] = [] + private startHandler: StartHandler = null as any + private stopHandler: StopHandler = null as any + private listeners: Array - constructor(handlers: RequestHandler[]) { - super([], 'worker-setup', handlers) + constructor(handlers: Array) { + super([], 'setup-worker', handlers) if (isNodeProcess()) { throw new Error( @@ -43,13 +41,12 @@ export class SetupWorkerApi extends SetupApi { ) } - this.context = {} as SetupWorkerInternalContext - - this.initializeWorkerContext() + this.listeners = [] + this.context = this.createWorkerContext() } - public initializeWorkerContext(): void { - this.context = { + private createWorkerContext(): SetupWorkerInternalContext { + const context = { // Mocking is not considered enabled until the worker // signals back the successful activation event. isMockingEnabled: false, @@ -160,12 +157,25 @@ export class SetupWorkerApi extends SetupApi { !('serviceWorker' in navigator) || location.protocol === 'file:', } - this.startHandler = this.context.useFallbackMode - ? createFallbackStart(this.context) - : createStartHandler(this.context) - this.stopHandler = this.context.useFallbackMode - ? createFallbackStop(this.context) - : createStop(this.context) + /** + * @todo Not sure I like this but "this.currentHandlers" + * updates never bubble to "this.context.requestHandlers". + */ + Object.defineProperties(context, { + requestHandlers: { + get: () => this.currentHandlers, + }, + }) + + this.startHandler = context.useFallbackMode + ? createFallbackStart(context) + : createStartHandler(context) + + this.stopHandler = context.useFallbackMode + ? createFallbackStop(context) + : createStop(context) + + return context } public async start(options: Record = {}): StartReturnType { @@ -174,21 +184,13 @@ export class SetupWorkerApi extends SetupApi { options, ) as SetupWorkerInternalContext['startOptions'] - return await this.startHandler(this.context.startOptions, options) - } - - public restoreHandlers(): void { - this.context.requestHandlers.forEach((handler) => { - handler.markAsSkipped(false) - }) + /** + * @fixme @todo Typings. + */ + return await this.startHandler(this.context.startOptions as any, options) } - public resetHandlers(...nextHandlers: RequestHandler[]) { - this.context.requestHandlers = - nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers] - } - - public printHandlers() { + public printHandlers(): void { const handlers = this.listHandlers() handlers.forEach((handler) => { @@ -204,11 +206,6 @@ export class SetupWorkerApi extends SetupApi { } console.log('Handler:', handler) - - if (handler instanceof RestHandler) { - console.log('Match:', `https://mswjs.io/repl?path=${handler.info.path}`) - } - console.groupEnd() }) } @@ -221,6 +218,13 @@ export class SetupWorkerApi extends SetupApi { } } -export function setupWorker(...handlers: RequestHandler[]) { +/** + * Sets up a requests interception in the browser with the given request handlers. + * @param {RequestHandler[]} handlers List of request handlers. + * @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker`} + */ +export function setupWorker( + ...handlers: Array +): SetupWorkerApi { return new SetupWorkerApi(handlers) } diff --git a/test/msw-api/setup-server/printHandlers.test.ts b/test/msw-api/setup-server/printHandlers.test.ts index a7e589590..855a8b5c2 100644 --- a/test/msw-api/setup-server/printHandlers.test.ts +++ b/test/msw-api/setup-server/printHandlers.test.ts @@ -84,11 +84,11 @@ test('respects runtime request handlers when listing handlers', () => { expect(console.log).toBeCalledWith(`\ ${bold('[rest] GET https://test.mswjs.io/book/:bookId')} - Declaration: ${__filename}:74:10 + Declaration: ${__filename}:75:10 `) expect(console.log).toBeCalledWith(`\ ${bold('[graphql] query GetRandomNumber (origin: *)')} - Declaration: ${__filename}:75:13 + Declaration: ${__filename}:76:13 `) }) diff --git a/test/msw-api/setup-worker/printHandlers.test.ts b/test/msw-api/setup-worker/printHandlers.test.ts index ae01288e5..016968f06 100644 --- a/test/msw-api/setup-worker/printHandlers.test.ts +++ b/test/msw-api/setup-worker/printHandlers.test.ts @@ -24,7 +24,6 @@ test('lists rest request handlers', async () => { }) const startGroupCollapsed = consoleSpy.get('startGroupCollapsed') - const log = consoleSpy.get('log') expect(startGroupCollapsed).toHaveLength(6) expect(startGroupCollapsed).toContain( @@ -41,12 +40,6 @@ test('lists rest request handlers', async () => { expect(startGroupCollapsed).toContain( '[graphql] all (origin: https://api.github.com)', ) - - const matchSuggestions = log.filter((message) => message.startsWith('Match:')) - expect(matchSuggestions).toHaveLength(1) - expect(matchSuggestions).toEqual([ - 'Match: https://mswjs.io/repl?path=https://test.mswjs.io/book/:bookId', - ]) }) test('includes runtime request handlers', async () => { @@ -63,7 +56,6 @@ test('includes runtime request handlers', async () => { }) const startGroupCollapsed = consoleSpy.get('startGroupCollapsed') - const log = consoleSpy.get('log') expect(startGroupCollapsed).toHaveLength(8) @@ -71,10 +63,4 @@ test('includes runtime request handlers', async () => { expect(startGroupCollapsed).toContain( '[graphql] query SubmitTransaction (origin: *)', ) - - const matchSuggestions = log.filter((message) => message.startsWith('Match:')) - expect(matchSuggestions).toHaveLength(2) - expect(matchSuggestions).toContain( - 'Match: https://mswjs.io/repl?path=/profile', - ) }) From eb3a152272eed2fe5b2f82f92ea1c6fdbd80d59b Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 7 Nov 2022 12:08:26 +0100 Subject: [PATCH 05/10] fix(SetupApi): abstract interceptors to child classes --- src/SetupApi.ts | 66 +++---- src/native/index.ts | 18 +- src/node/SetupServerApi.ts | 153 +++++++++++++++ src/node/createSetupServer.ts | 181 ------------------ src/node/index.ts | 3 +- src/node/setupServer.ts | 132 +------------ src/setupWorker/setupWorker.ts | 18 +- .../setup-server/input-validation.test.ts | 2 +- .../setup-worker/input-validation.test.ts | 2 +- 9 files changed, 209 insertions(+), 366 deletions(-) create mode 100644 src/node/SetupServerApi.ts delete mode 100644 src/node/createSetupServer.ts diff --git a/src/SetupApi.ts b/src/SetupApi.ts index 4faa2098f..100fa4c29 100644 --- a/src/SetupApi.ts +++ b/src/SetupApi.ts @@ -1,8 +1,4 @@ -import { - BatchInterceptor, - HttpRequestEventMap, - Interceptor, -} from '@mswjs/interceptors' +import { invariant } from 'outvariant' import { EventMapType, StrictEventEmitter } from 'strict-event-emitter' import { DefaultBodyType, @@ -16,59 +12,45 @@ import { toReadonlyArray } from './utils/internal/toReadonlyArray' import { MockedRequest } from './utils/request/MockedRequest' /** - * Generic class for the mock API setup + * Generic class for the mock API setup. */ export abstract class SetupApi { - protected readonly interceptor: BatchInterceptor< - Array>, - HttpRequestEventMap - > - protected readonly initialHandlers: Array + protected initialHandlers: ReadonlyArray protected currentHandlers: Array - protected readonly emitter = new StrictEventEmitter() - protected readonly publicEmitter = new StrictEventEmitter() + protected readonly emitter: StrictEventEmitter + protected readonly publicEmitter: StrictEventEmitter public readonly events: LifeCycleEventEmitter - constructor( - interceptors: Array<{ - new (): Interceptor - }>, - protected readonly interceptorName: string, - initialHandlers: Array, - ) { - initialHandlers.forEach((handler) => { - if (Array.isArray(handler)) - throw new Error( - devUtils.formatMessage( - `Failed to call "${this.constructor.name}" given an Array of request handlers (${this.constructor.name}([a, b])), expected to receive each handler individually: ${this.constructor.name}(a, b).`, - ), - ) - }) + constructor(initialHandlers: Array) { + this.validateHandlers(initialHandlers) - /** - * @todo Not all "setup*" APIs rely on interceptors. - * Consider moving this away to the child class. - */ - this.interceptor = new BatchInterceptor({ - name: interceptorName, - interceptors: interceptors.map((Interceptor) => new Interceptor()), - }) - this.initialHandlers = [...initialHandlers] + this.initialHandlers = toReadonlyArray(initialHandlers) this.currentHandlers = [...initialHandlers] + this.emitter = new StrictEventEmitter() + this.publicEmitter = new StrictEventEmitter() pipeEvents(this.emitter, this.publicEmitter) - this.events = this.registerEvents() + + this.events = this.createLifeCycleEvents() } - protected apply(): void { - this.interceptor.apply() + private validateHandlers(handlers: ReadonlyArray): void { + // Guard against incorrect call signature of the setup API. + for (const handler of handlers) { + invariant( + !Array.isArray(handler), + devUtils.formatMessage( + 'Failed to call "%s" given an Array of request handlers. Make sure you spread the request handlers when calling this function.', + ), + this.constructor.name, + ) + } } protected dispose(): void { this.emitter.removeAllListeners() this.publicEmitter.removeAllListeners() - this.interceptor.dispose() } public use(...runtimeHandlers: Array): void { @@ -97,7 +79,7 @@ export abstract class SetupApi { return toReadonlyArray(this.currentHandlers) } - private registerEvents(): LifeCycleEventEmitter { + private createLifeCycleEvents(): LifeCycleEventEmitter { return { on: (...args) => { return this.publicEmitter.on(...args) diff --git a/src/native/index.ts b/src/native/index.ts index 5f0b13776..26d2d364e 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -1,6 +1,16 @@ import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest' -import { createSetupServer } from '../node/createSetupServer' +import { RequestHandler } from '../handlers/RequestHandler' +import { SetupServerApi } from '../node/SetupServerApi' -// Provision request interception via patching the `XMLHttpRequest` class only -// in React Native. There is no `http`/`https` modules in that environment. -export const setupServer = createSetupServer(XMLHttpRequestInterceptor) +/** + * Sets up a requests interception in React Native with the given request handlers. + * @param {RequestHandler[]} handlers List of request handlers. + * @see {@link https://mswjs.io/docs/api/setup-server `setupServer`} + */ +export function setupServer( + ...handlers: Array +): SetupServerApi { + // Provision request interception via patching the `XMLHttpRequest` class only + // in React Native. There is no `http`/`https` modules in that environment. + return new SetupServerApi([XMLHttpRequestInterceptor], handlers) +} diff --git a/src/node/SetupServerApi.ts b/src/node/SetupServerApi.ts new file mode 100644 index 000000000..de59d7250 --- /dev/null +++ b/src/node/SetupServerApi.ts @@ -0,0 +1,153 @@ +import { bold } from 'chalk' +import { invariant } from 'outvariant' +import { + BatchInterceptor, + HttpRequestEventMap, + Interceptor, + InterceptorReadyState, + IsomorphicResponse, + MockedResponse as MockedInterceptedResponse, +} from '@mswjs/interceptors' +import { SetupApi } from '../SetupApi' +import { RequestHandler } from '../handlers/RequestHandler' +import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' +import { RequiredDeep } from '../typeUtils' +import { mergeRight } from '../utils/internal/mergeRight' +import { MockedRequest } from '../utils/request/MockedRequest' +import { handleRequest } from '../utils/handleRequest' +import { devUtils } from '../utils/internal/devUtils' + +export type ServerLifecycleEventsMap = LifeCycleEventsMap + +const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { + onUnhandledRequest: 'warn', +} + +export class SetupServerApi extends SetupApi { + protected readonly interceptor: BatchInterceptor< + Array>, + HttpRequestEventMap + > + private resolvedOptions: RequiredDeep + + constructor( + interceptors: Array<{ + new (): Interceptor + }>, + handlers: Array, + ) { + super(handlers) + + this.interceptor = new BatchInterceptor({ + name: 'setup-server', + interceptors: interceptors.map((Interceptor) => new Interceptor()), + }) + this.resolvedOptions = {} as RequiredDeep + + this.init() + } + + /** + * Subscribe to all requests that are using the interceptor object + */ + private init(): void { + this.interceptor.on('request', async (request) => { + const mockedRequest = new MockedRequest(request.url, { + ...request, + body: await request.arrayBuffer(), + }) + + const response = await handleRequest< + MockedInterceptedResponse & { delay?: number } + >( + mockedRequest, + this.currentHandlers, + this.resolvedOptions, + this.emitter, + { + transformResponse(response) { + return { + status: response.status, + statusText: response.statusText, + headers: response.headers.all(), + body: response.body, + delay: response.delay, + } + }, + }, + ) + + if (response) { + // Delay Node.js responses in the listener so that + // the response lookup logic is not concerned with responding + // in any way. The same delay is implemented in the worker. + if (response.delay) { + await new Promise((resolve) => { + setTimeout(resolve, response.delay) + }) + } + + request.respondWith(response) + } + + return + }) + + this.interceptor.on('response', (request, response) => { + if (!request.id) { + return + } + + if (response.headers.get('x-powered-by') === 'msw') { + this.emitter.emit('response:mocked', response, request.id) + } else { + this.emitter.emit('response:bypass', response, request.id) + } + }) + } + + public listen(options?: Partial): void { + this.resolvedOptions = mergeRight( + DEFAULT_LISTEN_OPTIONS, + options || {}, + ) as RequiredDeep + + // Apply the interceptor when starting the server. + this.interceptor.apply() + + // Assert that the interceptor has been applied successfully. + // Also guards us from forgetting to call "interceptor.apply()" + // as a part of the "listen" method. + invariant( + [InterceptorReadyState.APPLYING, InterceptorReadyState.APPLIED].includes( + this.interceptor.readyState, + ), + devUtils.formatMessage( + 'Failed to start "setupServer": the interceptor failed to apply. This is likely an issue with the library and you should report it at "%s".', + ), + 'https://github.com/mswjs/msw/issues/new/choose', + ) + } + + public printHandlers(): void { + const handlers = this.listHandlers() + + handlers.forEach((handler) => { + const { header, callFrame } = handler.info + + const pragma = handler.info.hasOwnProperty('operationType') + ? '[graphql]' + : '[rest]' + + console.log(`\ +${bold(`${pragma} ${header}`)} + Declaration: ${callFrame} +`) + }) + } + + public close(): void { + super.dispose() + this.interceptor.dispose() + } +} diff --git a/src/node/createSetupServer.ts b/src/node/createSetupServer.ts deleted file mode 100644 index 56b501ace..000000000 --- a/src/node/createSetupServer.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { bold } from 'chalk' -import { isNodeProcess } from 'is-node-process' -import { StrictEventEmitter } from 'strict-event-emitter' -import { - BatchInterceptor, - MockedResponse as MockedInterceptedResponse, - Interceptor, - HttpRequestEventMap, -} from '@mswjs/interceptors' -import * as requestHandlerUtils from '../utils/internal/requestHandlerUtils' -import { ServerLifecycleEventsMap, SetupServerApi } from './glossary' -import { SharedOptions } from '../sharedOptions' -import { RequestHandler } from '../handlers/RequestHandler' -import { handleRequest } from '../utils/handleRequest' -import { mergeRight } from '../utils/internal/mergeRight' -import { devUtils } from '../utils/internal/devUtils' -import { pipeEvents } from '../utils/internal/pipeEvents' -import { RequiredDeep } from '../typeUtils' -import { MockedRequest } from '../utils/request/MockedRequest' -import { toReadonlyArray } from '../utils/internal/toReadonlyArray' - -const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { - onUnhandledRequest: 'warn', -} - -/** - * Creates a `setupServer` API using given request interceptors. - * Useful to generate identical API using different patches to request issuing modules. - */ -export function createSetupServer( - ...interceptors: { new (): Interceptor }[] -) { - const emitter = new StrictEventEmitter() - const publicEmitter = new StrictEventEmitter() - pipeEvents(emitter, publicEmitter) - - return function setupServer( - ...requestHandlers: RequestHandler[] - ): SetupServerApi { - requestHandlers.forEach((handler) => { - if (Array.isArray(handler)) - throw new Error( - devUtils.formatMessage( - 'Failed to call "setupServer" given an Array of request handlers (setupServer([a, b])), expected to receive each handler individually: setupServer(a, b).', - ), - ) - }) - - // Store the list of request handlers for the current server instance, - // so it could be modified at a runtime. - let currentHandlers: RequestHandler[] = [...requestHandlers] - - // Error when attempting to run this function in a browser environment. - if (!isNodeProcess()) { - throw new Error( - devUtils.formatMessage( - 'Failed to execute `setupServer` in the environment that is not Node.js (i.e. a browser). Consider using `setupWorker` instead.', - ), - ) - } - - let resolvedOptions = {} as RequiredDeep - - const interceptor = new BatchInterceptor({ - name: 'setup-server', - interceptors: interceptors.map((Interceptor) => new Interceptor()), - }) - - interceptor.on('request', async function setupServerListener(request) { - const mockedRequest = new MockedRequest(request.url, { - ...request, - body: await request.arrayBuffer(), - }) - - const response = await handleRequest< - MockedInterceptedResponse & { delay?: number } - >(mockedRequest, currentHandlers, resolvedOptions, emitter, { - transformResponse(response) { - return { - status: response.status, - statusText: response.statusText, - headers: response.headers.all(), - body: response.body, - delay: response.delay, - } - }, - }) - - if (response) { - // Delay Node.js responses in the listener so that - // the response lookup logic is not concerned with responding - // in any way. The same delay is implemented in the worker. - if (response.delay) { - await new Promise((resolve) => { - setTimeout(resolve, response.delay) - }) - } - - request.respondWith(response) - } - - return - }) - - interceptor.on('response', (request, response) => { - if (!request.id) { - return - } - - if (response.headers.get('x-powered-by') === 'msw') { - emitter.emit('response:mocked', response, request.id) - } else { - emitter.emit('response:bypass', response, request.id) - } - }) - - return { - listen(options) { - resolvedOptions = mergeRight( - DEFAULT_LISTEN_OPTIONS, - options || {}, - ) as RequiredDeep - interceptor.apply() - }, - - use(...handlers) { - requestHandlerUtils.use(currentHandlers, ...handlers) - }, - - restoreHandlers() { - requestHandlerUtils.restoreHandlers(currentHandlers) - }, - - resetHandlers(...nextHandlers) { - currentHandlers = requestHandlerUtils.resetHandlers( - requestHandlers, - ...nextHandlers, - ) - }, - - listHandlers() { - return toReadonlyArray(currentHandlers) - }, - - printHandlers() { - const handlers = this.listHandlers() - - handlers.forEach((handler) => { - const { header, callFrame } = handler.info - - const pragma = handler.info.hasOwnProperty('operationType') - ? '[graphql]' - : '[rest]' - - console.log(`\ -${bold(`${pragma} ${header}`)} - Declaration: ${callFrame} -`) - }) - }, - - events: { - on(...args) { - return publicEmitter.on(...args) - }, - removeListener(...args) { - return publicEmitter.removeListener(...args) - }, - removeAllListeners(...args) { - return publicEmitter.removeAllListeners(...args) - }, - }, - - close() { - emitter.removeAllListeners() - publicEmitter.removeAllListeners() - interceptor.dispose() - }, - } - } -} diff --git a/src/node/index.ts b/src/node/index.ts index 66c422152..12f6ac48a 100644 --- a/src/node/index.ts +++ b/src/node/index.ts @@ -1,2 +1,3 @@ -export { setupServer, ServerLifecycleEventsMap } from './setupServer' +export { ServerLifecycleEventsMap } from './SetupServerApi' +export { setupServer } from './setupServer' export type { SetupServerApi } from './glossary' diff --git a/src/node/setupServer.ts b/src/node/setupServer.ts index 42e6d093f..f53011937 100644 --- a/src/node/setupServer.ts +++ b/src/node/setupServer.ts @@ -1,132 +1,7 @@ -import { - IsomorphicResponse, - MockedResponse as MockedInterceptedResponse, -} from '@mswjs/interceptors' import { ClientRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/ClientRequest' import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/lib/interceptors/XMLHttpRequest' -import { SetupApi } from '../SetupApi' import { RequestHandler } from '../handlers/RequestHandler' -import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' -import { RequiredDeep } from '../typeUtils' -import { mergeRight } from '../utils/internal/mergeRight' -import { bold } from 'chalk' -import { MockedRequest } from '../utils/request/MockedRequest' -import { handleRequest } from '../utils/handleRequest' - -export type ServerLifecycleEventsMap = LifeCycleEventsMap - -const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { - onUnhandledRequest: 'warn', -} - -/** - * Concrete class to implement the SetupApi for the node server environment, uses both the ClientRequestInterceptor - * and XMLHttpRequestInterceptor. - */ -export class SetupServerApi extends SetupApi { - private resolvedOptions: RequiredDeep - - constructor(handlers: Array) { - super( - [ClientRequestInterceptor, XMLHttpRequestInterceptor], - 'setup-server', - handlers, - ) - - this.resolvedOptions = {} as RequiredDeep - - this.init() - } - - /** - * Subscribe to all requests that are using the interceptor object - */ - public async init(): Promise { - this.interceptor.on('request', async (request) => { - const mockedRequest = new MockedRequest(request.url, { - ...request, - body: await request.arrayBuffer(), - }) - - const response = await handleRequest< - MockedInterceptedResponse & { delay?: number } - >( - mockedRequest, - this.currentHandlers, - this.resolvedOptions, - this.emitter, - { - transformResponse(response) { - return { - status: response.status, - statusText: response.statusText, - headers: response.headers.all(), - body: response.body, - delay: response.delay, - } - }, - }, - ) - - if (response) { - // Delay Node.js responses in the listener so that - // the response lookup logic is not concerned with responding - // in any way. The same delay is implemented in the worker. - if (response.delay) { - await new Promise((resolve) => { - setTimeout(resolve, response.delay) - }) - } - - request.respondWith(response) - } - - return - }) - - this.interceptor.on('response', (request, response) => { - if (!request.id) { - return - } - - if (response.headers.get('x-powered-by') === 'msw') { - this.emitter.emit('response:mocked', response, request.id) - } else { - this.emitter.emit('response:bypass', response, request.id) - } - }) - } - - public listen(options: Record = {}): void { - this.resolvedOptions = mergeRight( - DEFAULT_LISTEN_OPTIONS, - options, - ) as RequiredDeep - - super.apply() - } - - public printHandlers(): void { - const handlers = this.listHandlers() - - handlers.forEach((handler) => { - const { header, callFrame } = handler.info - - const pragma = handler.info.hasOwnProperty('operationType') - ? '[graphql]' - : '[rest]' - - console.log(`\ -${bold(`${pragma} ${header}`)} - Declaration: ${callFrame} -`) - }) - } - - public close(): void { - super.dispose() - } -} +import { SetupServerApi } from './SetupServerApi' /** * Sets up a requests interception in Node.js with the given request handlers. @@ -136,5 +11,8 @@ ${bold(`${pragma} ${header}`)} export const setupServer = ( ...handlers: Array ): SetupServerApi => { - return new SetupServerApi(handlers) + return new SetupServerApi( + [ClientRequestInterceptor, XMLHttpRequestInterceptor], + handlers, + ) } diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index 57b2b20a7..3f3adea3c 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -1,3 +1,4 @@ +import { invariant } from 'outvariant' import { isNodeProcess } from 'is-node-process' import { SetupWorkerInternalContext, @@ -31,15 +32,14 @@ export class SetupWorkerApi extends SetupApi { private listeners: Array constructor(handlers: Array) { - super([], 'setup-worker', handlers) - - if (isNodeProcess()) { - throw new Error( - devUtils.formatMessage( - 'Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.', - ), - ) - } + super(handlers) + + invariant( + !isNodeProcess(), + devUtils.formatMessage( + 'Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.', + ), + ) this.listeners = [] this.context = this.createWorkerContext() diff --git a/test/msw-api/setup-server/input-validation.test.ts b/test/msw-api/setup-server/input-validation.test.ts index afbb58154..e63799cc8 100644 --- a/test/msw-api/setup-server/input-validation.test.ts +++ b/test/msw-api/setup-server/input-validation.test.ts @@ -14,6 +14,6 @@ test('throws an error given an Array of request handlers to setupServer', async } expect(createServer).toThrow( - `[MSW] Failed to call "SetupServerApi" given an Array of request handlers (SetupServerApi([a, b])), expected to receive each handler individually: SetupServerApi(a, b).`, + `[MSW] Failed to call "SetupServerApi" given an Array of request handlers. Make sure you spread the request handlers when calling this function.`, ) }) diff --git a/test/msw-api/setup-worker/input-validation.test.ts b/test/msw-api/setup-worker/input-validation.test.ts index 391c3d96d..933d1ba57 100644 --- a/test/msw-api/setup-worker/input-validation.test.ts +++ b/test/msw-api/setup-worker/input-validation.test.ts @@ -16,7 +16,7 @@ test('throws an error given an Array of request handlers to "setupWorker"', asyn expect(exceptions).toEqual( expect.arrayContaining([ expect.stringContaining( - '[MSW] Failed to call "SetupWorkerApi" given an Array of request handlers (SetupWorkerApi([a, b])), expected to receive each handler individually: SetupWorkerApi(a, b).', + '[MSW] Failed to call "SetupWorkerApi" given an Array of request handlers. Make sure you spread the request handlers when calling this function.', ), ]), ) From 1de45bf69aa2f8efe48158b9f8e02eb409586577 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 7 Nov 2022 12:48:09 +0100 Subject: [PATCH 06/10] chore: import "chalk" for esm compatibility --- src/node/SetupServerApi.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node/SetupServerApi.ts b/src/node/SetupServerApi.ts index de59d7250..e33136e74 100644 --- a/src/node/SetupServerApi.ts +++ b/src/node/SetupServerApi.ts @@ -1,4 +1,4 @@ -import { bold } from 'chalk' +import chalk from 'chalk' import { invariant } from 'outvariant' import { BatchInterceptor, @@ -17,6 +17,11 @@ import { MockedRequest } from '../utils/request/MockedRequest' import { handleRequest } from '../utils/handleRequest' import { devUtils } from '../utils/internal/devUtils' +/** + * @see https://github.com/mswjs/msw/pull/1399 + */ +const { bold } = chalk + export type ServerLifecycleEventsMap = LifeCycleEventsMap const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { From 964f02f4657f0f61a002cf15dc47ad490d2f2a0e Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 7 Nov 2022 12:48:29 +0100 Subject: [PATCH 07/10] fix(setupWorker): call parent dispose --- src/setupWorker/setupWorker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index 3f3adea3c..e82009b28 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -211,9 +211,9 @@ export class SetupWorkerApi extends SetupApi { } public stop(): void { + super.dispose() this.context.events.removeAllListeners() this.context.emitter.removeAllListeners() - this.publicEmitter.removeAllListeners() this.stopHandler() } } From 751a3dd58fcbd03a6828b3a59b60b484123002db Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 7 Nov 2022 12:49:40 +0100 Subject: [PATCH 08/10] fix(setupWorker): annotate "start" options --- src/node/SetupServerApi.ts | 4 ++-- src/setupWorker/setupWorker.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/node/SetupServerApi.ts b/src/node/SetupServerApi.ts index e33136e74..c64cd2b2e 100644 --- a/src/node/SetupServerApi.ts +++ b/src/node/SetupServerApi.ts @@ -111,10 +111,10 @@ export class SetupServerApi extends SetupApi { }) } - public listen(options?: Partial): void { + public listen(options: Partial = {}): void { this.resolvedOptions = mergeRight( DEFAULT_LISTEN_OPTIONS, - options || {}, + options, ) as RequiredDeep // Apply the interceptor when starting the server. diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index e82009b28..c9f6e6611 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -7,6 +7,7 @@ import { StartReturnType, StopHandler, StartHandler, + StartOptions, } from './glossary' import { createStartHandler } from './start/createStartHandler' import { createStop } from './stop/createStop' @@ -178,7 +179,7 @@ export class SetupWorkerApi extends SetupApi { return context } - public async start(options: Record = {}): StartReturnType { + public async start(options: StartOptions = {}): StartReturnType { this.context.startOptions = mergeRight( DEFAULT_START_OPTIONS, options, From 5cff8f144814913bfbc2cbf2ac9af6855b752e91 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 7 Nov 2022 12:51:43 +0100 Subject: [PATCH 09/10] fix(setupWorker): mark "startOptions" as non-nullable --- src/setupWorker/glossary.ts | 2 +- src/setupWorker/setupWorker.ts | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/setupWorker/glossary.ts b/src/setupWorker/glossary.ts index 6734242d5..bcfa5b202 100644 --- a/src/setupWorker/glossary.ts +++ b/src/setupWorker/glossary.ts @@ -99,7 +99,7 @@ export type WorkerLifecycleEventsMap = LifeCycleEventsMap export interface SetupWorkerInternalContext { isMockingEnabled: boolean - startOptions?: RequiredDeep + startOptions: RequiredDeep worker: ServiceWorker | null registration: ServiceWorkerRegistration | null requestHandlers: RequestHandler[] diff --git a/src/setupWorker/setupWorker.ts b/src/setupWorker/setupWorker.ts index c9f6e6611..39ce50b23 100644 --- a/src/setupWorker/setupWorker.ts +++ b/src/setupWorker/setupWorker.ts @@ -51,7 +51,7 @@ export class SetupWorkerApi extends SetupApi { // Mocking is not considered enabled until the worker // signals back the successful activation event. isMockingEnabled: false, - startOptions: undefined, + startOptions: null as any, worker: null, registration: null, requestHandlers: this.currentHandlers, @@ -185,10 +185,7 @@ export class SetupWorkerApi extends SetupApi { options, ) as SetupWorkerInternalContext['startOptions'] - /** - * @fixme @todo Typings. - */ - return await this.startHandler(this.context.startOptions as any, options) + return await this.startHandler(this.context.startOptions, options) } public printHandlers(): void { From 8f728042b4252af11bed6fc00c16e75169eede42 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 7 Nov 2022 12:54:18 +0100 Subject: [PATCH 10/10] chore: adjust invalid handlers signature message --- src/SetupApi.ts | 2 +- test/msw-api/setup-server/input-validation.test.ts | 2 +- test/msw-api/setup-worker/input-validation.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SetupApi.ts b/src/SetupApi.ts index 100fa4c29..e5415b31a 100644 --- a/src/SetupApi.ts +++ b/src/SetupApi.ts @@ -41,7 +41,7 @@ export abstract class SetupApi { invariant( !Array.isArray(handler), devUtils.formatMessage( - 'Failed to call "%s" given an Array of request handlers. Make sure you spread the request handlers when calling this function.', + 'Failed to construct "%s" given an Array of request handlers. Make sure you spread the request handlers when calling the respective setup function.', ), this.constructor.name, ) diff --git a/test/msw-api/setup-server/input-validation.test.ts b/test/msw-api/setup-server/input-validation.test.ts index e63799cc8..4c9d0cb16 100644 --- a/test/msw-api/setup-server/input-validation.test.ts +++ b/test/msw-api/setup-server/input-validation.test.ts @@ -14,6 +14,6 @@ test('throws an error given an Array of request handlers to setupServer', async } expect(createServer).toThrow( - `[MSW] Failed to call "SetupServerApi" given an Array of request handlers. Make sure you spread the request handlers when calling this function.`, + `[MSW] Failed to construct "SetupServerApi" given an Array of request handlers. Make sure you spread the request handlers when calling the respective setup function.`, ) }) diff --git a/test/msw-api/setup-worker/input-validation.test.ts b/test/msw-api/setup-worker/input-validation.test.ts index 933d1ba57..d2a02661b 100644 --- a/test/msw-api/setup-worker/input-validation.test.ts +++ b/test/msw-api/setup-worker/input-validation.test.ts @@ -16,7 +16,7 @@ test('throws an error given an Array of request handlers to "setupWorker"', asyn expect(exceptions).toEqual( expect.arrayContaining([ expect.stringContaining( - '[MSW] Failed to call "SetupWorkerApi" given an Array of request handlers. Make sure you spread the request handlers when calling this function.', + '[MSW] Failed to construct "SetupWorkerApi" given an Array of request handlers. Make sure you spread the request handlers when calling the respective setup function.', ), ]), )