exisjs 0.4.3 → 0.4.5

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 +50 -17
  58. package/dist/utils/validator.js.map +1 -1
  59. package/package.json +8 -3
@@ -35,31 +35,68 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.activeAppInstance = exports.app = exports.App = void 0;
37
37
  exports.getActiveApp = getActiveApp;
38
- const node_http_1 = require("node:http");
39
- const node_https_1 = require("node:https");
40
- const node_http2_1 = require("node:http2");
41
38
  const ws_1 = require("ws");
42
- const node_url_1 = require("node:url");
43
- const validator_1 = require("../utils/validator");
44
- const request_1 = require("./request");
45
- const response_1 = require("./response");
46
- const context_1 = require("./context");
47
39
  const router_1 = require("../router/router");
48
40
  const middleware_1 = require("../middleware/middleware");
49
41
  const errors_1 = require("../utils/errors");
50
42
  const server_1 = require("../websocket/server");
51
- const socket_1 = require("../websocket/socket");
52
43
  const config_1 = require("../utils/config");
53
44
  const logger_1 = require("../utils/logger");
54
45
  const hot_reload_1 = require("./hot-reload");
55
- const dev_error_overlay_1 = require("./dev-error-overlay");
56
- const uws_adapter_1 = require("./uws-adapter");
57
- const client_1 = require("../queue/client");
58
- const worker_1 = require("../queue/worker");
59
- const scheduler_1 = require("../cron/scheduler");
60
46
  const container_1 = require("../di/container");
47
+ const bootstrapper_1 = require("./bootstrapper");
48
+ const ws_orchestrator_1 = require("./ws-orchestrator");
49
+ const plugin_manager_1 = require("./plugin-manager");
50
+ const route_scanner_1 = require("./route-scanner");
51
+ const queue_manager_1 = require("./queue-manager");
52
+ const request_handler_1 = require("./request-handler");
53
+ const controller_registrar_1 = require("./controller-registrar");
61
54
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
62
55
  class App {
56
+ get server() {
57
+ return this.bootstrapper.getServer();
58
+ }
59
+ get redirectServer() {
60
+ return this.bootstrapper.redirectServer;
61
+ }
62
+ get onStartHook() {
63
+ return this.pluginManager.onStartHook;
64
+ }
65
+ set onStartHook(cb) {
66
+ this.pluginManager.onStartHook = cb;
67
+ }
68
+ get onCloseHook() {
69
+ return this.pluginManager.onCloseHook;
70
+ }
71
+ set onCloseHook(cb) {
72
+ this.pluginManager.onCloseHook = cb;
73
+ }
74
+ // ─── Lifecycle Hooks Registry ───────────────────────────────────────────────
75
+ get hooks() {
76
+ return this.pluginManager.hooks;
77
+ }
78
+ // ─── Queue ──────────────────────────────────────────────────────────────────
79
+ get apiDir() {
80
+ return this.routeScanner.apiDir;
81
+ }
82
+ set apiDir(val) {
83
+ this.routeScanner.apiDir = val;
84
+ }
85
+ get _queueClient() {
86
+ return this.queueManager._queueClient;
87
+ }
88
+ get _queueWorker() {
89
+ return this.queueManager._queueWorker;
90
+ }
91
+ get _cronScheduler() {
92
+ return this.queueManager._cronScheduler;
93
+ }
94
+ get _pendingQueueJobs() {
95
+ return this.queueManager._pendingQueueJobs;
96
+ }
97
+ set _pendingQueueJobs(v) {
98
+ this.queueManager._pendingQueueJobs = v;
99
+ }
63
100
  get queueClient() {
64
101
  return this._queueClient;
65
102
  }
@@ -67,64 +104,40 @@ class App {
67
104
  return this._queueWorker;
68
105
  }
69
106
  constructor(options = {}) {
70
- this.redirectServer = null;
71
- this.apiDir = null;
72
107
  this.globalMiddleware = [];
73
108
  this.errorHandlers = [];
74
109
  this._configured = false;
75
110
  this._routesMounted = false;
76
111
  this._loggerCreated = false;
77
- this.registeredPlugins = new Map();
78
- this.routeMap = new Map();
79
112
  this.hotReloader = null;
80
- this.lazyRoutes = new Map();
81
113
  this.wsServer = new server_1.ExisWebSocketServer();
82
114
  this.rawWsServer = new ws_1.WebSocketServer({ noServer: true });
83
115
  // ─── uWebSockets.js Backend ──────────────────────────────────────────────────
84
116
  this._useUws = false;
85
117
  this._uwsApp = null;
86
118
  this._uwsListenToken = null;
87
- // ─── Lifecycle Hooks Registry ───────────────────────────────────────────────
88
- this.hooks = {
89
- ready: [],
90
- close: [],
91
- request: [],
92
- response: [],
93
- error: [],
94
- route: [],
95
- };
96
119
  // ─── Dataloaders Registry ───────────────────────────────────────────────────
97
120
  this._dataloaders = new Map();
98
121
  this.explicitOptions = {};
99
122
  // ─── Dependency Injection ───────────────────────────────────────────────────
100
123
  this.container = new container_1.Container();
101
- // ─── Queue ──────────────────────────────────────────────────────────────────
102
- this._queueClient = null;
103
- this._queueWorker = null;
104
- this._cronScheduler = null;
105
- this._pendingQueueJobs = [];
124
+ // WebSocket Upgrade logic has been moved to WsOrchestrator
106
125
  this.shutdownHooks = [];
107
- // ─── App Initialization ────────────────────────────────────────────────────────
126
+ // ─── App Initialization ───────────────────────────────────────────────────────
108
127
  this._configLoaded = false;
109
128
  this.explicitOptions = options;
110
129
  this.options = (0, config_1.mergeConfig)(config_1.defaultConfig, options);
111
130
  this.router = new router_1.Router();
112
131
  this.ensureLogger();
113
- // Queue setup is now deferred to app.create() when the config is loaded
132
+ this.pluginManager = new plugin_manager_1.PluginManager(this);
133
+ this.wsOrchestrator = new ws_orchestrator_1.WsOrchestrator(this);
134
+ this.bootstrapper = new bootstrapper_1.ServerBootstrapper(this);
135
+ this.routeScanner = new route_scanner_1.RouteScanner(this);
136
+ this.queueManager = new queue_manager_1.QueueManager(this);
137
+ this.requestHandler = new request_handler_1.RequestHandler(this);
138
+ this.controllerRegistrar = new controller_registrar_1.ControllerRegistrar(this);
114
139
  if (this.options.queue) {
115
- this._initQueue(this.options.queue);
116
- }
117
- // Auto-detect uWebSockets.js backend
118
- if (options.server === 'uws' ||
119
- (options.server !== 'node' &&
120
- (0, uws_adapter_1.isUwsAvailable)() &&
121
- this.options.env !== 'test')) {
122
- this._useUws = true;
123
- // Don't init Node server — we'll create the uWS app at listen() time
124
- }
125
- else {
126
- this._useUws = false;
127
- this.server = this._initServer();
140
+ this.queueManager._initQueue(this.options.queue);
128
141
  }
129
142
  }
130
143
  ensureLogger() {
@@ -137,58 +150,30 @@ class App {
137
150
  }
138
151
  this.log = (0, logger_1.createLogger)(loggerOptions);
139
152
  }
140
- _initServer() {
141
- let server;
142
- if (this.options.ssl) {
143
- if (this.options.http2 !== false) {
144
- server = (0, node_http2_1.createSecureServer)({ allowHTTP1: true, ...this.options.ssl }, this.handle.bind(this));
145
- }
146
- else {
147
- server = (0, node_https_1.createServer)(this.options.ssl, this.handle.bind(this));
148
- }
149
- }
150
- else {
151
- server = (0, node_http_1.createServer)(this.handle.bind(this));
152
- }
153
- server.on('upgrade', this._handleUpgrade.bind(this));
154
- const keepAlive = this.options.keepAlive;
155
- if (keepAlive) {
156
- const kaConfig = keepAlive === true ? {} : keepAlive;
157
- if ('keepAliveTimeout' in server)
158
- server.keepAliveTimeout = kaConfig.timeoutMs ?? 5000;
159
- if ('headersTimeout' in server)
160
- server.headersTimeout = kaConfig.headersTimeoutMs ?? 60000;
161
- if (kaConfig.maxRequests !== undefined &&
162
- 'maxRequestsPerSocket' in server) {
163
- ;
164
- server.maxRequestsPerSocket = kaConfig.maxRequests;
165
- }
166
- }
167
- return server;
168
- }
153
+ // _initServer logic has been moved to ServerBootstrapper
169
154
  // ─── Lifecycle Hooks Registration ───────────────────────────────────────────
170
155
  onReady(cb) {
171
- this.hooks.ready.push(cb);
156
+ this.pluginManager.onReady(cb);
172
157
  return this;
173
158
  }
174
159
  onClose(cb) {
175
- this.hooks.close.push(cb);
160
+ this.pluginManager.onClose(cb);
176
161
  return this;
177
162
  }
178
163
  onRequest(cb) {
179
- this.hooks.request.push(cb);
164
+ this.pluginManager.onRequest(cb);
180
165
  return this;
181
166
  }
182
167
  onResponse(cb) {
183
- this.hooks.response.push(cb);
168
+ this.pluginManager.onResponse(cb);
184
169
  return this;
185
170
  }
186
171
  onError(cb) {
187
- this.hooks.error.push(cb);
172
+ this.pluginManager.onError(cb);
188
173
  return this;
189
174
  }
190
175
  onRoute(cb) {
191
- this.hooks.route.push(cb);
176
+ this.pluginManager.onRoute(cb);
192
177
  return this;
193
178
  }
194
179
  // ─── Dataloaders ────────────────────────────────────────────────────────────
@@ -205,108 +190,10 @@ class App {
205
190
  }
206
191
  // ─── Plugin System ──────────────────────────────────────────────────────────
207
192
  hasPlugin(name) {
208
- return this.registeredPlugins.has(name);
193
+ return this.pluginManager.hasPlugin(name);
209
194
  }
210
195
  async register(pluginOrInstance, legacyOptions) {
211
- let plugin;
212
- let options;
213
- if ('plugin' in pluginOrInstance && !('register' in pluginOrInstance)) {
214
- plugin = pluginOrInstance.plugin;
215
- options = pluginOrInstance.options;
216
- }
217
- else {
218
- plugin = pluginOrInstance;
219
- options = legacyOptions;
220
- }
221
- if (this.registeredPlugins.has(plugin.name)) {
222
- throw new Error(`Plugin '${plugin.name}' is already registered`);
223
- }
224
- if (plugin.dependencies) {
225
- for (const dep of plugin.dependencies) {
226
- if (!this.registeredPlugins.has(dep)) {
227
- throw new Error(`Plugin '${plugin.name}' requires dependency '${dep}' which is not registered`);
228
- }
229
- }
230
- }
231
- // eslint-disable-next-line @typescript-eslint/no-this-alias
232
- let targetApp = this;
233
- let pluginRouter;
234
- if (plugin.encapsulate !== false) {
235
- pluginRouter = new router_1.Router();
236
- // Create a proxy that intercepts route and middleware registrations
237
- targetApp = new Proxy(this, {
238
- get(target, prop, receiver) {
239
- if ([
240
- 'get',
241
- 'post',
242
- 'put',
243
- 'patch',
244
- 'delete',
245
- 'httpOptions',
246
- 'head',
247
- 'all',
248
- 'ws',
249
- 'query',
250
- 'connect',
251
- 'trace',
252
- ].includes(prop)) {
253
- return (path, ...handlers) => {
254
- const routerMethod = prop === 'httpOptions' ? 'options' : prop;
255
- pluginRouter[routerMethod](path, ...handlers);
256
- return receiver;
257
- };
258
- }
259
- if (prop === 'use') {
260
- return (...handlers) => {
261
- for (const h of handlers) {
262
- if (h.length === 4) {
263
- // error handlers stay global for now, or we could scope them
264
- target.use(h);
265
- }
266
- else {
267
- pluginRouter.use(h);
268
- }
269
- }
270
- return receiver;
271
- };
272
- }
273
- // Encapsulate lifecycle hooks by converting them to scoped middleware!
274
- if (['onRequest', 'onResponse'].includes(prop)) {
275
- return (cb) => {
276
- if (prop === 'onRequest') {
277
- pluginRouter.use(async (req, res, next) => {
278
- try {
279
- await cb(req, res);
280
- next();
281
- }
282
- catch (err) {
283
- next(err);
284
- }
285
- });
286
- }
287
- else if (prop === 'onResponse') {
288
- pluginRouter.use((req, res, next) => {
289
- res.raw.on('finish', () => {
290
- cb(req, res).catch(() => {
291
- /* ignore */
292
- });
293
- });
294
- next();
295
- });
296
- }
297
- return receiver;
298
- };
299
- }
300
- const value = Reflect.get(target, prop, receiver);
301
- return typeof value === 'function' ? value.bind(target) : value;
302
- },
303
- });
304
- }
305
- await plugin.register(targetApp, options);
306
- if (pluginRouter) {
307
- this.mount('/', pluginRouter);
308
- }
309
- this.registeredPlugins.set(plugin.name, plugin);
196
+ await this.pluginManager.register(pluginOrInstance, legacyOptions);
310
197
  return this;
311
198
  }
312
199
  // ─── Middleware ─────────────────────────────────────────────────────────────
@@ -407,340 +294,7 @@ class App {
407
294
  }
408
295
  // ─── Class-Based Controllers Registration ──────────────────────────────────
409
296
  registerControllers(controllers, basePath = '', prefixMiddlewares = []) {
410
- const ROUTE_REGISTRY = Symbol.for('exisjs:routes');
411
- const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
412
- const CONTROLLER_HOST = Symbol.for('exisjs:controller_host');
413
- const MIDDLEWARE_REGISTRY = Symbol.for('exisjs:middlewares');
414
- const ROUTE_METADATA = Symbol.for('exisjs:route_metadata');
415
- const LIFECYCLE_METADATA = Symbol.for('exisjs:lifecycle_metadata');
416
- const PARAM_METADATA = Symbol.for('exisjs:param_metadata');
417
- for (const ControllerClass of controllers) {
418
- const prefix = ControllerClass.prototype[CONTROLLER_PREFIX] || '';
419
- const host = ControllerClass.prototype[CONTROLLER_HOST];
420
- const routes = ControllerClass.prototype[ROUTE_REGISTRY] || [];
421
- const classMiddlewares = [
422
- ...(Array.isArray(ControllerClass.prototype[MIDDLEWARE_REGISTRY])
423
- ? ControllerClass.prototype[MIDDLEWARE_REGISTRY]
424
- : ControllerClass.prototype[MIDDLEWARE_REGISTRY]?._classMiddlewares ||
425
- []),
426
- ];
427
- const methodMiddlewares = (!Array.isArray(ControllerClass.prototype[MIDDLEWARE_REGISTRY])
428
- ? ControllerClass.prototype[MIDDLEWARE_REGISTRY]
429
- : {}) || {};
430
- const routeMetadataMap = ControllerClass.prototype[ROUTE_METADATA] || {};
431
- const lifecycleMetadataMap = ControllerClass.prototype[LIFECYCLE_METADATA] || {};
432
- const paramMetadataMap = ControllerClass.prototype[PARAM_METADATA] || {};
433
- for (const route of routes) {
434
- const method = route.method.toLowerCase();
435
- const executeCore = async (req, res, next, streamOrSocket) => {
436
- let instance = this.container.resolve(ControllerClass);
437
- if (!instance) {
438
- // Auto-instantiate if not provided via DI
439
- instance = new ControllerClass();
440
- this.container.provide(ControllerClass, { useValue: instance });
441
- }
442
- try {
443
- // 0. Enforce Route Permissions (Role Authorization)
444
- const routeMetadata = routeMetadataMap[route.handlerName] || {};
445
- const permissions = routeMetadata.permissions;
446
- if (permissions && permissions.length > 0) {
447
- const userPerms = req.user?.permissions || (req.user?.role ? [req.user.role] : []);
448
- const hasPermission = permissions.some((p) => userPerms.includes(p));
449
- if (!hasPermission) {
450
- if (res) {
451
- res.status(403).json({
452
- success: false,
453
- error: {
454
- code: 'FORBIDDEN',
455
- message: 'Insufficient permissions to access this resource',
456
- },
457
- });
458
- }
459
- return;
460
- }
461
- }
462
- // 1. Run Guards
463
- const routeLifecycle = lifecycleMetadataMap[route.handlerName] || {};
464
- const guards = routeLifecycle.guards || [];
465
- for (const guard of guards) {
466
- let allowed = false;
467
- if (typeof guard === 'function') {
468
- if (guard.prototype &&
469
- typeof guard.prototype.canActivate === 'function') {
470
- let guardInstance = this.container.resolve(guard);
471
- if (!guardInstance) {
472
- guardInstance = new guard();
473
- }
474
- allowed = await guardInstance.canActivate(req);
475
- }
476
- else {
477
- allowed = await guard(req);
478
- }
479
- }
480
- if (!allowed) {
481
- if (res) {
482
- res.status(403).json({
483
- success: false,
484
- error: { code: 'FORBIDDEN', message: 'Forbidden by Guard' },
485
- });
486
- }
487
- return;
488
- }
489
- }
490
- // 2. Resolve parameters
491
- const paramMetadata = paramMetadataMap[route.handlerName] || [];
492
- const args = [];
493
- if (paramMetadata.length === 0) {
494
- if (method === 'ws' || method === 'sse') {
495
- args.push(req, streamOrSocket);
496
- }
497
- else {
498
- args.push(req, res, next);
499
- }
500
- }
501
- else {
502
- for (const param of paramMetadata) {
503
- if (!param) {
504
- args.push(undefined);
505
- continue;
506
- }
507
- let rawArg;
508
- switch (param.type) {
509
- case 'req':
510
- rawArg = req;
511
- break;
512
- case 'res':
513
- rawArg = res;
514
- break;
515
- case 'body':
516
- if (req.body === undefined &&
517
- ['POST', 'PUT', 'PATCH'].includes(req.method)) {
518
- const contentType = req.headers['content-type'] || '';
519
- if (contentType.includes('json')) {
520
- await req.json().catch(() => {
521
- /* noop */
522
- });
523
- }
524
- else if (contentType.includes('form')) {
525
- await req.formData().catch(() => {
526
- /* noop */
527
- });
528
- }
529
- }
530
- rawArg = req.body;
531
- break;
532
- case 'param':
533
- rawArg = param.name ? req.params[param.name] : req.params;
534
- break;
535
- case 'query':
536
- rawArg = param.name ? req.query[param.name] : req.query;
537
- break;
538
- case 'header':
539
- rawArg = param.name
540
- ? req.headers[param.name.toLowerCase()]
541
- : req.headers;
542
- break;
543
- case 'session':
544
- rawArg = req.session;
545
- break;
546
- case 'next':
547
- rawArg = next;
548
- break;
549
- case 'host':
550
- rawArg = param.name
551
- ? req.params[param.name]
552
- : req.hostname;
553
- break;
554
- case 'socket':
555
- case 'stream':
556
- rawArg = streamOrSocket;
557
- break;
558
- case 'ip':
559
- rawArg = req.ip;
560
- break;
561
- case 'uploadedFile':
562
- case 'uploadedFiles':
563
- if (req.body === undefined &&
564
- ['POST', 'PUT', 'PATCH'].includes(req.method)) {
565
- await req.formData().catch(() => {
566
- /* noop */
567
- });
568
- }
569
- if (req.files) {
570
- const isMulti = param.type === 'uploadedFiles';
571
- let files = [];
572
- if (param.name) {
573
- files = req.files.filter((f) => f.fieldname === param.name);
574
- }
575
- else {
576
- files = req.files;
577
- }
578
- rawArg = isMulti ? files : files[0];
579
- }
580
- break;
581
- default:
582
- rawArg = undefined;
583
- }
584
- if (param.pipes && param.pipes.length > 0) {
585
- for (const pipe of param.pipes) {
586
- if (typeof pipe === 'function' &&
587
- pipe.prototype?.transform) {
588
- let pipeInstance = this.container.resolve(pipe);
589
- if (!pipeInstance)
590
- pipeInstance = new pipe();
591
- rawArg = await pipeInstance.transform(rawArg, {
592
- type: param.type,
593
- name: param.name,
594
- });
595
- }
596
- else if (typeof pipe === 'object' &&
597
- typeof pipe.parse === 'function') {
598
- rawArg = await pipe.parse(rawArg);
599
- }
600
- else if (typeof pipe === 'object' &&
601
- typeof pipe.transform === 'function') {
602
- rawArg = await pipe.transform(rawArg, {
603
- type: param.type,
604
- name: param.name,
605
- });
606
- }
607
- else if (typeof pipe === 'function') {
608
- rawArg = await pipe(rawArg);
609
- }
610
- }
611
- }
612
- args.push(rawArg);
613
- }
614
- }
615
- // 3. Execute Custom Interceptors
616
- const interceptors = routeLifecycle.interceptors || [];
617
- for (const interceptor of interceptors) {
618
- if (typeof interceptor === 'function') {
619
- if (interceptor.prototype &&
620
- typeof interceptor.prototype.intercept === 'function') {
621
- let interceptorInstance = this.container.resolve(interceptor);
622
- if (!interceptorInstance) {
623
- interceptorInstance = new interceptor();
624
- }
625
- await interceptorInstance.intercept(req, res);
626
- }
627
- else {
628
- await interceptor(req, res);
629
- }
630
- }
631
- }
632
- // 4. Invoke the method
633
- const result = await instance[route.handlerName](...args);
634
- if (res && !res.headersSent) {
635
- const routeMeta = routeMetadataMap[route.handlerName] || {};
636
- if (routeMeta.redirect) {
637
- res.redirect(routeMeta.redirect.url, routeMeta.redirect.statusCode);
638
- return;
639
- }
640
- // Apply custom headers
641
- if (routeMeta.headers) {
642
- for (const [name, val] of Object.entries(routeMeta.headers)) {
643
- res.setHeader(name, val);
644
- }
645
- }
646
- // Apply custom HTTP Code
647
- if (routeMeta.httpCode) {
648
- res.status(routeMeta.httpCode);
649
- }
650
- if (result !== undefined) {
651
- res.json(result);
652
- }
653
- }
654
- }
655
- catch (err) {
656
- let handled = false;
657
- const routeLifecycle = lifecycleMetadataMap[route.handlerName] || {};
658
- const filters = routeLifecycle.filters || [];
659
- for (const filter of filters) {
660
- if (typeof filter === 'function' && filter.prototype?.catch) {
661
- let filterInstance = this.container.resolve(filter);
662
- if (!filterInstance)
663
- filterInstance = new filter();
664
- await filterInstance.catch(err, { req, res });
665
- handled = true;
666
- break;
667
- }
668
- else if (typeof filter === 'object' &&
669
- typeof filter.catch === 'function') {
670
- await filter.catch(err, { req, res });
671
- handled = true;
672
- break;
673
- }
674
- }
675
- if (!handled) {
676
- if (next)
677
- next(err);
678
- else
679
- console.error(`[Exis ${method.toUpperCase()} Error]`, err);
680
- }
681
- }
682
- };
683
- let finalHandler;
684
- if (method === 'ws' || method === 'sse') {
685
- finalHandler = async (streamOrSocket, rawReq) => {
686
- await executeCore(rawReq, undefined, undefined, streamOrSocket);
687
- };
688
- }
689
- else {
690
- finalHandler = async (req, res, next) => {
691
- await executeCore(req, res, next);
692
- };
693
- }
694
- const fullPath = (basePath + prefix + route.path).replace(/\/+/g, '/') || '/';
695
- const routeSpecificMiddlewares = methodMiddlewares[route.handlerName] || [];
696
- const allMiddlewares = [
697
- ...prefixMiddlewares,
698
- ...classMiddlewares,
699
- ...routeSpecificMiddlewares,
700
- ];
701
- const routeMeta = routeMetadataMap[route.handlerName] || {};
702
- const finalHost = routeMeta.hosts || host;
703
- let schema = route.schema;
704
- const paramMetadata = paramMetadataMap[route.handlerName] || [];
705
- let extractedBodySchema = undefined;
706
- let extractedQuerySchema = undefined;
707
- for (const param of paramMetadata) {
708
- if (!param)
709
- continue;
710
- if (param.type === 'body') {
711
- const bodyPipe = param.pipes?.find((p) => p && typeof p.parse === 'function');
712
- if (bodyPipe)
713
- extractedBodySchema = bodyPipe;
714
- }
715
- if (param.type === 'query') {
716
- const queryPipe = param.pipes?.find((p) => p && typeof p.parse === 'function');
717
- if (queryPipe)
718
- extractedQuerySchema = queryPipe;
719
- }
720
- }
721
- const extractedResponseSchema = routeMeta.responseSchema;
722
- if (schema ||
723
- finalHost ||
724
- extractedBodySchema ||
725
- extractedQuerySchema ||
726
- extractedResponseSchema) {
727
- schema = schema || {};
728
- if (finalHost)
729
- schema.host = finalHost;
730
- if (extractedBodySchema && !schema.body)
731
- schema.body = extractedBodySchema;
732
- if (extractedQuerySchema && !schema.query)
733
- schema.query = extractedQuerySchema;
734
- if (extractedResponseSchema && !schema.response)
735
- schema.response = extractedResponseSchema;
736
- this.router[method](fullPath, ...allMiddlewares, schema, finalHandler);
737
- }
738
- else {
739
- ;
740
- this.router[method](fullPath, ...allMiddlewares, finalHandler);
741
- }
742
- }
743
- }
297
+ this.controllerRegistrar.registerControllers(controllers, basePath, prefixMiddlewares);
744
298
  return this;
745
299
  }
746
300
  // ─── Dependency Injection ───────────────────────────────────────────────────
@@ -788,681 +342,32 @@ class App {
788
342
  this.errorHandlers.push((0, errors_1.createErrorHandler)(this.options.env === 'development'));
789
343
  }
790
344
  }
791
- getCompiledPipeline() {
792
- if (this._compiledPipeline)
793
- return this._compiledPipeline;
794
- this.applyBuiltins();
795
- this._compiledPipeline = [
796
- ...this.globalMiddleware,
797
- (req, res, next) => this.router.handle(req, res, next),
798
- middleware_1.notFound,
799
- ];
800
- return this._compiledPipeline;
801
- }
802
- // ─── Web Standard Fetch (Edge Runtime) & Inject ──────────────────────────────
803
345
  async fetch(request, env, ctx) {
804
- if (!this._routesMounted) {
805
- if (typeof this.create === 'function')
806
- await this.create();
807
- if (typeof this.onStartHook === 'function')
808
- await this.onStartHook(this);
809
- }
810
- const { handleFetch } = await Promise.resolve().then(() => __importStar(require('../adapters/fetch')));
811
- return handleFetch(this, request, env, ctx);
346
+ return this.requestHandler.fetch(request, env, ctx);
812
347
  }
813
348
  async inject(options) {
814
- const method = (options.method || 'GET').toUpperCase();
815
- const url = options.url.startsWith('http')
816
- ? options.url
817
- : `http://localhost${options.url}`;
818
- const headers = new Headers();
819
- if (options.headers) {
820
- for (const [k, v] of Object.entries(options.headers)) {
821
- headers.set(k, v);
822
- }
823
- }
824
- let bodyStr;
825
- if (options.body) {
826
- bodyStr =
827
- typeof options.body === 'string'
828
- ? options.body
829
- : JSON.stringify(options.body);
830
- if (!headers.has('content-type') && typeof options.body !== 'string') {
831
- headers.set('content-type', 'application/json');
832
- }
833
- }
834
- const req = new Request(url, {
835
- method,
836
- headers,
837
- body: ['GET', 'HEAD', 'OPTIONS'].includes(method) ? undefined : bodyStr,
838
- });
839
- const fetchRes = await this.fetch(req);
840
- const resText = await fetchRes.text();
841
- let parsedBody = resText;
842
- try {
843
- parsedBody = JSON.parse(resText);
844
- }
845
- catch {
846
- // ignore
847
- }
848
- const resHeaders = {};
849
- fetchRes.headers.forEach((v, k) => {
850
- resHeaders[k] = v;
851
- });
852
- return {
853
- status: fetchRes.status,
854
- headers: resHeaders,
855
- body: parsedBody,
856
- text: resText,
857
- };
349
+ return this.requestHandler.inject(options);
858
350
  }
859
351
  // ─── Private Internal Pipeline Handler ─────────────────────────────────────────────────────
860
- _executeWithContext(req, res, execution) {
861
- const store = {
862
- state: {},
863
- afterCallbacks: [],
864
- req: req,
865
- res: res,
866
- app: this,
867
- diCache: new Map(),
868
- };
869
- res.raw.on('finish', () => {
870
- for (const cb of store.afterCallbacks) {
871
- try {
872
- const promise = cb();
873
- if (promise instanceof Promise) {
874
- promise.catch((err) => {
875
- this.log.error({ err }, 'Error in after() background task');
876
- });
877
- }
878
- }
879
- catch (err) {
880
- this.log.error({ err }, 'Error in after() background task');
881
- }
882
- }
883
- });
884
- context_1.executionContext.run(store, execution);
352
+ getCompiledPipeline() {
353
+ return this.requestHandler.getCompiledPipeline();
885
354
  }
886
355
  handle(rawReq, rawRes) {
887
- const res = new response_1.ExisResponse(rawRes);
888
- const req = new request_1.ExisRequest(rawReq, res, this.options.trustProxy, this.options.bodyLimit);
889
- res.req = req;
890
- res.etagEnabled = this.options.etag === true;
891
- req.log = this.log;
892
- req._dataloaderFns = this._dataloaders;
893
- this._executeWithContext(req, res, () => {
894
- if (this.hooks.request.length === 0 && this.hooks.response.length === 0) {
895
- (0, router_1.runHandlers)(this.getCompiledPipeline(), req, res, (err) => {
896
- if (err) {
897
- this._runErrorHandlers(err, req, res).catch((e) => {
898
- this.log.error({ err: e }, 'Error in error handler');
899
- });
900
- }
901
- });
902
- return;
903
- }
904
- this._handleWithHooks(req, res, rawRes);
905
- });
356
+ this.requestHandler.handle(rawReq, rawRes);
906
357
  }
907
358
  /**
908
359
  * Handle a request from the uWebSockets.js backend.
909
360
  * Uses the same middleware pipeline as Node HTTP but with uWS shims.
910
361
  */
911
362
  handleUws(shimReq, shimRes) {
912
- // Wrap uWS shims in ExisRequest/ExisResponse using type coercion
913
- // The shims implement the same interface that ExisRequest/ExisResponse expect
914
- const res = new response_1.ExisResponse(shimRes);
915
- const req = new request_1.ExisRequest(shimReq, res, this.options.trustProxy, this.options.bodyLimit);
916
- res.req = req;
917
- res.etagEnabled = this.options.etag === true;
918
- req.log = this.log;
919
- req._dataloaderFns = this._dataloaders;
920
- this._executeWithContext(req, res, () => {
921
- if (this.hooks.request.length === 0 && this.hooks.response.length === 0) {
922
- (0, router_1.runHandlers)(this.getCompiledPipeline(), req, res, (err) => {
923
- if (err) {
924
- this._runErrorHandlers(err, req, res).catch((e) => {
925
- this.log.error({ err: e }, 'Error in error handler');
926
- });
927
- }
928
- });
929
- return;
930
- }
931
- this._handleWithHooks(req, res, shimRes);
932
- });
933
- }
934
- async _handleWithHooks(req, res, rawRes) {
935
- // ─── onRequest Hook ───
936
- if (this.hooks.request.length > 0) {
937
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
938
- for (let i = 0; i < this.hooks.request.length; i++) {
939
- await this.hooks.request[i](req, res);
940
- }
941
- }
942
- if (res.headersSent)
943
- return;
944
- // ─── onResponse Hook ───
945
- if (this.hooks.response.length > 0) {
946
- rawRes.on('finish', () => {
947
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
948
- for (let i = 0; i < this.hooks.response.length; i++) {
949
- Promise.resolve(this.hooks.response[i](req, res)).catch((err) => {
950
- this.log.error({ err }, 'Error in onResponse hook');
951
- });
952
- }
953
- });
954
- }
955
- (0, router_1.runHandlers)(this.getCompiledPipeline(), req, res, (err) => {
956
- if (err) {
957
- this._runErrorHandlers(err, req, res).catch((e) => {
958
- this.log.error({ err: e }, 'Error in error handler');
959
- });
960
- }
961
- });
962
- }
963
- async _runErrorHandlers(err, req, res) {
964
- // ─── onError Hook ───
965
- if (this.hooks.error.length > 0) {
966
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
967
- for (let i = 0; i < this.hooks.error.length; i++) {
968
- await this.hooks.error[i](err, req, res);
969
- }
970
- }
971
- if (res.headersSent)
972
- return;
973
- for (const handler of this.errorHandlers) {
974
- let handled = false;
975
- await handler(err, req, res, () => {
976
- handled = true;
977
- });
978
- if (!handled || res.headersSent)
979
- return;
980
- }
981
- // fallback if no error handler ran
982
- const defaultHandler = (0, errors_1.createErrorHandler)(this.options.env === 'development');
983
- await defaultHandler(err, req, res, () => {
984
- /* noop */
985
- });
986
- }
987
- // ─── WebSocket Upgrade Handler ───────────────────────────────────────────────
988
- async _handleUpgrade(rawReq, socket, head) {
989
- this.applyBuiltins();
990
- // Match route
991
- const reqPath = (rawReq.url || '').split('?')[0];
992
- const matched = this.router.match('WS', reqPath);
993
- if (!matched) {
994
- socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n');
995
- socket.destroy();
996
- return;
997
- }
998
- // Create a dummy ServerResponse so standard HTTP middleware can run and reject if needed
999
- const rawRes = new node_http_1.ServerResponse(rawReq);
1000
- rawRes.assignSocket(socket);
1001
- const res = new response_1.ExisResponse(rawRes);
1002
- const originalMethod = rawReq.method;
1003
- const req = new request_1.ExisRequest(rawReq, res, this.options.trustProxy, this.options.bodyLimit);
1004
- res.req = req;
1005
- req.method = 'WS';
1006
- req.params = matched.params;
1007
- req.log = this.log;
1008
- // Build the middleware pipeline specifically for this WebSocket route
1009
- const pipeline = [
1010
- ...this.globalMiddleware,
1011
- async (req, res, next) => {
1012
- // Exclude the last handler which is the actual WsHandler wrapper
1013
- const routeHandlers = matched.route.handlers.slice(0, -1);
1014
- (0, router_1.runHandlers)(routeHandlers, req, res, (err) => {
1015
- if (err)
1016
- return next(err);
1017
- next();
1018
- });
1019
- },
1020
- ];
1021
- await (0, router_1.runHandlers)(pipeline, req, res, async (err) => {
1022
- if (err) {
1023
- this.log.error({ err }, 'Error during WebSocket upgrade middleware');
1024
- socket.write('HTTP/1.1 500 Internal Server Error\\r\\n\\r\\n');
1025
- socket.destroy();
1026
- return;
1027
- }
1028
- if (res.headersSent) {
1029
- // A middleware intercepted and sent an HTTP response (e.g. 401 Unauthorized)
1030
- socket.destroy();
1031
- return;
1032
- }
1033
- // Restore original HTTP method (usually 'GET') so the `ws` package doesn't reject with 405
1034
- rawReq.method = originalMethod;
1035
- // Middleware passed, perform upgrade
1036
- this.rawWsServer.handleUpgrade(rawReq, socket, head, (ws) => {
1037
- const exisWs = new socket_1.ExisWebSocket(ws, req, this.wsServer);
1038
- this.wsServer.track(exisWs);
1039
- req.ws = exisWs;
1040
- const finalHandler = matched.route.handlers[matched.route.handlers.length - 1];
1041
- Promise.resolve(finalHandler(req, res, () => {
1042
- /* noop */
1043
- })).catch((err) => {
1044
- this.log.error({ err }, 'Error in WebSocket handler');
1045
- ws.close(1011, 'Internal Server Error');
1046
- });
1047
- });
1048
- });
363
+ this.requestHandler.handleUws(shimReq, shimRes);
1049
364
  }
1050
- /**
1051
- * WebSocket Upgrade Handler for uWebSockets.js
1052
- * Runs the middleware pipeline and if successful, upgrades the connection.
1053
- */
1054
- async handleUwsUpgrade(shimReq, uwsRes, context) {
1055
- this.applyBuiltins();
1056
- const reqPath = (shimReq.url || '').split('?')[0];
1057
- const matched = this.router.match('WS', reqPath);
1058
- if (!matched) {
1059
- // 404
1060
- uwsRes.cork(() => {
1061
- uwsRes.writeStatus('404 Not Found');
1062
- uwsRes.end();
1063
- });
1064
- return;
1065
- }
1066
- let isAborted = false;
1067
- uwsRes.onAborted(() => {
1068
- isAborted = true;
1069
- });
1070
- const shimRes = new uws_adapter_1.UwsServerResponse(uwsRes);
1071
- const res = new response_1.ExisResponse(shimRes);
1072
- const req = new request_1.ExisRequest(shimReq, res, this.options.trustProxy, this.options.bodyLimit);
1073
- res.req = req;
1074
- req.method = 'WS';
1075
- req.params = matched.params;
1076
- req.log = this.log;
1077
- const pipeline = [
1078
- ...this.globalMiddleware,
1079
- async (req, res, next) => {
1080
- const routeHandlers = matched.route.handlers.slice(0, -1);
1081
- (0, router_1.runHandlers)(routeHandlers, req, res, (err) => {
1082
- if (err)
1083
- return next(err);
1084
- next();
1085
- });
1086
- },
1087
- ];
1088
- this._executeWithContext(req, res, () => {
1089
- (0, router_1.runHandlers)(pipeline, req, res, async (err) => {
1090
- if (isAborted)
1091
- return;
1092
- if (err) {
1093
- this.log.error({ err }, 'Error during uWS WebSocket upgrade middleware');
1094
- uwsRes.cork(() => {
1095
- uwsRes.writeStatus('500 Internal Server Error');
1096
- uwsRes.end();
1097
- });
1098
- return;
1099
- }
1100
- if (res.headersSent) {
1101
- return; // Middleware sent an HTTP response
1102
- }
1103
- // We need to fetch headers directly from the shimReq.headers
1104
- const secWebSocketKey = shimReq.headers['sec-websocket-key'] || '';
1105
- const secWebSocketProtocol = shimReq.headers['sec-websocket-protocol'] || '';
1106
- const secWebSocketExtensions = shimReq.headers['sec-websocket-extensions'] || '';
1107
- // Prepare user data for the upgraded websocket
1108
- const userData = {
1109
- req,
1110
- res,
1111
- exisWs: new socket_1.ExisWebSocket(null, req, this.wsServer), // the raw will be set to shim in open()
1112
- finalHandler: matched.route.handlers[matched.route.handlers.length - 1],
1113
- };
1114
- this.wsServer.track(userData.exisWs);
1115
- req.ws = userData.exisWs;
1116
- uwsRes.cork(() => {
1117
- uwsRes.upgrade(userData, secWebSocketKey, secWebSocketProtocol, secWebSocketExtensions, context);
1118
- });
1119
- Promise.resolve(userData.finalHandler(req, res, () => {
1120
- /* noop */
1121
- })).catch((e) => {
1122
- this.log.error({ err: e }, 'Error in uWS WebSocket handler');
1123
- });
1124
- });
1125
- });
365
+ getErrorHandlers() {
366
+ return this.errorHandlers;
1126
367
  }
1127
368
  // ─── Listen ───────────────────────────────────────────────────────────────────
1128
- listen(portOrOptions, callback) {
1129
- if (process.env.EXIS_CLI_MODE === '1') {
1130
- return undefined;
1131
- }
1132
- this.applyBuiltins();
1133
- let port;
1134
- let host;
1135
- let onListen;
1136
- if (typeof portOrOptions === 'number') {
1137
- port = portOrOptions;
1138
- host = this.options.host;
1139
- onListen = callback ? () => callback() : undefined;
1140
- }
1141
- else if (typeof portOrOptions === 'object') {
1142
- if (portOrOptions.ssl) {
1143
- this.options.ssl = portOrOptions.ssl;
1144
- // Re-initialize server if ssl is passed during listen
1145
- this.server = this._initServer();
1146
- }
1147
- if (portOrOptions.redirectHttp !== undefined) {
1148
- this.options.redirectHttp = portOrOptions.redirectHttp;
1149
- }
1150
- port = portOrOptions.port ?? this.options.port;
1151
- host = portOrOptions.host ?? this.options.host;
1152
- onListen = portOrOptions.onListen;
1153
- }
1154
- else {
1155
- port = this.options.port;
1156
- host = this.options.host;
1157
- }
1158
- // Auto HTTP -> HTTPS redirect server (only for Node HTTP backend)
1159
- // ─── uWebSockets.js listen path ───
1160
- if (this._useUws) {
1161
- this._uwsApp = (0, uws_adapter_1.createUwsApp)(this.handleUws.bind(this), this.handleUwsUpgrade.bind(this), this.options.ssl);
1162
- this._uwsApp.listen(port, host, async (token) => {
1163
- if (!token) {
1164
- this.log.error(`Failed to listen on ${host}:${port} (uWS)`);
1165
- return;
1166
- }
1167
- this._uwsListenToken = token;
1168
- const address = { port, host };
1169
- if (this.hotReloader) {
1170
- this.hotReloader.stop();
1171
- }
1172
- if (onListen) {
1173
- onListen(address);
1174
- }
1175
- else {
1176
- if (!process.env.__EXIS_DEV_SERVER && !process.env.__EXIS_CLI) {
1177
- const url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
1178
- this.log.info({ url, env: this.options.env, backend: 'uws' }, `Server running at ${url} (uWebSockets.js)`);
1179
- }
1180
- }
1181
- if (this.options.env === 'production') {
1182
- const handleShutdown = async () => {
1183
- this.log.info('Received shutdown signal (SIGTERM/SIGINT)');
1184
- try {
1185
- await this.close();
1186
- process.exit(0);
1187
- }
1188
- catch (err) {
1189
- this.log.error({ err }, 'Error during graceful shutdown');
1190
- process.exit(1);
1191
- }
1192
- };
1193
- process.once('SIGTERM', handleShutdown);
1194
- process.once('SIGINT', handleShutdown);
1195
- }
1196
- // ─── onReady Hook ───
1197
- for (const hook of this.hooks.ready) {
1198
- await hook();
1199
- }
1200
- if (callback)
1201
- callback();
1202
- });
1203
- // Return a minimal server-like object for compatibility
1204
- return this.server;
1205
- }
1206
- // ─── Node HTTP listen path ───
1207
- // Auto HTTP -> HTTPS redirect server
1208
- if (this.options.ssl &&
1209
- this.options.redirectHttp !== undefined &&
1210
- this.options.redirectHttp !== false) {
1211
- const redirectPort = typeof this.options.redirectHttp === 'number'
1212
- ? this.options.redirectHttp
1213
- : 80;
1214
- this.redirectServer = (0, node_http_1.createServer)((req, res) => {
1215
- const targetHost = req.headers.host?.split(':')[0] || host;
1216
- const actualPort = port === 0
1217
- ? this.server.address()?.port ||
1218
- port
1219
- : port;
1220
- const targetPort = actualPort === 443 ? '' : `:${actualPort}`;
1221
- res.writeHead(301, {
1222
- Location: `https://${targetHost}${targetPort}${req.url || '/'}`,
1223
- });
1224
- res.end();
1225
- });
1226
- this.redirectServer.listen(redirectPort, host, () => {
1227
- this.log.info(`Redirect server listening on port ${redirectPort} -> HTTPS port ${port}`);
1228
- });
1229
- }
1230
- this.server.on('error', (err) => {
1231
- if (err.code === 'EADDRINUSE') {
1232
- console.error(`\n\x1b[31m✗ Port ${port} is already in use.\x1b[0m`);
1233
- console.error(` Try killing the process or use a different port in exis.config.ts\n`);
1234
- process.exit(1);
1235
- }
1236
- else {
1237
- this.log.error({ err }, 'Failed to start server');
1238
- process.exit(1);
1239
- }
1240
- });
1241
- this.server.listen(port, host, async () => {
1242
- const address = { port, host };
1243
- if (this.options.env === 'production') {
1244
- const handleShutdown = async () => {
1245
- this.log.info('Received shutdown signal (SIGTERM/SIGINT)');
1246
- try {
1247
- await this.close();
1248
- process.exit(0);
1249
- }
1250
- catch (err) {
1251
- this.log.error({ err }, 'Error during graceful shutdown');
1252
- process.exit(1);
1253
- }
1254
- };
1255
- process.once('SIGTERM', handleShutdown);
1256
- process.once('SIGINT', handleShutdown);
1257
- }
1258
- // ─── onReady Hook ───
1259
- for (const hook of this.hooks.ready) {
1260
- await hook();
1261
- }
1262
- // Start queue worker if initialized
1263
- if (this._queueWorker) {
1264
- await this._queueWorker.start();
1265
- }
1266
- if (onListen) {
1267
- onListen(address);
1268
- }
1269
- else {
1270
- if (!process.env.__EXIS_DEV_SERVER && !process.env.__EXIS_CLI) {
1271
- const url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
1272
- this.log.info({ url, env: this.options.env }, `Server running at ${url}`);
1273
- }
1274
- }
1275
- if (callback)
1276
- callback();
1277
- });
1278
- return this.server;
1279
- }
1280
- // ─── Banner ───────────────────────────────────────────────────────────────────
1281
- async _printStartupBanner() {
1282
- const port = this.options.port ?? 3000;
1283
- const host = this.options.host ?? 'localhost';
1284
- const c = {
1285
- cyan: '\x1b[36m',
1286
- green: '\x1b[32m',
1287
- gray: '\x1b[90m',
1288
- dim: '\x1b[2m',
1289
- bold: '\x1b[1m',
1290
- reset: '\x1b[0m',
1291
- magenta: '\x1b[35m',
1292
- bgMagenta: '\x1b[45m',
1293
- white: '\x1b[37m',
1294
- primary: '\x1b[38;2;160;70;255m',
1295
- blue: '\x1b[38;2;41;169;206m',
1296
- };
1297
- let fwVersion = '0.0.0';
1298
- try {
1299
- const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
1300
- const path = await Promise.resolve().then(() => __importStar(require('node:path')));
1301
- const pkgPath = path.join(__dirname, '../../package.json');
1302
- fwVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
1303
- }
1304
- catch {
1305
- /* ignore */
1306
- }
1307
- const displayHost = host === '0.0.0.0' || host === '127.0.0.1' ? 'localhost' : host;
1308
- let localIp = '<YOUR_IP>';
1309
- try {
1310
- const os = await Promise.resolve().then(() => __importStar(require('node:os')));
1311
- const nets = os.networkInterfaces();
1312
- for (const name of Object.keys(nets)) {
1313
- for (const net of nets[name] || []) {
1314
- if (net.family === 'IPv4' && !net.internal) {
1315
- localIp = net.address;
1316
- break;
1317
- }
1318
- }
1319
- if (localIp !== '<YOUR_IP>')
1320
- break;
1321
- }
1322
- }
1323
- catch {
1324
- /* ignore */
1325
- }
1326
- const networkHost = host === '0.0.0.0'
1327
- ? `http://${localIp}:${port}`
1328
- : 'use --host 0.0.0.0 to expose';
1329
- const readyMs = 125;
1330
- const displayEnv = this.options.env || process.env.NODE_ENV || 'development';
1331
- const workerCount = process.env.__EXIS_CLUSTER_WORKERS;
1332
- const cluster = await Promise.resolve().then(() => __importStar(require('node:cluster')));
1333
- // In cluster mode, ONLY let Worker #1 print the banner so we don't spam the console 12 times
1334
- if (cluster.default.isWorker && cluster.default.worker?.id !== 1) {
1335
- return;
1336
- }
1337
- console.log(`\n ${c.primary}${c.bold}EXIS v${fwVersion}${c.reset} ready in ${c.bold}${readyMs} ms${c.reset}\n`);
1338
- console.log(` ${c.white}➜${c.reset} ${c.bold}Local:${c.reset} ${c.blue}http://${displayHost}:${port}/${c.reset}`);
1339
- console.log(` ${c.white}➜${c.reset} ${c.dim}Network:${c.reset} ${c.blue}${networkHost}/${c.reset}`);
1340
- console.log(` ${c.white}➜${c.reset} ${c.dim}Environ:${c.reset} ${c.green}${displayEnv}${c.reset}`);
1341
- if (workerCount && Number(workerCount) > 1) {
1342
- console.log(` ${c.white}➜${c.reset} ${c.dim}Workers:${c.reset} ${c.magenta}${workerCount}${c.reset}`);
1343
- }
1344
- console.log(` ${c.white}➜${c.reset} press ${c.bold}h + enter${c.reset} ${c.dim}to show help${c.reset}\n`);
1345
- }
1346
- // ─── Graceful Shutdown ────────────────────────────────────────────────────────
1347
- onShutdown(hook) {
1348
- this.shutdownHooks.push(hook);
1349
- return this;
1350
- }
1351
- close(timeout = 5000) {
1352
- return new Promise((resolve, reject) => {
1353
- this.log.info('Initiating graceful shutdown');
1354
- let finishCalled = false;
1355
- const finish = async () => {
1356
- if (finishCalled)
1357
- return;
1358
- finishCalled = true;
1359
- try {
1360
- for (const hook of this.shutdownHooks) {
1361
- await hook();
1362
- }
1363
- // Also execute onClose hooks
1364
- for (const hook of this.hooks.close) {
1365
- await hook();
1366
- }
1367
- if (this.onCloseHook) {
1368
- await this.onCloseHook(this);
1369
- }
1370
- if (this._cronScheduler) {
1371
- this._cronScheduler.stop();
1372
- }
1373
- if (this._queueWorker) {
1374
- await this._queueWorker.stop();
1375
- }
1376
- if (this._queueClient) {
1377
- await this._queueClient.close();
1378
- }
1379
- if (this.hotReloader) {
1380
- await this.hotReloader.stop();
1381
- }
1382
- this.log.info('Graceful shutdown completed');
1383
- resolve();
1384
- }
1385
- catch (err) {
1386
- this.log.error({ err }, 'Error executing shutdown hooks');
1387
- reject(err);
1388
- }
1389
- };
1390
- if (!this._useUws && (!this.server || !this.server.listening)) {
1391
- finish();
1392
- return;
1393
- }
1394
- if (this._useUws && !this._uwsListenToken) {
1395
- finish();
1396
- return;
1397
- }
1398
- // Close idle keep-alive connections immediately
1399
- if (!this._useUws &&
1400
- this.server &&
1401
- 'closeIdleConnections' in this.server) {
1402
- ;
1403
- this.server.closeIdleConnections?.();
1404
- }
1405
- // Set timeout to force close active connections
1406
- const timer = setTimeout(() => {
1407
- this.log.warn(`Shutdown timeout of ${timeout}ms exceeded, forcefully terminating active connections`);
1408
- if (!this._useUws &&
1409
- this.server &&
1410
- 'closeAllConnections' in this.server) {
1411
- ;
1412
- this.server.closeAllConnections?.();
1413
- }
1414
- finish(); // Ensure finish is called on timeout
1415
- }, timeout);
1416
- const cleanupAndFinish = async () => {
1417
- clearTimeout(timer);
1418
- await finish();
1419
- };
1420
- this.wsServer.close();
1421
- for (const client of this.rawWsServer.clients) {
1422
- client.terminate();
1423
- }
1424
- this.rawWsServer.close();
1425
- if (this.redirectServer) {
1426
- this.redirectServer.close();
1427
- }
1428
- if (this._useUws && this._uwsApp) {
1429
- this._uwsApp.close(this._uwsListenToken);
1430
- this._uwsListenToken = null;
1431
- cleanupAndFinish();
1432
- }
1433
- else {
1434
- this.server.close((err) => {
1435
- if (err)
1436
- reject(err);
1437
- else
1438
- cleanupAndFinish();
1439
- });
1440
- }
1441
- });
1442
- }
1443
- // ─── Expose internals ─────────────────────────────────────────────────────────
1444
- getRouter() {
1445
- return this.router;
1446
- }
1447
- async loadAllRoutes() {
1448
- for (const [filePath, entry] of this.lazyRoutes.entries()) {
1449
- if (!entry.loaded) {
1450
- entry.loaded = true;
1451
- this.router.removeRoutesBySource('lazy:' + filePath);
1452
- const routePath = this.routeMap.get(filePath);
1453
- if (routePath) {
1454
- try {
1455
- await this.mountRouteFile(filePath, routePath);
1456
- }
1457
- catch (e) {
1458
- this.log.error({ err: e, file: filePath }, 'Failed to eager-load route');
1459
- }
1460
- }
1461
- }
1462
- }
1463
- }
1464
369
  getServer() {
1465
- return this.server;
370
+ return this.bootstrapper.getServer();
1466
371
  }
1467
372
  getConfig() {
1468
373
  return this.options;
@@ -1481,6 +386,16 @@ class App {
1481
386
  this._loggerCreated = false;
1482
387
  this.ensureLogger();
1483
388
  this._configLoaded = true;
389
+ if (this.options.env === 'development') {
390
+ const { getAvailablePort } = await Promise.resolve().then(() => __importStar(require('../utils/port')));
391
+ const requestedPort = this.options.port || 3000;
392
+ const host = this.options.host || '0.0.0.0';
393
+ const availablePort = await getAvailablePort(requestedPort, host);
394
+ if (availablePort !== requestedPort) {
395
+ this.log.info(`Port ${requestedPort} is in use, using ${availablePort} instead.`);
396
+ }
397
+ this.options.port = availablePort;
398
+ }
1484
399
  if ((process.env.__EXIS_DEV_SERVER || process.env.__EXIS_CLI) &&
1485
400
  !process.env.__EXIS_REPL &&
1486
401
  !process.env.__EXIS_TEST) {
@@ -1488,910 +403,71 @@ class App {
1488
403
  }
1489
404
  }
1490
405
  // Initialize Queue after config is loaded
1491
- if (this.options.queue && !this._queueClient) {
1492
- this._initQueue(this.options.queue);
406
+ if (this.options.queue && !this.queueManager._queueClient) {
407
+ this.queueManager._initQueue(this.options.queue);
1493
408
  }
1494
409
  // Flush any pending jobs that were registered before config was loaded
1495
- if (this._pendingQueueJobs.length > 0) {
1496
- if (!this._queueWorker) {
410
+ if (this.queueManager._pendingQueueJobs.length > 0) {
411
+ if (!this.queueManager._queueWorker) {
1497
412
  if (this.options.queue?.enableWorkers !== false) {
1498
413
  throw new Error('Queue worker is not initialized. Please configure ExisConfig.queue first.');
1499
414
  }
1500
415
  }
1501
416
  else {
1502
- for (const job of this._pendingQueueJobs) {
1503
- this._queueWorker.registerJob(job);
1504
- if (this._cronScheduler && job.cron) {
1505
- this._cronScheduler.registerJob(job);
417
+ for (const job of this.queueManager._pendingQueueJobs) {
418
+ this.queueManager._queueWorker.registerJob(job);
419
+ if (this.queueManager._cronScheduler && job.cron) {
420
+ this.queueManager._cronScheduler.registerJob(job);
1506
421
  }
1507
422
  }
1508
423
  }
1509
- this._pendingQueueJobs = [];
424
+ this.queueManager._pendingQueueJobs = [];
1510
425
  }
1511
426
  if (this._routesMounted)
1512
427
  return this;
1513
428
  // Automatically scan and mount file-based routes and jobs
1514
- await this.autoMountRoutes(root);
1515
- await this.autoMountJobs(root);
429
+ await this.routeScanner.autoMountRoutes(root);
430
+ await this.routeScanner.autoMountJobs(root);
1516
431
  this._routesMounted = true;
1517
432
  // Start Hot Route Reloading in dev mode
1518
- if (this.options.env === 'development' && this._allApiDirs) {
433
+ if (this.options.env === 'development' &&
434
+ this.routeScanner._allApiDirs) {
1519
435
  this.hotReloader = new hot_reload_1.HotReloader({
1520
- apiDirs: this._allApiDirs,
436
+ apiDirs: this.routeScanner._allApiDirs,
1521
437
  router: this.router,
1522
- routeMap: this.routeMap,
1523
- mountRoute: (filePath, routePath) => this.mountRouteFile(filePath, routePath),
438
+ routeMap: this.routeScanner.routeMap,
439
+ mountRoute: (filePath, routePath) => this.routeScanner.mountRouteFile(filePath, routePath),
1524
440
  });
1525
441
  await this.hotReloader.start();
1526
442
  }
1527
443
  return this;
1528
444
  }
1529
- // ─── Auto-Mount File-Based Routes ─────────────────────────────────────────────
1530
- async autoMountRoutes(root) {
1531
- const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
1532
- const path = await Promise.resolve().then(() => __importStar(require('node:path')));
1533
- const isProd = process.env.NODE_ENV === 'production';
1534
- const isDev = this.options.env === 'development';
1535
- // Try to load the pre-built manifest first (O(1) boot)
1536
- const manifestPath = path.join(root, '.exis', 'routes-manifest.js');
1537
- try {
1538
- const stat = await fs.stat(manifestPath);
1539
- if (stat.isFile()) {
1540
- let url = (0, node_url_1.pathToFileURL)(manifestPath).href;
1541
- if (!isProd)
1542
- url += '?t=' + Date.now(); // cache bust for dev
1543
- let mod;
1544
- if (process.env.VITEST || process.env.NODE_ENV === 'test') {
1545
- mod = await Promise.resolve(`${url}`).then(s => __importStar(require(s)));
1546
- }
1547
- else {
1548
- const dynamicImport = new Function('specifier', 'return import(specifier)');
1549
- mod = await dynamicImport(url);
1550
- }
1551
- const manifest = mod.manifest;
1552
- if (Array.isArray(manifest)) {
1553
- for (const entry of manifest) {
1554
- const { routePath, module: routeMod, filePath } = entry;
1555
- const normalizedPath = path.resolve(root, filePath);
1556
- this.routeMap.set(normalizedPath, routePath);
1557
- const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
1558
- const isClassController = (obj) => obj &&
1559
- obj.prototype &&
1560
- obj.prototype[CONTROLLER_PREFIX] !== undefined;
1561
- const unwrappedMod = routeMod.default && routeMod.default.default
1562
- ? routeMod.default.default
1563
- : routeMod.default
1564
- ? routeMod.default
1565
- : routeMod;
1566
- const functionalControllerObj = unwrappedMod && unwrappedMod.__isController ? unwrappedMod : null;
1567
- const routerInstance = routeMod.router || routeMod.default || routeMod;
1568
- if (functionalControllerObj) {
1569
- const router = this.compileFunctionalController(functionalControllerObj);
1570
- this.mountRouteWithSource(routePath, router, normalizedPath);
1571
- }
1572
- else if (isClassController(routerInstance)) {
1573
- this.registerControllers([routerInstance], routePath);
1574
- }
1575
- else {
1576
- this.mountRouteWithSource(routePath, routerInstance, normalizedPath);
1577
- }
1578
- }
1579
- // Skip the filesystem scan completely!
1580
- return;
1581
- }
1582
- }
1583
- }
1584
- catch (err) {
1585
- if (err.code !== 'ENOENT') {
1586
- console.warn('exisjs: Failed to load routes manifest:', err.message);
1587
- }
1588
- // Fall back to scanning the filesystem
1589
- }
1590
- const searchDirs = isProd
1591
- ? [
1592
- path.join(root, '.exis', 'server', 'src', 'http'),
1593
- path.join(root, '.exis', 'server', 'http'),
1594
- path.join(root, 'dist', 'src', 'http'),
1595
- path.join(root, 'dist', 'http'),
1596
- path.join(root, 'src', 'http'),
1597
- path.join(root, 'http'),
1598
- ]
1599
- : [path.join(root, 'src', 'http'), path.join(root, 'http')];
1600
- const appDirs = [];
1601
- for (const dir of searchDirs) {
1602
- try {
1603
- const stat = await fs.stat(dir);
1604
- if (stat.isDirectory()) {
1605
- const baseName = path.basename(path.dirname(dir)) === 'src' ? 'src/http' : 'http';
1606
- const alreadyHasCompiledOrRawForThisBase = appDirs.some((existing) => {
1607
- const existingBase = path.basename(path.dirname(existing)) === 'src'
1608
- ? 'src/http'
1609
- : 'http';
1610
- return existingBase === baseName;
1611
- });
1612
- if (!alreadyHasCompiledOrRawForThisBase) {
1613
- appDirs.push(dir);
1614
- }
1615
- }
1616
- }
1617
- catch {
1618
- /* ignore */
1619
- }
1620
- }
1621
- if (appDirs.length === 0)
1622
- return;
1623
- this.apiDir = appDirs[0] // keep property name for backwards compatibility
1624
- ;
1625
- this._allApiDirs = appDirs;
1626
- const cc = {
1627
- green: '\x1b[32m',
1628
- cyan: '\x1b[36m',
1629
- gray: '\x1b[90m',
1630
- reset: '\x1b[0m',
1631
- };
1632
- for (const appDir of appDirs) {
1633
- const routes = await this.scanDirectory(appDir);
1634
- for (const { filePath, routePath } of routes) {
1635
- if (filePath.endsWith('route.ts') || filePath.endsWith('route.js')) {
1636
- const normalized = path.resolve(filePath);
1637
- this.routeMap.set(normalized, routePath);
1638
- if (isDev) {
1639
- const lazyKey = normalized;
1640
- this.lazyRoutes.set(lazyKey, {
1641
- filePath: normalized,
1642
- loaded: false,
1643
- });
1644
- const lazyHandler = async (req, res, next) => {
1645
- const entry = this.lazyRoutes.get(lazyKey);
1646
- if (entry && !entry.loaded) {
1647
- entry.loaded = true;
1648
- this.router.removeRoutesBySource('lazy:' + lazyKey);
1649
- try {
1650
- await this.mountRouteFile(normalized, routePath);
1651
- const relative = path
1652
- .relative(process.cwd(), normalized)
1653
- .replace(/\\/g, '/');
1654
- console.log(` ${cc.green}✓${cc.reset} Lazy loaded ${cc.cyan}${relative}${cc.reset}`);
1655
- }
1656
- catch (err) {
1657
- (0, dev_error_overlay_1.formatDevError)(err, normalized);
1658
- return;
1659
- }
1660
- await this.router.handle(req, res, next);
1661
- }
1662
- else {
1663
- next?.();
1664
- }
1665
- };
1666
- const lazyRouter = new router_1.Router();
1667
- lazyRouter.all(routePath, lazyHandler);
1668
- lazyRouter.all(routePath + '/*path', lazyHandler);
1669
- for (const r of lazyRouter.getRoutes()) {
1670
- r.sourceFile = 'lazy:' + lazyKey;
1671
- this.router.addRawRoute(r);
1672
- }
1673
- }
1674
- else {
1675
- try {
1676
- await this.mountRouteFile(normalized, routePath);
1677
- }
1678
- catch (err) {
1679
- this.log.error({ err, file: filePath }, `Failed to load route file: ${filePath}`);
1680
- }
1681
- }
1682
- }
1683
- }
1684
- }
1685
- }
1686
- // ─── Route File Mounting (shared by autoMount and HotReloader) ──────────────
1687
- // ─── Auto-Mount File-Based Jobs ──────────────────────────────────────────────
1688
- async autoMountJobs(root) {
1689
- if (!this._queueWorker)
1690
- return;
1691
- const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
1692
- const path = await Promise.resolve().then(() => __importStar(require('node:path')));
1693
- const isProd = process.env.NODE_ENV === 'production';
1694
- const searchDirs = isProd
1695
- ? [
1696
- path.join(root, '.exis', 'server', 'src', 'jobs'),
1697
- path.join(root, '.exis', 'server', 'jobs'),
1698
- path.join(root, 'dist', 'src', 'jobs'),
1699
- path.join(root, 'dist', 'jobs'),
1700
- path.join(root, 'src', 'jobs'),
1701
- path.join(root, 'jobs'),
1702
- ]
1703
- : [path.join(root, 'src', 'jobs'), path.join(root, 'jobs')];
1704
- let jobsDir = '';
1705
- for (const dir of searchDirs) {
1706
- try {
1707
- const stat = await fs.stat(dir);
1708
- if (stat.isDirectory()) {
1709
- jobsDir = dir;
1710
- break;
1711
- }
1712
- }
1713
- catch {
1714
- /* ignore */
1715
- }
1716
- }
1717
- if (!jobsDir)
1718
- return;
1719
- try {
1720
- const entries = await fs.readdir(jobsDir, { withFileTypes: true });
1721
- for (const entry of entries) {
1722
- if (entry.isFile() &&
1723
- (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
1724
- const filePath = path.join(jobsDir, entry.name);
1725
- const name = entry.name.replace(/\.(ts|js)$/, '');
1726
- try {
1727
- // Load the definition directly (no compilation needed here just to get the schema)
1728
- const mod = await Promise.resolve(`${filePath}`).then(s => __importStar(require(s)));
1729
- const jobDef = mod.default || mod;
1730
- if (jobDef) {
1731
- const def = {
1732
- name: jobDef.name || name,
1733
- filePath, // Attach filePath for Thread Pool execution
1734
- cron: jobDef.cron,
1735
- schema: jobDef.schema,
1736
- defaultOptions: jobDef.defaultOptions,
1737
- };
1738
- this._queueWorker.registerJob(def);
1739
- if (this._cronScheduler && jobDef.cron) {
1740
- this._cronScheduler.registerJob(def);
1741
- }
1742
- this.log.info(`Registered background job: ${name}`);
1743
- }
1744
- }
1745
- catch (err) {
1746
- this.log.error(`Failed to load job file ${filePath}: ${err}`);
1747
- }
1748
- }
1749
- }
1750
- }
1751
- catch {
1752
- // Ignore if jobs directory does not exist
1753
- }
1754
- }
1755
- async mountRouteFile(filePath, routePath) {
1756
- let mod;
1757
- try {
1758
- const url = process.env.VITEST || process.env.NODE_ENV === 'test'
1759
- ? (0, node_url_1.pathToFileURL)(filePath).href
1760
- : (0, node_url_1.pathToFileURL)(filePath).href + '?t=' + Date.now();
1761
- if (process.env.VITEST || process.env.NODE_ENV === 'test') {
1762
- mod = await Promise.resolve(`${url}`).then(s => __importStar(require(s)));
1763
- }
1764
- else {
1765
- const dynamicImport = new Function('specifier', 'return import(specifier)');
1766
- mod = await dynamicImport(url);
1767
- }
1768
- }
1769
- catch {
1770
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1771
- mod = require(filePath);
1772
- }
1773
- const path = await Promise.resolve().then(() => __importStar(require('node:path')));
1774
- const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
1775
- const dirname = path.dirname(filePath);
1776
- const apiDir = this.apiDir || dirname;
1777
- const normDirname = path.resolve(dirname).replace(/\\/g, '/').toLowerCase();
1778
- const normApiDir = path.resolve(apiDir).replace(/\\/g, '/').toLowerCase();
1779
- const segments = normDirname.startsWith(normApiDir)
1780
- ? normDirname.slice(normApiDir.length).split('/').filter(Boolean)
1781
- : [];
1782
- const allMiddlewares = [];
1783
- const allFilters = [];
1784
- const allGuards = [];
1785
- const allInterceptors = [];
1786
- let allMetadata = {};
1787
- let allCors = undefined;
1788
- let allHeaders = {};
1789
- const activeGateways = [];
1790
- const dirsToCheck = [apiDir];
1791
- let tempDir = apiDir;
1792
- for (const segment of segments) {
1793
- tempDir = path.join(tempDir, segment);
1794
- dirsToCheck.push(tempDir);
1795
- }
1796
- for (const dir of dirsToCheck) {
1797
- try {
1798
- const gwPathJs = path.join(dir, 'gateway.js');
1799
- const gwPathTs = path.join(dir, 'gateway.ts');
1800
- let targetGw = '';
1801
- if (await fs.stat(gwPathTs).catch(() => null))
1802
- targetGw = gwPathTs;
1803
- else if (await fs.stat(gwPathJs).catch(() => null))
1804
- targetGw = gwPathJs;
1805
- if (targetGw) {
1806
- activeGateways.push(targetGw);
1807
- const gwUrl = process.env.VITEST || process.env.NODE_ENV === 'test'
1808
- ? (0, node_url_1.pathToFileURL)(targetGw).href
1809
- : (0, node_url_1.pathToFileURL)(targetGw).href + '?t=' + Date.now();
1810
- let gwMod;
1811
- try {
1812
- if (process.env.VITEST || process.env.NODE_ENV === 'test') {
1813
- gwMod = await Promise.resolve(`${gwUrl}`).then(s => __importStar(require(s)));
1814
- }
1815
- else {
1816
- const dynamicImportGw = new Function('specifier', 'return import(specifier)');
1817
- gwMod = await dynamicImportGw(gwUrl);
1818
- }
1819
- }
1820
- catch {
1821
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1822
- gwMod = require(targetGw);
1823
- }
1824
- const gwConfig = gwMod && gwMod.default && gwMod.default.default
1825
- ? gwMod.default.default
1826
- : gwMod && gwMod.default
1827
- ? gwMod.default
1828
- : gwMod;
1829
- if (gwConfig) {
1830
- const isExcluded = (reqPath, reqMethod) => {
1831
- if (!gwConfig.exclude)
1832
- return false;
1833
- for (const rule of gwConfig.exclude) {
1834
- if (typeof rule === 'string') {
1835
- if (rule.endsWith('/*')) {
1836
- if (reqPath.startsWith(rule.slice(0, -2)))
1837
- return true;
1838
- }
1839
- else if (reqPath === rule)
1840
- return true;
1841
- }
1842
- else {
1843
- const pathMatch = rule.path.endsWith('/*')
1844
- ? reqPath.startsWith(rule.path.slice(0, -2))
1845
- : reqPath === rule.path;
1846
- if (pathMatch) {
1847
- if (!rule.methods ||
1848
- rule.methods.includes(reqMethod))
1849
- return true;
1850
- }
1851
- }
1852
- }
1853
- return false;
1854
- };
1855
- if (gwConfig.middleware) {
1856
- const wrapped = gwConfig.middleware.map((m) => (req, res, next) => isExcluded(req.path, req.method) ? next() : m(req, res, next));
1857
- allMiddlewares.push(...wrapped);
1858
- }
1859
- if (gwConfig.filters) {
1860
- const wrapped = gwConfig.filters.map((f) => (err, req, res, next) => isExcluded(req.path, req.method)
1861
- ? next(err)
1862
- : f.prototype && f.prototype.catch
1863
- ? new f().catch(err, req, res, next)
1864
- : f(err, req, res, next));
1865
- allFilters.push(...wrapped);
1866
- }
1867
- if (gwConfig.guards) {
1868
- const wrapped = gwConfig.guards.map((g) => {
1869
- return async (req) => {
1870
- if (isExcluded(req.path, req.method))
1871
- return true;
1872
- return typeof g === 'function'
1873
- ? g.prototype?.canActivate
1874
- ? new g().canActivate(req)
1875
- : g(req)
1876
- : g.canActivate(req);
1877
- };
1878
- });
1879
- allGuards.push(...wrapped);
1880
- }
1881
- if (gwConfig.interceptors) {
1882
- const wrapped = gwConfig.interceptors.map((i) => {
1883
- return async (req, res) => {
1884
- if (isExcluded(req.path, req.method))
1885
- return;
1886
- return typeof i === 'function'
1887
- ? i.prototype?.intercept
1888
- ? new i().intercept(req, res)
1889
- : i(req, res)
1890
- : i.intercept(req, res);
1891
- };
1892
- });
1893
- allInterceptors.push(...wrapped);
1894
- }
1895
- if (gwConfig.timeout !== undefined) {
1896
- const baseTimeout = gwConfig.timeout;
1897
- const timeoutMiddleware = (req, res, next) => {
1898
- if (isExcluded(req.path, req.method))
1899
- return next();
1900
- const ms = typeof baseTimeout === 'function'
1901
- ? baseTimeout(req)
1902
- : baseTimeout;
1903
- if (ms) {
1904
- req.timeoutTimer = setTimeout(() => {
1905
- if (!res.headersSent) {
1906
- res.status(408).json({
1907
- success: false,
1908
- error: {
1909
- code: 'REQUEST_TIMEOUT',
1910
- message: 'Request Timeout',
1911
- },
1912
- });
1913
- }
1914
- }, ms);
1915
- res.raw.on('finish', () => clearTimeout(req.timeoutTimer));
1916
- }
1917
- next();
1918
- };
1919
- allMiddlewares.push(timeoutMiddleware);
1920
- }
1921
- if (gwConfig.metadata) {
1922
- allMetadata = { ...allMetadata, ...gwConfig.metadata };
1923
- }
1924
- if (gwConfig.cors !== undefined)
1925
- allCors = gwConfig.cors;
1926
- if (gwConfig.headers)
1927
- allHeaders = { ...allHeaders, ...gwConfig.headers };
1928
- if (gwConfig.plugins) {
1929
- for (const plugin of gwConfig.plugins) {
1930
- await this.register(plugin);
1931
- }
1932
- }
1933
- if (gwConfig.imports) {
1934
- for (const mod of gwConfig.imports) {
1935
- const name = 'plugin' in mod ? mod.plugin.name : mod.name;
1936
- if (!this.hasPlugin(name)) {
1937
- await this.register(mod);
1938
- }
1939
- }
1940
- }
1941
- if (gwConfig.providers) {
1942
- for (const [token, providerConfig] of gwConfig.providers) {
1943
- this.provide(token, providerConfig);
1944
- }
1945
- }
1946
- }
1947
- }
1948
- }
1949
- catch {
1950
- /* ignore */
1951
- }
1952
- }
1953
- const currentMod = mod && mod.default && mod.default.default
1954
- ? mod.default.default
1955
- : mod && mod.default
1956
- ? mod.default
1957
- : mod;
1958
- const isControllerObj = currentMod && currentMod.__isController === true;
1959
- const routeConfig = {
1960
- ...(isControllerObj ? currentMod : {}),
1961
- ...(mod.config || {}),
1962
- };
1963
- // Fallback to global app.options.cors if neither gateway nor route specify it
1964
- if (routeConfig.cors !== undefined)
1965
- allCors = routeConfig.cors;
1966
- else if (allCors === undefined && this.options.cors !== undefined)
1967
- allCors = this.options.cors;
1968
- if (routeConfig.middleware)
1969
- allMiddlewares.push(...(Array.isArray(routeConfig.middleware)
1970
- ? routeConfig.middleware
1971
- : [routeConfig.middleware]));
1972
- if (routeConfig.headers)
1973
- allHeaders = { ...allHeaders, ...routeConfig.headers };
1974
- if (routeConfig.plugins) {
1975
- for (const plugin of routeConfig.plugins) {
1976
- await this.register(plugin);
1977
- }
1978
- }
1979
- // Pass gateway features to controllers via metadata if necessary
1980
- const combinedFilters = [
1981
- ...allFilters,
1982
- ...(routeConfig.filters
1983
- ? Array.isArray(routeConfig.filters)
1984
- ? routeConfig.filters
1985
- : [routeConfig.filters]
1986
- : []),
1987
- ];
1988
- const combinedGuards = [
1989
- ...allGuards,
1990
- ...(routeConfig.guards
1991
- ? Array.isArray(routeConfig.guards)
1992
- ? routeConfig.guards
1993
- : [routeConfig.guards]
1994
- : []),
1995
- ];
1996
- const combinedInterceptors = [
1997
- ...allInterceptors,
1998
- ...(routeConfig.interceptors
1999
- ? Array.isArray(routeConfig.interceptors)
2000
- ? routeConfig.interceptors
2001
- : [routeConfig.interceptors]
2002
- : []),
2003
- ];
2004
- // Inject into the default export if it's a functional controller
2005
- if (mod.default && typeof mod.default === 'object') {
2006
- try {
2007
- if (!mod.default.filters)
2008
- mod.default.filters = [];
2009
- mod.default.filters.push(...combinedFilters);
2010
- if (!mod.default.guards)
2011
- mod.default.guards = [];
2012
- mod.default.guards.push(...combinedGuards);
2013
- if (!mod.default.interceptors)
2014
- mod.default.interceptors = [];
2015
- mod.default.interceptors.push(...combinedInterceptors);
2016
- if (!mod.default.metadata)
2017
- mod.default.metadata = {};
2018
- Object.assign(mod.default.metadata, {
2019
- ...allMetadata,
2020
- ...(routeConfig.metadata || {}),
2021
- });
2022
- }
2023
- catch {
2024
- // If mod.default is frozen, ignore
2025
- }
2026
- }
2027
- const prefixMiddlewares = [];
2028
- if (this.options.debugRouting) {
2029
- const pathLib = await Promise.resolve().then(() => __importStar(require('node:path')));
2030
- const relFile = pathLib
2031
- .relative(process.cwd(), filePath)
2032
- .replace(/\\/g, '/');
2033
- const gwStr = activeGateways.length > 0
2034
- ? activeGateways
2035
- .map((g) => pathLib.relative(process.cwd(), g).replace(/\\/g, '/'))
2036
- .join(' -> ') + ' -> '
2037
- : '';
2038
- prefixMiddlewares.push((req, res, next) => {
2039
- this.log.debug(`[Request] ${req.method} ${req.path} -> ${gwStr}${relFile}`);
2040
- next();
2041
- });
2042
- }
2043
- if (allCors !== undefined) {
2044
- if (allCors === true)
2045
- prefixMiddlewares.push((0, middleware_1.cors)({}));
2046
- else if (allCors !== false)
2047
- prefixMiddlewares.push((0, middleware_1.cors)(allCors));
2048
- }
2049
- if (Object.keys(allHeaders).length > 0) {
2050
- prefixMiddlewares.push((req, res, next) => {
2051
- for (const [k, v] of Object.entries(allHeaders))
2052
- res.setHeader(k, v);
2053
- next();
2054
- });
2055
- }
2056
- prefixMiddlewares.push(...allMiddlewares);
2057
- const isRouter = (obj) => obj && 'handle' in obj && 'get' in obj;
2058
- const CONTROLLER_PREFIX = Symbol.for('exisjs:controller_prefix');
2059
- const isController = (obj) => obj && obj.prototype && obj.prototype[CONTROLLER_PREFIX] !== undefined;
2060
- const unwrappedMod = mod && mod.default && mod.default.default
2061
- ? mod.default.default
2062
- : mod && mod.default
2063
- ? mod.default
2064
- : mod;
2065
- const functionalControllerObj = unwrappedMod && unwrappedMod.__isController ? unwrappedMod : null;
2066
- if (functionalControllerObj) {
2067
- // ─── THE NEW PERFECT FUNCTIONAL CONTROLLER ───
2068
- const compiledRouter = this.compileFunctionalController(functionalControllerObj);
2069
- this.mountRouteWithSource(routePath, compiledRouter, filePath, prefixMiddlewares);
2070
- }
2071
- else if (mod.router && isRouter(mod.router)) {
2072
- this.mountRouteWithSource(routePath, mod.router, filePath, prefixMiddlewares);
2073
- }
2074
- else if (unwrappedMod && isController(unwrappedMod)) {
2075
- this.registerControllers([unwrappedMod], routePath, prefixMiddlewares);
2076
- }
2077
- else if (unwrappedMod && isRouter(unwrappedMod)) {
2078
- this.mountRouteWithSource(routePath, unwrappedMod, filePath, prefixMiddlewares);
2079
- }
2080
- else if (isRouter(mod)) {
2081
- this.mountRouteWithSource(routePath, mod, filePath, prefixMiddlewares);
2082
- }
2083
- else {
2084
- this.log.warn(`\n[WARN] Route file \x1b[36m${filePath}\x1b[0m does not export a recognized router or controller.\n`);
2085
- }
445
+ // ─── Listen ───────────────────────────────────────────────────────────────────
446
+ listen(portOrOptions, callback) {
447
+ return this.bootstrapper.listen(portOrOptions, callback);
2086
448
  }
2087
- compileFunctionalController(config) {
2088
- const router = new router_1.Router();
2089
- const fileMiddleware = [];
2090
- if (config.cors) {
2091
- fileMiddleware.push(config.cors === true ? (0, middleware_1.cors)({}) : (0, middleware_1.cors)(config.cors));
2092
- }
2093
- if (config.middleware) {
2094
- fileMiddleware.push(...(Array.isArray(config.middleware)
2095
- ? config.middleware
2096
- : [config.middleware]));
2097
- }
2098
- const { onError, onResponse } = config;
2099
- for (const [key, routeConfig] of Object.entries(config)) {
2100
- if ([
2101
- 'cors',
2102
- 'middleware',
2103
- 'onError',
2104
- 'onResponse',
2105
- '__isController',
2106
- ].includes(key))
2107
- continue;
2108
- const rc = routeConfig;
2109
- if (!rc || !rc.method || !rc.path || !rc.handle)
2110
- continue;
2111
- const routeMiddlewares = [...fileMiddleware];
2112
- if (rc.cors) {
2113
- routeMiddlewares.push(rc.cors === true ? (0, middleware_1.cors)({}) : (0, middleware_1.cors)(rc.cors));
2114
- }
2115
- if (rc.middleware) {
2116
- routeMiddlewares.push(...(Array.isArray(rc.middleware) ? rc.middleware : [rc.middleware]));
2117
- }
2118
- // ─── SUPER HANDLER WRAPPER ───
2119
- const superHandler = async (req, res, next) => {
2120
- if (onResponse) {
2121
- res.raw.on('finish', () => onResponse(req, res));
2122
- }
2123
- try {
2124
- // 0. Enforce Route Permissions (Role Authorization)
2125
- if (rc.permissions && rc.permissions.length > 0) {
2126
- if (!req.user) {
2127
- res.status(401).json({
2128
- success: false,
2129
- error: {
2130
- code: 'UNAUTHORIZED',
2131
- message: 'Unauthorized: User not found on request',
2132
- },
2133
- });
2134
- return;
2135
- }
2136
- const userPerms = req.user.permissions || req.user.roles || req.user.role || [];
2137
- const permsArray = Array.isArray(userPerms)
2138
- ? userPerms
2139
- : [userPerms];
2140
- const hasPerm = rc.permissions.every((p) => permsArray.includes(p));
2141
- if (!hasPerm) {
2142
- res.status(403).json({
2143
- success: false,
2144
- error: {
2145
- code: 'FORBIDDEN',
2146
- message: 'Forbidden: Insufficient permissions',
2147
- },
2148
- });
2149
- return;
2150
- }
2151
- }
2152
- // 1. Run Guards
2153
- const routeGuards = [...(config.guards || []), ...(rc.guards || [])];
2154
- for (const guard of routeGuards) {
2155
- const allowed = await (typeof guard === 'function'
2156
- ? guard.prototype?.canActivate
2157
- ? new guard().canActivate(req)
2158
- : guard(req)
2159
- : guard.canActivate(req));
2160
- if (!allowed) {
2161
- if (res && !res.headersSent) {
2162
- res.status(403).json({
2163
- success: false,
2164
- error: { code: 'FORBIDDEN', message: 'Forbidden by Guard' },
2165
- });
2166
- }
2167
- return;
2168
- }
2169
- }
2170
- // Build Context
2171
- const ctx = {
2172
- body: req.body,
2173
- query: req.query,
2174
- params: req.params,
2175
- headers: req.headers,
2176
- req,
2177
- res,
2178
- app: this,
2179
- socket: req.ws,
2180
- state: context_1.executionContext.getStore()?.state || {},
2181
- ...req, // Spread req to allow access to user, session, etc.
2182
- };
2183
- const result = await rc.handle(ctx);
2184
- // 2. Run Interceptors
2185
- const routeInterceptors = [
2186
- ...(config.interceptors || []),
2187
- ...(rc.interceptors || []),
2188
- ];
2189
- for (const interceptor of routeInterceptors) {
2190
- await (typeof interceptor === 'function'
2191
- ? interceptor.prototype?.intercept
2192
- ? new interceptor().intercept(req, res)
2193
- : interceptor(req, res)
2194
- : interceptor.intercept(req, res));
2195
- }
2196
- if (result !== undefined && !res.headersSent) {
2197
- if (typeof result === 'object' && result !== null) {
2198
- res.json(result);
2199
- }
2200
- else {
2201
- res.send(String(result));
2202
- }
2203
- }
2204
- }
2205
- catch (err) {
2206
- if (onError) {
2207
- try {
2208
- await onError(err, req, res);
2209
- }
2210
- catch (hookErr) {
2211
- next(hookErr);
2212
- }
2213
- }
2214
- else {
2215
- next(err); // Hands off to ExisJS global error handler (which handles HttpErrors automatically!)
2216
- }
2217
- }
2218
- };
2219
- // Mount to router
2220
- const method = rc.method.toLowerCase();
2221
- const schema = {};
2222
- const isValidatorOrPipe = (v) => v.parse ||
2223
- v.transform ||
2224
- (typeof v === 'function' && v.prototype?.transform);
2225
- if (rc.body) {
2226
- schema.body = isValidatorOrPipe(rc.body) ? rc.body : validator_1.v.object(rc.body);
2227
- }
2228
- if (rc.query) {
2229
- schema.query = isValidatorOrPipe(rc.query)
2230
- ? rc.query
2231
- : validator_1.v.object(rc.query);
2232
- }
2233
- if (rc.params) {
2234
- schema.params = isValidatorOrPipe(rc.params)
2235
- ? rc.params
2236
- : validator_1.v.object(rc.params);
2237
- }
2238
- if (rc.host) {
2239
- schema.host = rc.host;
2240
- }
2241
- const combinedFilters = [
2242
- ...(config.filters
2243
- ? Array.isArray(config.filters)
2244
- ? config.filters
2245
- : [config.filters]
2246
- : []),
2247
- ...(rc.filters
2248
- ? Array.isArray(rc.filters)
2249
- ? rc.filters
2250
- : [rc.filters]
2251
- : []),
2252
- ];
2253
- if (combinedFilters.length > 0) {
2254
- schema.filters = combinedFilters;
2255
- }
2256
- const combinedMetadata = {
2257
- ...(config.metadata || {}),
2258
- ...(rc.metadata || {}),
2259
- };
2260
- if (Object.keys(combinedMetadata).length > 0) {
2261
- schema.metadata = combinedMetadata;
2262
- }
2263
- if (method === 'sse') {
2264
- const sseHandler = async (stream, rawReq) => {
2265
- try {
2266
- const ctx = {
2267
- body: rawReq.body,
2268
- query: rawReq.query,
2269
- params: rawReq.params,
2270
- headers: rawReq.headers,
2271
- req: rawReq,
2272
- res: undefined, // Res is not available in standard SSE after handshake
2273
- app: this,
2274
- stream,
2275
- ...rawReq,
2276
- };
2277
- await rc.handle(ctx);
2278
- }
2279
- catch (err) {
2280
- if (onError) {
2281
- await onError(err, rawReq, {});
2282
- }
2283
- else {
2284
- console.error('[Exis SSE Error]', err);
2285
- }
2286
- }
2287
- };
2288
- router.sse(rc.path, ...routeMiddlewares, sseHandler);
2289
- }
2290
- else {
2291
- if (Object.keys(schema).length > 0) {
2292
- ;
2293
- router[method](rc.path, ...routeMiddlewares, schema, superHandler);
2294
- }
2295
- else {
2296
- ;
2297
- router[method](rc.path, ...routeMiddlewares, superHandler);
2298
- }
2299
- }
2300
- }
2301
- return router;
449
+ // ─── Banner ───────────────────────────────────────────────────────────────────
450
+ async _printStartupBanner() {
451
+ return this.bootstrapper.printStartupBanner();
2302
452
  }
2303
- mountRouteWithSource(prefix, subRouter, sourceFile, prefixMiddlewares = []) {
2304
- for (const route of subRouter.getRoutes()) {
2305
- const fullPath = (prefix + route.path).replace(/\/+/g, '/');
2306
- const r = { ...route, path: fullPath, sourceFile };
2307
- if (prefixMiddlewares.length > 0) {
2308
- r.handlers = [...prefixMiddlewares, ...r.handlers];
2309
- }
2310
- this.router.addRawRoute(r);
2311
- if (this.hooks.route.length > 0) {
2312
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
2313
- for (let i = 0; i < this.hooks.route.length; i++) {
2314
- this.hooks.route[i]({ method: route.method, path: fullPath });
2315
- }
2316
- }
2317
- }
453
+ // ─── Graceful Shutdown ────────────────────────────────────────────────────────
454
+ onShutdown(hook) {
455
+ this.bootstrapper.onShutdown(hook);
2318
456
  return this;
2319
457
  }
2320
- async scanDirectory(dir, baseRoute = '/') {
2321
- const fs = await Promise.resolve().then(() => __importStar(require('node:fs/promises')));
2322
- const path = await Promise.resolve().then(() => __importStar(require('node:path')));
2323
- const entries = await fs.readdir(dir, { withFileTypes: true });
2324
- const results = [];
2325
- for (const entry of entries) {
2326
- const fullPath = path.join(dir, entry.name);
2327
- if (entry.isDirectory()) {
2328
- let segment = entry.name;
2329
- if (segment.startsWith('(') && segment.endsWith(')')) {
2330
- const subResults = await this.scanDirectory(fullPath, baseRoute);
2331
- results.push(...subResults);
2332
- continue;
2333
- }
2334
- if (segment.startsWith('[...') && segment.endsWith(']')) {
2335
- segment = '*' + segment.slice(4, -1);
2336
- }
2337
- else {
2338
- segment = segment.replace(/\[(.*?)\]/g, ':$1');
2339
- }
2340
- const nextBase = baseRoute === '/' ? `/${segment}` : `${baseRoute}/${segment}`;
2341
- const subResults = await this.scanDirectory(fullPath, nextBase);
2342
- results.push(...subResults);
2343
- }
2344
- else {
2345
- results.push({ filePath: fullPath, routePath: baseRoute });
2346
- }
2347
- }
2348
- return results;
458
+ close(timeout = 5000) {
459
+ return this.bootstrapper.close(timeout);
460
+ }
461
+ // ─── Expose internals ─────────────────────────────────────────────────────────
462
+ getRouter() {
463
+ return this.router;
2349
464
  }
2350
465
  // ─── Queues ─────────────────────────────────────────────────────────────────
2351
- /**
2352
- * Registers a background job worker handler.
2353
- */
2354
466
  queue(name, handler, options) {
2355
- if (!this._queueWorker) {
2356
- if (this.options.queue?.enableWorkers === false) {
2357
- // workers disabled explicitly, gracefully skip registering
2358
- return;
2359
- }
2360
- // Worker not initialized yet (e.g. config loading is pending), buffer the job
2361
- this._pendingQueueJobs.push({
2362
- name,
2363
- handler: handler,
2364
- ...options,
2365
- });
2366
- return;
2367
- }
2368
- this._queueWorker.registerJob({
2369
- name,
2370
- handler: handler,
2371
- ...options,
2372
- });
467
+ return this.queueManager.queue(name, handler, options);
2373
468
  }
2374
- /**
2375
- * Enqueues a payload for background processing.
2376
- */
2377
469
  async enqueue(name, payload, opts) {
2378
- if (!this._queueClient) {
2379
- throw new Error(`Queue is not initialized. Please configure ExisConfig.queue first.`);
2380
- }
2381
- return this._queueClient.enqueue(name, payload, opts);
2382
- }
2383
- _initQueue(qConfig) {
2384
- if (!this._queueClient) {
2385
- this._queueClient = new client_1.ExisQueue(qConfig);
2386
- }
2387
- const enableWorkers = qConfig.enableWorkers ?? true;
2388
- if (enableWorkers && !this._queueWorker) {
2389
- this._queueWorker = new worker_1.ExisWorker(qConfig, this.log, this._queueClient.getDriver());
2390
- this._queueWorker.start();
2391
- const redisClient = this._queueWorker.redis || null;
2392
- this._cronScheduler = new scheduler_1.CronScheduler(this._queueWorker, redisClient, this.log);
2393
- this._cronScheduler.start();
2394
- }
470
+ return this.queueManager.enqueue(name, payload, opts);
2395
471
  }
2396
472
  }
2397
473
  exports.App = App;