Skip to content

Commit

Permalink
Add passthrough support
Browse files Browse the repository at this point in the history
  • Loading branch information
IanVS committed Nov 9, 2023
1 parent 1692fb0 commit 4ab8e8c
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 11 deletions.
84 changes: 74 additions & 10 deletions lib/msw-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
type RestRequest,
type SetupWorkerApi,
} from 'msw';
import PassthroughRegistry from './passthrough-registry';
import type { Server } from 'miragejs';
import type { RouteHandler, ServerConfig } from 'miragejs/server';
import type { HTTPVerb, RouteHandler, ServerConfig } from 'miragejs/server';
import type { AnyFactories, AnyModels, AnyRegistry } from 'miragejs/-types';

type RawHandler = RouteHandler<AnyRegistry> | {};
Expand All @@ -16,15 +17,6 @@ type ResponseCode = number;
/** code, headers, serialized response */
type ResponseData = [ResponseCode, { [k: string]: string }, string | undefined];

type HTTPVerb =
| 'get'
| 'put'
| 'post'
| 'patch'
| 'delete'
| 'options'
| 'head';

/** e.g. "/movies/:id" */
type Shorthand = string;

Expand Down Expand Up @@ -52,6 +44,8 @@ type MirageServer = {
options?: unknown
) => (request: RestRequest) => ResponseData | PromiseLike<ResponseData>;

shouldLog: () => boolean;

get?: BaseHandler;
post?: BaseHandler;
put?: BaseHandler;
Expand Down Expand Up @@ -141,6 +135,8 @@ export default class MswConfig {

handlers: RestHandler[] = [];

private passthroughs;

get?: BaseHandler;
post?: BaseHandler;
put?: BaseHandler;
Expand All @@ -150,6 +146,10 @@ export default class MswConfig {
head?: BaseHandler;
options?: BaseHandler;

constructor() {
this.passthroughs = new PassthroughRegistry();
}

create(
server: MirageServer,
mirageConfig: ServerConfig<AnyModels, AnyFactories>
Expand Down Expand Up @@ -409,12 +409,76 @@ export default class MswConfig {
return fullPath;
}

passthrough(...args: (string | HTTPVerb[])[]) {
let verbs: HTTPVerb[] = [
'get',
'post',
'put',
'delete',
'patch',
'options',
'head',
];
let lastArg = args[args.length - 1];
let paths: string[] = [];

if (args.length === 0) {
paths = ['/**', '/'];
} else if (Array.isArray(lastArg)) {
verbs = lastArg;
// Need to loop because TS doesn't know if they're strings or arrays
for (const arg of args) {
if (typeof arg === 'string') {
paths.push(arg);
}
}
}

paths.forEach((path) => {
if (typeof path === 'function') {
// TODO: handle this case
} else {
let fullPath = this._getFullPath(path);
this.passthroughs.add(fullPath, verbs);
}
});
}

start() {
this.msw = setupWorker(...this.handlers);

let logging = this.mirageConfig?.logging || false;
this.msw.start({
quiet: !logging,
onUnhandledRequest: (req) => {
const verb = req.method.toUpperCase();
const path = req.url.pathname;
const recognized = this.passthroughs
.retrieve(req.url.host)
?.get(verb)
?.recognize(path);
const match = recognized?.[0];

if (match) {
if (this.mirageServer?.shouldLog()) {
console.log(
`Mirage: Passthrough request for ${verb} ${req.url.href}`
);
}
// @ts-expect-error this seems to be an issue in msw types
req.passthrough();
} else if (this.mirageServer?.shouldLog()) {
let namespaceError = '';
if (this.namespace === '') {
namespaceError = 'There is no existing namespace defined.';
} else {
namespaceError = `The existing namespace is ${this.namespace}`;
}
console.warn(
`Mirage: Your app tried to ${verb} '${req.url.href}', but there was no route defined to handle this request. Add a passthrough or define a route for this endpoint in your routes() config.\nDid you forget to define a namespace? ${namespaceError}`
);
}
},
});
}

Expand Down
84 changes: 84 additions & 0 deletions lib/passthrough-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import RouteRecognizer from 'route-recognizer';
import type { HTTPVerb } from 'miragejs/server';

const noOpHandler = () => {};
const allVerbs = ['GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];

/**
* Registry
*
* A registry is a map of HTTP verbs to route recognizers.
*/
const createPassthroughRegistry = (path: string, verbs: HTTPVerb[]) => {
const registry = new Map();

const uppercaseUserVerbs = verbs.map((v) => v.toUpperCase());

const matchingVerbs = allVerbs.filter((v) => {
// If the user didn't specify verbs, then use everything
if (!verbs || !Array.isArray(verbs) || verbs.length === 0) return true;

return uppercaseUserVerbs.includes(v);
});

matchingVerbs.forEach((mv) => {
const recognizer = new RouteRecognizer();
recognizer.add([{ path, handler: noOpHandler }]);
registry.set(mv, recognizer);
});

return registry;
};

/**
* Hosts
*
* a map of hosts to Registries, ultimately allowing
* a per-host-and-port, per HTTP verb lookup of RouteRecognizers
*/
export default class PassthroughRegistry {
registries: Map<string, Map<string, RouteRecognizer>>;

constructor() {
this.registries = new Map();
return this;
}

/**
* Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers
* for a given URL
*
* @param {String} url a URL
* @param {String[]} verbs a list of HTTP verbs to passthrough. Defaults to all verbs if not specified.
* @return {Registry} a map of HTTP verbs to RouteRecognizers
* corresponding to the provided URL's
* hostname and port
*/
add(url: string, verbs: HTTPVerb[]) {
const { host, pathname } = new URL(url);
const registry = this.registries.get(host);

if (registry === undefined) {
this.registries.set(host, createPassthroughRegistry(pathname, verbs));
} else {
const verbsToSet =
Array.isArray(verbs) && verbs.length
? verbs.map((v) => v.toUpperCase())
: allVerbs;
verbsToSet.forEach((v) => {
const existingRecognizer = registry.get(v);
if (existingRecognizer) {
existingRecognizer.add([{ path: pathname, handler: noOpHandler }]);
} else {
const recognizer = new RouteRecognizer();
recognizer.add([{ path: pathname, handler: noOpHandler }]);
registry.set(v, recognizer);
}
});
}
}

retrieve(url: string) {
return this.registries.get(url);
}
}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"route-recognizer": "^0.3.4"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.2",
"miragejs": "^0.1.47",
Expand Down
6 changes: 5 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4ab8e8c

Please sign in to comment.