Skip to content

Commit

Permalink
chore: separate SetupServerApi for node and native
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Feb 5, 2024
1 parent 5d77008 commit 5106393
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 133 deletions.
8 changes: 4 additions & 4 deletions src/native/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import { SetupServerApi } from '../node/SetupServerApi'
import type { RequestHandler } from '~/core/handlers/RequestHandler'
import { SetupServerCommonApi } from '../node/SetupServerCommonApi'

/**
* Sets up a requests interception in React Native with the given request handlers.
Expand All @@ -10,8 +10,8 @@ import { SetupServerApi } from '../node/SetupServerApi'
*/
export function setupServer(
...handlers: Array<RequestHandler>
): SetupServerApi {
): SetupServerCommonApi {
// 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)
return new SetupServerCommonApi([XMLHttpRequestInterceptor], handlers)
}
118 changes: 14 additions & 104 deletions src/node/SetupServerApi.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import { AsyncLocalStorage } from 'node:async_hooks'
import {
BatchInterceptor,
HttpRequestEventMap,
Interceptor,
InterceptorReadyState,
} from '@mswjs/interceptors'
import { invariant } from 'outvariant'
import { HandlersController, SetupApi } from '~/core/SetupApi'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions'
import { RequiredDeep } from '~/core/typeUtils'
import { handleRequest } from '~/core/utils/handleRequest'
import { devUtils } from '~/core/utils/internal/devUtils'
import { mergeRight } from '~/core/utils/internal/mergeRight'
import { SetupServer } from './glossary'

const DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = {
onUnhandledRequest: 'warn',
}
import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest'
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
import { HandlersController } from '~/core/SetupApi'
import type { RequestHandler } from '~/core/handlers/RequestHandler'
import type { SetupServer } from './glossary'
import { SetupServerCommonApi } from './SetupServerCommonApi'

const store = new AsyncLocalStorage<RequestHandlersContext>()

Expand Down Expand Up @@ -60,94 +48,16 @@ class AsyncHandlersController implements HandlersController {
}

export class SetupServerApi
extends SetupApi<LifeCycleEventsMap>
extends SetupServerCommonApi
implements SetupServer
{
protected readonly interceptor: BatchInterceptor<
Array<Interceptor<HttpRequestEventMap>>,
HttpRequestEventMap
>
private resolvedOptions: RequiredDeep<SharedOptions>

constructor(
interceptors: Array<{
new (): Interceptor<HttpRequestEventMap>
}>,
...handlers: Array<RequestHandler>
) {
super(...handlers)

this.handlersController = new AsyncHandlersController(handlers)

this.interceptor = new BatchInterceptor({
name: 'setup-server',
interceptors: interceptors.map((Interceptor) => new Interceptor()),
})
this.resolvedOptions = {} as RequiredDeep<SharedOptions>

this.init()
}

/**
* Subscribe to all requests that are using the interceptor object
*/
private init(): void {
this.interceptor.on('request', async ({ request, requestId }) => {
const response = await handleRequest(
request,
requestId,
this.handlersController.currentHandlers(),
this.resolvedOptions,
this.emitter,
)

if (response) {
request.respondWith(response)
}

return
})

this.interceptor.on(
'response',
({ response, isMockedResponse, request, requestId }) => {
this.emitter.emit(
isMockedResponse ? 'response:mocked' : 'response:bypass',
{
response,
request,
requestId,
},
)
},
constructor(handlers: Array<RequestHandler>) {
super(
[ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor],
handlers,
)
}

public listen(options: Partial<SharedOptions> = {}): void {
this.resolvedOptions = mergeRight(
DEFAULT_LISTEN_OPTIONS,
options,
) as RequiredDeep<SharedOptions>

// Apply the interceptor when starting the server.
this.interceptor.apply()

this.subscriptions.push(() => {
this.interceptor.dispose()
})

// 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',
)
this.handlersController = new AsyncHandlersController(handlers)
}

public boundary<Fn extends (...args: Array<any>) => unknown>(
Expand All @@ -166,7 +76,7 @@ export class SetupServerApi
}

public close(): void {
this.dispose()
super.close()
store.disable()
}
}
116 changes: 116 additions & 0 deletions src/node/SetupServerCommonApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* @note This API is extended by both "msw/node" and "msw/native"
* so be minding about the things you import!
*/
import type { RequiredDeep } from 'type-fest'
import { invariant } from 'outvariant'
import {
BatchInterceptor,
InterceptorReadyState,
type HttpRequestEventMap,
type Interceptor,
} from '@mswjs/interceptors'
import type { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions'
import { SetupApi } from '~/core/SetupApi'
import { handleRequest } from '~/core/utils/handleRequest'
import type { RequestHandler } from '~/core/handlers/RequestHandler'
import { mergeRight } from '~/core/utils/internal/mergeRight'
import { devUtils } from '~/core/utils/internal/devUtils'
import type { SetupServerCommon } from './glossary'

export const DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = {
onUnhandledRequest: 'warn',
}

export class SetupServerCommonApi
extends SetupApi<LifeCycleEventsMap>
implements SetupServerCommon
{
protected readonly interceptor: BatchInterceptor<
Array<Interceptor<HttpRequestEventMap>>,
HttpRequestEventMap
>
private resolvedOptions: RequiredDeep<SharedOptions>

constructor(
interceptors: Array<{ new (): Interceptor<HttpRequestEventMap> }>,
handlers: Array<RequestHandler>,
) {
super(...handlers)

this.interceptor = new BatchInterceptor({
name: 'setup-server',
interceptors: interceptors.map((Interceptor) => new Interceptor()),
})

this.resolvedOptions = {} as RequiredDeep<SharedOptions>

this.init()
}

/**
* Subscribe to all requests that are using the interceptor object
*/
private init(): void {
this.interceptor.on('request', async ({ request, requestId }) => {
const response = await handleRequest(
request,
requestId,
this.handlersController.currentHandlers(),
this.resolvedOptions,
this.emitter,
)

if (response) {
request.respondWith(response)
}

return
})

this.interceptor.on(
'response',
({ response, isMockedResponse, request, requestId }) => {
this.emitter.emit(
isMockedResponse ? 'response:mocked' : 'response:bypass',
{
response,
request,
requestId,
},
)
},
)
}

public listen(options: Partial<SharedOptions> = {}): void {
this.resolvedOptions = mergeRight(
DEFAULT_LISTEN_OPTIONS,
options,
) as RequiredDeep<SharedOptions>

// Apply the interceptor when starting the server.
this.interceptor.apply()

this.subscriptions.push(() => {
this.interceptor.dispose()
})

// 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 close(): void {
this.dispose()
}
}
32 changes: 17 additions & 15 deletions src/node/glossary.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
import type { PartialDeep } from 'type-fest'
import {
import type {
RequestHandler,
RequestHandlerDefaultInfo,
} from '~/core/handlers/RequestHandler'
import {
import type {
LifeCycleEventEmitter,
LifeCycleEventsMap,
SharedOptions,
} from '~/core/sharedOptions'

export interface SetupServer {
export interface SetupServerCommon {
/**
* Starts requests interception based on the previously provided request handlers.
*
* @see {@link https://mswjs.io/docs/api/setup-server/listen `server.listen()` API reference}
*/
listen(options?: PartialDeep<SharedOptions>): void

/**
* Wraps the given function in a boundary. Any changes to the
* network behavior (e.g. adding runtime request handlers via
* `server.use()`) will be scoped to this boundary only.
* @param callback A function to run (e.g. a test)
*
* @see {@link https://mswjs.io/docs/api/setup-server/boundary `server.boundary()` API reference}
*/
boundary<Fn extends (...args: Array<any>) => unknown>(
callback: Fn,
): (...args: Parameters<Fn>) => ReturnType<Fn>

/**
* Stops requests interception by restoring all augmented modules.
*
Expand Down Expand Up @@ -72,3 +60,17 @@ export interface SetupServer {
*/
events: LifeCycleEventEmitter<LifeCycleEventsMap>
}

export interface SetupServer extends SetupServerCommon {
/**
* Wraps the given function in a boundary. Any changes to the
* network behavior (e.g. adding runtime request handlers via
* `server.use()`) will be scoped to this boundary only.
* @param callback A function to run (e.g. a test)
*
* @see {@link https://mswjs.io/docs/api/setup-server/boundary `server.boundary()` API reference}
*/
boundary<Fn extends (...args: Array<any>) => unknown>(
callback: Fn,
): (...args: Parameters<Fn>) => ReturnType<Fn>
}
13 changes: 3 additions & 10 deletions src/node/setupServer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest'
import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'
import { FetchInterceptor } from '@mswjs/interceptors/fetch'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import type { RequestHandler } from '~/core/handlers/RequestHandler'
import { SetupServerApi } from './SetupServerApi'
import { SetupServer } from './glossary'

/**
* Sets up a requests interception in Node.js with the given request handlers.
Expand All @@ -13,9 +9,6 @@ import { SetupServer } from './glossary'
*/
export const setupServer = (
...handlers: Array<RequestHandler>
): SetupServer => {
return new SetupServerApi(
[ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor],
...handlers,
)
): SetupServerApi => {
return new SetupServerApi(handlers)
}

0 comments on commit 5106393

Please sign in to comment.