Comparing version 0.1.1 to 0.1.2
self.addEventListener('install', (event) => { | ||
event.waitUntil(self.skipWaiting()) | ||
return self.skipWaiting() | ||
}) | ||
self.addEventListener('activate', (event) => { | ||
event.waitUntil(self.clients.claim()) | ||
console.log( | ||
@@ -11,2 +10,3 @@ '%cMockServiceWorker is activated!', | ||
) | ||
return self.clients.claim() | ||
}) | ||
@@ -56,2 +56,3 @@ | ||
/* Converts "Headres" to the plain Object to be stringified */ | ||
const reqHeaders = {} | ||
@@ -78,3 +79,11 @@ req.headers.forEach((value, name) => { | ||
const res = JSON.parse(clientResponse) | ||
const res = JSON.parse(clientResponse, (key, value) => { | ||
return key === 'headers' | ||
? value.reduce((acc, [headerName, headerValue]) => { | ||
acc.append(headerName, headerValue) | ||
return acc | ||
}, new Headers()) | ||
: value | ||
}) | ||
const mockedResponse = new Response(res.body, res) | ||
@@ -81,0 +90,0 @@ |
{ | ||
"name": "msw", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "Serverless client-side API mocking without a single change to the codebase.", | ||
@@ -11,3 +11,3 @@ "main": "lib/index.js", | ||
"example": "node example/server", | ||
"prepublishOnly": "NODE_ENV=production npm run build" | ||
"prepublishOnly": "npm run build" | ||
}, | ||
@@ -25,2 +25,3 @@ "author": { | ||
"@babel/preset-env": "^7.1.5", | ||
"@types/jest": "^23.3.9", | ||
"@types/ramda": "^0.25.41", | ||
@@ -34,2 +35,3 @@ "awesome-typescript-loader": "^5.2.1", | ||
"prettier": "^1.15.2", | ||
"ts-jest": "^23.10.4", | ||
"typescript": "^3.1.6", | ||
@@ -36,0 +38,0 @@ "webpack": "^4.25.1", |
@@ -6,5 +6,11 @@ ## Motivation | ||
- Often relies on a mocking server which you need to run and maintain; | ||
- Doesn't really mock requests, rather **replaces** requests' urls, so they go to the mocking server, instead of the production server; | ||
- Doesn't really mock requests, rather **replaces** their urls to point to a mocking server, instead of a production server; | ||
- Brings extra dependencies to your application, instead of being a dependency-free development tool; | ||
### Benefits of `msw`: | ||
- **Serverless**. Doesn't establish any mocking servers whatsoever; | ||
- **Deviation-free**. Request the same resources as you would in production, let the library handle response mocking of those that match your defined routes; | ||
- **A tool**. Mocking is a development process, thus enable/disable it at any point, change the routes without any rebuilds, control the lifecycle from your browser's DevTools; | ||
## Getting started | ||
@@ -15,13 +21,11 @@ | ||
```bash | ||
– | ||
npm install msw --save-dev | ||
``` | ||
### Configure routes | ||
### Use | ||
```js | ||
import { MSW } from 'msw' | ||
// app/mocks.js | ||
import { msw } from 'msw' | ||
/* Create a new instance of MockServiceWorker */ | ||
const msw = new MSW() | ||
/* Configure mocking routes */ | ||
@@ -31,6 +35,6 @@ msw.get( | ||
(req, res, { status, set, delay, json }) => { | ||
const { repoName } = req.params // acces request's params | ||
const { repoName } = req.params // access request's params | ||
return res( | ||
status(403), // set custom response status | ||
status(403), // set custom status | ||
set({ 'Custom-Header': 'foo' }), // set headers | ||
@@ -46,5 +50,12 @@ delay(1000), // delay the response | ||
Import your `mocks.js` module anywhere in your application to enable the mocking: | ||
```js | ||
// app/index.js | ||
import './mocks.js' | ||
``` | ||
## How does this work? | ||
The library spawns a ServiceWorker that broadcasts any outgoing request on a page to the application. The listener then matches the request against the schema of mocking routes, and resolves with the mocked response whenever present. | ||
The library spawns a ServiceWorker that notifies the library about any outgoing requests from your application. A request is than matched against the mocking routes you have defined, and is mocked with the first match. | ||
@@ -51,0 +62,0 @@ ## Browser support |
import * as R from 'ramda' | ||
import { MockedResponse, ResponseTransformer } from './response' | ||
import { ResponseTransformer } from './response' | ||
@@ -57,9 +57,16 @@ export interface MockedContext { | ||
const set = (name, value) => | ||
R.ifElse( | ||
R.always(R.is(Object, name)), | ||
R.mergeDeepLeft({ headers: name }), | ||
R.assocPath(['headers', name as string], value), | ||
) | ||
const set = (name, value) => { | ||
return (res) => { | ||
if (typeof name === 'object') { | ||
Object.keys(name).forEach((headerName) => { | ||
res.headers.append(headerName, name[headerName]) | ||
}) | ||
} else { | ||
res.headers.append(name, value) | ||
} | ||
return res | ||
} | ||
} | ||
const status = (statusCode, statusText) => | ||
@@ -66,0 +73,0 @@ R.compose( |
@@ -1,3 +0,3 @@ | ||
import MSW from './msw' | ||
export const msw = new MSW() | ||
export { default as msw } from './msw' | ||
export { default as res } from './response' | ||
export { default as context } from './context' |
143
src/msw.ts
import * as R from 'ramda' | ||
import parseRoute, { ParsedRoute } from './utils/parseRoutes' | ||
import assertUrl, { Mask, ParsedUrl } from './utils/assertUrl' | ||
import stringifyMask from './utils/stringifyMask' | ||
import res, { MockedResponse, ResponseComposition } from './response' | ||
import context, { MockedContext } from './context' | ||
enum RESTMethod { | ||
get = 'get', | ||
post = 'post', | ||
put = 'put', | ||
delete = 'delete', | ||
export enum RESTMethod { | ||
GET = 'GET', | ||
POST = 'POST', | ||
PUT = 'PUT', | ||
PATCH = 'PATCH', | ||
OPTIONS = 'OPTIONS', | ||
DELETE = 'DELETE', | ||
} | ||
type Handler = ( | ||
type Resolver = ( | ||
req: Request, | ||
@@ -19,11 +22,7 @@ res: ResponseComposition, | ||
interface Routes { | ||
[method: string]: { | ||
[route: string]: Handler | ||
} | ||
} | ||
type Routes = Record<RESTMethod, { [route: string]: Resolver }> | ||
const serviceWorkerPath = '/mockServiceWorker.js' | ||
export default class MockServiceWorker { | ||
export class MockServiceWorker { | ||
worker: ServiceWorker | ||
@@ -34,2 +33,9 @@ workerRegistration: ServiceWorkerRegistration | ||
constructor() { | ||
if (!('serviceWorker' in navigator)) { | ||
console.error( | ||
'Failed to instantiate MockServiceWorker: Your current environment does not support Service Workers.', | ||
) | ||
return null | ||
} | ||
/** @todo Consider removing event listeners upon destruction */ | ||
@@ -50,42 +56,9 @@ navigator.serviceWorker.addEventListener('message', this.interceptRequest) | ||
return this | ||
} | ||
interceptRequest = (event) => { | ||
const req = JSON.parse(event.data) | ||
const relevantRoutes = this.routes[req.method.toLowerCase()] || {} | ||
const parsedRoute = Object.keys(relevantRoutes).reduce<ParsedRoute>( | ||
(acc, mask) => { | ||
const parsedRoute = parseRoute(mask, req.url) | ||
return parsedRoute.matches ? parsedRoute : acc | ||
}, | ||
null, | ||
) | ||
if (parsedRoute === null) { | ||
return this.postMessage(event, 'not-found') | ||
if (typeof window !== 'undefined') { | ||
;(window as any).msw = this | ||
} | ||
const handler = relevantRoutes[parsedRoute.mask] | ||
const resolvedResponse = | ||
handler({ ...req, params: parsedRoute.params }, res, context) || {} | ||
if (!resolvedResponse) { | ||
console.warn( | ||
'Expected a mocking handler function to return an Object, but got: %s. ', | ||
resolvedResponse, | ||
) | ||
} | ||
return this.postMessage(event, JSON.stringify(resolvedResponse)) | ||
return this | ||
} | ||
/** | ||
* Posts a message to the active ServiceWorker. | ||
*/ | ||
postMessage(event, message: any) { | ||
event.ports[0].postMessage(message) | ||
} | ||
start(): Promise<ServiceWorkerRegistration | void> { | ||
@@ -96,9 +69,2 @@ if (this.workerRegistration) { | ||
if (!('serviceWorker' in navigator)) { | ||
console.error( | ||
'Failed to start MockServiceWorker: Your current browser does not support Service Workers.', | ||
) | ||
return void null | ||
} | ||
navigator.serviceWorker | ||
@@ -113,2 +79,4 @@ .register(serviceWorkerPath, { scope: '/' }) | ||
const foo = setInterval(() => this.workerRegistration.update(), 1000) | ||
return reg | ||
@@ -121,3 +89,3 @@ }) | ||
if (!this.workerRegistration) { | ||
return console.warn('No active instane of Service Worker is active.') | ||
return console.warn('No active instance of Service Worker is running.') | ||
} | ||
@@ -131,11 +99,60 @@ | ||
addRoute = R.curry((method: RESTMethod, route: string, handler: Handler) => { | ||
this.routes = R.assocPath([method, route], handler, this.routes) | ||
addRoute = R.curry((method: RESTMethod, mask: Mask, resolver: Resolver) => { | ||
const resolvedMask = stringifyMask(mask) | ||
this.routes = R.assocPath( | ||
[method.toLowerCase(), resolvedMask], | ||
resolver, | ||
this.routes, | ||
) | ||
return this | ||
}) | ||
get = this.addRoute(RESTMethod.get) | ||
post = this.addRoute(RESTMethod.post) | ||
put = this.addRoute(RESTMethod.put) | ||
delete = this.addRoute(RESTMethod.delete) | ||
get = this.addRoute(RESTMethod.GET) | ||
post = this.addRoute(RESTMethod.POST) | ||
put = this.addRoute(RESTMethod.PUT) | ||
patch = this.addRoute(RESTMethod.PATCH) | ||
options = this.addRoute(RESTMethod.OPTIONS) | ||
delete = this.addRoute(RESTMethod.DELETE) | ||
interceptRequest = (event) => { | ||
const req = JSON.parse(event.data) | ||
const relevantRoutes = this.routes[req.method.toLowerCase()] || {} | ||
const parsedRoute = Object.keys(relevantRoutes).reduce<ParsedUrl>( | ||
(acc, mask) => { | ||
const parsedRoute = assertUrl(mask, req.url) | ||
return parsedRoute.matches ? parsedRoute : acc | ||
}, | ||
null, | ||
) | ||
if (parsedRoute === null) { | ||
return this.postMessage(event, 'not-found') | ||
} | ||
const resolver = relevantRoutes[parsedRoute.mask as string] | ||
const resolvedResponse = | ||
resolver({ ...req, params: parsedRoute.params }, res, context) || {} | ||
resolvedResponse.headers = Array.from(resolvedResponse.headers.entries()) | ||
if (!resolvedResponse) { | ||
console.warn( | ||
'Expected a mocking resolver function to return an Object, but got: %s. ', | ||
resolvedResponse, | ||
) | ||
} | ||
this.postMessage(event, JSON.stringify(resolvedResponse)) | ||
} | ||
/** | ||
* Posts a message to the active ServiceWorker. | ||
* Uses a port of the message channel created in the ServiceWorker. | ||
*/ | ||
postMessage(event, message: any) { | ||
event.ports[0].postMessage(message) | ||
} | ||
} | ||
export default new MockServiceWorker() |
import * as R from 'ramda' | ||
import context from './context' | ||
@@ -8,3 +7,3 @@ export interface MockedResponse { | ||
statusText: string | ||
headers: Object | ||
headers?: Headers | ||
delay: number | ||
@@ -23,3 +22,3 @@ } | ||
const defaultResponse: MockedResponse = { | ||
export const defaultResponse: MockedResponse = { | ||
status: 200, | ||
@@ -29,22 +28,26 @@ statusText: 'OK', | ||
delay: 0, | ||
headers: { | ||
Mocked: true, | ||
}, | ||
} | ||
const response: ResponseComposition = (...transformers) => { | ||
const headers = new Headers() | ||
headers.set('Mocked', 'true') | ||
const initialResponse = { | ||
...defaultResponse, | ||
headers, | ||
} | ||
if (transformers && transformers.length > 0) { | ||
/** | ||
* Ignore the arity annotation from Ramda. | ||
* Apparently, TypeScript assumes "transformers" may be modified | ||
* before they get into pipe as arguments, thus screams at | ||
* potentially empty array. | ||
* Apparently, TypeScript assumes "transformers" may be modified before | ||
* they get into pipe as arguments, thus screams at potentially empty array. | ||
*/ | ||
// @ts-ignore | ||
return R.pipe(...transformers)(defaultResponse) | ||
return R.pipe(...transformers)(initialResponse) | ||
} | ||
return defaultResponse | ||
return initialResponse | ||
} | ||
export default response |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
404608
15
11156
63
15
2