UNPKG

file-routing-expressjs

Version:

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

393 lines 14.5 kB
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