UNPKG

file-routing-expressjs

Version:

A dependency-free, flexible, system-based file routing for Express.

408 lines 16.9 kB
"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