@asenajs/ergenecore 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +636 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/Ergenecore.d.ts +598 -0
- package/dist/lib/Ergenecore.js +1406 -0
- package/dist/lib/Ergenecore.js.map +1 -0
- package/dist/lib/ErgenecoreContextWrapper.d.ts +190 -0
- package/dist/lib/ErgenecoreContextWrapper.js +387 -0
- package/dist/lib/ErgenecoreContextWrapper.js.map +1 -0
- package/dist/lib/ErgenecoreWebsocketAdapter.d.ts +68 -0
- package/dist/lib/ErgenecoreWebsocketAdapter.js +233 -0
- package/dist/lib/ErgenecoreWebsocketAdapter.js.map +1 -0
- package/dist/lib/defaults/ConfigService.d.ts +39 -0
- package/dist/lib/defaults/ConfigService.js +30 -0
- package/dist/lib/defaults/ConfigService.js.map +1 -0
- package/dist/lib/defaults/MiddlewareService.d.ts +39 -0
- package/dist/lib/defaults/MiddlewareService.js +30 -0
- package/dist/lib/defaults/MiddlewareService.js.map +1 -0
- package/dist/lib/defaults/StaticServeService.d.ts +24 -0
- package/dist/lib/defaults/StaticServeService.js +23 -0
- package/dist/lib/defaults/StaticServeService.js.map +1 -0
- package/dist/lib/defaults/ValidationService.d.ts +35 -0
- package/dist/lib/defaults/ValidationService.js +34 -0
- package/dist/lib/defaults/ValidationService.js.map +1 -0
- package/dist/lib/defaults/index.d.ts +25 -0
- package/dist/lib/defaults/index.js +26 -0
- package/dist/lib/defaults/index.js.map +1 -0
- package/dist/lib/errors.d.ts +84 -0
- package/dist/lib/errors.js +108 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/index.d.ts +36 -0
- package/dist/lib/index.js +37 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/middlewares/CorsMiddleware.d.ts +147 -0
- package/dist/lib/middlewares/CorsMiddleware.js +163 -0
- package/dist/lib/middlewares/CorsMiddleware.js.map +1 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +330 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.js +300 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -0
- package/dist/lib/types/Handler.d.ts +65 -0
- package/dist/lib/types/Handler.js +10 -0
- package/dist/lib/types/Handler.js.map +1 -0
- package/dist/lib/types/StaticServeExtras.d.ts +29 -0
- package/dist/lib/types/StaticServeExtras.js +2 -0
- package/dist/lib/types/StaticServeExtras.js.map +1 -0
- package/dist/lib/types/Validation.d.ts +7 -0
- package/dist/lib/types/Validation.js +2 -0
- package/dist/lib/types/Validation.js.map +1 -0
- package/dist/lib/types/index.d.ts +24 -0
- package/dist/lib/types/index.js +25 -0
- package/dist/lib/types/index.js.map +1 -0
- package/dist/lib/utils/factory.d.ts +121 -0
- package/dist/lib/utils/factory.js +144 -0
- package/dist/lib/utils/factory.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
|
|
2
|
+
import { blue, green, red, yellow } from '@asenajs/asena/logger';
|
|
3
|
+
import { shouldApplyMiddleware } from '@asenajs/asena/utils/patternMatcher';
|
|
4
|
+
import { ErgenecoreWebsocketAdapter } from './ErgenecoreWebsocketAdapter';
|
|
5
|
+
import { ErgenecoreContextWrapper } from './ErgenecoreContextWrapper';
|
|
6
|
+
import * as Bun from 'bun';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { HttpException, MiddlewareResponseError } from './errors';
|
|
9
|
+
/**
|
|
10
|
+
* Static response headers for performance
|
|
11
|
+
*
|
|
12
|
+
* Pre-allocated header objects to avoid creating new objects for each response.
|
|
13
|
+
* This reduces garbage collection pressure and improves performance.
|
|
14
|
+
*/
|
|
15
|
+
const STATIC_JSON_HEADERS = Object.freeze({ 'Content-Type': 'application/json' });
|
|
16
|
+
/**
|
|
17
|
+
* CoreAdapter - Native Bun adapter for Asenajs
|
|
18
|
+
*
|
|
19
|
+
* High-performance HTTP adapter using Bun's native APIs exclusively:
|
|
20
|
+
* - SIMD-accelerated routing
|
|
21
|
+
* - Native Request/Response
|
|
22
|
+
* - Zero framework overhead
|
|
23
|
+
* - Built-in parameter extraction
|
|
24
|
+
*/
|
|
25
|
+
export class Ergenecore extends AsenaAdapter {
|
|
26
|
+
/**
|
|
27
|
+
* Adapter name
|
|
28
|
+
*/
|
|
29
|
+
name = 'Ergenecore';
|
|
30
|
+
/**
|
|
31
|
+
* Server port (inherited from AsenaAdapter but needs override for initialization)
|
|
32
|
+
*/
|
|
33
|
+
port = 3000;
|
|
34
|
+
/**
|
|
35
|
+
* Server hostname
|
|
36
|
+
*/
|
|
37
|
+
_hostname;
|
|
38
|
+
/**
|
|
39
|
+
* Bun server instance
|
|
40
|
+
*/
|
|
41
|
+
server;
|
|
42
|
+
/**
|
|
43
|
+
* Route queue for deferred registration
|
|
44
|
+
* Routes are queued during initialization and built when server starts
|
|
45
|
+
*/
|
|
46
|
+
routeQueue = [];
|
|
47
|
+
/**
|
|
48
|
+
* WebSocket route queue for deferred registration
|
|
49
|
+
* WebSocket routes are queued during initialization and built when server starts
|
|
50
|
+
*/
|
|
51
|
+
wsRouteQueue = [];
|
|
52
|
+
/**
|
|
53
|
+
* Flag to track if routes have been built
|
|
54
|
+
*/
|
|
55
|
+
routesBuilt = false;
|
|
56
|
+
/**
|
|
57
|
+
* Error handler function
|
|
58
|
+
*/
|
|
59
|
+
errorHandler;
|
|
60
|
+
/**
|
|
61
|
+
* Global middlewares with route configuration
|
|
62
|
+
* Structure: Array<{ middleware, config }>
|
|
63
|
+
* Config is optional - if not provided, middleware applies to all routes
|
|
64
|
+
*/
|
|
65
|
+
globalMiddlewares = [];
|
|
66
|
+
options = {};
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new CoreAdapter instance
|
|
69
|
+
*
|
|
70
|
+
* @param logger - Server logger instance
|
|
71
|
+
* @param websocketAdapter - WebSocket adapter instance (optional)
|
|
72
|
+
*/
|
|
73
|
+
constructor(logger, websocketAdapter) {
|
|
74
|
+
// Call parent constructor with logger and websocketAdapter
|
|
75
|
+
super(logger, websocketAdapter || new ErgenecoreWebsocketAdapter(logger));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Registers a WebSocket route (deferred registration)
|
|
79
|
+
*
|
|
80
|
+
* Routes are queued and will be built into Bun's native router format
|
|
81
|
+
* when the server starts. Supports middleware chain execution before
|
|
82
|
+
* WebSocket upgrade.
|
|
83
|
+
*
|
|
84
|
+
* @param params - WebSocket route parameters (path, middlewares, websocketService)
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* adapter.registerWebsocketRoute({
|
|
89
|
+
* path: '/chat',
|
|
90
|
+
* middlewares: [authMiddleware],
|
|
91
|
+
* websocketService: chatService
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
registerWebsocketRoute(params) {
|
|
96
|
+
// Queue WebSocket route for building during start()
|
|
97
|
+
this.wsRouteQueue.push(params);
|
|
98
|
+
// Register WebSocket service with adapter using the route path (not namespace)
|
|
99
|
+
if (this.websocketAdapter && params.websocketService) {
|
|
100
|
+
this.websocketAdapter.registerWebSocket(params.websocketService);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sets the server port
|
|
105
|
+
*
|
|
106
|
+
* @param port - Port number
|
|
107
|
+
*/
|
|
108
|
+
setPort(port) {
|
|
109
|
+
this.port = port;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Registers a global middleware with optional pattern matching
|
|
113
|
+
*
|
|
114
|
+
* @param middleware - Middleware instance
|
|
115
|
+
* @param config - Optional route configuration for pattern matching
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // Old API (still supported) - applies to all routes
|
|
120
|
+
* adapter.use(loggerMiddleware);
|
|
121
|
+
*
|
|
122
|
+
* // New API with pattern matching - applies only to matching routes
|
|
123
|
+
* adapter.use(authMiddleware, {
|
|
124
|
+
* include: ['/api/*', '/admin/*'],
|
|
125
|
+
* exclude: ['/api/health']
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
use(middleware, config) {
|
|
130
|
+
this.globalMiddlewares.push({ middleware, config });
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Registers a route (deferred registration)
|
|
134
|
+
*
|
|
135
|
+
* Routes are queued and will be built into Bun's native router format
|
|
136
|
+
* when the server starts.
|
|
137
|
+
*
|
|
138
|
+
* @param params - Route parameters
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* adapter.registerRoute({
|
|
143
|
+
* method: HttpMethod.GET,
|
|
144
|
+
* path: '/users/:id',
|
|
145
|
+
* middlewares: [],
|
|
146
|
+
* handler: async (ctx) => ctx.send({ id: ctx.getParam('id') })
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
registerRoute(params) {
|
|
151
|
+
this.routeQueue.push(params);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Starts the Bun server
|
|
155
|
+
*
|
|
156
|
+
* Builds routes from queue (HTTP + WebSocket) and starts Bun.serve() with native router.
|
|
157
|
+
*
|
|
158
|
+
* Process:
|
|
159
|
+
* 1. Build HTTP routes from route queue
|
|
160
|
+
* 2. Build WebSocket routes from WebSocket route queue
|
|
161
|
+
* 3. Check for path collisions (HTTP GET vs WebSocket GET)
|
|
162
|
+
* 4. Merge HTTP and WebSocket routes
|
|
163
|
+
* 5. Prepare WebSocket adapter
|
|
164
|
+
* 6. Start Bun server with merged routes
|
|
165
|
+
*
|
|
166
|
+
* @param port - Optional port to override default
|
|
167
|
+
* @returns Bun server instance
|
|
168
|
+
*/
|
|
169
|
+
start(port) {
|
|
170
|
+
// Build routes if not built yet
|
|
171
|
+
const serverHostname = this._hostname;
|
|
172
|
+
if (!this.routesBuilt) {
|
|
173
|
+
// 1. Build HTTP routes
|
|
174
|
+
const httpRoutes = this.buildBunRoutes();
|
|
175
|
+
// 2. Build WebSocket routes
|
|
176
|
+
const wsRoutes = this.buildWebSocketRoutes();
|
|
177
|
+
// 3. Check for path collisions
|
|
178
|
+
this.checkPathCollisions(httpRoutes, wsRoutes);
|
|
179
|
+
// 4. Merge routes
|
|
180
|
+
const finalRoutes = this.mergeRoutes(httpRoutes, wsRoutes);
|
|
181
|
+
// 5. Prepare WebSocket before starting server
|
|
182
|
+
this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
|
|
183
|
+
const serverPort = port ?? this.port;
|
|
184
|
+
// 6. Start Bun server with merged routes
|
|
185
|
+
this.server = Bun.serve({
|
|
186
|
+
...this.options.serveOptions,
|
|
187
|
+
port: serverPort,
|
|
188
|
+
hostname: serverHostname,
|
|
189
|
+
routes: finalRoutes,
|
|
190
|
+
websocket: this.websocketAdapter.websocket,
|
|
191
|
+
});
|
|
192
|
+
// Start WebSocket server (initializes AsenaWebSocketServer for each namespace)
|
|
193
|
+
this.websocketAdapter.startWebsocket(this.server);
|
|
194
|
+
this.routesBuilt = true;
|
|
195
|
+
// Log controller summary first
|
|
196
|
+
if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
|
|
197
|
+
this.logControllerSummary();
|
|
198
|
+
// Then log detailed route list
|
|
199
|
+
this.logger.info(this.buildControllerBasedLog());
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.logger.info('No routes registered');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const hostDisplay = serverHostname || 'localhost';
|
|
206
|
+
this.logger.info(`Server ready → http://${hostDisplay}:${this.server.port}`);
|
|
207
|
+
return this.server;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Stops the server
|
|
211
|
+
*
|
|
212
|
+
* @param closeActiveConnections - Whether to close active connections
|
|
213
|
+
*/
|
|
214
|
+
async stop(closeActiveConnections = true) {
|
|
215
|
+
if (this.server) {
|
|
216
|
+
await this.server.stop(closeActiveConnections);
|
|
217
|
+
this.logger.info('Server stopped');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Sets the error handler
|
|
222
|
+
*
|
|
223
|
+
* @param errorHandler - Error handler function
|
|
224
|
+
*/
|
|
225
|
+
onError(errorHandler) {
|
|
226
|
+
this.errorHandler = errorHandler;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Sets serve options
|
|
230
|
+
*
|
|
231
|
+
* @param options - Serve options function
|
|
232
|
+
*/
|
|
233
|
+
async serveOptions(options) {
|
|
234
|
+
this.options = await options();
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Builds Bun native router object from queued routes
|
|
238
|
+
*
|
|
239
|
+
* Optimizations applied:
|
|
240
|
+
* 1. Base path extraction - Groups routes by static base path
|
|
241
|
+
* 2. Common middleware detection - Identifies shared middlewares per group
|
|
242
|
+
* 3. Middleware deduplication - Reduces redundant middleware execution
|
|
243
|
+
*
|
|
244
|
+
* Converts Asena route format to Bun's native router format:
|
|
245
|
+
* ```typescript
|
|
246
|
+
* {
|
|
247
|
+
* "/users": {
|
|
248
|
+
* GET: (req) => Response,
|
|
249
|
+
* POST: (req) => Response
|
|
250
|
+
* },
|
|
251
|
+
* "/users/:id": {
|
|
252
|
+
* GET: (req) => Response
|
|
253
|
+
* }
|
|
254
|
+
* }
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* @returns Bun router object
|
|
258
|
+
*/
|
|
259
|
+
buildBunRoutes() {
|
|
260
|
+
const routes = {};
|
|
261
|
+
// Group routes by base path for optimization
|
|
262
|
+
const routeGroups = this.groupRoutesByBasePath(this.routeQueue);
|
|
263
|
+
// Process each base path group
|
|
264
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
265
|
+
for (const [_, groupRoutes] of routeGroups) {
|
|
266
|
+
// Extract common middlewares for this group
|
|
267
|
+
const commonMiddlewares = this.extractCommonMiddlewares(groupRoutes);
|
|
268
|
+
// Group routes by exact path (same as before)
|
|
269
|
+
const routesByPath = new Map();
|
|
270
|
+
for (const route of groupRoutes) {
|
|
271
|
+
if (!routesByPath.has(route.path)) {
|
|
272
|
+
routesByPath.set(route.path, []);
|
|
273
|
+
}
|
|
274
|
+
routesByPath.get(route.path).push(route);
|
|
275
|
+
}
|
|
276
|
+
// Build Bun router object for each path
|
|
277
|
+
for (const [path, pathRoutes] of routesByPath) {
|
|
278
|
+
routes[path] = {};
|
|
279
|
+
for (const route of pathRoutes) {
|
|
280
|
+
const method = route.method.toUpperCase();
|
|
281
|
+
// Fast Path Optimization with Pattern Matching
|
|
282
|
+
// ✅ Check if this route has any applicable global middlewares
|
|
283
|
+
const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
|
|
284
|
+
// Check if route is simple (no middleware, validation, or static serve)
|
|
285
|
+
const isSimpleRoute = applicableGlobalMiddlewares.length === 0 &&
|
|
286
|
+
(!route.middlewares || route.middlewares.length === 0) &&
|
|
287
|
+
!route.validator &&
|
|
288
|
+
!route.staticServe;
|
|
289
|
+
if (isSimpleRoute) {
|
|
290
|
+
// Use fast path handler for simple routes (minimal overhead)
|
|
291
|
+
routes[path][method] = this.createFastPathHandler(route);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Use full-featured handler for complex routes
|
|
295
|
+
routes[path][method] = this.createRouteHandler(route, commonMiddlewares);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Add 404 handler with static headers
|
|
301
|
+
routes['/*'] = () => {
|
|
302
|
+
return new Response(JSON.stringify({ error: 'Not Found' }), {
|
|
303
|
+
status: 404,
|
|
304
|
+
headers: STATIC_JSON_HEADERS,
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
return routes;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Builds WebSocket routes from queued WebSocket route registrations
|
|
311
|
+
*
|
|
312
|
+
* Converts Asena WebSocket route format to Bun's native router format
|
|
313
|
+
* with middleware chain support. Each WebSocket route creates a GET
|
|
314
|
+
* handler that executes middlewares before attempting WebSocket upgrade.
|
|
315
|
+
*
|
|
316
|
+
* @returns Bun router object for WebSocket routes
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* // Returns:
|
|
321
|
+
* {
|
|
322
|
+
* "/chat": {
|
|
323
|
+
* GET: (req) => Response | undefined // upgrade handler with middlewares
|
|
324
|
+
* }
|
|
325
|
+
* }
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
buildWebSocketRoutes() {
|
|
329
|
+
const routes = {};
|
|
330
|
+
for (const wsRoute of this.wsRouteQueue) {
|
|
331
|
+
// Normalize path - ensure it starts with /
|
|
332
|
+
const path = wsRoute.path.startsWith('/') ? wsRoute.path : `/${wsRoute.path}`;
|
|
333
|
+
// Initialize path object
|
|
334
|
+
routes[path] = routes[path] || {};
|
|
335
|
+
// Add GET handler for WebSocket upgrade (with middleware chain)
|
|
336
|
+
routes[path]['GET'] = this.createWebSocketUpgradeHandler(wsRoute);
|
|
337
|
+
}
|
|
338
|
+
return routes;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Executes middleware chain with recursive next() pattern
|
|
342
|
+
*
|
|
343
|
+
* Middlewares can control execution flow by:
|
|
344
|
+
* - Calling await next() to proceed to next middleware
|
|
345
|
+
* - Returning false to stop pipeline (403 response)
|
|
346
|
+
* - Returning Response to send custom response and stop pipeline
|
|
347
|
+
* - Throwing HttpException to send custom HTTP error response
|
|
348
|
+
* - Throwing error to trigger error handler
|
|
349
|
+
*
|
|
350
|
+
* @param context - Request context
|
|
351
|
+
* @param middlewares - Array of middlewares to execute
|
|
352
|
+
* @param index - Current middleware index (internal)
|
|
353
|
+
* @returns Promise<boolean | Response> - true to continue, false to stop with 403, Response to send custom response
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```typescript
|
|
357
|
+
* const result = await this.executeMiddlewares(context, globalMiddlewares);
|
|
358
|
+
* if (result === false) {
|
|
359
|
+
* return new Response('Forbidden', { status: 403 });
|
|
360
|
+
* }
|
|
361
|
+
* if (result instanceof Response) {
|
|
362
|
+
* return result;
|
|
363
|
+
* }
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
async executeMiddlewares(context, middlewares, index = 0) {
|
|
367
|
+
// Base case: all middlewares executed successfully
|
|
368
|
+
if (index >= middlewares.length) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
const middleware = middlewares[index];
|
|
372
|
+
// Create next() function that executes the next middleware in chain
|
|
373
|
+
const next = async () => {
|
|
374
|
+
const result = await this.executeMiddlewares(context, middlewares, index + 1);
|
|
375
|
+
// If next middleware returned Response, throw MiddlewareResponseError to propagate it
|
|
376
|
+
if (result instanceof Response) {
|
|
377
|
+
throw new MiddlewareResponseError(result);
|
|
378
|
+
}
|
|
379
|
+
// If next middleware returned false, throw to stop current middleware
|
|
380
|
+
if (result === false) {
|
|
381
|
+
throw new Error('MIDDLEWARE_CHAIN_STOPPED');
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
try {
|
|
385
|
+
// Execute middleware with real next() function
|
|
386
|
+
const result = await middleware.handle(context, next);
|
|
387
|
+
// If middleware returns Response, stop pipeline and return response
|
|
388
|
+
if (result instanceof Response) {
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
// If middleware returns false, stop pipeline
|
|
392
|
+
if (result === false) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
// Middleware executed successfully
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
// If middleware threw HttpException, convert to Response
|
|
400
|
+
if (error instanceof HttpException) {
|
|
401
|
+
return error.getResponse();
|
|
402
|
+
}
|
|
403
|
+
// If a downstream middleware returned a Response, propagate it
|
|
404
|
+
if (error instanceof MiddlewareResponseError) {
|
|
405
|
+
return error.response;
|
|
406
|
+
}
|
|
407
|
+
// If a downstream middleware stopped the chain with false, propagate the stop
|
|
408
|
+
if (error instanceof Error && error.message === 'MIDDLEWARE_CHAIN_STOPPED') {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
// Other errors should be thrown (will be caught by route handler)
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Creates a WebSocket upgrade handler with middleware chain support
|
|
417
|
+
*
|
|
418
|
+
* The handler executes in the following order:
|
|
419
|
+
* 1. Create context wrapper
|
|
420
|
+
* 2. Execute global middlewares (pattern-filtered, if any return false, reject with 403)
|
|
421
|
+
* 3. Execute route-specific middlewares (if any return false, reject with 403)
|
|
422
|
+
* 4. Attempt WebSocket upgrade via server.upgrade()
|
|
423
|
+
* 5. Return undefined if upgrade successful, error response otherwise
|
|
424
|
+
*
|
|
425
|
+
* @param wsRoute - WebSocket route parameters
|
|
426
|
+
* @returns Bun-compatible WebSocket upgrade handler
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* const handler = createWebSocketUpgradeHandler({
|
|
431
|
+
* path: '/chat',
|
|
432
|
+
* middlewares: [authMiddleware],
|
|
433
|
+
* websocketService: chatService
|
|
434
|
+
* });
|
|
435
|
+
* // handler(req) => Response | undefined
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
createWebSocketUpgradeHandler(wsRoute) {
|
|
439
|
+
// ✅ Filter global middlewares by path pattern (ONCE during route building)
|
|
440
|
+
// This happens at server startup, NOT on every request → zero runtime overhead
|
|
441
|
+
const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(wsRoute.path);
|
|
442
|
+
return async (req) => {
|
|
443
|
+
try {
|
|
444
|
+
// Create context wrapper
|
|
445
|
+
const context = new ErgenecoreContextWrapper(req);
|
|
446
|
+
// Execute filtered global middlewares with real next() chain
|
|
447
|
+
if (applicableGlobalMiddlewares.length > 0) {
|
|
448
|
+
const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
|
|
449
|
+
// If middleware returned a custom response, return it
|
|
450
|
+
if (result instanceof Response) {
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
// If middleware returned false, return 403
|
|
454
|
+
if (result === false) {
|
|
455
|
+
return new Response('Forbidden', { status: 403 });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Execute route-specific middlewares with real next() chain
|
|
459
|
+
if (wsRoute.middlewares && wsRoute.middlewares.length > 0) {
|
|
460
|
+
const result = await this.executeMiddlewares(context, wsRoute.middlewares);
|
|
461
|
+
// If middleware returned a custom response, return it
|
|
462
|
+
if (result instanceof Response) {
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
// If middleware returned false, return 403
|
|
466
|
+
if (result === false) {
|
|
467
|
+
return new Response('Forbidden', { status: 403 });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Attempt WebSocket upgrade
|
|
471
|
+
// Use wsRoute.path (actual route path) instead of namespace (which might be the service name)
|
|
472
|
+
const upgraded = this.server.upgrade(req, {
|
|
473
|
+
data: {
|
|
474
|
+
path: wsRoute.path,
|
|
475
|
+
id: `conn-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
476
|
+
values: context.getWebSocketValue(),
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
if (upgraded) {
|
|
480
|
+
return undefined; // Upgrade successful
|
|
481
|
+
}
|
|
482
|
+
// Upgrade failed
|
|
483
|
+
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
// If HttpException was thrown, convert to Response
|
|
487
|
+
if (error instanceof HttpException) {
|
|
488
|
+
return error.getResponse();
|
|
489
|
+
}
|
|
490
|
+
// Log and handle other errors
|
|
491
|
+
this.logger.error('WebSocket upgrade handler error:', error);
|
|
492
|
+
return new Response(JSON.stringify({
|
|
493
|
+
error: error instanceof Error ? error.message : 'WebSocket upgrade failed',
|
|
494
|
+
}), {
|
|
495
|
+
status: 500,
|
|
496
|
+
headers: { 'Content-Type': 'application/json' },
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Checks for path collisions between HTTP and WebSocket routes
|
|
503
|
+
*
|
|
504
|
+
* Validates that no HTTP GET route conflicts with a WebSocket route
|
|
505
|
+
* on the same path, as both use GET method for their handlers.
|
|
506
|
+
*
|
|
507
|
+
* @param httpRoutes - HTTP routes object from buildBunRoutes()
|
|
508
|
+
* @param wsRoutes - WebSocket routes object from buildWebSocketRoutes()
|
|
509
|
+
* @throws Error if collision detected
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* ```typescript
|
|
513
|
+
* checkPathCollisions(
|
|
514
|
+
* { "/api/data": { GET: handler1 } },
|
|
515
|
+
* { "/api/data": { GET: wsHandler } }
|
|
516
|
+
* );
|
|
517
|
+
* // Throws: Route collision detected at path "/api/data": Both HTTP and WebSocket routes define GET method
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
520
|
+
checkPathCollisions(httpRoutes, wsRoutes) {
|
|
521
|
+
for (const wsPath of Object.keys(wsRoutes)) {
|
|
522
|
+
// Check if HTTP routes have the same path
|
|
523
|
+
if (httpRoutes[wsPath]) {
|
|
524
|
+
// Check if HTTP route has GET method (collision with WebSocket GET)
|
|
525
|
+
if (httpRoutes[wsPath]['GET']) {
|
|
526
|
+
throw new Error(`Route collision detected at path "${wsPath}": Both HTTP and WebSocket routes define GET method. ` +
|
|
527
|
+
`WebSocket routes use GET for upgrade handshake, so HTTP GET cannot be registered on the same path.`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Merges HTTP and WebSocket routes into a single router object
|
|
534
|
+
*
|
|
535
|
+
* Combines routes from buildBunRoutes() and buildWebSocketRoutes(),
|
|
536
|
+
* ensuring that methods on the same path are properly merged.
|
|
537
|
+
*
|
|
538
|
+
* @param httpRoutes - HTTP routes object
|
|
539
|
+
* @param wsRoutes - WebSocket routes object
|
|
540
|
+
* @returns Merged routes object
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```typescript
|
|
544
|
+
* mergeRoutes(
|
|
545
|
+
* { "/api": { POST: handler1, PUT: handler2 } },
|
|
546
|
+
* { "/api": { GET: wsHandler } }
|
|
547
|
+
* );
|
|
548
|
+
* // Returns: { "/api": { POST: handler1, PUT: handler2, GET: wsHandler } }
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
mergeRoutes(httpRoutes, wsRoutes) {
|
|
552
|
+
const merged = { ...httpRoutes };
|
|
553
|
+
for (const [path, methods] of Object.entries(wsRoutes)) {
|
|
554
|
+
if (merged[path]) {
|
|
555
|
+
// Merge methods for existing path
|
|
556
|
+
merged[path] = { ...merged[path], ...methods };
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
// Add new path
|
|
560
|
+
merged[path] = methods;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return merged;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Creates a fast path handler for simple routes
|
|
567
|
+
*
|
|
568
|
+
* This handler is optimized for routes without:
|
|
569
|
+
* - Middlewares (global or route-level)
|
|
570
|
+
* - Validators
|
|
571
|
+
* - Static file serving
|
|
572
|
+
*
|
|
573
|
+
* Benefits:
|
|
574
|
+
* - Minimal object allocation
|
|
575
|
+
* - No conditional checks
|
|
576
|
+
* - Direct handler execution
|
|
577
|
+
* - Reduced try-catch overhead
|
|
578
|
+
*
|
|
579
|
+
* @param route - Route parameters
|
|
580
|
+
* @returns Bun-compatible fast path handler
|
|
581
|
+
*/
|
|
582
|
+
createFastPathHandler(route) {
|
|
583
|
+
// If no error handler is set, use ultra-minimal version with default error handling
|
|
584
|
+
if (!this.errorHandler) {
|
|
585
|
+
return async (req) => {
|
|
586
|
+
const context = new ErgenecoreContextWrapper(req);
|
|
587
|
+
try {
|
|
588
|
+
// Inject Bun's native route params if present
|
|
589
|
+
// @ts-ignore - Bun adds params to Request
|
|
590
|
+
if (req.params) {
|
|
591
|
+
// @ts-ignore - Bun adds params to Request
|
|
592
|
+
const params = req.params;
|
|
593
|
+
// eslint-disable-next-line guard-for-in
|
|
594
|
+
for (const key in params) {
|
|
595
|
+
context.setValue(`param:${key}`, params[key]);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Execute handler directly
|
|
599
|
+
const response = await route.handler(context);
|
|
600
|
+
// Return response (check if already a Response object)
|
|
601
|
+
if (response instanceof Response) {
|
|
602
|
+
return response;
|
|
603
|
+
}
|
|
604
|
+
// Wrap in Response with static headers
|
|
605
|
+
return new Response(JSON.stringify(response), {
|
|
606
|
+
status: 200,
|
|
607
|
+
headers: STATIC_JSON_HEADERS,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
// If handler threw HttpException, convert to Response
|
|
612
|
+
if (error instanceof HttpException) {
|
|
613
|
+
return error.getResponse();
|
|
614
|
+
}
|
|
615
|
+
// Default error handling when no custom error handler is registered
|
|
616
|
+
this.logger.error('Route handler error:', error);
|
|
617
|
+
return new Response(JSON.stringify({
|
|
618
|
+
error: error instanceof Error ? error.message : 'Internal Server Error',
|
|
619
|
+
}), {
|
|
620
|
+
status: 500,
|
|
621
|
+
headers: STATIC_JSON_HEADERS,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
// With error handler (minimal try-catch)
|
|
627
|
+
return async (req) => {
|
|
628
|
+
const context = new ErgenecoreContextWrapper(req);
|
|
629
|
+
try {
|
|
630
|
+
// Inject Bun's native route params if present
|
|
631
|
+
// @ts-ignore - Bun adds params to Request
|
|
632
|
+
if (req.params) {
|
|
633
|
+
// @ts-ignore - Bun adds params to Request
|
|
634
|
+
const params = req.params;
|
|
635
|
+
// eslint-disable-next-line guard-for-in
|
|
636
|
+
for (const key in params) {
|
|
637
|
+
context.setValue(`param:${key}`, params[key]);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Execute handler directly
|
|
641
|
+
const response = await route.handler(context);
|
|
642
|
+
// Return response (check if already a Response object)
|
|
643
|
+
if (response instanceof Response) {
|
|
644
|
+
return response;
|
|
645
|
+
}
|
|
646
|
+
// Wrap in Response with static headers
|
|
647
|
+
return new Response(JSON.stringify(response), {
|
|
648
|
+
status: 200,
|
|
649
|
+
headers: STATIC_JSON_HEADERS,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
// If handler threw HttpException, convert to Response
|
|
654
|
+
if (error instanceof HttpException) {
|
|
655
|
+
return error.getResponse();
|
|
656
|
+
}
|
|
657
|
+
// Handle other errors with custom error handler
|
|
658
|
+
return this.errorHandler(error, context);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Creates a route handler function for Bun's router
|
|
664
|
+
*
|
|
665
|
+
* Wraps the Asena handler with:
|
|
666
|
+
* - CoreContextWrapper for context abstraction
|
|
667
|
+
* - Parameter injection from Bun's native parser
|
|
668
|
+
* - Global and route middlewares (pattern-filtered)
|
|
669
|
+
* - Error handling
|
|
670
|
+
*
|
|
671
|
+
* @param route - Route parameters
|
|
672
|
+
* @param _commonMiddlewares - Common middlewares for this route group (for future optimization)
|
|
673
|
+
* @returns Bun-compatible route handler
|
|
674
|
+
*/
|
|
675
|
+
createRouteHandler(route, _commonMiddlewares = []) {
|
|
676
|
+
// ✅ Filter global middlewares by path pattern (ONCE during route building)
|
|
677
|
+
// This happens at server startup, NOT on every request → zero runtime overhead
|
|
678
|
+
const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
|
|
679
|
+
return async (req) => {
|
|
680
|
+
// Create context wrapper outside try block so it's accessible in catch
|
|
681
|
+
const context = new ErgenecoreContextWrapper(req);
|
|
682
|
+
// Inject Bun's native route params
|
|
683
|
+
// @ts-ignore - Bun adds params to Request
|
|
684
|
+
if (req.params) {
|
|
685
|
+
// @ts-ignore - Bun adds params to Request
|
|
686
|
+
const params = req.params;
|
|
687
|
+
// eslint-disable-next-line guard-for-in
|
|
688
|
+
for (const key in params) {
|
|
689
|
+
context.setValue(`param:${key}`, params[key]);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
// Execute filtered global middlewares with real next() chain
|
|
694
|
+
if (applicableGlobalMiddlewares.length > 0) {
|
|
695
|
+
const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
|
|
696
|
+
// If middleware returned a custom response, return it
|
|
697
|
+
if (result instanceof Response) {
|
|
698
|
+
return result;
|
|
699
|
+
}
|
|
700
|
+
// If middleware returned false, return 403
|
|
701
|
+
if (result === false) {
|
|
702
|
+
return new Response('Forbidden', { status: 403 });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Execute route middlewares with real next() chain
|
|
706
|
+
if (route.middlewares && route.middlewares.length > 0) {
|
|
707
|
+
const result = await this.executeMiddlewares(context, route.middlewares);
|
|
708
|
+
// If middleware returned a custom response, return it
|
|
709
|
+
if (result instanceof Response) {
|
|
710
|
+
return result;
|
|
711
|
+
}
|
|
712
|
+
// If middleware returned false, return 403
|
|
713
|
+
if (result === false) {
|
|
714
|
+
return new Response('Forbidden', { status: 403 });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Execute validation
|
|
718
|
+
if (route.validator) {
|
|
719
|
+
const validationResult = await this.validateRequest(context, route.validator);
|
|
720
|
+
if (validationResult)
|
|
721
|
+
return validationResult;
|
|
722
|
+
}
|
|
723
|
+
// Handle static file serving
|
|
724
|
+
if (route.staticServe) {
|
|
725
|
+
const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
|
|
726
|
+
if (staticResponse)
|
|
727
|
+
return staticResponse;
|
|
728
|
+
}
|
|
729
|
+
// Execute route handler
|
|
730
|
+
const response = await route.handler(context);
|
|
731
|
+
// If handler returns Response, return it directly
|
|
732
|
+
if (response instanceof Response) {
|
|
733
|
+
return response;
|
|
734
|
+
}
|
|
735
|
+
// Otherwise, wrap in Response
|
|
736
|
+
return new Response(JSON.stringify(response), {
|
|
737
|
+
status: 200,
|
|
738
|
+
headers: { 'Content-Type': 'application/json' },
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
// If handler or middleware threw HttpException, convert to Response
|
|
743
|
+
if (error instanceof HttpException) {
|
|
744
|
+
return error.getResponse();
|
|
745
|
+
}
|
|
746
|
+
// Handle other errors with custom error handler if available
|
|
747
|
+
if (this.errorHandler) {
|
|
748
|
+
// Pass the original context with params already injected
|
|
749
|
+
return await this.errorHandler(error, context);
|
|
750
|
+
}
|
|
751
|
+
// Default error response
|
|
752
|
+
this.logger.error('Route handler error:', error);
|
|
753
|
+
return new Response(JSON.stringify({
|
|
754
|
+
error: error instanceof Error ? error.message : 'Internal Server Error',
|
|
755
|
+
}), {
|
|
756
|
+
status: 500,
|
|
757
|
+
headers: { 'Content-Type': 'application/json' },
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Type guard to check if validation schema has hook format
|
|
764
|
+
*
|
|
765
|
+
* Discriminates between ValidationSchemaWithHook and plain ValidationSchema.
|
|
766
|
+
* Uses runtime check for 'schema' property to determine type.
|
|
767
|
+
*
|
|
768
|
+
* @param schema - Validation schema to check
|
|
769
|
+
* @returns true if schema is ValidationSchemaWithHook format
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```typescript
|
|
773
|
+
* if (isValidationSchemaWithHook(schema)) {
|
|
774
|
+
* // schema.schema and schema.hook are available
|
|
775
|
+
* } else {
|
|
776
|
+
* // schema is a plain Zod schema
|
|
777
|
+
* }
|
|
778
|
+
* ```
|
|
779
|
+
*/
|
|
780
|
+
isValidationSchemaWithHook(schema) {
|
|
781
|
+
return typeof schema === 'object' && schema !== null && 'schema' in schema;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Validates request data using Zod schemas
|
|
785
|
+
*
|
|
786
|
+
* Checks each validation target (body, query, param, header) and runs
|
|
787
|
+
* the Zod schema validation. If validation fails and a hook is provided,
|
|
788
|
+
* the hook is called to generate a custom error response.
|
|
789
|
+
*
|
|
790
|
+
* Supports two validation formats:
|
|
791
|
+
* 1. Plain Zod schema: z.object({...})
|
|
792
|
+
* 2. Schema with hook: { schema: z.object({...}), hook?: (...) => {...} }
|
|
793
|
+
*
|
|
794
|
+
* @param context - Request context
|
|
795
|
+
* @param validator - Validator instance with schema definitions
|
|
796
|
+
* @returns Response if validation fails, null if passes
|
|
797
|
+
*/
|
|
798
|
+
async validateRequest(context, validator) {
|
|
799
|
+
// Iterate through all validator methods (body, query, param, header)
|
|
800
|
+
for (const key of VALIDATOR_METHODS) {
|
|
801
|
+
const validatorHandler = validator[key];
|
|
802
|
+
// Skip if validator not defined for this target
|
|
803
|
+
if (!validatorHandler || typeof validatorHandler.handle !== 'function') {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
// Get validation schema
|
|
807
|
+
const validationSchema = await validatorHandler.handle();
|
|
808
|
+
// Skip if no validation schema provided
|
|
809
|
+
if (!validationSchema) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
// Use type guard to discriminate between formats
|
|
813
|
+
const schema = this.isValidationSchemaWithHook(validationSchema)
|
|
814
|
+
? validationSchema.schema
|
|
815
|
+
: validationSchema;
|
|
816
|
+
const hook = this.isValidationSchemaWithHook(validationSchema) ? (validationSchema.hook ?? null) : null;
|
|
817
|
+
// Extract data to validate
|
|
818
|
+
const data = await this.extractValidationData(context, key);
|
|
819
|
+
// Run Zod validation
|
|
820
|
+
const result = schema.safeParse(data);
|
|
821
|
+
// If validation fails
|
|
822
|
+
if (!result.success) {
|
|
823
|
+
// Use custom hook if provided
|
|
824
|
+
if (hook) {
|
|
825
|
+
const hookResponse = await hook(result, context);
|
|
826
|
+
if (hookResponse)
|
|
827
|
+
return hookResponse;
|
|
828
|
+
}
|
|
829
|
+
// Default error response
|
|
830
|
+
return new Response(JSON.stringify({
|
|
831
|
+
error: 'Validation failed',
|
|
832
|
+
details: result.error.flatten(),
|
|
833
|
+
}), {
|
|
834
|
+
status: 400,
|
|
835
|
+
headers: { 'Content-Type': 'application/json' },
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// All validations passed
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Extracts data from request based on validation target
|
|
844
|
+
*
|
|
845
|
+
* @param context - Request context
|
|
846
|
+
* @param target - Validation target (body, query, param, header)
|
|
847
|
+
* @returns Data to be validated
|
|
848
|
+
*/
|
|
849
|
+
async extractValidationData(context, target) {
|
|
850
|
+
switch (target) {
|
|
851
|
+
case 'json':
|
|
852
|
+
return await context.getBody();
|
|
853
|
+
case 'body': {
|
|
854
|
+
// 'json' is the correct Asena validator method name
|
|
855
|
+
// 'body' is kept for backwards compatibility
|
|
856
|
+
return await context.getBody();
|
|
857
|
+
}
|
|
858
|
+
case 'query': {
|
|
859
|
+
const url = new URL(context.req.url);
|
|
860
|
+
const queryObj = {};
|
|
861
|
+
url.searchParams.forEach((value, key) => {
|
|
862
|
+
queryObj[key] = value;
|
|
863
|
+
});
|
|
864
|
+
return queryObj;
|
|
865
|
+
}
|
|
866
|
+
case 'param': {
|
|
867
|
+
// Extract all params from context
|
|
868
|
+
const params = {};
|
|
869
|
+
// We stored params with 'param:' prefix in context
|
|
870
|
+
for (const [key, value] of context.values.entries()) {
|
|
871
|
+
if (key.startsWith('param:')) {
|
|
872
|
+
params[key.replace('param:', '')] = value;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return params;
|
|
876
|
+
}
|
|
877
|
+
case 'header': {
|
|
878
|
+
const headers = {};
|
|
879
|
+
context.req.headers.forEach((value, key) => {
|
|
880
|
+
headers[key.toLowerCase()] = value;
|
|
881
|
+
});
|
|
882
|
+
return headers;
|
|
883
|
+
}
|
|
884
|
+
default:
|
|
885
|
+
return {};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Serves static files using Bun.file() API
|
|
890
|
+
*
|
|
891
|
+
* This method handles static file serving with the following features:
|
|
892
|
+
* - Path rewriting via rewriteRequestPath
|
|
893
|
+
* - Root directory resolution
|
|
894
|
+
* - File existence checking
|
|
895
|
+
* - onFound/onNotFound hooks
|
|
896
|
+
* - Automatic MIME type detection (via Bun.file())
|
|
897
|
+
*
|
|
898
|
+
* @param req - Native Bun Request object
|
|
899
|
+
* @param context - ErgenecoreContextWrapper instance
|
|
900
|
+
* @param staticServe - Static serve configuration from route
|
|
901
|
+
* @returns Response object if file found/not found, null if hooks override
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* ```typescript
|
|
905
|
+
* const response = await this.serveStaticFile(req, context, {
|
|
906
|
+
* root: '/public',
|
|
907
|
+
* rewriteRequestPath: (path) => path.replace('/static', ''),
|
|
908
|
+
* onFound: { handler: async () => {}, override: false },
|
|
909
|
+
* onNotFound: { handler: async () => {}, override: false }
|
|
910
|
+
* });
|
|
911
|
+
* ```
|
|
912
|
+
*/
|
|
913
|
+
async serveStaticFile(req, context, staticServe) {
|
|
914
|
+
try {
|
|
915
|
+
// 1. Extract request path from URL
|
|
916
|
+
const url = new URL(req.url);
|
|
917
|
+
const requestPath = decodeURIComponent(url.pathname); // Decode URL encoding
|
|
918
|
+
// 2. Apply path rewriting if provided
|
|
919
|
+
const rewrittenPath = staticServe.rewriteRequestPath ? staticServe.rewriteRequestPath(requestPath) : requestPath;
|
|
920
|
+
// 3. Build absolute file path
|
|
921
|
+
const filePath = path.join(staticServe.root, rewrittenPath);
|
|
922
|
+
// 4. Security: Resolve and validate path to prevent traversal attacks
|
|
923
|
+
// Resolve both paths to absolute canonical paths
|
|
924
|
+
const resolvedFilePath = path.resolve(filePath);
|
|
925
|
+
const resolvedRoot = path.resolve(staticServe.root);
|
|
926
|
+
// Check if resolved file path is within root directory
|
|
927
|
+
if (!resolvedFilePath.startsWith(resolvedRoot)) {
|
|
928
|
+
this.logger.warn(`Path traversal attempt detected: ${requestPath} -> ${resolvedFilePath}`);
|
|
929
|
+
return new Response('Forbidden', { status: 403 });
|
|
930
|
+
}
|
|
931
|
+
// 5. Create Bun.file() instance
|
|
932
|
+
const file = Bun.file(resolvedFilePath);
|
|
933
|
+
// 5. Check if file exists
|
|
934
|
+
const fileExists = await file.exists();
|
|
935
|
+
// 6. File not found → trigger onNotFound hook
|
|
936
|
+
if (!fileExists) {
|
|
937
|
+
if (staticServe.onNotFound) {
|
|
938
|
+
await staticServe.onNotFound.handler(rewrittenPath, context);
|
|
939
|
+
// If hook overrides, return null (let handler continue)
|
|
940
|
+
if (staticServe.onNotFound.override) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// Default 404 response
|
|
945
|
+
return new Response('Not Found', { status: 404 });
|
|
946
|
+
}
|
|
947
|
+
// 7. File found → trigger onFound hook
|
|
948
|
+
if (staticServe.onFound) {
|
|
949
|
+
await staticServe.onFound.handler(rewrittenPath, context);
|
|
950
|
+
// If hook overrides, return null (let handler continue)
|
|
951
|
+
if (staticServe.onFound.override) {
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// 8. Create Bun file response with automatic Content-Type detection
|
|
956
|
+
const fileResponse = new Response(file);
|
|
957
|
+
// 9. Build response headers starting with Bun's Content-Type
|
|
958
|
+
const finalHeaders = new Headers(fileResponse.headers);
|
|
959
|
+
// 10. Add cache validation headers (ETag and Last-Modified)
|
|
960
|
+
// ETag format: W/"<size>-<lastModified>" (weak validator)
|
|
961
|
+
const etag = `W/"${file.size}-${file.lastModified}"`;
|
|
962
|
+
finalHeaders.set('ETag', etag);
|
|
963
|
+
// Last-Modified: HTTP date format (RFC 7231)
|
|
964
|
+
const lastModified = new Date(file.lastModified).toUTCString();
|
|
965
|
+
finalHeaders.set('Last-Modified', lastModified);
|
|
966
|
+
// 11. Add custom headers (can override defaults)
|
|
967
|
+
const customHeaders = this.buildStaticFileHeaders(resolvedFilePath, staticServe.extra);
|
|
968
|
+
for (const [key, value] of Object.entries(customHeaders)) {
|
|
969
|
+
finalHeaders.set(key, value);
|
|
970
|
+
}
|
|
971
|
+
// 12. Add default Cache-Control if not provided
|
|
972
|
+
// Using 'public, max-age=0' allows caching but requires revalidation
|
|
973
|
+
if (!finalHeaders.has('Cache-Control')) {
|
|
974
|
+
finalHeaders.set('Cache-Control', 'public, max-age=0');
|
|
975
|
+
}
|
|
976
|
+
// 13. Return response with all headers
|
|
977
|
+
// This preserves Bun's zero-copy file serving while adding cache headers
|
|
978
|
+
return new Response(fileResponse.body, {
|
|
979
|
+
status: fileResponse.status,
|
|
980
|
+
headers: finalHeaders,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
catch (error) {
|
|
984
|
+
this.logger.error('Static file serving error:', error);
|
|
985
|
+
return new Response(JSON.stringify({
|
|
986
|
+
error: error instanceof Error ? error.message : 'Static file serving failed',
|
|
987
|
+
}), {
|
|
988
|
+
status: 500,
|
|
989
|
+
headers: { 'Content-Type': 'application/json' },
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Builds response headers for static file serving
|
|
995
|
+
*
|
|
996
|
+
* Constructs headers with:
|
|
997
|
+
* - Content-Type (custom MIME or Bun default)
|
|
998
|
+
* - Cache-Control (if specified)
|
|
999
|
+
* - Custom headers (if specified)
|
|
1000
|
+
*
|
|
1001
|
+
* @param filePath - Resolved absolute file path
|
|
1002
|
+
* @param extras - StaticServeExtras configuration
|
|
1003
|
+
* @returns Headers object for Response
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* ```typescript
|
|
1007
|
+
* const headers = this.buildStaticFileHeaders('/app/public/style.css', {
|
|
1008
|
+
* cacheControl: 'public, max-age=86400',
|
|
1009
|
+
* headers: { 'X-Custom': 'value' },
|
|
1010
|
+
* mimes: { '.css': 'text/css; charset=utf-8' }
|
|
1011
|
+
* });
|
|
1012
|
+
* // Returns: { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': '...', 'X-Custom': 'value' }
|
|
1013
|
+
* ```
|
|
1014
|
+
*/
|
|
1015
|
+
buildStaticFileHeaders(filePath, extras) {
|
|
1016
|
+
const headers = {};
|
|
1017
|
+
if (!extras) {
|
|
1018
|
+
return headers;
|
|
1019
|
+
}
|
|
1020
|
+
// 1. Content-Type: Check custom MIME types first
|
|
1021
|
+
if (extras.mimes) {
|
|
1022
|
+
const ext = path.extname(filePath);
|
|
1023
|
+
const customMime = extras.mimes[ext];
|
|
1024
|
+
if (customMime) {
|
|
1025
|
+
headers['Content-Type'] = customMime;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// Note: If no custom MIME, Bun.file() will set Content-Type automatically
|
|
1029
|
+
// 2. Cache-Control header
|
|
1030
|
+
if (extras.cacheControl) {
|
|
1031
|
+
headers['Cache-Control'] = extras.cacheControl;
|
|
1032
|
+
}
|
|
1033
|
+
// 3. Custom headers
|
|
1034
|
+
if (extras.headers) {
|
|
1035
|
+
for (const [key, value] of Object.entries(extras.headers)) {
|
|
1036
|
+
headers[key] = value;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return headers;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Extracts base path from a route path
|
|
1043
|
+
*
|
|
1044
|
+
* Removes dynamic segments (parameters and wildcards) from the path
|
|
1045
|
+
* to find the static base path for grouping routes.
|
|
1046
|
+
*
|
|
1047
|
+
* @param path - Route path (e.g., "/api/users/:id")
|
|
1048
|
+
* @returns Base path without dynamic segments (e.g., "/api/users")
|
|
1049
|
+
*
|
|
1050
|
+
* @example
|
|
1051
|
+
* ```typescript
|
|
1052
|
+
* extractBasePath('/api/users/:id') // => '/api/users'
|
|
1053
|
+
* extractBasePath('/api/users/:id/posts/:postId') // => '/api/users'
|
|
1054
|
+
* extractBasePath('/static/*') // => '/static'
|
|
1055
|
+
* extractBasePath('/') // => '/'
|
|
1056
|
+
* ```
|
|
1057
|
+
*/
|
|
1058
|
+
extractBasePath(path) {
|
|
1059
|
+
// Remove trailing slash (except for root)
|
|
1060
|
+
let normalized = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
|
|
1061
|
+
// Split path into segments
|
|
1062
|
+
const segments = normalized.split('/');
|
|
1063
|
+
const baseSegments = [];
|
|
1064
|
+
// Keep segments until we hit a parameter or wildcard
|
|
1065
|
+
for (const segment of segments) {
|
|
1066
|
+
// Stop if segment is a parameter (:param) or wildcard (*)
|
|
1067
|
+
if (segment.startsWith(':') || segment.includes('*')) {
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
baseSegments.push(segment);
|
|
1071
|
+
}
|
|
1072
|
+
// Join back into path, handle root case
|
|
1073
|
+
return baseSegments.join('/') || '/';
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Extracts common middlewares across multiple routes
|
|
1077
|
+
*
|
|
1078
|
+
* Identifies middlewares that are present in ALL routes and can be
|
|
1079
|
+
* moved to group level for optimization.
|
|
1080
|
+
*
|
|
1081
|
+
* @param routes - Array of routes to analyze
|
|
1082
|
+
* @returns Array of common middleware instances
|
|
1083
|
+
*
|
|
1084
|
+
* @example
|
|
1085
|
+
* ```typescript
|
|
1086
|
+
* extractCommonMiddlewares([
|
|
1087
|
+
* { middlewares: [auth, log, rate] },
|
|
1088
|
+
* { middlewares: [auth, log] },
|
|
1089
|
+
* { middlewares: [auth, log] }
|
|
1090
|
+
* ]) // => [auth, log]
|
|
1091
|
+
* ```
|
|
1092
|
+
*/
|
|
1093
|
+
extractCommonMiddlewares(routes) {
|
|
1094
|
+
// Need at least 2 routes to have common middlewares
|
|
1095
|
+
if (routes.length === 0 || routes.length === 1) {
|
|
1096
|
+
return [];
|
|
1097
|
+
}
|
|
1098
|
+
// Get middlewares from first route as baseline
|
|
1099
|
+
const firstRouteMiddlewares = routes[0].middlewares || [];
|
|
1100
|
+
// Filter to only middlewares present in ALL routes
|
|
1101
|
+
return firstRouteMiddlewares.filter((middleware) => {
|
|
1102
|
+
return routes.every((route) => {
|
|
1103
|
+
return (route.middlewares || []).some((mw) => {
|
|
1104
|
+
// Compare by constructor name (class identity)
|
|
1105
|
+
return mw.constructor.name === middleware.constructor.name;
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Groups routes by their base path
|
|
1112
|
+
*
|
|
1113
|
+
* Creates a map of base paths to routes for optimization.
|
|
1114
|
+
* Routes with the same base path will share common middlewares.
|
|
1115
|
+
*
|
|
1116
|
+
* @param routes - Array of routes to group
|
|
1117
|
+
* @returns Map of base paths to route arrays
|
|
1118
|
+
*
|
|
1119
|
+
* @example
|
|
1120
|
+
* ```typescript
|
|
1121
|
+
* groupRoutesByBasePath([
|
|
1122
|
+
* { path: '/api/users' },
|
|
1123
|
+
* { path: '/api/users/:id' },
|
|
1124
|
+
* { path: '/api/posts' }
|
|
1125
|
+
* ])
|
|
1126
|
+
* // => Map {
|
|
1127
|
+
* // '/api/users' => [route1, route2],
|
|
1128
|
+
* // '/api/posts' => [route3]
|
|
1129
|
+
* // }
|
|
1130
|
+
* ```
|
|
1131
|
+
*/
|
|
1132
|
+
groupRoutesByBasePath(routes) {
|
|
1133
|
+
const groups = new Map();
|
|
1134
|
+
for (const route of routes) {
|
|
1135
|
+
const basePath = this.extractBasePath(route.path);
|
|
1136
|
+
if (!groups.has(basePath)) {
|
|
1137
|
+
groups.set(basePath, []);
|
|
1138
|
+
}
|
|
1139
|
+
groups.get(basePath).push(route);
|
|
1140
|
+
}
|
|
1141
|
+
return groups;
|
|
1142
|
+
}
|
|
1143
|
+
get hostname() {
|
|
1144
|
+
return this._hostname;
|
|
1145
|
+
}
|
|
1146
|
+
set hostname(value) {
|
|
1147
|
+
this._hostname = value;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Groups HTTP routes by controller name
|
|
1151
|
+
*
|
|
1152
|
+
* Creates a map of controller names to their routes for organized logging.
|
|
1153
|
+
* Each group contains the controller's base path and all its routes.
|
|
1154
|
+
*
|
|
1155
|
+
* @returns Map of controller names to route groups
|
|
1156
|
+
*
|
|
1157
|
+
* @example
|
|
1158
|
+
* ```typescript
|
|
1159
|
+
* groupRoutesByController()
|
|
1160
|
+
* // => Map {
|
|
1161
|
+
* // 'UserController' => {
|
|
1162
|
+
* // basePath: '/users',
|
|
1163
|
+
* // routes: [{ method: 'GET', path: '/users' }, { method: 'POST', path: '/users' }]
|
|
1164
|
+
* // }
|
|
1165
|
+
* // }
|
|
1166
|
+
* ```
|
|
1167
|
+
*/
|
|
1168
|
+
groupRoutesByController() {
|
|
1169
|
+
const groups = new Map();
|
|
1170
|
+
for (const route of this.routeQueue) {
|
|
1171
|
+
const controllerName = route.controllerName || 'Unknown';
|
|
1172
|
+
const controllerBasePath = route.controllerBasePath || '/';
|
|
1173
|
+
if (!groups.has(controllerName)) {
|
|
1174
|
+
groups.set(controllerName, {
|
|
1175
|
+
basePath: controllerBasePath,
|
|
1176
|
+
routes: [],
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
groups.get(controllerName).routes.push({
|
|
1180
|
+
method: route.method.toUpperCase(),
|
|
1181
|
+
path: route.path,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
return groups;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Groups WebSocket routes by controller name
|
|
1188
|
+
*
|
|
1189
|
+
* Creates a map of controller names to their WebSocket routes for organized logging.
|
|
1190
|
+
*
|
|
1191
|
+
* @returns Map of controller names to WebSocket route groups
|
|
1192
|
+
*
|
|
1193
|
+
* @example
|
|
1194
|
+
* ```typescript
|
|
1195
|
+
* groupWebSocketRoutesByController()
|
|
1196
|
+
* // => Map {
|
|
1197
|
+
* // 'ChatController' => {
|
|
1198
|
+
* // basePath: '/chat',
|
|
1199
|
+
* // routes: [{ path: '/chat' }]
|
|
1200
|
+
* // }
|
|
1201
|
+
* // }
|
|
1202
|
+
* ```
|
|
1203
|
+
*/
|
|
1204
|
+
groupWebSocketRoutesByController() {
|
|
1205
|
+
const groups = new Map();
|
|
1206
|
+
for (const wsRoute of this.wsRouteQueue) {
|
|
1207
|
+
const controllerName = wsRoute.controllerName || 'Unknown';
|
|
1208
|
+
const controllerBasePath = wsRoute.path; // WebSocket uses path as base path
|
|
1209
|
+
if (!groups.has(controllerName)) {
|
|
1210
|
+
groups.set(controllerName, {
|
|
1211
|
+
basePath: controllerBasePath,
|
|
1212
|
+
routes: [],
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
groups.get(controllerName).routes.push({
|
|
1216
|
+
path: wsRoute.path,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
return groups;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Filters global middlewares for a specific route path
|
|
1223
|
+
*
|
|
1224
|
+
* Uses pattern matching to determine which middlewares should apply to this path.
|
|
1225
|
+
* Pattern matching happens ONCE during route building (deferred registration),
|
|
1226
|
+
* not on every request, ensuring zero runtime overhead.
|
|
1227
|
+
*
|
|
1228
|
+
* @param path - Route path (e.g., '/api/users', '/users/:id')
|
|
1229
|
+
* @returns Array of middlewares that should apply to this path
|
|
1230
|
+
*
|
|
1231
|
+
* @example
|
|
1232
|
+
* ```typescript
|
|
1233
|
+
* // Given:
|
|
1234
|
+
* // - LoggerMiddleware (no config → applies to all routes)
|
|
1235
|
+
* // - AuthMiddleware (include: ['/api/*'])
|
|
1236
|
+
* // - RateLimitMiddleware (exclude: ['/health'])
|
|
1237
|
+
*
|
|
1238
|
+
* getGlobalMiddlewaresForPath('/api/users')
|
|
1239
|
+
* // => [LoggerMiddleware, AuthMiddleware, RateLimitMiddleware]
|
|
1240
|
+
*
|
|
1241
|
+
* getGlobalMiddlewaresForPath('/health')
|
|
1242
|
+
* // => [LoggerMiddleware, AuthMiddleware] (RateLimit excluded)
|
|
1243
|
+
*
|
|
1244
|
+
* getGlobalMiddlewaresForPath('/public/page')
|
|
1245
|
+
* // => [LoggerMiddleware, RateLimitMiddleware] (Auth not included)
|
|
1246
|
+
* ```
|
|
1247
|
+
*/
|
|
1248
|
+
getGlobalMiddlewaresForPath(path) {
|
|
1249
|
+
return this.globalMiddlewares
|
|
1250
|
+
.filter(({ config }) => shouldApplyMiddleware(path, config))
|
|
1251
|
+
.map(({ middleware }) => middleware);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Logs a summary of registered controllers
|
|
1255
|
+
*
|
|
1256
|
+
* Displays colored success messages for each controller with route counts.
|
|
1257
|
+
* HTTP controllers show total HTTP routes, WebSocket-only controllers are
|
|
1258
|
+
* displayed separately.
|
|
1259
|
+
*
|
|
1260
|
+
* @example
|
|
1261
|
+
* Output format:
|
|
1262
|
+
* ```
|
|
1263
|
+
* ✓ Successfully registered CONTROLLER UserController (2 routes)
|
|
1264
|
+
* ✓ Successfully registered WEBSOCKET ChatController (1 route)
|
|
1265
|
+
* ```
|
|
1266
|
+
*/
|
|
1267
|
+
logControllerSummary() {
|
|
1268
|
+
const httpGroups = this.groupRoutesByController();
|
|
1269
|
+
const wsGroups = this.groupWebSocketRoutesByController();
|
|
1270
|
+
// Log HTTP controllers
|
|
1271
|
+
for (const [controllerName, group] of httpGroups) {
|
|
1272
|
+
const routeCount = group.routes.length;
|
|
1273
|
+
const routeText = routeCount === 1 ? 'route' : 'routes';
|
|
1274
|
+
this.logger.info(`${green('✓')} Successfully registered ${yellow('CONTROLLER')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
|
|
1275
|
+
}
|
|
1276
|
+
// Log WebSocket controllers (only those that don't have HTTP routes)
|
|
1277
|
+
for (const [controllerName, group] of wsGroups) {
|
|
1278
|
+
if (!httpGroups.has(controllerName)) {
|
|
1279
|
+
const routeCount = group.routes.length;
|
|
1280
|
+
const routeText = routeCount === 1 ? 'route' : 'routes';
|
|
1281
|
+
this.logger.info(`${green('✓')} Successfully registered ${yellow('WEBSOCKET')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Builds controller-based log output with colors
|
|
1287
|
+
*
|
|
1288
|
+
* Creates a formatted string showing all routes grouped by controller,
|
|
1289
|
+
* with HTTP controllers listed first, then WebSocket namespaces separately.
|
|
1290
|
+
*
|
|
1291
|
+
* Color scheme:
|
|
1292
|
+
* - Controller name: blue
|
|
1293
|
+
* - Base path: yellow
|
|
1294
|
+
* - GET method: green
|
|
1295
|
+
* - POST method: blue
|
|
1296
|
+
* - PUT method: yellow
|
|
1297
|
+
* - DELETE method: red
|
|
1298
|
+
* - WS method: blue
|
|
1299
|
+
*
|
|
1300
|
+
* @returns Formatted log string with color codes
|
|
1301
|
+
*
|
|
1302
|
+
* @example
|
|
1303
|
+
* Output format:
|
|
1304
|
+
* ```
|
|
1305
|
+
* UserController (/users):
|
|
1306
|
+
* GET /users
|
|
1307
|
+
* GET /users/:id
|
|
1308
|
+
* POST /users
|
|
1309
|
+
*
|
|
1310
|
+
* ChatNamespace (chat):
|
|
1311
|
+
* WS /chat
|
|
1312
|
+
* ```
|
|
1313
|
+
*/
|
|
1314
|
+
buildControllerBasedLog() {
|
|
1315
|
+
const httpGroups = this.groupRoutesByController();
|
|
1316
|
+
const wsGroups = this.groupWebSocketRoutesByController();
|
|
1317
|
+
// Build log output with colors
|
|
1318
|
+
const lines = ['']; // Start with empty line for better spacing
|
|
1319
|
+
// 1. First, log HTTP-only controllers (sorted alphabetically)
|
|
1320
|
+
const httpOnlyControllers = Array.from(httpGroups.entries())
|
|
1321
|
+
.filter(([controllerName]) => !wsGroups.has(controllerName))
|
|
1322
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
1323
|
+
for (const [controllerName, group] of httpOnlyControllers) {
|
|
1324
|
+
lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
|
|
1325
|
+
// Sort routes: GET first, then POST, PUT, PATCH, DELETE
|
|
1326
|
+
const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
1327
|
+
const sortedRoutes = group.routes.sort((a, b) => {
|
|
1328
|
+
const orderA = methodOrder.indexOf(a.method);
|
|
1329
|
+
const orderB = methodOrder.indexOf(b.method);
|
|
1330
|
+
return orderA - orderB;
|
|
1331
|
+
});
|
|
1332
|
+
for (const route of sortedRoutes) {
|
|
1333
|
+
// Colorize method based on type
|
|
1334
|
+
let coloredMethod = route.method;
|
|
1335
|
+
if (route.method === 'GET') {
|
|
1336
|
+
coloredMethod = green(route.method);
|
|
1337
|
+
}
|
|
1338
|
+
else if (route.method === 'POST') {
|
|
1339
|
+
coloredMethod = blue(route.method);
|
|
1340
|
+
}
|
|
1341
|
+
else if (route.method === 'PUT') {
|
|
1342
|
+
coloredMethod = yellow(route.method);
|
|
1343
|
+
}
|
|
1344
|
+
else if (route.method === 'DELETE') {
|
|
1345
|
+
coloredMethod = red(route.method);
|
|
1346
|
+
}
|
|
1347
|
+
lines.push(` ${coloredMethod} ${route.path}`);
|
|
1348
|
+
}
|
|
1349
|
+
lines.push(''); // Empty line between controllers
|
|
1350
|
+
}
|
|
1351
|
+
// 2. Then, log mixed controllers (HTTP + WebSocket)
|
|
1352
|
+
const mixedControllers = Array.from(httpGroups.entries())
|
|
1353
|
+
.filter(([controllerName]) => wsGroups.has(controllerName))
|
|
1354
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
1355
|
+
for (const [controllerName, group] of mixedControllers) {
|
|
1356
|
+
// Merge WebSocket routes into this controller
|
|
1357
|
+
const wsGroup = wsGroups.get(controllerName);
|
|
1358
|
+
const allRoutes = [
|
|
1359
|
+
...group.routes,
|
|
1360
|
+
...wsGroup.routes.map((r) => ({ method: 'WS', path: r.path })),
|
|
1361
|
+
];
|
|
1362
|
+
lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
|
|
1363
|
+
// Sort routes: GET first, then POST, PUT, PATCH, DELETE, WS
|
|
1364
|
+
const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'WS'];
|
|
1365
|
+
const sortedRoutes = allRoutes.sort((a, b) => {
|
|
1366
|
+
const orderA = methodOrder.indexOf(a.method);
|
|
1367
|
+
const orderB = methodOrder.indexOf(b.method);
|
|
1368
|
+
return orderA - orderB;
|
|
1369
|
+
});
|
|
1370
|
+
for (const route of sortedRoutes) {
|
|
1371
|
+
// Colorize method based on type
|
|
1372
|
+
let coloredMethod = route.method;
|
|
1373
|
+
if (route.method === 'GET') {
|
|
1374
|
+
coloredMethod = green(route.method);
|
|
1375
|
+
}
|
|
1376
|
+
else if (route.method === 'POST') {
|
|
1377
|
+
coloredMethod = blue(route.method);
|
|
1378
|
+
}
|
|
1379
|
+
else if (route.method === 'PUT') {
|
|
1380
|
+
coloredMethod = yellow(route.method);
|
|
1381
|
+
}
|
|
1382
|
+
else if (route.method === 'DELETE') {
|
|
1383
|
+
coloredMethod = red(route.method);
|
|
1384
|
+
}
|
|
1385
|
+
else if (route.method === 'WS') {
|
|
1386
|
+
coloredMethod = blue(route.method);
|
|
1387
|
+
}
|
|
1388
|
+
lines.push(` ${coloredMethod} ${route.path}`);
|
|
1389
|
+
}
|
|
1390
|
+
lines.push(''); // Empty line between controllers
|
|
1391
|
+
}
|
|
1392
|
+
// 3. Finally, log WebSocket-only namespaces
|
|
1393
|
+
const wsOnlyNamespaces = Array.from(wsGroups.entries())
|
|
1394
|
+
.filter(([controllerName]) => !httpGroups.has(controllerName))
|
|
1395
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
1396
|
+
for (const [namespaceName, group] of wsOnlyNamespaces) {
|
|
1397
|
+
lines.push(` ${blue(namespaceName)} ${yellow(`(${group.basePath})`)}`);
|
|
1398
|
+
for (const route of group.routes) {
|
|
1399
|
+
lines.push(` ${blue('WS')} ${route.path}`);
|
|
1400
|
+
}
|
|
1401
|
+
lines.push(''); // Empty line between namespaces
|
|
1402
|
+
}
|
|
1403
|
+
return lines.join('\n');
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
//# sourceMappingURL=Ergenecore.js.map
|