flight-path
Version:
Express style router for Fastly Compute@Edge
152 lines (151 loc) • 5.07 kB
JavaScript
import { Route } from "./route";
import FPRequest from "./request";
import FPResponse from "./response";
import { Middleware } from "./middleware";
export class Router {
constructor(config) {
this.routes = [];
this.middlewares = [];
this.config = {
templatesDir: "templates",
parseCookies: true,
};
this.config = {
...this.config,
...config
};
}
listen() {
addEventListener("fetch", (event) => event.respondWith(this.handler(event)));
}
async handler(event) {
try {
const req = new FPRequest(this.config, event);
const res = new FPResponse(this.config);
await this.runMiddlewares(req, res);
if (!res.hasEnded) {
await this.runRoutes(req, res);
}
return serializeResponse(res);
}
catch (e) {
console.log(e);
}
}
async runMiddlewares(req, res) {
for (let m of this.middlewares) {
if (m.check(req)) {
await m.run(req, res);
}
}
}
async runRoutes(req, res) {
const matchedRoute = this.routes.find((route) => route.check(req));
if (matchedRoute) {
await matchedRoute.run(req, res);
}
}
use(path, callback) {
if (path instanceof Function) {
this.middlewares.push(new Middleware(() => true, path));
}
else {
this.middlewares.push(new Middleware(basicRouteMatcher("*", path, false), callback));
}
}
route(method, pattern, callback) {
this.routes.push(new Route(basicRouteMatcher(method, pattern), callback));
}
all(pattern, callback) {
this.route("*", pattern, callback);
}
get(pattern, callback) {
this.route("GET", pattern, callback);
}
post(pattern, callback) {
this.route("POST", pattern, callback);
}
put(pattern, callback) {
this.route("PUT", pattern, callback);
}
delete(pattern, callback) {
this.route("DELETE", pattern, callback);
}
head(pattern, callback) {
this.route("HEAD", pattern, callback);
}
options(pattern, callback) {
this.route("OPTIONS", pattern, callback);
}
patch(pattern, callback) {
this.route("PATCH", pattern, callback);
}
}
function serializeResponse(res) {
res.setDefaults();
let response = new Response(res.body, {
headers: res._headers,
status: res.status,
});
// Looping cookie headers manually to work around this bug: https://github.com/fastly/js-compute-runtime/issues/47
for (let [_, c] of res._cookies) {
response.headers.append("Set-Cookie", c);
}
return response;
}
/**
* This function creates another function which will be used to check if a request matches the route.
* e.g. does the method and the pattern match?
* @param method the HTTP method, GET, POST etc
* @param pattern Express style path
* @returns A function which returns a boolean, true = "matched, run this route"
*/
export function basicRouteMatcher(method, pattern, extractParams = true) {
const isRegexMatch = (pattern.indexOf("*") !== -1 || pattern.indexOf(":") !== -1) &&
pattern.length > 1;
function simpleMatch(req) {
if (req.method.toUpperCase() != method.toUpperCase() && method != "*")
return false;
return pattern == "*" || req.url.pathname == pattern;
}
let checkFunction = isRegexMatch
? makeRegexMatch(pattern, extractParams)
: simpleMatch;
return (req) => {
return checkFunction(req);
};
}
/**
* Take the path of a route which can include parameters such as ":id" and turn those into regex matches
* @param pattern Express style path pattern, e.g "/user/:userid/profile"
* @returns
*/
function makeRegexMatch(pattern, extractParams = true) {
pattern = pattern
.replace(/\$/g, "$")
.replace(/\^/g, "^")
.replace(/\*/g, "(.*)")
.replace(/\//g, "\\/")
.replace(/((?<=\:)[a-zA-Z0-9]+)/g, "(?<$&>[a-zA-Z0-9_-]+)")
.replace(/\:/g, "");
// Above regex does this:
// '/user/:userid/profile' -> '\\/user\\/(?<userid>[a-zA-Z0-9_-]+)\\/profile'
// Importantly, we are making this at compile time and not runtime
const matchRegexp = new RegExp(`^${pattern}$`, "i");
// Not sure how required this is, but use the regex to verify it is actually compiled.
matchRegexp.test("Make sure RegExp is compiled at build time.");
return (req) => {
let matches;
if ((matches = matchRegexp.exec(req.url.pathname)) !== null) {
// Take matches and put in req.params
if (matches.groups) {
let matchKeys = Object.keys(matches.groups);
matchKeys.map((k) => {
req.params[k] = matches.groups[k];
});
}
return true;
}
return false;
};
}