file-routing-expressjs
Version:
A dependency-free, flexible, system-based file routing for Express.
408 lines • 16.9 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
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());
});
};
var file_based_routing_exports = {};
__export(file_based_routing_exports, {
default: () => file_based_routing_default
});
module.exports = __toCommonJS(file_based_routing_exports);
var import_fs = __toESM(require("fs"));
var import_path = __toESM(require("path"));
var import_extractors = require("../helpers/extractors");
var import_builders = require("../helpers/builders");
var import_validators = require("../helpers/validators");
var import_url = require("url");
var import_exception = require("../guards/exception");
var import_exceptions = require("../types/exceptions");
var import_tracing = require("../guards/tracing");
var import_logging = require("../helpers/logging");
class FileBasedRouting {
constructor({ app, target, errorGuard, plugins, collectEndpoints }) {
this.plugins = /* @__PURE__ */ new Map();
this.collectEndpoints = false;
this.base = target || import_path.default.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 ((0, import_validators.isFunction)(errorGuard)) {
this.errorGuard = errorGuard;
} else if (errorGuard === true) {
this.errorGuard = import_exception.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 (!import_fs.default.existsSync(target)) throw new import_exceptions.RoutesRootNotFound(target);
let parent = ((_a = parentGroup == null ? void 0 : parentGroup.route) == null ? void 0 : _a.trim()) || "/";
const targetStat = import_fs.default.statSync(target);
const basename = import_path.default.basename(target.replace(this.base, ""));
const { name, isParam } = (0, import_extractors.extractEndpointName)(import_path.default.parse(basename).name);
route = (0, import_builders.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 = import_path.default.join(parent, basename);
const routes = import_fs.default.readdirSync(target);
const { config, middlewares, errorHandler } = (0, import_extractors.extractDirContext)(target);
const endpoint = (0, import_builders.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 = import_path.default.join(target, item);
if (!import_fs.default.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 (!(0, import_validators.filenameIsJSorTS)(basename)) return;
const targetAbsolutePath = (0, import_url.pathToFileURL)(import_path.default.resolve(target)).href;
const module2 = yield import(targetAbsolutePath);
if (!module2) return;
const handlers = {
get: (_b = module2._get) != null ? _b : (_a = module2.default) == null ? void 0 : _a._get,
post: (_d = module2._post) != null ? _d : (_c = module2.default) == null ? void 0 : _c._post,
delete: (_f = module2._delete) != null ? _f : (_e = module2.default) == null ? void 0 : _e._delete,
put: (_h = module2._put) != null ? _h : (_g = module2.default) == null ? void 0 : _g._put,
patch: (_j = module2._patch) != null ? _j : (_i = module2.default) == null ? void 0 : _i._patch,
all: (_l = module2._all) != null ? _l : (_k = module2.default) == null ? void 0 : _k._all
};
let { config, middlewares, errorHandler } = (0, import_extractors.extractFileContext)(target);
for (const [method, handler] of Object.entries(handlers)) {
if (!(0, import_validators.methodIsExpressMethod)(method) || (0, import_validators.isNotDefined)(handler)) continue;
this.isRequestHandlerOrThrow(handler, method);
const endpoint = (0, import_builders.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,
import_tracing.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 ((0, import_validators.isErrorHandler)(resolvedError)) {
this._app.use(endpoint, resolvedError);
if ((0, import_validators.isDefined)(routerEndpoint)) routerEndpoint.errorHandler = resolvedError.name;
}
}
if ((0, import_validators.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 ((0, import_validators.isNotDefined)(routePlugins)) return handler;
const active = [];
for (const [name, value] of Object.entries(routePlugins)) {
const plugin = this.plugins.get(name);
if ((0, import_validators.isNotDefined)(plugin)) {
if (this.collectEndpoints) {
import_logging.logger.warn(`Unknown plugin "${name}" in route "${route}"`);
continue;
}
throw new import_exceptions.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 ((0, import_validators.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 ((0, import_validators.isDefined)(plugin.onResponse)) {
yield plugin.onResponse(ctx);
}
executed.pop();
}
} catch (err) {
while (executed.length > 0) {
const { config, plugin } = executed.pop();
ctx.config = config;
if ((0, import_validators.isDefined)(plugin.onError)) {
yield plugin.onError(err, ctx);
}
}
throw err;
}
});
}
isRequestHandlerOrThrow(handler, method) {
if (!(0, import_validators.isRequestHandler)(handler)) throw new import_exceptions.InvalidRouteHandler(method, handler);
}
buildPluginMap(plugins) {
const map = /* @__PURE__ */ new Map();
for (const plugin of plugins) {
if (!plugin || typeof plugin !== "object") {
throw new import_exceptions.InvalidPluginError(`Invalid plugin: expected object`);
}
if (typeof plugin.name !== "string" || plugin.name.trim() === "") {
throw new import_exceptions.InvalidPluginError(`Plugin must have a valid name`);
}
if (map.has(plugin.name)) {
throw new import_exceptions.InvalidPluginError(`Duplicate plugin name "${plugin.name}"`);
}
if ((0, import_validators.isDefined)(plugin.wrap) && !(0, import_validators.isFunction)(plugin.wrap)) {
throw new import_exceptions.InvalidPluginError(`Plugin "${plugin.name}": wrap must be a function`);
}
if ((0, import_validators.isDefined)(plugin.onRequest) && !(0, import_validators.isFunction)(plugin.onRequest)) {
throw new import_exceptions.InvalidPluginError(`Plugin "${plugin.name}": onRequest must be a function`);
}
if ((0, import_validators.isDefined)(plugin.onResponse) && !(0, import_validators.isFunction)(plugin.onResponse)) {
throw new import_exceptions.InvalidPluginError(`Plugin "${plugin.name}": onResponse must be a function`);
}
if ((0, import_validators.isDefined)(plugin.onError) && !(0, import_validators.isFunction)(plugin.onError)) {
throw new import_exceptions.InvalidPluginError(`Plugin "${plugin.name}": onError must be a function`);
}
if ((0, import_validators.isDefined)(plugin.validateConfig) && !(0, import_validators.isFunction)(plugin.validateConfig)) {
throw new import_exceptions.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;
//# sourceMappingURL=file-based-routing.js.map