@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.
- package/README.md +2 -2
- package/dist/lib/HonoAdapter.d.ts +49 -4
- package/dist/lib/HonoAdapter.js +209 -51
- package/dist/lib/HonoAdapter.js.map +1 -1
- package/dist/lib/HonoContextWrapper.d.ts +17 -3
- package/dist/lib/HonoContextWrapper.js +85 -2
- package/dist/lib/HonoContextWrapper.js.map +1 -1
- package/dist/lib/HonoWebsocketAdapter.d.ts +2 -16
- package/dist/lib/HonoWebsocketAdapter.js +26 -50
- package/dist/lib/HonoWebsocketAdapter.js.map +1 -1
- package/dist/lib/defaults/ValidationService.d.ts +4 -4
- package/dist/lib/middlewares/CorsMiddleware.d.ts +1 -1
- package/dist/lib/middlewares/CorsMiddleware.js +5 -6
- package/dist/lib/middlewares/CorsMiddleware.js.map +1 -1
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +1 -1
- package/dist/lib/middlewares/RateLimiterMiddleware.js +9 -7
- package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -1
- package/dist/lib/types/HonoAdapterOptions.d.ts +27 -0
- package/dist/lib/types/HonoAdapterOptions.js +2 -0
- package/dist/lib/types/HonoAdapterOptions.js.map +1 -0
- package/dist/lib/types/SendPingStrategy.d.ts +4 -0
- package/dist/lib/types/SendPingStrategy.js +2 -0
- package/dist/lib/types/SendPingStrategy.js.map +1 -0
- package/dist/lib/types/index.d.ts +2 -0
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/utils/createHonoAdapter.d.ts +2 -1
- package/dist/lib/utils/createHonoAdapter.js +9 -1
- package/dist/lib/utils/createHonoAdapter.js.map +1 -1
- package/dist/lib/utils/middlewareParser.d.ts +5 -1
- package/dist/lib/utils/middlewareParser.js +16 -6
- package/dist/lib/utils/middlewareParser.js.map +1 -1
- package/dist/lib/utils/routePriority.d.ts +40 -0
- package/dist/lib/utils/routePriority.js +75 -0
- package/dist/lib/utils/routePriority.js.map +1 -0
- 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/
|
|
48
|
-
import { Get } from '@asenajs/asena/
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
}
|
package/dist/lib/HonoAdapter.js
CHANGED
|
@@ -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/
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
520
|
-
[HttpMethod.
|
|
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
|
-
|
|
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(
|
|
676
|
+
methodHandler(normalizedRelativePath, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
|
|
544
677
|
}
|
|
545
678
|
else {
|
|
546
679
|
const handler = this.prepareHandler(route.handler);
|
|
547
|
-
methodHandler(
|
|
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
|
-
//
|
|
563
|
-
const allMiddlewares = [...validators, ...
|
|
564
|
-
const methodHandler = this.methodMap[route.method]
|
|
565
|
-
|
|
566
|
-
|
|
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(
|
|
701
|
+
methodHandler(normalizedPath, ...allMiddlewares, serveStatic(this.prepareStaticServeOptions(route.staticServe)));
|
|
570
702
|
}
|
|
571
703
|
else {
|
|
572
704
|
const handler = this.prepareHandler(route.handler);
|
|
573
|
-
methodHandler(
|
|
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
|