file-routing-expressjs
Version:
A dependency-free, flexible, system-based file routing for Express.
393 lines • 14.5 kB
JavaScript
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
import fs from "fs";
import path from "path";
import {
extractDirContext,
extractEndpointName,
extractFileContext
} from "../helpers/extractors";
import {
buildRoutePattern,
buildRouteWithOneLeadingSlash
} from "../helpers/builders";
import {
filenameIsJSorTS,
isDefined,
isErrorHandler,
isFunction,
isNotDefined,
isRequestHandler,
methodIsExpressMethod
} from "../helpers/validators";
import { pathToFileURL } from "url";
import { errorGuardMiddleware } from "../guards/exception";
import { InvalidPluginError, InvalidRouteHandler, RoutesRootNotFound, UnknownPluginError } from "../types/exceptions";
import { tracingMiddleware } from "../guards/tracing";
import { logger } from "../helpers/logging";
class FileBasedRouting {
constructor({ app, target, errorGuard, plugins, collectEndpoints }) {
this.plugins = /* @__PURE__ */ new Map();
this.collectEndpoints = false;
this.base = target || path.resolve(process.cwd(), "src", "routes");
this.endpoints = [];
this.endpointsQueue = [];
this.plugins = this.buildPluginMap(plugins != null ? plugins : []);
this.collectEndpoints = collectEndpoints != null ? collectEndpoints : false;
this._app = app;
if (isFunction(errorGuard)) {
this.errorGuard = errorGuard;
} else if (errorGuard === true) {
this.errorGuard = errorGuardMiddleware;
}
}
createRoutes() {
return __async(this, null, function* () {
yield this.mapRoutes({
target: this.base,
route: "/",
parentGroup: void 0
});
});
}
mapRoutes(_0) {
return __async(this, arguments, function* ({ target, route, parentGroup }) {
var _a;
if (!fs.existsSync(target)) throw new RoutesRootNotFound(target);
let parent = ((_a = parentGroup == null ? void 0 : parentGroup.route) == null ? void 0 : _a.trim()) || "/";
const targetStat = fs.statSync(target);
const basename = path.basename(target.replace(this.base, ""));
const { name, isParam } = extractEndpointName(path.parse(basename).name);
route = buildRouteWithOneLeadingSlash(route);
if (targetStat.isDirectory()) {
return this.handleDir({
route,
parent,
name,
basename,
target,
isParam
});
}
if (targetStat.isFile()) {
return this.handleFile({
route,
name,
basename,
target,
isParam
});
}
});
}
handleDir(_0) {
return __async(this, arguments, function* ({ route, parent, name, basename, target, isParam }) {
var _a, _b;
parent = path.join(parent, basename);
const routes = fs.readdirSync(target);
const { config, middlewares, errorHandler } = extractDirContext(target);
const endpoint = buildRoutePattern(
route,
name,
isParam,
typeof (config == null ? void 0 : config.pattern) === "string" || config.pattern instanceof RegExp ? config.pattern : void 0
);
middlewares.forEach((middleware) => this._app.use(endpoint, middleware));
if (this.collectEndpoints) {
const routeEndpoint = {
depth: this.endpointsQueue.length,
name,
endpoint,
method: "-",
middlewares: middlewares.map((item) => item.name),
errorHandler: (errorHandler == null ? void 0 : errorHandler.name) || "-",
plugins: [],
children: []
};
const parent2 = (_b = (_a = this.endpointsQueue.at(this.endpointsQueue.length - 1)) == null ? void 0 : _a.children) != null ? _b : this.endpoints;
parent2.push(routeEndpoint);
this.endpointsQueue.push(routeEndpoint);
}
for (const item of routes) {
if (item.startsWith("_")) return;
const newTarget = path.join(target, item);
if (!fs.existsSync(newTarget)) return;
yield this.mapRoutes({
target: newTarget,
route: endpoint,
parentGroup: {
route: parent
}
});
}
if (this.collectEndpoints) {
this.endpointsQueue.pop();
}
if (errorHandler) this._app.use(endpoint, errorHandler);
});
}
handleFile(_0) {
return __async(this, arguments, function* ({ route, name, basename, target, isParam }) {
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v;
if (!filenameIsJSorTS(basename)) return;
const targetAbsolutePath = pathToFileURL(path.resolve(target)).href;
const module = yield import(targetAbsolutePath);
if (!module) return;
const handlers = {
get: (_b = module._get) != null ? _b : (_a = module.default) == null ? void 0 : _a._get,
post: (_d = module._post) != null ? _d : (_c = module.default) == null ? void 0 : _c._post,
delete: (_f = module._delete) != null ? _f : (_e = module.default) == null ? void 0 : _e._delete,
put: (_h = module._put) != null ? _h : (_g = module.default) == null ? void 0 : _g._put,
patch: (_j = module._patch) != null ? _j : (_i = module.default) == null ? void 0 : _i._patch,
all: (_l = module._all) != null ? _l : (_k = module.default) == null ? void 0 : _k._all
};
let { config, middlewares, errorHandler } = extractFileContext(target);
for (const [method, handler] of Object.entries(handlers)) {
if (!methodIsExpressMethod(method) || isNotDefined(handler)) continue;
this.isRequestHandlerOrThrow(handler, method);
const endpoint = buildRoutePattern(
route,
name,
isParam,
config.pattern instanceof RegExp || typeof config.pattern === "string" ? config.pattern : ((_m = config.pattern) == null ? void 0 : _m[method]) || ((_n = config.pattern) == null ? void 0 : _n.all),
true
);
const routeMiddlewares = this.pushMiddlewares(middlewares, method);
const methodPlugins = (_r = (_q = (_o = config.plugins) == null ? void 0 : _o[method]) != null ? _q : (_p = config.plugins) == null ? void 0 : _p.all) != null ? _r : {};
const wrappedHandler = this.bindPlugins(handler, route, methodPlugins);
this._app[method](
endpoint,
tracingMiddleware,
...routeMiddlewares,
(_t = (_s = this.errorGuard) == null ? void 0 : _s.call(this, wrappedHandler)) != null ? _t : wrappedHandler
);
let routerEndpoint;
if (this.collectEndpoints) {
routerEndpoint = {
depth: this.endpointsQueue.length,
name,
endpoint,
method,
middlewares: [],
errorHandler: "-",
plugins: Object.keys(methodPlugins),
children: []
};
routerEndpoint.middlewares.push(...routeMiddlewares.map((middleware) => middleware.name));
}
if (errorHandler) {
let resolvedError = errorHandler;
if (typeof resolvedError !== "function") {
resolvedError = (resolvedError == null ? void 0 : resolvedError[method]) || (resolvedError == null ? void 0 : resolvedError.all);
}
if (isErrorHandler(resolvedError)) {
this._app.use(endpoint, resolvedError);
if (isDefined(routerEndpoint)) routerEndpoint.errorHandler = resolvedError.name;
}
}
if (isDefined(routerEndpoint)) {
const parent = (_v = (_u = this.endpointsQueue.at(this.endpointsQueue.length - 1)) == null ? void 0 : _u.children) != null ? _v : this.endpoints;
parent.push(routerEndpoint);
}
;
}
});
}
pushMiddlewares(middlewares, method) {
var _a, _b, _c, _d;
const routeMiddlewares = [];
if (Array.isArray(middlewares)) {
middlewares.forEach((middleware) => {
var _a2, _b2;
this.isRequestHandlerOrThrow(middleware, method);
routeMiddlewares.push((_b2 = (_a2 = this.errorGuard) == null ? void 0 : _a2.call(this, middleware)) != null ? _b2 : middleware);
});
} else if (middlewares && typeof middlewares === "object") {
const methodMiddlewares = middlewares[method] || middlewares.all;
if (Array.isArray(methodMiddlewares)) {
methodMiddlewares.forEach((middleware) => {
var _a2, _b2;
this.isRequestHandlerOrThrow(middleware, method);
routeMiddlewares.push((_b2 = (_a2 = this.errorGuard) == null ? void 0 : _a2.call(this, middleware)) != null ? _b2 : middleware);
});
} else if (methodMiddlewares) {
this.isRequestHandlerOrThrow(methodMiddlewares, method);
routeMiddlewares.push((_b = (_a = this.errorGuard) == null ? void 0 : _a.call(this, methodMiddlewares)) != null ? _b : methodMiddlewares);
}
} else if (middlewares) {
this.isRequestHandlerOrThrow(middlewares, method);
routeMiddlewares.push((_d = (_c = this.errorGuard) == null ? void 0 : _c.call(this, middlewares)) != null ? _d : middlewares);
}
return routeMiddlewares;
}
bindPlugins(handler, route, routePlugins) {
var _a, _b, _c, _d;
if (isNotDefined(routePlugins)) return handler;
const active = [];
for (const [name, value] of Object.entries(routePlugins)) {
const plugin = this.plugins.get(name);
if (isNotDefined(plugin)) {
if (this.collectEndpoints) {
logger.warn(`Unknown plugin "${name}" in route "${route}"`);
continue;
}
throw new UnknownPluginError(
name,
route,
[...this.plugins.keys()]
);
}
let enabled = true;
let config = void 0;
if (typeof value === "boolean") {
enabled = value;
} else {
enabled = (_a = value.enabled) != null ? _a : true;
config = value.config;
}
if (!enabled) continue;
(_b = plugin.validateConfig) == null ? void 0 : _b.call(plugin, config);
active.push({ plugin, config });
}
let wrapped = handler;
for (const { plugin, config } of active) {
wrapped = (_d = (_c = plugin.wrap) == null ? void 0 : _c.call(plugin, wrapped, config)) != null ? _d : wrapped;
}
return (req, res, next) => __async(this, null, function* () {
const ctx = {
req,
res,
state: {},
config: void 0
};
const executed = [];
const delegate = (i) => __async(this, null, function* () {
if (i >= active.length) {
yield wrapped(req, res, next);
return;
}
;
let called = false;
const nextFn = () => __async(this, null, function* () {
if (called) return;
called = true;
yield delegate(i + 1);
});
const entry = active[i];
const { plugin, config } = entry;
executed.push(entry);
ctx.config = config;
if (isDefined(plugin.onRequest)) {
yield plugin.onRequest(ctx, nextFn);
} else {
yield delegate(i + 1);
}
});
try {
yield delegate(0);
while (executed.length > 0) {
const { config, plugin } = executed[executed.length - 1];
ctx.config = config;
if (isDefined(plugin.onResponse)) {
yield plugin.onResponse(ctx);
}
executed.pop();
}
} catch (err) {
while (executed.length > 0) {
const { config, plugin } = executed.pop();
ctx.config = config;
if (isDefined(plugin.onError)) {
yield plugin.onError(err, ctx);
}
}
throw err;
}
});
}
isRequestHandlerOrThrow(handler, method) {
if (!isRequestHandler(handler)) throw new InvalidRouteHandler(method, handler);
}
buildPluginMap(plugins) {
const map = /* @__PURE__ */ new Map();
for (const plugin of plugins) {
if (!plugin || typeof plugin !== "object") {
throw new InvalidPluginError(`Invalid plugin: expected object`);
}
if (typeof plugin.name !== "string" || plugin.name.trim() === "") {
throw new InvalidPluginError(`Plugin must have a valid name`);
}
if (map.has(plugin.name)) {
throw new InvalidPluginError(`Duplicate plugin name "${plugin.name}"`);
}
if (isDefined(plugin.wrap) && !isFunction(plugin.wrap)) {
throw new InvalidPluginError(`Plugin "${plugin.name}": wrap must be a function`);
}
if (isDefined(plugin.onRequest) && !isFunction(plugin.onRequest)) {
throw new InvalidPluginError(`Plugin "${plugin.name}": onRequest must be a function`);
}
if (isDefined(plugin.onResponse) && !isFunction(plugin.onResponse)) {
throw new InvalidPluginError(`Plugin "${plugin.name}": onResponse must be a function`);
}
if (isDefined(plugin.onError) && !isFunction(plugin.onError)) {
throw new InvalidPluginError(`Plugin "${plugin.name}": onError must be a function`);
}
if (isDefined(plugin.validateConfig) && !isFunction(plugin.validateConfig)) {
throw new InvalidPluginError(`Plugin "${plugin.name}": validateConfig must be a function`);
}
Object.freeze(plugin);
map.set(plugin.name, plugin);
}
return map;
}
static collectRoutes(target) {
return __async(this, null, function* () {
const fakeApp = {
use: () => {
},
get: () => {
},
post: () => {
},
put: () => {
},
patch: () => {
},
delete: () => {
},
all: () => {
}
};
const instance = new FileBasedRouting({
app: fakeApp,
target,
collectEndpoints: true
});
yield instance.createRoutes();
return instance.endpoints;
});
}
}
var file_based_routing_default = FileBasedRouting;
export {
file_based_routing_default as default
};
//# sourceMappingURL=file-based-routing.mjs.map