@asenajs/hono-adapter 1.1.0 → 1.2.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.
@@ -1,8 +1,10 @@
1
1
  import { Hono } from 'hono';
2
+ import { HTTPException } from 'hono/http-exception';
2
3
  import * as bun from 'bun';
3
4
  import { HonoContextWrapper } from './HonoContextWrapper';
4
5
  import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
5
- import { green, yellow } from '@asenajs/asena/logger';
6
+ import { shouldApplyMiddleware } from '@asenajs/asena/utils/patternMatcher';
7
+ import { blue, green, red, yellow } from '@asenajs/asena/logger';
6
8
  import { zValidator } from '@hono/zod-validator';
7
9
  import { middlewareParser } from './utils/middlewareParser';
8
10
  import { HttpMethod } from '@asenajs/asena/web-types';
@@ -14,6 +16,19 @@ export class HonoAdapter extends AsenaAdapter {
14
16
  server;
15
17
  // @ts-ignore
16
18
  options = {};
19
+ // Deferred route registration
20
+ routeQueue = [];
21
+ /**
22
+ * WebSocket route queue for deferred registration
23
+ * WebSocket routes are queued during initialization and registered when server starts
24
+ */
25
+ wsRouteQueue = [];
26
+ /**
27
+ * Global middlewares with optional path filtering configuration
28
+ * Middlewares are stored with their pattern config and applied during route registration
29
+ */
30
+ globalMiddlewares = [];
31
+ routesRegistered = false;
17
32
  methodMap = {
18
33
  [HttpMethod.GET]: (path, ...handlers) => this.app.get(path, ...handlers),
19
34
  [HttpMethod.POST]: (path, ...handlers) => this.app.post(path, ...handlers),
@@ -35,48 +50,62 @@ export class HonoAdapter extends AsenaAdapter {
35
50
  this.websocketAdapter.logger = this.logger;
36
51
  }
37
52
  }
38
- use(middleware, path) {
39
- const normalizedPath = path ? this.normalizePath(path) : undefined;
40
- const preparedMiddlewares = this.prepareMiddlewares([middleware]);
41
- if (normalizedPath) {
42
- this.app.use(normalizedPath, ...preparedMiddlewares);
43
- return;
44
- }
45
- this.app.use(...preparedMiddlewares);
53
+ /**
54
+ * Registers a global middleware with optional path filtering
55
+ *
56
+ * Middlewares are stored and applied during route registration based on include/exclude patterns.
57
+ * This enables zero-runtime-overhead pattern matching.
58
+ *
59
+ * @param middleware - The middleware to register globally
60
+ * @param config - Optional path filtering configuration with include/exclude patterns
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * // Apply to all routes
65
+ * adapter.use(authMiddleware);
66
+ *
67
+ * // Apply only to specific patterns
68
+ * adapter.use(authMiddleware, {
69
+ * include: ['/api/*', '/admin/*'],
70
+ * exclude: ['/api/health']
71
+ * });
72
+ *
73
+ * // Apply to all except specific patterns
74
+ * adapter.use(rateLimitMiddleware, {
75
+ * exclude: ['/health', '/metrics']
76
+ * });
77
+ * ```
78
+ */
79
+ use(middleware, config) {
80
+ // Store middleware with config for deferred application during route registration
81
+ this.globalMiddlewares.push({ middleware, config });
46
82
  }
47
- async registerRoute({ method, path, middlewares, handler, staticServe, validator, }) {
48
- const prepareMiddlewares = this.prepareMiddlewares(middlewares);
49
- const allMiddlewares = validator
50
- ? [...(await this.prepareValidator(validator)), ...prepareMiddlewares]
51
- : prepareMiddlewares;
52
- const methodHandler = this.methodMap[method];
53
- if (!methodHandler) {
54
- throw new Error('Invalid method');
55
- }
56
- if (staticServe) {
57
- methodHandler(path, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(staticServe)));
58
- this.logger.info(`${green('Successfully')} registered ${yellow('Static Serve ' + method.toUpperCase())} route for PATH: ${green(`${path}`)}`);
59
- return;
60
- }
61
- const routeHandler = [...allMiddlewares, this.prepareHandler(handler)];
62
- methodHandler(path, ...routeHandler);
63
- this.logger.info(`${green('Successfully')} registered ${yellow(method.toUpperCase())} route for PATH: ${green(`${path}`)}`);
83
+ async registerRoute(params) {
84
+ // Queue the route for deferred registration
85
+ this.routeQueue.push(params);
64
86
  }
65
- registerWebsocketRoute({ path, websocketService, middlewares, }) {
66
- const preparedMiddlewares = this.prepareMiddlewares(middlewares);
67
- this.app.get(`/${path}`, ...preparedMiddlewares, async (c, next) => {
68
- const websocketData = c.get('_websocketData') || {};
69
- const id = bun.randomUUIDv7();
70
- const data = { values: websocketData, id, path: path };
71
- const upgradeResult = this.server.upgrade(c.req.raw, { data });
72
- if (upgradeResult) {
73
- return new Response(null);
74
- }
75
- await next(); // Failed
76
- });
77
- this.websocketAdapter.registerWebSocket(websocketService);
87
+ /**
88
+ * Registers a WebSocket route (deferred registration)
89
+ *
90
+ * Routes are queued and will be registered when the server starts.
91
+ * This allows for controller-based logging and optimization.
92
+ *
93
+ * @param params - WebSocket route parameters
94
+ */
95
+ registerWebsocketRoute(params) {
96
+ // Queue WebSocket route for building during start()
97
+ this.wsRouteQueue.push(params);
98
+ // Also register WebSocket service with adapter for namespace tracking
99
+ if (this.websocketAdapter && params.websocketService) {
100
+ this.websocketAdapter.registerWebSocket(params.websocketService);
101
+ }
78
102
  }
79
103
  async start() {
104
+ // Register all queued routes with optimization
105
+ if (!this.routesRegistered) {
106
+ await this.optimizeAndRegisterRoutes();
107
+ this.routesRegistered = true;
108
+ }
80
109
  this.websocketAdapter.prepareWebSocket(this.options?.wsOptions);
81
110
  this.server = bun.serve({
82
111
  ...this.options.serveOptions,
@@ -85,11 +114,80 @@ export class HonoAdapter extends AsenaAdapter {
85
114
  websocket: this.websocketAdapter.websocket,
86
115
  });
87
116
  this.websocketAdapter.startWebsocket(this.server);
117
+ // Log controller-based route information
118
+ if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
119
+ this.logger.info(this.buildControllerBasedLog());
120
+ }
121
+ this.logger.info(`Server running at http://localhost:${this.server.port}`);
88
122
  return this.server;
89
123
  }
124
+ /**
125
+ * Registers global error handler with enhanced error handling capabilities
126
+ *
127
+ * Supports Hono's HTTPException for proper status code handling:
128
+ * - HTTPException instances are passed to user handler for custom handling
129
+ * - If user handler returns a response, it's used
130
+ * - Otherwise, HTTPException's default response is returned
131
+ * - Other errors follow normal error handling flow
132
+ *
133
+ * @param errorHandler - Custom error handler function
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * adapter.onError((error, context) => {
138
+ * // HTTPException handling (optional custom behavior)
139
+ * if (error instanceof HTTPException) {
140
+ * // Add custom logging or return custom response
141
+ * return context.send({ custom: 'response' }, error.status);
142
+ * }
143
+ *
144
+ * // Other errors
145
+ * return context.send({ error: 'Internal error' }, 500);
146
+ * });
147
+ * ```
148
+ */
90
149
  onError(errorHandler) {
91
150
  this.app.onError((error, context) => {
92
- return errorHandler(error, new HonoContextWrapper(context));
151
+ // Log error with full details
152
+ this.logger.error('Application error occurred:', {
153
+ message: error.message,
154
+ stack: error.stack,
155
+ path: context.req.path,
156
+ method: context.req.method,
157
+ timestamp: new Date().toISOString(),
158
+ });
159
+ // Wrap context for user handler
160
+ const wrapper = new HonoContextWrapper(context);
161
+ // Handle HTTPException with Hono's standard pattern
162
+ if (error instanceof HTTPException) {
163
+ try {
164
+ // Allow user to customize HTTPException handling
165
+ const customResponse = errorHandler(error, wrapper);
166
+ // If user returned a response, use it
167
+ if (customResponse) {
168
+ return customResponse;
169
+ }
170
+ }
171
+ catch (handlerError) {
172
+ // User handler failed, log and fallback to HTTPException's default response
173
+ this.logger.error('Error handler threw an error for HTTPException, using default response:', handlerError);
174
+ }
175
+ // Return HTTPException's default response (proper status code + message)
176
+ return error.getResponse();
177
+ }
178
+ // Handle other errors through user-defined handler
179
+ try {
180
+ return errorHandler(error, wrapper);
181
+ }
182
+ catch (handlerError) {
183
+ // Fallback if error handler itself throws
184
+ this.logger.error('Error handler threw an error:', handlerError);
185
+ return context.json({
186
+ error: 'Internal server error',
187
+ message: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : error.message,
188
+ timestamp: new Date().toISOString(),
189
+ }, 500);
190
+ }
93
191
  });
94
192
  }
95
193
  async serveOptions(options) {
@@ -104,6 +202,11 @@ export class HonoAdapter extends AsenaAdapter {
104
202
  prepareHandler(handler) {
105
203
  return (c) => handler(new HonoContextWrapper(c));
106
204
  }
205
+ /**
206
+ * Prepares static serve options with enhanced features
207
+ * @param staticServe - Base static serve parameters
208
+ * @returns Configured static serve options for Hono
209
+ */
107
210
  prepareStaticServeOptions(staticServe) {
108
211
  let staticServeOptions = {
109
212
  root: staticServe.root,
@@ -117,11 +220,42 @@ export class HonoAdapter extends AsenaAdapter {
117
220
  staticServeOptions.onFound = staticServe.onFound.handler;
118
221
  }
119
222
  else {
120
- staticServeOptions.onFound = (path, c) => {
121
- staticServe.onFound.handler(path, new HonoContextWrapper(c));
223
+ staticServeOptions.onFound = async (path, c) => {
224
+ // Add cache headers and custom headers
225
+ if (staticServe.extra?.cacheControl) {
226
+ c.header('Cache-Control', staticServe.extra.cacheControl);
227
+ }
228
+ else {
229
+ // Default cache control for static files
230
+ c.header('Cache-Control', 'public, max-age=31536000');
231
+ }
232
+ // Add custom headers if provided
233
+ if (staticServe.extra?.headers) {
234
+ Object.entries(staticServe.extra.headers).forEach(([key, value]) => {
235
+ c.header(key, value);
236
+ });
237
+ }
238
+ // Call user callback
239
+ await staticServe.onFound.handler(path, new HonoContextWrapper(c));
122
240
  };
123
241
  }
124
242
  }
243
+ else if (staticServe.extra?.cacheControl || staticServe.extra?.headers) {
244
+ // Add default onFound handler if cache or custom headers are specified
245
+ staticServeOptions.onFound = (_path, c) => {
246
+ if (staticServe.extra?.cacheControl) {
247
+ c.header('Cache-Control', staticServe.extra.cacheControl);
248
+ }
249
+ else {
250
+ c.header('Cache-Control', 'public, max-age=31536000');
251
+ }
252
+ if (staticServe.extra?.headers) {
253
+ Object.entries(staticServe.extra.headers).forEach(([key, value]) => {
254
+ c.header(key, value);
255
+ });
256
+ }
257
+ };
258
+ }
125
259
  if (staticServe.onNotFound) {
126
260
  if (staticServe.onNotFound.override) {
127
261
  // @ts-ignore
@@ -139,33 +273,465 @@ export class HonoAdapter extends AsenaAdapter {
139
273
  }
140
274
  return staticServeOptions;
141
275
  }
276
+ /**
277
+ * Prepares validation middleware with enhanced error handling
278
+ * @param baseValidator - The validator to prepare
279
+ * @returns Array of Hono middleware handlers for validation
280
+ */
142
281
  async prepareValidator(baseValidator) {
143
282
  if (!baseValidator) {
144
283
  return [];
145
284
  }
146
285
  const validators = [];
147
286
  for (const key of VALIDATOR_METHODS) {
148
- // if the key is not a validator method, skip
149
- if (typeof baseValidator[key]?.handle !== 'function') {
287
+ const validatorHandler = baseValidator[key];
288
+ // Skip if not a valid validator
289
+ if (!validatorHandler || typeof validatorHandler.handle !== 'function') {
150
290
  continue;
151
291
  }
152
- const validator = baseValidator[key];
153
- const validationSchema = await validator.handle();
154
- let schema;
155
- let hook;
156
- if ('schema' in validationSchema) {
157
- schema = validationSchema['schema'];
158
- hook = validationSchema['hook'];
292
+ try {
293
+ const validationSchema = await validatorHandler.handle();
294
+ // Validate that we got a valid schema
295
+ if (!validationSchema) {
296
+ this.logger.warn(`Validator for '${key}' returned null/undefined, skipping`);
297
+ continue;
298
+ }
299
+ let schema;
300
+ let hook;
301
+ if (typeof validationSchema === 'object' && 'schema' in validationSchema) {
302
+ schema = validationSchema.schema;
303
+ hook = validationSchema.hook;
304
+ // Validate schema is actually a Zod schema
305
+ if (!schema || typeof schema.parse !== 'function') {
306
+ throw new Error(`Invalid Zod schema provided for '${key}' validator`);
307
+ }
308
+ }
309
+ else {
310
+ schema = validationSchema;
311
+ if (!schema || typeof schema.parse !== 'function') {
312
+ throw new Error(`Invalid Zod schema provided for '${key}' validator`);
313
+ }
314
+ }
315
+ // Create validator with proper error handling
316
+ const validator = zValidator(key, schema, hook ||
317
+ ((result, c) => {
318
+ // Default hook with better error formatting
319
+ if (!result.success) {
320
+ return c.json({
321
+ error: 'Validation failed',
322
+ details: result.error.flatten(),
323
+ target: key,
324
+ }, 400);
325
+ }
326
+ }));
327
+ validators.push(validator);
159
328
  }
160
- else {
161
- schema = validationSchema;
329
+ catch (error) {
330
+ this.logger.error(`Failed to prepare validator for '${key}':`, error);
331
+ throw new Error(`Validator preparation failed for '${key}': ${error instanceof Error ? error.message : String(error)}`);
162
332
  }
163
- validators.push(zValidator(key, schema, hook));
164
333
  }
165
334
  return validators;
166
335
  }
167
- normalizePath(path) {
168
- return `${path.endsWith('/') ? path : `${path}/`}*`;
336
+ /**
337
+ * Gets global middlewares that should be applied to a specific path
338
+ *
339
+ * Filters global middlewares based on their include/exclude patterns.
340
+ * Pattern matching happens once during route registration (zero runtime overhead).
341
+ *
342
+ * @param path - The route path to check against middleware patterns
343
+ * @returns Array of middlewares that should be applied to this path
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * // If global middlewares are:
348
+ * // - authMiddleware with { include: ['/api/*'], exclude: ['/api/health'] }
349
+ * // - rateLimitMiddleware with no config
350
+ *
351
+ * getGlobalMiddlewaresForPath('/api/users') // => [authMiddleware, rateLimitMiddleware]
352
+ * getGlobalMiddlewaresForPath('/api/health') // => [rateLimitMiddleware]
353
+ * getGlobalMiddlewaresForPath('/public/file') // => [rateLimitMiddleware]
354
+ * ```
355
+ */
356
+ getGlobalMiddlewaresForPath(path) {
357
+ return this.globalMiddlewares
358
+ .filter(({ config }) => shouldApplyMiddleware(path, config))
359
+ .map(({ middleware }) => middleware);
360
+ }
361
+ /**
362
+ * Extracts base path from a route path
363
+ * Examples:
364
+ * - "/api/users/:id" -> "/api/users"
365
+ * - "/users" -> "/users"
366
+ * - "/" -> "/"
367
+ */
368
+ extractBasePath(path) {
369
+ // Remove trailing slash
370
+ let normalized = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
371
+ // Find the last segment without parameters
372
+ const segments = normalized.split('/');
373
+ const baseSegments = [];
374
+ for (const segment of segments) {
375
+ if (segment.startsWith(':') || segment.includes('*')) {
376
+ break;
377
+ }
378
+ baseSegments.push(segment);
379
+ }
380
+ return baseSegments.join('/') || '/';
381
+ }
382
+ /**
383
+ * Groups routes by their base path
384
+ * Returns a map of base path -> routes with that base path
385
+ */
386
+ groupRoutesByBasePath(routes) {
387
+ const groups = new Map();
388
+ for (const route of routes) {
389
+ const basePath = this.extractBasePath(route.path);
390
+ if (!groups.has(basePath)) {
391
+ groups.set(basePath, []);
392
+ }
393
+ groups.get(basePath).push(route);
394
+ }
395
+ return groups;
396
+ }
397
+ /**
398
+ * Finds common middlewares across all routes in a group
399
+ * Returns middlewares that appear in ALL routes
400
+ */
401
+ extractCommonMiddlewares(routes) {
402
+ if (routes.length === 0)
403
+ return [];
404
+ if (routes.length === 1)
405
+ return [];
406
+ // Get middleware from first route
407
+ const firstRouteMiddlewares = routes[0].middlewares || [];
408
+ // Find middlewares that exist in all routes
409
+ return firstRouteMiddlewares.filter((middleware) => {
410
+ return routes.every((route) => {
411
+ return (route.middlewares || []).some((mw) => {
412
+ // Compare by constructor name (class identity)
413
+ return mw.constructor.name === middleware.constructor.name;
414
+ });
415
+ });
416
+ });
417
+ }
418
+ /**
419
+ * Registers routes to Hono app with optimization
420
+ * Groups routes by base path and applies common middlewares at group level
421
+ * Also registers WebSocket routes
422
+ */
423
+ async optimizeAndRegisterRoutes() {
424
+ // Register HTTP routes
425
+ if (this.routeQueue.length > 0) {
426
+ // Group routes by base path
427
+ const routeGroups = this.groupRoutesByBasePath(this.routeQueue);
428
+ for (const [basePath, routes] of routeGroups) {
429
+ // Find common middlewares for this group
430
+ const commonMiddlewares = this.extractCommonMiddlewares(routes);
431
+ if (routes.length > 1 && commonMiddlewares.length > 0) {
432
+ // Multiple routes with common middlewares - use grouping
433
+ await this.registerRouteGroup(basePath, commonMiddlewares, routes);
434
+ }
435
+ else {
436
+ // Single route or no common middlewares - register individually
437
+ for (const route of routes) {
438
+ await this.registerRouteDirect(route);
439
+ }
440
+ }
441
+ }
442
+ }
443
+ // Register WebSocket routes
444
+ if (this.wsRouteQueue.length > 0) {
445
+ for (const wsRoute of this.wsRouteQueue) {
446
+ await this.registerWebsocketRouteDirect(wsRoute);
447
+ }
448
+ }
449
+ // Log controller summary
450
+ this.logControllerSummary();
451
+ }
452
+ /**
453
+ * Logs a summary of registered controllers
454
+ */
455
+ logControllerSummary() {
456
+ const httpGroups = this.groupRoutesByController();
457
+ const wsGroups = this.groupWebSocketRoutesByController();
458
+ // Log HTTP controllers
459
+ for (const [controllerName, group] of httpGroups) {
460
+ const routeCount = group.routes.length;
461
+ const routeText = routeCount === 1 ? 'route' : 'routes';
462
+ this.logger.info(`${green('✓')} Successfully registered ${yellow('CONTROLLER')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
463
+ }
464
+ // Log WebSocket controllers (only those that don't have HTTP routes)
465
+ for (const [controllerName, group] of wsGroups) {
466
+ if (!httpGroups.has(controllerName)) {
467
+ const routeCount = group.routes.length;
468
+ const routeText = routeCount === 1 ? 'route' : 'routes';
469
+ this.logger.info(`${green('✓')} Successfully registered ${yellow('WEBSOCKET')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
470
+ }
471
+ }
472
+ }
473
+ /**
474
+ * Registers a WebSocket route directly
475
+ * Converts HTTP GET route to WebSocket upgrade endpoint
476
+ */
477
+ async registerWebsocketRouteDirect(wsRoute) {
478
+ // Get filtered global middlewares for this WebSocket route
479
+ const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(wsRoute.path);
480
+ const preparedGlobalMiddlewares = this.prepareMiddlewares(applicableGlobalMiddlewares);
481
+ const preparedMiddlewares = this.prepareMiddlewares(wsRoute.middlewares || []);
482
+ // Combine: global middlewares -> route middlewares
483
+ const allMiddlewares = [...preparedGlobalMiddlewares, ...preparedMiddlewares];
484
+ this.app.get(`/${wsRoute.path}`, ...allMiddlewares, async (c, next) => {
485
+ const websocketData = c.get('_websocketData') || {};
486
+ const id = bun.randomUUIDv7();
487
+ const data = { values: websocketData, id, path: wsRoute.path };
488
+ const upgradeResult = this.server.upgrade(c.req.raw, { data });
489
+ if (upgradeResult) {
490
+ return new Response(null);
491
+ }
492
+ await next(); // Failed
493
+ });
494
+ }
495
+ /**
496
+ * Registers a group of routes with common middlewares at base path level
497
+ */
498
+ async registerRouteGroup(basePath, commonMiddlewares, routes) {
499
+ // Create grouped Hono instance
500
+ const group = new Hono();
501
+ // Get filtered global middlewares for this base path
502
+ const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(basePath);
503
+ const preparedGlobalMiddlewares = this.prepareMiddlewares(applicableGlobalMiddlewares);
504
+ // Apply global middlewares first (before common middlewares)
505
+ if (preparedGlobalMiddlewares.length > 0) {
506
+ group.use('*', ...preparedGlobalMiddlewares);
507
+ }
508
+ // Apply common middlewares to the entire group
509
+ const preparedCommonMiddlewares = this.prepareMiddlewares(commonMiddlewares);
510
+ if (preparedCommonMiddlewares.length > 0) {
511
+ group.use('*', ...preparedCommonMiddlewares);
512
+ }
513
+ // Method map for group
514
+ const groupMethodMap = {
515
+ [HttpMethod.GET]: (path, ...handlers) => group.get(path, ...handlers),
516
+ [HttpMethod.POST]: (path, ...handlers) => group.post(path, ...handlers),
517
+ [HttpMethod.PUT]: (path, ...handlers) => group.put(path, ...handlers),
518
+ [HttpMethod.DELETE]: (path, ...handlers) => group.delete(path, ...handlers),
519
+ [HttpMethod.PATCH]: (path, ...handlers) => group.patch(path, ...handlers),
520
+ [HttpMethod.OPTIONS]: (path, ...handlers) => group.options(path, ...handlers),
521
+ [HttpMethod.CONNECT]: (path, ...handlers) => group.on(HttpMethod.CONNECT.toUpperCase(), path, ...handlers),
522
+ [HttpMethod.HEAD]: (path, ...handlers) => group.on(HttpMethod.HEAD.toUpperCase(), path, ...handlers),
523
+ [HttpMethod.TRACE]: (path, ...handlers) => group.on(HttpMethod.TRACE.toUpperCase(), path, ...handlers),
524
+ };
525
+ // Register each route in the group
526
+ for (const route of routes) {
527
+ // Remove common middlewares from route middlewares
528
+ const uniqueMiddlewares = (route.middlewares || []).filter((mw) => !commonMiddlewares.some((common) => common.constructor.name === mw.constructor.name));
529
+ const preparedMiddlewares = this.prepareMiddlewares(uniqueMiddlewares);
530
+ const validators = route.validator ? await this.prepareValidator(route.validator) : [];
531
+ const allMiddlewares = [...validators, ...preparedMiddlewares];
532
+ const methodHandler = groupMethodMap[route.method];
533
+ if (!methodHandler) {
534
+ throw new Error(`Invalid HTTP method: ${route.method}`);
535
+ }
536
+ // Calculate relative path
537
+ let relativePath = route.path;
538
+ if (route.path.startsWith(basePath) && basePath !== '/') {
539
+ relativePath = route.path.slice(basePath.length);
540
+ }
541
+ if (!relativePath.startsWith('/')) {
542
+ relativePath = '/' + relativePath;
543
+ }
544
+ // Register to group
545
+ if (route.staticServe) {
546
+ methodHandler(relativePath, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
547
+ }
548
+ else {
549
+ const handler = this.prepareHandler(route.handler);
550
+ methodHandler(relativePath, ...allMiddlewares, handler);
551
+ }
552
+ }
553
+ // Mount group to main app
554
+ this.app.route(basePath, group);
555
+ }
556
+ /**
557
+ * Registers a single route directly without grouping
558
+ */
559
+ async registerRouteDirect(route) {
560
+ // Get filtered global middlewares for this route
561
+ const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
562
+ const preparedGlobalMiddlewares = this.prepareMiddlewares(applicableGlobalMiddlewares);
563
+ const preparedMiddlewares = this.prepareMiddlewares(route.middlewares || []);
564
+ const validators = route.validator ? await this.prepareValidator(route.validator) : [];
565
+ // Combine: validators -> global middlewares -> route middlewares
566
+ const allMiddlewares = [...validators, ...preparedGlobalMiddlewares, ...preparedMiddlewares];
567
+ const methodHandler = this.methodMap[route.method];
568
+ if (!methodHandler) {
569
+ throw new Error(`Invalid HTTP method: ${route.method}`);
570
+ }
571
+ if (route.staticServe) {
572
+ methodHandler(route.path, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
573
+ }
574
+ else {
575
+ const handler = this.prepareHandler(route.handler);
576
+ methodHandler(route.path, ...allMiddlewares, handler);
577
+ }
578
+ }
579
+ /**
580
+ * Groups HTTP routes by controller name
581
+ *
582
+ * Creates a map of controller names to their routes for organized logging.
583
+ * Each group contains the controller's base path and all its routes.
584
+ *
585
+ * @returns Map of controller names to route groups
586
+ *
587
+ * @example
588
+ * ```typescript
589
+ * groupRoutesByController()
590
+ * // => Map {
591
+ * // 'UserController' => {
592
+ * // basePath: '/users',
593
+ * // routes: [{ method: 'GET', path: '/users' }, { method: 'POST', path: '/users' }]
594
+ * // }
595
+ * // }
596
+ * ```
597
+ */
598
+ groupRoutesByController() {
599
+ const groups = new Map();
600
+ for (const route of this.routeQueue) {
601
+ const controllerName = route.controllerName || 'Unknown';
602
+ const controllerBasePath = route.controllerBasePath || '/';
603
+ if (!groups.has(controllerName)) {
604
+ groups.set(controllerName, {
605
+ basePath: controllerBasePath,
606
+ routes: [],
607
+ });
608
+ }
609
+ groups.get(controllerName).routes.push({
610
+ method: route.method.toUpperCase(),
611
+ path: route.path,
612
+ });
613
+ }
614
+ return groups;
615
+ }
616
+ /**
617
+ * Groups WebSocket routes by controller name
618
+ *
619
+ * Creates a map of controller names to their WebSocket routes for organized logging.
620
+ *
621
+ * @returns Map of controller names to WebSocket route groups
622
+ *
623
+ * @example
624
+ * ```typescript
625
+ * groupWebSocketRoutesByController()
626
+ * // => Map {
627
+ * // 'ChatController' => {
628
+ * // basePath: '/chat',
629
+ * // routes: [{ path: '/chat' }]
630
+ * // }
631
+ * // }
632
+ * ```
633
+ */
634
+ groupWebSocketRoutesByController() {
635
+ const groups = new Map();
636
+ for (const wsRoute of this.wsRouteQueue) {
637
+ const controllerName = wsRoute.controllerName || 'Unknown';
638
+ const controllerBasePath = wsRoute.path; // WebSocket uses path as base path
639
+ if (!groups.has(controllerName)) {
640
+ groups.set(controllerName, {
641
+ basePath: controllerBasePath,
642
+ routes: [],
643
+ });
644
+ }
645
+ groups.get(controllerName).routes.push({
646
+ path: wsRoute.path,
647
+ });
648
+ }
649
+ return groups;
650
+ }
651
+ /**
652
+ * Builds controller-based log output
653
+ *
654
+ * Creates a formatted string showing all routes grouped by controller,
655
+ * with HTTP and WebSocket routes clearly organized.
656
+ *
657
+ * @returns Formatted log string
658
+ *
659
+ * @example
660
+ * Output format:
661
+ * ```
662
+ * Registered routes:
663
+ *
664
+ * UserController (/users):
665
+ * GET /users
666
+ * GET /users/:id
667
+ * POST /users
668
+ *
669
+ * ChatController (/chat):
670
+ * WS /chat
671
+ * ```
672
+ */
673
+ buildControllerBasedLog() {
674
+ const httpGroups = this.groupRoutesByController();
675
+ const wsGroups = this.groupWebSocketRoutesByController();
676
+ // Merge WebSocket groups into HTTP groups
677
+ for (const [controllerName, wsGroup] of wsGroups) {
678
+ if (httpGroups.has(controllerName)) {
679
+ // Add WebSocket routes to existing controller group
680
+ const httpGroup = httpGroups.get(controllerName);
681
+ for (const wsRoute of wsGroup.routes) {
682
+ httpGroup.routes.push({
683
+ method: 'WS',
684
+ path: wsRoute.path,
685
+ });
686
+ }
687
+ }
688
+ else {
689
+ // Create new group for WebSocket-only controller
690
+ httpGroups.set(controllerName, {
691
+ basePath: wsGroup.basePath,
692
+ routes: wsGroup.routes.map((r) => ({
693
+ method: 'WS',
694
+ path: r.path,
695
+ })),
696
+ });
697
+ }
698
+ }
699
+ // Build log output with colors
700
+ const lines = [];
701
+ // Sort controllers alphabetically for consistent output
702
+ const sortedControllers = Array.from(httpGroups.entries()).sort(([a], [b]) => a.localeCompare(b));
703
+ for (const [controllerName, group] of sortedControllers) {
704
+ lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
705
+ // Sort routes: GET first, then POST, PUT, PATCH, DELETE, WS
706
+ const methodOrder = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'WS'];
707
+ const sortedRoutes = group.routes.sort((a, b) => {
708
+ const orderA = methodOrder.indexOf(a.method);
709
+ const orderB = methodOrder.indexOf(b.method);
710
+ return orderA - orderB;
711
+ });
712
+ for (const route of sortedRoutes) {
713
+ // Colorize method based on type
714
+ let coloredMethod = route.method;
715
+ if (route.method === 'GET') {
716
+ coloredMethod = green(route.method);
717
+ }
718
+ else if (route.method === 'POST') {
719
+ coloredMethod = blue(route.method);
720
+ }
721
+ else if (route.method === 'PUT') {
722
+ coloredMethod = yellow(route.method);
723
+ }
724
+ else if (route.method === 'DELETE') {
725
+ coloredMethod = red(route.method);
726
+ }
727
+ else if (route.method === 'WS') {
728
+ coloredMethod = blue(route.method);
729
+ }
730
+ lines.push(` ${coloredMethod} ${route.path}`);
731
+ }
732
+ lines.push(''); // Empty line between controllers
733
+ }
734
+ return lines.join('\n');
169
735
  }
170
736
  }
171
737
  //# sourceMappingURL=HonoAdapter.js.map