@asenajs/hono-adapter 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/HonoAdapter.d.ts +49 -4
  3. package/dist/lib/HonoAdapter.js +209 -51
  4. package/dist/lib/HonoAdapter.js.map +1 -1
  5. package/dist/lib/HonoContextWrapper.d.ts +17 -3
  6. package/dist/lib/HonoContextWrapper.js +85 -2
  7. package/dist/lib/HonoContextWrapper.js.map +1 -1
  8. package/dist/lib/HonoWebsocketAdapter.d.ts +2 -16
  9. package/dist/lib/HonoWebsocketAdapter.js +26 -50
  10. package/dist/lib/HonoWebsocketAdapter.js.map +1 -1
  11. package/dist/lib/defaults/ValidationService.d.ts +4 -4
  12. package/dist/lib/middlewares/CorsMiddleware.d.ts +1 -1
  13. package/dist/lib/middlewares/CorsMiddleware.js +5 -6
  14. package/dist/lib/middlewares/CorsMiddleware.js.map +1 -1
  15. package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +1 -1
  16. package/dist/lib/middlewares/RateLimiterMiddleware.js +9 -7
  17. package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -1
  18. package/dist/lib/types/HonoAdapterOptions.d.ts +27 -0
  19. package/dist/lib/types/HonoAdapterOptions.js +2 -0
  20. package/dist/lib/types/HonoAdapterOptions.js.map +1 -0
  21. package/dist/lib/types/SendPingStrategy.d.ts +4 -0
  22. package/dist/lib/types/SendPingStrategy.js +2 -0
  23. package/dist/lib/types/SendPingStrategy.js.map +1 -0
  24. package/dist/lib/types/index.d.ts +2 -0
  25. package/dist/lib/types/index.js +2 -0
  26. package/dist/lib/types/index.js.map +1 -1
  27. package/dist/lib/utils/createHonoAdapter.d.ts +2 -1
  28. package/dist/lib/utils/createHonoAdapter.js +9 -1
  29. package/dist/lib/utils/createHonoAdapter.js.map +1 -1
  30. package/dist/lib/utils/middlewareParser.d.ts +5 -1
  31. package/dist/lib/utils/middlewareParser.js +16 -6
  32. package/dist/lib/utils/middlewareParser.js.map +1 -1
  33. package/dist/lib/utils/routePriority.d.ts +40 -0
  34. package/dist/lib/utils/routePriority.js +75 -0
  35. package/dist/lib/utils/routePriority.js.map +1 -0
  36. package/package.json +16 -13
package/README.md CHANGED
@@ -44,8 +44,8 @@ await new AsenaServer(adapter)
44
44
  ### Controller Example
45
45
 
46
46
  ```typescript
47
- import { Controller } from '@asenajs/asena/server';
48
- import { Get } from '@asenajs/asena/web';
47
+ import { Controller } from '@asenajs/asena/decorators';
48
+ import { Get } from '@asenajs/asena/decorators/http';
49
49
  import type { Context } from '@asenajs/hono-adapter';
50
50
 
51
51
  @Controller()
@@ -1,18 +1,28 @@
1
1
  import { Hono } from 'hono';
2
2
  import type { Server } from 'bun';
3
- import type { AsenaWebsocketAdapter, WebsocketRouteParams } from '@asenajs/asena/adapter';
3
+ import type { WebsocketRouteParams } from '@asenajs/asena/adapter';
4
4
  import { AsenaAdapter, type AsenaServeOptions, type BaseMiddleware, type RouteParams } from '@asenajs/asena/adapter';
5
5
  import type { GlobalMiddlewareRouteConfig } from '@asenajs/asena/server/config';
6
- import type { HonoErrorHandler, StaticServeExtras } from './types';
6
+ import type { HonoAdapterOptions, HonoErrorHandler, StaticServeExtras } from './types';
7
7
  import { type ServerLogger } from '@asenajs/asena/logger';
8
8
  import type { ValidationSchema } from './defaults';
9
9
  import type { Context as HonoAdapterContext } from './defaults/Context';
10
+ import { HonoWebsocketAdapter } from './HonoWebsocketAdapter';
10
11
  import type { WebSocketData } from '@asenajs/asena/web-socket';
11
12
  export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, ValidationSchema> {
12
13
  name: string;
13
- app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
14
+ app: Hono;
15
+ private readonly _strict;
14
16
  private server;
15
17
  private options;
18
+ /**
19
+ * Normalizes a route path for Hono compatibility.
20
+ * Strips trailing slash (except root '/') because Hono's strict:false
21
+ * normalizes incoming request paths by removing trailing slashes.
22
+ * Registered routes must match this normalized form.
23
+ */
24
+ private normalizePath;
25
+ stop(closeActiveConnections?: boolean): Promise<void>;
16
26
  private routeQueue;
17
27
  /**
18
28
  * WebSocket route queue for deferred registration
@@ -24,9 +34,19 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
24
34
  * Middlewares are stored with their pattern config and applied during route registration
25
35
  */
26
36
  private globalMiddlewares;
37
+ /**
38
+ * HTML routes for FrontendController pages
39
+ * Stored separately and merged into Bun.serve() routes at start time
40
+ */
41
+ private htmlRoutes;
42
+ /**
43
+ * Queue of FrontendController route metadata for deferred logging at start time
44
+ */
45
+ private frontEndRouteQueue;
27
46
  private routesRegistered;
28
47
  private readonly methodMap;
29
- constructor(logger: ServerLogger, websocketAdapter?: AsenaWebsocketAdapter);
48
+ constructor(options: HonoAdapterOptions);
49
+ constructor(logger: ServerLogger, websocketAdapter?: HonoWebsocketAdapter, app?: Hono<any, any, any>);
30
50
  /**
31
51
  * Registers a global middleware with optional path filtering
32
52
  *
@@ -53,6 +73,14 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
53
73
  * });
54
74
  * ```
55
75
  */
76
+ /**
77
+ * Registers an HTML route for FrontendController pages.
78
+ * HTML routes bypass the middleware chain and are served directly by Bun.serve().
79
+ *
80
+ * @param path - Full URL path (e.g., '/ui/home')
81
+ * @param htmlBundle - The HTML bundle returned by importing an .html file
82
+ */
83
+ registerHTMLRoute(path: string, htmlBundle: unknown, controllerName: string, controllerBasePath: string): void;
56
84
  use(middleware: BaseMiddleware<HonoAdapterContext>, config?: GlobalMiddlewareRouteConfig): void;
57
85
  registerRoute(params: RouteParams<HonoAdapterContext, ValidationSchema, StaticServeExtras>): Promise<void>;
58
86
  /**
@@ -107,6 +135,19 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
107
135
  * @returns Array of Hono middleware handlers for validation
108
136
  */
109
137
  private prepareValidator;
138
+ /**
139
+ * Registers global middlewares at the top level of the Hono app
140
+ *
141
+ * Must be called BEFORE route registration so middlewares run before route matching.
142
+ * This ensures global middlewares (like CORS) handle all requests including
143
+ * OPTIONS preflight requests that don't have explicit route handlers.
144
+ *
145
+ * Uses Hono's native path-based middleware registration:
146
+ * - No config → app.use('*', mw)
147
+ * - Only include patterns → app.use('/api/*', mw) per pattern
148
+ * - Exclude patterns → runtime shouldApplyMiddleware check (Hono has no native exclude)
149
+ */
150
+ private registerGlobalMiddlewaresTopLevel;
110
151
  /**
111
152
  * Gets global middlewares that should be applied to a specific path
112
153
  *
@@ -231,4 +272,8 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
231
272
  * ```
232
273
  */
233
274
  private buildControllerBasedLog;
275
+ /**
276
+ * Groups FrontendController routes by controller name for logging
277
+ */
278
+ private groupFrontEndRoutesByController;
234
279
  }
@@ -3,18 +3,37 @@ import { HTTPException } from 'hono/http-exception';
3
3
  import * as bun from 'bun';
4
4
  import { HonoContextWrapper } from './HonoContextWrapper';
5
5
  import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
6
- import { shouldApplyMiddleware } from '@asenajs/asena/utlis';
6
+ import { shouldApplyMiddleware } from '@asenajs/asena/utils';
7
7
  import { blue, green, red, yellow } from '@asenajs/asena/logger';
8
8
  import { zValidator } from '@hono/zod-validator';
9
9
  import { middlewareParser } from './utils/middlewareParser';
10
+ import { compareRoutePriority } from './utils/routePriority';
10
11
  import { HttpMethod } from '@asenajs/asena/web-types';
11
12
  import { HonoWebsocketAdapter } from './HonoWebsocketAdapter';
12
13
  import { serveStatic } from 'hono/bun';
13
14
  export class HonoAdapter extends AsenaAdapter {
14
15
  name = 'HonoAdapter';
15
- app = new Hono();
16
+ app;
17
+ _strict = true;
16
18
  server;
17
19
  options = {};
20
+ /**
21
+ * Normalizes a route path for Hono compatibility.
22
+ * Strips trailing slash (except root '/') because Hono's strict:false
23
+ * normalizes incoming request paths by removing trailing slashes.
24
+ * Registered routes must match this normalized form.
25
+ */
26
+ normalizePath(path) {
27
+ if (path.length > 1 && path.endsWith('/')) {
28
+ return path.slice(0, -1);
29
+ }
30
+ return path;
31
+ }
32
+ async stop(closeActiveConnections = true) {
33
+ if (this.server) {
34
+ await this.server.stop(closeActiveConnections);
35
+ }
36
+ }
18
37
  // Deferred route registration
19
38
  routeQueue = [];
20
39
  /**
@@ -27,6 +46,15 @@ export class HonoAdapter extends AsenaAdapter {
27
46
  * Middlewares are stored with their pattern config and applied during route registration
28
47
  */
29
48
  globalMiddlewares = [];
49
+ /**
50
+ * HTML routes for FrontendController pages
51
+ * Stored separately and merged into Bun.serve() routes at start time
52
+ */
53
+ htmlRoutes = new Map();
54
+ /**
55
+ * Queue of FrontendController route metadata for deferred logging at start time
56
+ */
57
+ frontEndRouteQueue = [];
30
58
  routesRegistered = false;
31
59
  methodMap = {
32
60
  [HttpMethod.GET]: (path, ...handlers) => this.app.get(path, ...handlers),
@@ -35,19 +63,40 @@ export class HonoAdapter extends AsenaAdapter {
35
63
  [HttpMethod.DELETE]: (path, ...handlers) => this.app.delete(path, ...handlers),
36
64
  [HttpMethod.PATCH]: (path, ...handlers) => this.app.patch(path, ...handlers),
37
65
  [HttpMethod.OPTIONS]: (path, ...handlers) => this.app.options(path, ...handlers),
38
- [HttpMethod.CONNECT]: (path, ...handlers) => this.app.on(HttpMethod.CONNECT.toUpperCase(), path, ...handlers),
39
- [HttpMethod.HEAD]: (path, ...handlers) => this.app.on(HttpMethod.HEAD.toUpperCase(), path, ...handlers),
40
- [HttpMethod.TRACE]: (path, ...handlers) => this.app.on(HttpMethod.TRACE.toUpperCase(), path, ...handlers),
66
+ [HttpMethod.CONNECT]: (path, ...handlers) => this.app.on([HttpMethod.CONNECT.toUpperCase()], path, ...handlers),
67
+ // HEAD is handled automatically by Hono via GET routes (HTTP spec: HEAD = GET without body).
68
+ // Registering via app.on(['HEAD'], ...) does not work in Hono — it silently 404s.
69
+ [HttpMethod.HEAD]: (path, ...handlers) => this.app.get(path, ...handlers),
70
+ [HttpMethod.TRACE]: (path, ...handlers) => this.app.on([HttpMethod.TRACE.toUpperCase()], path, ...handlers),
71
+ [HttpMethod.ALL]: (path, ...handlers) => this.app.all(path, ...handlers),
41
72
  };
42
- constructor(logger, websocketAdapter) {
43
- super(logger, websocketAdapter);
44
- if (!this.websocketAdapter) {
45
- this.websocketAdapter = new HonoWebsocketAdapter(logger);
73
+ constructor(loggerOrOptions, websocketAdapter, app) {
74
+ let logger;
75
+ let wsAdapter;
76
+ let honoApp;
77
+ if (typeof loggerOrOptions === 'object' && 'logger' in loggerOrOptions) {
78
+ logger = loggerOrOptions.logger;
79
+ wsAdapter = loggerOrOptions.websocketAdapter ?? new HonoWebsocketAdapter(logger);
80
+ honoApp = loggerOrOptions.app;
81
+ }
82
+ else {
83
+ logger = loggerOrOptions;
84
+ wsAdapter = websocketAdapter ?? new HonoWebsocketAdapter(logger);
85
+ honoApp = app;
46
86
  }
47
- // to ensure that the logger is set
87
+ super(logger, wsAdapter);
48
88
  if (!this.websocketAdapter.logger && logger) {
49
89
  this.websocketAdapter.logger = this.logger;
50
90
  }
91
+ this._strict = typeof loggerOrOptions === 'object' && 'logger' in loggerOrOptions
92
+ ? loggerOrOptions.strict ?? true
93
+ : true;
94
+ if (honoApp) {
95
+ this.app = honoApp;
96
+ }
97
+ else {
98
+ this.app = new Hono({ strict: this._strict });
99
+ }
51
100
  }
52
101
  /**
53
102
  * Registers a global middleware with optional path filtering
@@ -75,6 +124,27 @@ export class HonoAdapter extends AsenaAdapter {
75
124
  * });
76
125
  * ```
77
126
  */
127
+ /**
128
+ * Registers an HTML route for FrontendController pages.
129
+ * HTML routes bypass the middleware chain and are served directly by Bun.serve().
130
+ *
131
+ * @param path - Full URL path (e.g., '/ui/home')
132
+ * @param htmlBundle - The HTML bundle returned by importing an .html file
133
+ */
134
+ registerHTMLRoute(path, htmlBundle, controllerName, controllerBasePath) {
135
+ if (this.htmlRoutes.has(path)) {
136
+ throw new Error(`Duplicate HTML route: "${path}" is already registered.`);
137
+ }
138
+ this.htmlRoutes.set(path, htmlBundle);
139
+ this.frontEndRouteQueue.push({ path, controllerName, controllerBasePath });
140
+ // Register trailing slash variant for consistent routing
141
+ if (path !== '/' && !path.endsWith('/')) {
142
+ this.htmlRoutes.set(`${path}/`, htmlBundle);
143
+ }
144
+ else if (path !== '/' && path.endsWith('/')) {
145
+ this.htmlRoutes.set(path.slice(0, -1), htmlBundle);
146
+ }
147
+ }
78
148
  use(middleware, config) {
79
149
  // Store middleware with config for deferred application during route registration
80
150
  this.globalMiddlewares.push({ middleware, config });
@@ -102,19 +172,28 @@ export class HonoAdapter extends AsenaAdapter {
102
172
  async start() {
103
173
  // Register all queued routes with optimization
104
174
  if (!this.routesRegistered) {
175
+ // Register global middlewares at top level BEFORE routes
176
+ // This ensures they run for ALL requests including OPTIONS preflight
177
+ this.registerGlobalMiddlewaresTopLevel();
105
178
  await this.optimizeAndRegisterRoutes();
106
179
  this.routesRegistered = true;
107
180
  }
108
181
  this.websocketAdapter.prepareWebSocket(this.options?.wsOptions);
109
- this.server = bun.serve({
182
+ const serveConfig = {
110
183
  ...this.options.serveOptions,
111
184
  port: this.port,
112
185
  fetch: this.app.fetch,
113
186
  websocket: this.websocketAdapter.websocket,
114
- });
187
+ };
188
+ // Add HTML routes if any are registered (FrontendController pages)
189
+ // Bun checks routes first, then falls through to fetch (Hono) for API routes
190
+ if (this.htmlRoutes.size > 0) {
191
+ serveConfig.routes = Object.fromEntries(this.htmlRoutes);
192
+ }
193
+ this.server = bun.serve(serveConfig);
115
194
  this.websocketAdapter.startWebsocket(this.server);
116
195
  // Log controller-based route information
117
- if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
196
+ if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0 || this.frontEndRouteQueue.length > 0) {
118
197
  this.logger.info('\n' + this.buildControllerBasedLog());
119
198
  }
120
199
  this.logger.info(`Server running at http://localhost:${this.server.port}`);
@@ -156,7 +235,7 @@ export class HonoAdapter extends AsenaAdapter {
156
235
  timestamp: new Date().toISOString(),
157
236
  });
158
237
  // Wrap context for user handler
159
- const wrapper = new HonoContextWrapper(context);
238
+ const wrapper = new HonoContextWrapper(context, this.server);
160
239
  // Handle HTTPException with Hono's standard pattern
161
240
  if (error instanceof HTTPException) {
162
241
  try {
@@ -196,10 +275,10 @@ export class HonoAdapter extends AsenaAdapter {
196
275
  this.port = port;
197
276
  }
198
277
  prepareMiddlewares(middlewares) {
199
- return middlewareParser(middlewares);
278
+ return middlewareParser(middlewares, () => this.server);
200
279
  }
201
280
  prepareHandler(handler) {
202
- return (c) => handler(new HonoContextWrapper(c));
281
+ return (c) => handler(new HonoContextWrapper(c, this.server));
203
282
  }
204
283
  /**
205
284
  * Prepares static serve options with enhanced features
@@ -234,7 +313,7 @@ export class HonoAdapter extends AsenaAdapter {
234
313
  });
235
314
  }
236
315
  // Call user callback
237
- await staticServe.onFound.handler(path, new HonoContextWrapper(c));
316
+ await staticServe.onFound.handler(path, new HonoContextWrapper(c, this.server));
238
317
  };
239
318
  }
240
319
  }
@@ -260,7 +339,7 @@ export class HonoAdapter extends AsenaAdapter {
260
339
  }
261
340
  else {
262
341
  staticServeOptions.onNotFound = (path, c) => {
263
- staticServe.onNotFound.handler(path, new HonoContextWrapper(c));
342
+ staticServe.onNotFound.handler(path, new HonoContextWrapper(c, this.server));
264
343
  };
265
344
  }
266
345
  }
@@ -330,6 +409,49 @@ export class HonoAdapter extends AsenaAdapter {
330
409
  }
331
410
  return validators;
332
411
  }
412
+ /**
413
+ * Registers global middlewares at the top level of the Hono app
414
+ *
415
+ * Must be called BEFORE route registration so middlewares run before route matching.
416
+ * This ensures global middlewares (like CORS) handle all requests including
417
+ * OPTIONS preflight requests that don't have explicit route handlers.
418
+ *
419
+ * Uses Hono's native path-based middleware registration:
420
+ * - No config → app.use('*', mw)
421
+ * - Only include patterns → app.use('/api/*', mw) per pattern
422
+ * - Exclude patterns → runtime shouldApplyMiddleware check (Hono has no native exclude)
423
+ */
424
+ registerGlobalMiddlewaresTopLevel() {
425
+ for (const { middleware, config } of this.globalMiddlewares) {
426
+ const parsed = middlewareParser([middleware], () => this.server);
427
+ if (!config) {
428
+ // No pattern filter → apply to all routes
429
+ this.app.use('*', ...parsed);
430
+ }
431
+ else if (config.include && !config.exclude) {
432
+ // Only include patterns → use Hono's native path matching
433
+ for (const pattern of config.include) {
434
+ this.app.use(pattern, ...parsed);
435
+ }
436
+ }
437
+ else {
438
+ // Exclude patterns exist → runtime filtering required
439
+ this.app.use('*', async (c, next) => {
440
+ if (shouldApplyMiddleware(new URL(c.req.url).pathname, config)) {
441
+ for (const mw of parsed) {
442
+ let called = false;
443
+ await mw(c, async () => {
444
+ called = true;
445
+ });
446
+ if (!called)
447
+ return;
448
+ }
449
+ }
450
+ await next();
451
+ });
452
+ }
453
+ }
454
+ }
333
455
  /**
334
456
  * Gets global middlewares that should be applied to a specific path
335
457
  *
@@ -420,9 +542,16 @@ export class HonoAdapter extends AsenaAdapter {
420
542
  async optimizeAndRegisterRoutes() {
421
543
  // Register HTTP routes
422
544
  if (this.routeQueue.length > 0) {
545
+ // Sort routes by priority: static > param > wildcard, more specific first.
546
+ // Hono matches in registration order — correct ordering prevents /:id
547
+ // from catching static routes like /count or /search.
548
+ // @see routePriority.ts for the segment-based comparison algorithm.
549
+ this.routeQueue.sort((a, b) => compareRoutePriority(a.path, b.path));
423
550
  // Group routes by base path
424
551
  const routeGroups = this.groupRoutesByBasePath(this.routeQueue);
425
- for (const [basePath, routes] of routeGroups) {
552
+ // Sort groups by the same priority algorithm to ensure correct inter-group order
553
+ const sortedGroups = [...routeGroups.entries()].sort(([a], [b]) => compareRoutePriority(a, b));
554
+ for (const [basePath, routes] of sortedGroups) {
426
555
  // Find common middlewares for this group
427
556
  const commonMiddlewares = this.extractCommonMiddlewares(routes);
428
557
  if (routes.length > 1 && commonMiddlewares.length > 0) {
@@ -466,6 +595,13 @@ export class HonoAdapter extends AsenaAdapter {
466
595
  this.logger.info(`${green('✓')} Successfully registered ${yellow('WEBSOCKET')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
467
596
  }
468
597
  }
598
+ // Log FrontendControllers
599
+ const frontEndGroups = this.groupFrontEndRoutesByController();
600
+ for (const [controllerName, group] of frontEndGroups) {
601
+ const routeCount = group.routes.length;
602
+ const routeText = routeCount === 1 ? 'route' : 'routes';
603
+ this.logger.info(`${green('✓')} Successfully registered ${yellow('FRONTEND')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
604
+ }
469
605
  }
470
606
  /**
471
607
  * Registers a WebSocket route directly
@@ -478,10 +614,12 @@ export class HonoAdapter extends AsenaAdapter {
478
614
  const preparedMiddlewares = this.prepareMiddlewares(wsRoute.middlewares || []);
479
615
  // Combine: global middlewares -> route middlewares
480
616
  const allMiddlewares = [...preparedGlobalMiddlewares, ...preparedMiddlewares];
481
- this.app.get(`/${wsRoute.path}`, ...allMiddlewares, async (c, next) => {
617
+ const normalizedWsPath = this.normalizePath(`/${wsRoute.path}`);
618
+ this.app.get(normalizedWsPath, ...allMiddlewares, async (c, next) => {
482
619
  const websocketData = c.get('_websocketData') || {};
483
620
  const id = bun.randomUUIDv7();
484
- const data = { values: websocketData, id, path: wsRoute.path };
621
+ const dataPath = normalizedWsPath.startsWith('/') ? normalizedWsPath.slice(1) : normalizedWsPath;
622
+ const data = { values: websocketData, id, path: dataPath };
485
623
  const upgradeResult = this.server.upgrade(c.req.raw, { data });
486
624
  if (upgradeResult) {
487
625
  return new Response(null);
@@ -494,14 +632,8 @@ export class HonoAdapter extends AsenaAdapter {
494
632
  */
495
633
  async registerRouteGroup(basePath, commonMiddlewares, routes) {
496
634
  // Create grouped Hono instance
497
- const group = new Hono();
498
- // Get filtered global middlewares for this base path
499
- const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(basePath);
500
- const preparedGlobalMiddlewares = this.prepareMiddlewares(applicableGlobalMiddlewares);
501
- // Apply global middlewares first (before common middlewares)
502
- if (preparedGlobalMiddlewares.length > 0) {
503
- group.use('*', ...preparedGlobalMiddlewares);
504
- }
635
+ const group = new Hono({ strict: this._strict });
636
+ // Global middlewares are registered at top level via registerGlobalMiddlewaresTopLevel()
505
637
  // Apply common middlewares to the entire group
506
638
  const preparedCommonMiddlewares = this.prepareMiddlewares(commonMiddlewares);
507
639
  if (preparedCommonMiddlewares.length > 0) {
@@ -515,9 +647,10 @@ export class HonoAdapter extends AsenaAdapter {
515
647
  [HttpMethod.DELETE]: (path, ...handlers) => group.delete(path, ...handlers),
516
648
  [HttpMethod.PATCH]: (path, ...handlers) => group.patch(path, ...handlers),
517
649
  [HttpMethod.OPTIONS]: (path, ...handlers) => group.options(path, ...handlers),
518
- [HttpMethod.CONNECT]: (path, ...handlers) => group.on(HttpMethod.CONNECT.toUpperCase(), path, ...handlers),
519
- [HttpMethod.HEAD]: (path, ...handlers) => group.on(HttpMethod.HEAD.toUpperCase(), path, ...handlers),
520
- [HttpMethod.TRACE]: (path, ...handlers) => group.on(HttpMethod.TRACE.toUpperCase(), path, ...handlers),
650
+ [HttpMethod.CONNECT]: (path, ...handlers) => group.on([HttpMethod.CONNECT.toUpperCase()], path, ...handlers),
651
+ // HEAD is handled automatically by Hono via GET routes (HTTP spec: HEAD = GET without body).
652
+ [HttpMethod.HEAD]: (path, ...handlers) => group.get(path, ...handlers),
653
+ [HttpMethod.TRACE]: (path, ...handlers) => group.on([HttpMethod.TRACE.toUpperCase()], path, ...handlers),
521
654
  };
522
655
  // Register each route in the group
523
656
  for (const route of routes) {
@@ -526,10 +659,8 @@ export class HonoAdapter extends AsenaAdapter {
526
659
  const preparedMiddlewares = this.prepareMiddlewares(uniqueMiddlewares);
527
660
  const validators = route.validator ? await this.prepareValidator(route.validator) : [];
528
661
  const allMiddlewares = [...validators, ...preparedMiddlewares];
529
- const methodHandler = groupMethodMap[route.method];
530
- if (!methodHandler) {
531
- throw new Error(`Invalid HTTP method: ${route.method}`);
532
- }
662
+ const methodHandler = groupMethodMap[route.method] ??
663
+ ((path, ...handlers) => group.on([route.method.toUpperCase()], path, ...handlers));
533
664
  // Calculate relative path
534
665
  let relativePath = route.path;
535
666
  if (route.path.startsWith(basePath) && basePath !== '/') {
@@ -538,39 +669,40 @@ export class HonoAdapter extends AsenaAdapter {
538
669
  if (!relativePath.startsWith('/')) {
539
670
  relativePath = '/' + relativePath;
540
671
  }
672
+ // Normalize path for Hono trailing slash compatibility
673
+ const normalizedRelativePath = this.normalizePath(relativePath);
541
674
  // Register to group
542
675
  if (route.staticServe) {
543
- methodHandler(relativePath, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
676
+ methodHandler(normalizedRelativePath, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
544
677
  }
545
678
  else {
546
679
  const handler = this.prepareHandler(route.handler);
547
- methodHandler(relativePath, ...allMiddlewares, handler);
680
+ methodHandler(normalizedRelativePath, ...allMiddlewares, handler);
548
681
  }
549
682
  }
550
- // Mount group to main app
551
- this.app.route(basePath, group);
683
+ // Mount group to main app (normalize basePath too)
684
+ this.app.route(this.normalizePath(basePath), group);
552
685
  }
553
686
  /**
554
687
  * Registers a single route directly without grouping
555
688
  */
556
689
  async registerRouteDirect(route) {
557
- // Get filtered global middlewares for this route
558
- const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
559
- const preparedGlobalMiddlewares = this.prepareMiddlewares(applicableGlobalMiddlewares);
560
690
  const preparedMiddlewares = this.prepareMiddlewares(route.middlewares || []);
561
691
  const validators = route.validator ? await this.prepareValidator(route.validator) : [];
562
- // Combine: validators -> global middlewares -> route middlewares
563
- const allMiddlewares = [...validators, ...preparedGlobalMiddlewares, ...preparedMiddlewares];
564
- const methodHandler = this.methodMap[route.method];
565
- if (!methodHandler) {
566
- throw new Error(`Invalid HTTP method: ${route.method}`);
567
- }
692
+ // Global middlewares are registered at top level via registerGlobalMiddlewaresTopLevel()
693
+ const allMiddlewares = [...validators, ...preparedMiddlewares];
694
+ const methodHandler = this.methodMap[route.method] ??
695
+ ((path, ...handlers) => this.app.on([route.method.toUpperCase()], path, ...handlers));
696
+ // Normalize path for Hono: strip trailing slash (except root '/')
697
+ // Hono strict:false strips trailing slash from incoming requests,
698
+ // so registered paths must also not have trailing slash to match.
699
+ const normalizedPath = this.normalizePath(route.path);
568
700
  if (route.staticServe) {
569
- methodHandler(route.path, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
701
+ methodHandler(normalizedPath, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
570
702
  }
571
703
  else {
572
704
  const handler = this.prepareHandler(route.handler);
573
- methodHandler(route.path, ...allMiddlewares, handler);
705
+ methodHandler(normalizedPath, ...allMiddlewares, handler);
574
706
  }
575
707
  }
576
708
  /**
@@ -728,7 +860,33 @@ export class HonoAdapter extends AsenaAdapter {
728
860
  }
729
861
  lines.push(''); // Empty line between controllers
730
862
  }
863
+ // Add FrontendController groups
864
+ const frontEndGroups = this.groupFrontEndRoutesByController();
865
+ const sortedFrontEnd = Array.from(frontEndGroups.entries()).sort(([a], [b]) => a.localeCompare(b));
866
+ for (const [controllerName, group] of sortedFrontEnd) {
867
+ lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
868
+ for (const route of group.routes) {
869
+ lines.push(` ${green('HTML')} ${route.path}`);
870
+ }
871
+ lines.push('');
872
+ }
731
873
  return lines.join('\n');
732
874
  }
875
+ /**
876
+ * Groups FrontendController routes by controller name for logging
877
+ */
878
+ groupFrontEndRoutesByController() {
879
+ const groups = new Map();
880
+ for (const route of this.frontEndRouteQueue) {
881
+ if (!groups.has(route.controllerName)) {
882
+ groups.set(route.controllerName, {
883
+ basePath: route.controllerBasePath,
884
+ routes: [],
885
+ });
886
+ }
887
+ groups.get(route.controllerName).routes.push({ path: route.path });
888
+ }
889
+ return groups;
890
+ }
733
891
  }
734
892
  //# sourceMappingURL=HonoAdapter.js.map