next-connect
Version:
The method routing and middleware layer for Next.js (and many others)
117 lines (116 loc) • 4.34 kB
JavaScript
/**
* Agnostic router class
* Adapted from lukeed/trouter library:
* https://github.com/lukeed/trouter/blob/master/index.mjs
*/
import { parse } from "regexparam";
export class Router {
constructor(base = "/", routes = []) {
this.base = base;
this.routes = routes;
}
add(method, route, ...fns) {
if (typeof route === "function") {
fns.unshift(route);
route = "";
}
if (route === "")
this.routes.push({ matchAll: true, method, fns, isMiddle: false });
else {
const { keys, pattern } = parse(route);
this.routes.push({ keys, pattern, method, fns, isMiddle: false });
}
return this;
}
use(base, ...fns) {
if (typeof base === "function" || base instanceof Router) {
fns.unshift(base);
base = "/";
}
// mount subrouter
fns = fns.map((fn) => {
if (fn instanceof Router) {
if (typeof base === "string")
return fn.clone(base);
throw new Error("Mounting a router to RegExp base is not supported");
}
return fn;
});
const { keys, pattern } = parse(base, true);
this.routes.push({ keys, pattern, method: "", fns, isMiddle: true });
return this;
}
clone(base) {
return new Router(base, Array.from(this.routes));
}
static async exec(fns, ...args) {
let i = 0;
const next = () => fns[++i](...args, next);
return fns[i](...args, next);
}
find(method, pathname) {
let middleOnly = true;
const fns = [];
const params = {};
const isHead = method === "HEAD";
for (const route of this.routes) {
if (route.method !== method &&
// matches any method
route.method !== "" &&
// The HEAD method requests that the target resource transfer a representation of its state, as for a GET request...
!(isHead && route.method === "GET")) {
continue;
}
let matched = false;
if ("matchAll" in route) {
matched = true;
}
else {
if (route.keys === false) {
// routes.key is RegExp: https://github.com/lukeed/regexparam/blob/master/src/index.js#L2
const matches = route.pattern.exec(pathname);
if (matches === null)
continue;
if (matches.groups !== void 0)
for (const k in matches.groups)
params[k] = matches.groups[k];
matched = true;
}
else if (route.keys.length > 0) {
const matches = route.pattern.exec(pathname);
if (matches === null)
continue;
for (let j = 0; j < route.keys.length;)
params[route.keys[j]] = matches[++j];
matched = true;
}
else if (route.pattern.test(pathname)) {
matched = true;
} // else not a match
}
if (matched) {
fns.push(...route.fns
.map((fn) => {
if (fn instanceof Router) {
const base = fn.base;
let stripPathname = pathname.substring(base.length);
// fix stripped pathname, not sure why this happens
if (stripPathname[0] != "/")
stripPathname = `/${stripPathname}`;
const result = fn.find(method, stripPathname);
if (!result.middleOnly)
middleOnly = false;
// merge params
Object.assign(params, result.params);
return result.fns;
}
return fn;
})
.flat());
if (!route.isMiddle)
middleOnly = false;
}
}
return { fns, params, middleOnly };
}
}