exisjs 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/build.js +43 -55
  3. package/dist/cli/commands/build.js.map +1 -1
  4. package/dist/cli/manifest.d.ts +1 -0
  5. package/dist/cli/manifest.d.ts.map +1 -1
  6. package/dist/cli/manifest.js +76 -17
  7. package/dist/cli/manifest.js.map +1 -1
  8. package/dist/exports/sanitize.d.ts +2 -0
  9. package/dist/exports/sanitize.d.ts.map +1 -0
  10. package/dist/exports/sanitize.js +6 -0
  11. package/dist/exports/sanitize.js.map +1 -0
  12. package/dist/middleware/middleware.d.ts.map +1 -1
  13. package/dist/middleware/middleware.js +5 -0
  14. package/dist/middleware/middleware.js.map +1 -1
  15. package/dist/server/app.d.ts +51 -55
  16. package/dist/server/app.d.ts.map +1 -1
  17. package/dist/server/app.js +123 -2047
  18. package/dist/server/app.js.map +1 -1
  19. package/dist/server/bootstrapper.d.ts +24 -0
  20. package/dist/server/bootstrapper.d.ts.map +1 -0
  21. package/dist/server/bootstrapper.js +408 -0
  22. package/dist/server/bootstrapper.js.map +1 -0
  23. package/dist/server/controller-registrar.d.ts +8 -0
  24. package/dist/server/controller-registrar.d.ts.map +1 -0
  25. package/dist/server/controller-registrar.js +351 -0
  26. package/dist/server/controller-registrar.js.map +1 -0
  27. package/dist/server/plugin-manager.d.ts +26 -0
  28. package/dist/server/plugin-manager.d.ts.map +1 -0
  29. package/dist/server/plugin-manager.js +150 -0
  30. package/dist/server/plugin-manager.js.map +1 -0
  31. package/dist/server/queue-manager.d.ts +18 -0
  32. package/dist/server/queue-manager.d.ts.map +1 -0
  33. package/dist/server/queue-manager.js +54 -0
  34. package/dist/server/queue-manager.js.map +1 -0
  35. package/dist/server/request-handler.d.ts +25 -0
  36. package/dist/server/request-handler.d.ts.map +1 -0
  37. package/dist/server/request-handler.js +230 -0
  38. package/dist/server/request-handler.js.map +1 -0
  39. package/dist/server/route-scanner.d.ts +20 -0
  40. package/dist/server/route-scanner.d.ts.map +1 -0
  41. package/dist/server/route-scanner.js +892 -0
  42. package/dist/server/route-scanner.js.map +1 -0
  43. package/dist/server/ws-orchestrator.d.ts +15 -0
  44. package/dist/server/ws-orchestrator.d.ts.map +1 -0
  45. package/dist/server/ws-orchestrator.js +156 -0
  46. package/dist/server/ws-orchestrator.js.map +1 -0
  47. package/dist/utils/port.d.ts +2 -0
  48. package/dist/utils/port.d.ts.map +1 -0
  49. package/dist/utils/port.js +30 -0
  50. package/dist/utils/port.js.map +1 -0
  51. package/dist/utils/sanitize.d.ts +33 -0
  52. package/dist/utils/sanitize.d.ts.map +1 -0
  53. package/dist/utils/sanitize.js +95 -0
  54. package/dist/utils/sanitize.js.map +1 -0
  55. package/dist/utils/validator.d.ts +2 -0
  56. package/dist/utils/validator.d.ts.map +1 -1
  57. package/dist/utils/validator.js +55 -17
  58. package/dist/utils/validator.js.map +1 -1
  59. package/package.json +6 -1
@@ -0,0 +1,892 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.RouteScanner = void 0;
37
+ const router_1 = require("../router/router");
38
+ const middleware_1 = require("../middleware/middleware");
39
+ const validator_1 = require("../utils/validator");
40
+ const node_url_1 = require("node:url");
41
+ const context_1 = require("./context");
42
+ const dev_error_overlay_1 = require("./dev-error-overlay");
43
+ class RouteScanner {
44
+ constructor(app) {
45
+ this.app = app;
46
+ this.lazyRoutes = new Map();
47
+ this.routeMap = new Map();
48
+ this.apiDir = null;
49
+ }
50
+ async loadAllRoutes() {
51
+ for (const [filePath, entry] of this.lazyRoutes.entries()) {
52
+ if (!entry.loaded) {
53
+ entry.loaded = true;
54
+ this.app.getRouter().removeRoutesBySource('lazy:' + filePath);
55
+ const routePath = this.routeMap.get(filePath);
56
+ if (routePath) {
57
+ try {
58
+ await this.mountRouteFile(filePath, routePath);
59
+ }
60
+ catch (e) {
61
+ this.app.log.error({ err: e, file: filePath }, 'Failed to eager-load route');
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ async autoMountRoutes(root) {
68
+ const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
69
+ const path = await Promise.resolve().then(() => __importStar(require('node:path')));
70
+ const isProd = process.env.NODE_ENV === 'production';
71
+ const isDev = this.app.options.env === 'development';
72
+ // Try to load the pre-built manifest first (O(1) boot)
73
+ const manifestPath = path.join(root, '.exis', 'routes-manifest.js');
74
+ try {
75
+ const stat = await fs.stat(manifestPath);
76
+ if (stat.isFile()) {
77
+ let url = (0, node_url_1.pathToFileURL)(manifestPath).href;
78
+ if (!isProd)
79
+ url += '?t=' + Date.now(); // cache bust for dev
80
+ let mod;
81
+ if (process.env.VITEST || process.env.NODE_ENV === 'test') {
82
+ mod = await Promise.resolve(`${url}`).then(s => __importStar(require(s)));
83
+ }
84
+ else {
85
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
86
+ mod = await dynamicImport(url);
87
+ }
88
+ const manifest = mod.manifest;
89
+ if (Array.isArray(manifest)) {
90
+ for (const entry of manifest) {
91
+ const { routePath, module: routeMod, filePath } = entry;
92
+ const normalizedPath = path.resolve(root, filePath);
93
+ this.routeMap.set(normalizedPath, routePath);
94
+ const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
95
+ const isClassController = (obj) => obj &&
96
+ obj.prototype &&
97
+ obj.prototype[CONTROLLER_PREFIX] !== undefined;
98
+ const unwrappedMod = routeMod.default && routeMod.default.default
99
+ ? routeMod.default.default
100
+ : routeMod.default
101
+ ? routeMod.default
102
+ : routeMod;
103
+ const functionalControllerObj = unwrappedMod && unwrappedMod.__isController ? unwrappedMod : null;
104
+ const routerInstance = routeMod.router || routeMod.default || routeMod;
105
+ if (functionalControllerObj) {
106
+ const router = this.compileFunctionalController(functionalControllerObj);
107
+ this.mountRouteWithSource(routePath, router, normalizedPath);
108
+ }
109
+ else if (isClassController(routerInstance)) {
110
+ this.app.registerControllers([routerInstance], routePath);
111
+ }
112
+ else {
113
+ this.mountRouteWithSource(routePath, routerInstance, normalizedPath);
114
+ }
115
+ }
116
+ // Skip the filesystem scan completely!
117
+ return;
118
+ }
119
+ }
120
+ }
121
+ catch (err) {
122
+ if (err.code !== 'ENOENT') {
123
+ console.warn('exisjs: Failed to load routes manifest:', err.message);
124
+ }
125
+ // Fall back to scanning the filesystem
126
+ }
127
+ const searchDirs = isProd
128
+ ? [
129
+ path.join(root, '.exis', 'server', 'src', 'http'),
130
+ path.join(root, '.exis', 'server', 'http'),
131
+ path.join(root, 'dist', 'src', 'http'),
132
+ path.join(root, 'dist', 'http'),
133
+ path.join(root, 'src', 'http'),
134
+ path.join(root, 'http'),
135
+ ]
136
+ : [path.join(root, 'src', 'http'), path.join(root, 'http')];
137
+ const appDirs = [];
138
+ for (const dir of searchDirs) {
139
+ try {
140
+ const stat = await fs.stat(dir);
141
+ if (stat.isDirectory()) {
142
+ const baseName = path.basename(path.dirname(dir)) === 'src' ? 'src/http' : 'http';
143
+ const alreadyHasCompiledOrRawForThisBase = appDirs.some((existing) => {
144
+ const existingBase = path.basename(path.dirname(existing)) === 'src'
145
+ ? 'src/http'
146
+ : 'http';
147
+ return existingBase === baseName;
148
+ });
149
+ if (!alreadyHasCompiledOrRawForThisBase) {
150
+ appDirs.push(dir);
151
+ }
152
+ }
153
+ }
154
+ catch {
155
+ /* ignore */
156
+ }
157
+ }
158
+ if (appDirs.length === 0)
159
+ return;
160
+ this.apiDir = appDirs[0] // keep property name for backwards compatibility
161
+ ;
162
+ this._allApiDirs = appDirs;
163
+ const cc = {
164
+ green: '\x1b[32m',
165
+ cyan: '\x1b[36m',
166
+ gray: '\x1b[90m',
167
+ reset: '\x1b[0m',
168
+ };
169
+ for (const appDir of appDirs) {
170
+ const routes = await this.scanDirectory(appDir);
171
+ for (const { filePath, routePath } of routes) {
172
+ if (filePath.endsWith('route.ts') || filePath.endsWith('route.js')) {
173
+ const normalized = path.resolve(filePath);
174
+ this.routeMap.set(normalized, routePath);
175
+ if (isDev) {
176
+ const lazyKey = normalized;
177
+ this.lazyRoutes.set(lazyKey, {
178
+ filePath: normalized,
179
+ loaded: false,
180
+ });
181
+ const lazyHandler = async (req, res, next) => {
182
+ const entry = this.lazyRoutes.get(lazyKey);
183
+ if (entry && !entry.loaded) {
184
+ entry.loaded = true;
185
+ this.app.getRouter().removeRoutesBySource('lazy:' + lazyKey);
186
+ try {
187
+ await this.mountRouteFile(normalized, routePath);
188
+ const relative = path
189
+ .relative(process.cwd(), normalized)
190
+ .replace(/\\/g, '/');
191
+ console.log(` ${cc.green}✓${cc.reset} Lazy loaded ${cc.cyan}${relative}${cc.reset}`);
192
+ }
193
+ catch (err) {
194
+ (0, dev_error_overlay_1.formatDevError)(err, normalized);
195
+ return;
196
+ }
197
+ await this.app.getRouter().handle(req, res, next);
198
+ }
199
+ else {
200
+ next?.();
201
+ }
202
+ };
203
+ const lazyRouter = new router_1.Router();
204
+ lazyRouter.all(routePath, lazyHandler);
205
+ lazyRouter.all(routePath + '/*path', lazyHandler);
206
+ for (const r of lazyRouter.getRoutes()) {
207
+ r.sourceFile = 'lazy:' + lazyKey;
208
+ this.app.getRouter().addRawRoute(r);
209
+ }
210
+ }
211
+ else {
212
+ try {
213
+ await this.mountRouteFile(normalized, routePath);
214
+ }
215
+ catch (err) {
216
+ this.app.log.error({ err, file: filePath }, `Failed to load route file: ${filePath}`);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ // ─── Route File Mounting (shared by autoMount and HotReloader) ──────────────
224
+ // ─── Auto-Mount File-Based Jobs ──────────────────────────────────────────────
225
+ async autoMountJobs(root) {
226
+ if (!this.app._queueWorker)
227
+ return;
228
+ const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
229
+ const path = await Promise.resolve().then(() => __importStar(require('node:path')));
230
+ const isProd = process.env.NODE_ENV === 'production';
231
+ const searchDirs = isProd
232
+ ? [
233
+ path.join(root, '.exis', 'server', 'src', 'jobs'),
234
+ path.join(root, '.exis', 'server', 'jobs'),
235
+ path.join(root, 'dist', 'src', 'jobs'),
236
+ path.join(root, 'dist', 'jobs'),
237
+ path.join(root, 'src', 'jobs'),
238
+ path.join(root, 'jobs'),
239
+ ]
240
+ : [path.join(root, 'src', 'jobs'), path.join(root, 'jobs')];
241
+ let jobsDir = '';
242
+ for (const dir of searchDirs) {
243
+ try {
244
+ const stat = await fs.stat(dir);
245
+ if (stat.isDirectory()) {
246
+ jobsDir = dir;
247
+ break;
248
+ }
249
+ }
250
+ catch {
251
+ /* ignore */
252
+ }
253
+ }
254
+ if (!jobsDir)
255
+ return;
256
+ try {
257
+ const entries = await fs.readdir(jobsDir, { withFileTypes: true });
258
+ for (const entry of entries) {
259
+ if (entry.isFile() &&
260
+ (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
261
+ const filePath = path.join(jobsDir, entry.name);
262
+ const name = entry.name.replace(/\.(ts|js)$/, '');
263
+ try {
264
+ // Load the definition directly (no compilation needed here just to get the schema)
265
+ const mod = await Promise.resolve(`${filePath}`).then(s => __importStar(require(s)));
266
+ const jobDef = mod.default || mod;
267
+ if (jobDef) {
268
+ const def = {
269
+ name: jobDef.name || name,
270
+ filePath, // Attach filePath for Thread Pool execution
271
+ cron: jobDef.cron,
272
+ schema: jobDef.schema,
273
+ defaultOptions: jobDef.defaultOptions,
274
+ };
275
+ this.app._queueWorker.registerJob(def);
276
+ if (this.app._cronScheduler && jobDef.cron) {
277
+ this.app._cronScheduler.registerJob(def);
278
+ }
279
+ this.app.log.info(`Registered background job: ${name}`);
280
+ }
281
+ }
282
+ catch (err) {
283
+ this.app.log.error(`Failed to load job file ${filePath}: ${err}`);
284
+ }
285
+ }
286
+ }
287
+ }
288
+ catch {
289
+ // Ignore if jobs directory does not exist
290
+ }
291
+ }
292
+ async mountRouteFile(filePath, routePath) {
293
+ let mod;
294
+ try {
295
+ const url = process.env.VITEST || process.env.NODE_ENV === 'test'
296
+ ? (0, node_url_1.pathToFileURL)(filePath).href
297
+ : (0, node_url_1.pathToFileURL)(filePath).href + '?t=' + Date.now();
298
+ if (process.env.VITEST || process.env.NODE_ENV === 'test') {
299
+ mod = await Promise.resolve(`${url}`).then(s => __importStar(require(s)));
300
+ }
301
+ else {
302
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
303
+ mod = await dynamicImport(url);
304
+ }
305
+ }
306
+ catch {
307
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
308
+ mod = require(filePath);
309
+ }
310
+ const path = await Promise.resolve().then(() => __importStar(require('node:path')));
311
+ const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
312
+ const dirname = path.dirname(filePath);
313
+ const apiDir = this.apiDir || dirname;
314
+ const normDirname = path.resolve(dirname).replace(/\\/g, '/').toLowerCase();
315
+ const normApiDir = path.resolve(apiDir).replace(/\\/g, '/').toLowerCase();
316
+ const segments = normDirname.startsWith(normApiDir)
317
+ ? normDirname.slice(normApiDir.length).split('/').filter(Boolean)
318
+ : [];
319
+ const allMiddlewares = [];
320
+ const allFilters = [];
321
+ const allGuards = [];
322
+ const allInterceptors = [];
323
+ let allMetadata = {};
324
+ let allCors = undefined;
325
+ let allHeaders = {};
326
+ const activeGateways = [];
327
+ const dirsToCheck = [apiDir];
328
+ let tempDir = apiDir;
329
+ for (const segment of segments) {
330
+ tempDir = path.join(tempDir, segment);
331
+ dirsToCheck.push(tempDir);
332
+ }
333
+ for (const dir of dirsToCheck) {
334
+ try {
335
+ const gwPathJs = path.join(dir, 'gateway.js');
336
+ const gwPathTs = path.join(dir, 'gateway.ts');
337
+ let targetGw = '';
338
+ if (await fs.stat(gwPathTs).catch(() => null))
339
+ targetGw = gwPathTs;
340
+ else if (await fs.stat(gwPathJs).catch(() => null))
341
+ targetGw = gwPathJs;
342
+ if (targetGw) {
343
+ activeGateways.push(targetGw);
344
+ const gwUrl = process.env.VITEST || process.env.NODE_ENV === 'test'
345
+ ? (0, node_url_1.pathToFileURL)(targetGw).href
346
+ : (0, node_url_1.pathToFileURL)(targetGw).href + '?t=' + Date.now();
347
+ let gwMod;
348
+ try {
349
+ if (process.env.VITEST || process.env.NODE_ENV === 'test') {
350
+ gwMod = await Promise.resolve(`${gwUrl}`).then(s => __importStar(require(s)));
351
+ }
352
+ else {
353
+ const dynamicImportGw = new Function('specifier', 'return import(specifier)');
354
+ gwMod = await dynamicImportGw(gwUrl);
355
+ }
356
+ }
357
+ catch {
358
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
359
+ gwMod = require(targetGw);
360
+ }
361
+ const gwConfig = gwMod && gwMod.default && gwMod.default.default
362
+ ? gwMod.default.default
363
+ : gwMod && gwMod.default
364
+ ? gwMod.default
365
+ : gwMod;
366
+ if (gwConfig) {
367
+ const isExcluded = (reqPath, reqMethod) => {
368
+ if (!gwConfig.exclude)
369
+ return false;
370
+ for (const rule of gwConfig.exclude) {
371
+ if (typeof rule === 'string') {
372
+ if (rule.endsWith('/*')) {
373
+ if (reqPath.startsWith(rule.slice(0, -2)))
374
+ return true;
375
+ }
376
+ else if (reqPath === rule)
377
+ return true;
378
+ }
379
+ else {
380
+ const pathMatch = rule.path.endsWith('/*')
381
+ ? reqPath.startsWith(rule.path.slice(0, -2))
382
+ : reqPath === rule.path;
383
+ if (pathMatch) {
384
+ if (!rule.methods ||
385
+ rule.methods.includes(reqMethod))
386
+ return true;
387
+ }
388
+ }
389
+ }
390
+ return false;
391
+ };
392
+ if (gwConfig.middleware) {
393
+ const wrapped = gwConfig.middleware.map((m) => (req, res, next) => isExcluded(req.path, req.method) ? next() : m(req, res, next));
394
+ allMiddlewares.push(...wrapped);
395
+ }
396
+ if (gwConfig.filters) {
397
+ const wrapped = gwConfig.filters.map((f) => (err, req, res, next) => isExcluded(req.path, req.method)
398
+ ? next(err)
399
+ : f.prototype && f.prototype.catch
400
+ ? new f().catch(err, req, res, next)
401
+ : f(err, req, res, next));
402
+ allFilters.push(...wrapped);
403
+ }
404
+ if (gwConfig.guards) {
405
+ const wrapped = gwConfig.guards.map((g) => {
406
+ return async (req) => {
407
+ if (isExcluded(req.path, req.method))
408
+ return true;
409
+ return typeof g === 'function'
410
+ ? g.prototype?.canActivate
411
+ ? new g().canActivate(req)
412
+ : g(req)
413
+ : g.canActivate(req);
414
+ };
415
+ });
416
+ allGuards.push(...wrapped);
417
+ }
418
+ if (gwConfig.interceptors) {
419
+ const wrapped = gwConfig.interceptors.map((i) => {
420
+ return async (req, res) => {
421
+ if (isExcluded(req.path, req.method))
422
+ return;
423
+ return typeof i === 'function'
424
+ ? i.prototype?.intercept
425
+ ? new i().intercept(req, res)
426
+ : i(req, res)
427
+ : i.intercept(req, res);
428
+ };
429
+ });
430
+ allInterceptors.push(...wrapped);
431
+ }
432
+ if (gwConfig.timeout !== undefined) {
433
+ const baseTimeout = gwConfig.timeout;
434
+ const timeoutMiddleware = (req, res, next) => {
435
+ if (isExcluded(req.path, req.method))
436
+ return next();
437
+ const ms = typeof baseTimeout === 'function'
438
+ ? baseTimeout(req)
439
+ : baseTimeout;
440
+ if (ms) {
441
+ req.timeoutTimer = setTimeout(() => {
442
+ if (!res.headersSent) {
443
+ res.status(408).json({
444
+ success: false,
445
+ error: {
446
+ code: 'REQUEST_TIMEOUT',
447
+ message: 'Request Timeout',
448
+ },
449
+ });
450
+ }
451
+ }, ms);
452
+ res.raw.on('finish', () => clearTimeout(req.timeoutTimer));
453
+ }
454
+ next();
455
+ };
456
+ allMiddlewares.push(timeoutMiddleware);
457
+ }
458
+ if (gwConfig.metadata) {
459
+ allMetadata = { ...allMetadata, ...gwConfig.metadata };
460
+ }
461
+ if (gwConfig.cors !== undefined)
462
+ allCors = gwConfig.cors;
463
+ if (gwConfig.headers)
464
+ allHeaders = { ...allHeaders, ...gwConfig.headers };
465
+ if (gwConfig.plugins) {
466
+ for (const plugin of gwConfig.plugins) {
467
+ await this.app.pluginManager.register(plugin);
468
+ }
469
+ }
470
+ if (gwConfig.imports) {
471
+ for (const mod of gwConfig.imports) {
472
+ const name = 'plugin' in mod ? mod.plugin.name : mod.name;
473
+ if (!this.app.pluginManager.hasPlugin(name)) {
474
+ await this.app.pluginManager.register(mod);
475
+ }
476
+ }
477
+ }
478
+ if (gwConfig.providers) {
479
+ for (const [token, providerConfig] of gwConfig.providers) {
480
+ this.app.container.provide(token, providerConfig);
481
+ }
482
+ }
483
+ }
484
+ }
485
+ }
486
+ catch {
487
+ /* ignore */
488
+ }
489
+ }
490
+ const currentMod = mod && mod.default && mod.default.default
491
+ ? mod.default.default
492
+ : mod && mod.default
493
+ ? mod.default
494
+ : mod;
495
+ const isControllerObj = currentMod && currentMod.__isController === true;
496
+ const routeConfig = {
497
+ ...(isControllerObj ? currentMod : {}),
498
+ ...(mod.config || {}),
499
+ };
500
+ // Fallback to global app.options.cors if neither gateway nor route specify it
501
+ if (routeConfig.cors !== undefined)
502
+ allCors = routeConfig.cors;
503
+ else if (allCors === undefined && this.app.options.cors !== undefined)
504
+ allCors = this.app.options.cors;
505
+ if (routeConfig.middleware)
506
+ allMiddlewares.push(...(Array.isArray(routeConfig.middleware)
507
+ ? routeConfig.middleware
508
+ : [routeConfig.middleware]));
509
+ if (routeConfig.headers)
510
+ allHeaders = { ...allHeaders, ...routeConfig.headers };
511
+ if (routeConfig.plugins) {
512
+ for (const plugin of routeConfig.plugins) {
513
+ await this.app.pluginManager.register(plugin);
514
+ }
515
+ }
516
+ // Pass gateway features to controllers via metadata if necessary
517
+ const combinedFilters = [
518
+ ...allFilters,
519
+ ...(routeConfig.filters
520
+ ? Array.isArray(routeConfig.filters)
521
+ ? routeConfig.filters
522
+ : [routeConfig.filters]
523
+ : []),
524
+ ];
525
+ const combinedGuards = [
526
+ ...allGuards,
527
+ ...(routeConfig.guards
528
+ ? Array.isArray(routeConfig.guards)
529
+ ? routeConfig.guards
530
+ : [routeConfig.guards]
531
+ : []),
532
+ ];
533
+ const combinedInterceptors = [
534
+ ...allInterceptors,
535
+ ...(routeConfig.interceptors
536
+ ? Array.isArray(routeConfig.interceptors)
537
+ ? routeConfig.interceptors
538
+ : [routeConfig.interceptors]
539
+ : []),
540
+ ];
541
+ // Inject into the default export if it's a functional controller
542
+ if (mod.default && typeof mod.default === 'object') {
543
+ try {
544
+ if (!mod.default.filters)
545
+ mod.default.filters = [];
546
+ mod.default.filters.push(...combinedFilters);
547
+ if (!mod.default.guards)
548
+ mod.default.guards = [];
549
+ mod.default.guards.push(...combinedGuards);
550
+ if (!mod.default.interceptors)
551
+ mod.default.interceptors = [];
552
+ mod.default.interceptors.push(...combinedInterceptors);
553
+ if (!mod.default.metadata)
554
+ mod.default.metadata = {};
555
+ Object.assign(mod.default.metadata, {
556
+ ...allMetadata,
557
+ ...(routeConfig.metadata || {}),
558
+ });
559
+ }
560
+ catch {
561
+ // If mod.default is frozen, ignore
562
+ }
563
+ }
564
+ const prefixMiddlewares = [];
565
+ if (this.app.options.debugRouting) {
566
+ const pathLib = await Promise.resolve().then(() => __importStar(require('node:path')));
567
+ const relFile = pathLib
568
+ .relative(process.cwd(), filePath)
569
+ .replace(/\\/g, '/');
570
+ const gwStr = activeGateways.length > 0
571
+ ? activeGateways
572
+ .map((g) => pathLib.relative(process.cwd(), g).replace(/\\/g, '/'))
573
+ .join(' -> ') + ' -> '
574
+ : '';
575
+ prefixMiddlewares.push((req, res, next) => {
576
+ this.app.log.debug(`[Request] ${req.method} ${req.path} -> ${gwStr}${relFile}`);
577
+ next();
578
+ });
579
+ }
580
+ if (allCors !== undefined) {
581
+ if (allCors === true)
582
+ prefixMiddlewares.push((0, middleware_1.cors)({}));
583
+ else if (allCors !== false)
584
+ prefixMiddlewares.push((0, middleware_1.cors)(allCors));
585
+ }
586
+ if (Object.keys(allHeaders).length > 0) {
587
+ prefixMiddlewares.push((req, res, next) => {
588
+ for (const [k, v] of Object.entries(allHeaders))
589
+ res.setHeader(k, v);
590
+ next();
591
+ });
592
+ }
593
+ prefixMiddlewares.push(...allMiddlewares);
594
+ const isRouter = (obj) => obj && 'handle' in obj && 'get' in obj;
595
+ const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
596
+ const isController = (obj) => obj && obj.prototype && obj.prototype[CONTROLLER_PREFIX] !== undefined;
597
+ const unwrappedMod = mod && mod.default && mod.default.default
598
+ ? mod.default.default
599
+ : mod && mod.default
600
+ ? mod.default
601
+ : mod;
602
+ const functionalControllerObj = unwrappedMod && unwrappedMod.__isController ? unwrappedMod : null;
603
+ if (functionalControllerObj) {
604
+ // ─── THE NEW PERFECT FUNCTIONAL CONTROLLER ───
605
+ const compiledRouter = this.compileFunctionalController(functionalControllerObj);
606
+ this.mountRouteWithSource(routePath, compiledRouter, filePath, prefixMiddlewares);
607
+ }
608
+ else if (mod.router && isRouter(mod.router)) {
609
+ this.mountRouteWithSource(routePath, mod.router, filePath, prefixMiddlewares);
610
+ }
611
+ else if (unwrappedMod && isController(unwrappedMod)) {
612
+ this.app.registerControllers([unwrappedMod], routePath, prefixMiddlewares);
613
+ }
614
+ else if (unwrappedMod && isRouter(unwrappedMod)) {
615
+ this.mountRouteWithSource(routePath, unwrappedMod, filePath, prefixMiddlewares);
616
+ }
617
+ else if (isRouter(mod)) {
618
+ this.mountRouteWithSource(routePath, mod, filePath, prefixMiddlewares);
619
+ }
620
+ else {
621
+ this.app.log.warn(`\n[WARN] Route file \x1b[36m${filePath}\x1b[0m does not export a recognized router or controller.\n`);
622
+ }
623
+ }
624
+ compileFunctionalController(config) {
625
+ const router = new router_1.Router();
626
+ const fileMiddleware = [];
627
+ if (config.cors) {
628
+ fileMiddleware.push(config.cors === true ? (0, middleware_1.cors)({}) : (0, middleware_1.cors)(config.cors));
629
+ }
630
+ if (config.middleware) {
631
+ fileMiddleware.push(...(Array.isArray(config.middleware)
632
+ ? config.middleware
633
+ : [config.middleware]));
634
+ }
635
+ const { onError, onResponse } = config;
636
+ for (const [key, routeConfig] of Object.entries(config)) {
637
+ if ([
638
+ 'cors',
639
+ 'middleware',
640
+ 'onError',
641
+ 'onResponse',
642
+ '__isController',
643
+ ].includes(key))
644
+ continue;
645
+ const rc = routeConfig;
646
+ if (!rc || !rc.method || !rc.path || !rc.handle)
647
+ continue;
648
+ const routeMiddlewares = [...fileMiddleware];
649
+ if (rc.cors) {
650
+ routeMiddlewares.push(rc.cors === true ? (0, middleware_1.cors)({}) : (0, middleware_1.cors)(rc.cors));
651
+ }
652
+ if (rc.middleware) {
653
+ routeMiddlewares.push(...(Array.isArray(rc.middleware) ? rc.middleware : [rc.middleware]));
654
+ }
655
+ // ─── SUPER HANDLER WRAPPER ───
656
+ const superHandler = async (req, res, next) => {
657
+ if (onResponse) {
658
+ res.raw.on('finish', () => onResponse(req, res));
659
+ }
660
+ try {
661
+ // 0. Enforce Route Permissions (Role Authorization)
662
+ if (rc.permissions && rc.permissions.length > 0) {
663
+ if (!req.user) {
664
+ res.status(401).json({
665
+ success: false,
666
+ error: {
667
+ code: 'UNAUTHORIZED',
668
+ message: 'Unauthorized: User not found on request',
669
+ },
670
+ });
671
+ return;
672
+ }
673
+ const userPerms = req.user.permissions || req.user.roles || req.user.role || [];
674
+ const permsArray = Array.isArray(userPerms)
675
+ ? userPerms
676
+ : [userPerms];
677
+ const hasPerm = rc.permissions.every((p) => permsArray.includes(p));
678
+ if (!hasPerm) {
679
+ res.status(403).json({
680
+ success: false,
681
+ error: {
682
+ code: 'FORBIDDEN',
683
+ message: 'Forbidden: Insufficient permissions',
684
+ },
685
+ });
686
+ return;
687
+ }
688
+ }
689
+ // 1. Run Guards
690
+ const routeGuards = [...(config.guards || []), ...(rc.guards || [])];
691
+ for (const guard of routeGuards) {
692
+ const allowed = await (typeof guard === 'function'
693
+ ? guard.prototype?.canActivate
694
+ ? new guard().canActivate(req)
695
+ : guard(req)
696
+ : guard.canActivate(req));
697
+ if (!allowed) {
698
+ if (res && !res.headersSent) {
699
+ res.status(403).json({
700
+ success: false,
701
+ error: { code: 'FORBIDDEN', message: 'Forbidden by Guard' },
702
+ });
703
+ }
704
+ return;
705
+ }
706
+ }
707
+ // Build Context
708
+ const ctx = {
709
+ body: req.body,
710
+ query: req.query,
711
+ params: req.params,
712
+ headers: req.headers,
713
+ req,
714
+ res,
715
+ app: this,
716
+ socket: req.ws,
717
+ state: context_1.executionContext.getStore()?.state || {},
718
+ ...req, // Spread req to allow access to user, session, etc.
719
+ };
720
+ const result = await rc.handle(ctx);
721
+ // 2. Run Interceptors
722
+ const routeInterceptors = [
723
+ ...(config.interceptors || []),
724
+ ...(rc.interceptors || []),
725
+ ];
726
+ for (const interceptor of routeInterceptors) {
727
+ await (typeof interceptor === 'function'
728
+ ? interceptor.prototype?.intercept
729
+ ? new interceptor().intercept(req, res)
730
+ : interceptor(req, res)
731
+ : interceptor.intercept(req, res));
732
+ }
733
+ if (result !== undefined && !res.headersSent) {
734
+ if (typeof result === 'object' && result !== null) {
735
+ res.json(result);
736
+ }
737
+ else {
738
+ res.send(String(result));
739
+ }
740
+ }
741
+ }
742
+ catch (err) {
743
+ if (onError) {
744
+ try {
745
+ await onError(err, req, res);
746
+ }
747
+ catch (hookErr) {
748
+ next(hookErr);
749
+ }
750
+ }
751
+ else {
752
+ next(err); // Hands off to ExisJS global error handler (which handles HttpErrors automatically!)
753
+ }
754
+ }
755
+ };
756
+ // Mount to router
757
+ const method = rc.method.toLowerCase();
758
+ const schema = {};
759
+ const isValidatorOrPipe = (v) => v.parse ||
760
+ v.transform ||
761
+ (typeof v === 'function' && v.prototype?.transform);
762
+ if (rc.body) {
763
+ schema.body = isValidatorOrPipe(rc.body) ? rc.body : validator_1.v.object(rc.body);
764
+ }
765
+ if (rc.query) {
766
+ schema.query = isValidatorOrPipe(rc.query)
767
+ ? rc.query
768
+ : validator_1.v.object(rc.query);
769
+ }
770
+ if (rc.params) {
771
+ schema.params = isValidatorOrPipe(rc.params)
772
+ ? rc.params
773
+ : validator_1.v.object(rc.params);
774
+ }
775
+ if (rc.host) {
776
+ schema.host = rc.host;
777
+ }
778
+ const combinedFilters = [
779
+ ...(config.filters
780
+ ? Array.isArray(config.filters)
781
+ ? config.filters
782
+ : [config.filters]
783
+ : []),
784
+ ...(rc.filters
785
+ ? Array.isArray(rc.filters)
786
+ ? rc.filters
787
+ : [rc.filters]
788
+ : []),
789
+ ];
790
+ if (combinedFilters.length > 0) {
791
+ schema.filters = combinedFilters;
792
+ }
793
+ const combinedMetadata = {
794
+ ...(config.metadata || {}),
795
+ ...(rc.metadata || {}),
796
+ };
797
+ if (Object.keys(combinedMetadata).length > 0) {
798
+ schema.metadata = combinedMetadata;
799
+ }
800
+ if (method === 'sse') {
801
+ const sseHandler = async (stream, rawReq) => {
802
+ try {
803
+ const ctx = {
804
+ body: rawReq.body,
805
+ query: rawReq.query,
806
+ params: rawReq.params,
807
+ headers: rawReq.headers,
808
+ req: rawReq,
809
+ res: undefined, // Res is not available in standard SSE after handshake
810
+ app: this,
811
+ stream,
812
+ ...rawReq,
813
+ };
814
+ await rc.handle(ctx);
815
+ }
816
+ catch (err) {
817
+ if (onError) {
818
+ await onError(err, rawReq, {});
819
+ }
820
+ else {
821
+ console.error('[Exis SSE Error]', err);
822
+ }
823
+ }
824
+ };
825
+ router.sse(rc.path, ...routeMiddlewares, sseHandler);
826
+ }
827
+ else {
828
+ if (Object.keys(schema).length > 0) {
829
+ ;
830
+ router[method](rc.path, ...routeMiddlewares, schema, superHandler);
831
+ }
832
+ else {
833
+ ;
834
+ router[method](rc.path, ...routeMiddlewares, superHandler);
835
+ }
836
+ }
837
+ }
838
+ return router;
839
+ }
840
+ mountRouteWithSource(prefix, subRouter, sourceFile, prefixMiddlewares = []) {
841
+ for (const route of subRouter.getRoutes()) {
842
+ const fullPath = (prefix + route.path).replace(/\/+/g, '/');
843
+ const r = { ...route, path: fullPath, sourceFile };
844
+ if (prefixMiddlewares.length > 0) {
845
+ r.handlers = [...prefixMiddlewares, ...r.handlers];
846
+ }
847
+ this.app.getRouter().addRawRoute(r);
848
+ if (this.app.pluginManager.hooks.route.length > 0) {
849
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
850
+ for (let i = 0; i < this.app.pluginManager.hooks.route.length; i++) {
851
+ this.app.pluginManager.hooks.route[i]({
852
+ method: route.method,
853
+ path: fullPath,
854
+ });
855
+ }
856
+ }
857
+ }
858
+ return this;
859
+ }
860
+ async scanDirectory(dir, baseRoute = '/') {
861
+ const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
862
+ const path = await Promise.resolve().then(() => __importStar(require('node:path')));
863
+ const entries = await fs.readdir(dir, { withFileTypes: true });
864
+ const results = [];
865
+ for (const entry of entries) {
866
+ const fullPath = path.join(dir, entry.name);
867
+ if (entry.isDirectory()) {
868
+ let segment = entry.name;
869
+ if (segment.startsWith('(') && segment.endsWith(')')) {
870
+ const subResults = await this.scanDirectory(fullPath, baseRoute);
871
+ results.push(...subResults);
872
+ continue;
873
+ }
874
+ if (segment.startsWith('[...') && segment.endsWith(']')) {
875
+ segment = '*' + segment.slice(4, -1);
876
+ }
877
+ else {
878
+ segment = segment.replace(/\[(.*?)\]/g, ':$1');
879
+ }
880
+ const nextBase = baseRoute === '/' ? `/${segment}` : `${baseRoute}/${segment}`;
881
+ const subResults = await this.scanDirectory(fullPath, nextBase);
882
+ results.push(...subResults);
883
+ }
884
+ else {
885
+ results.push({ filePath: fullPath, routePath: baseRoute });
886
+ }
887
+ }
888
+ return results;
889
+ }
890
+ }
891
+ exports.RouteScanner = RouteScanner;
892
+ //# sourceMappingURL=route-scanner.js.map