@asenajs/hono-adapter 1.0.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/HonoAdapter.d.ts +222 -14
- package/dist/lib/HonoAdapter.js +672 -51
- package/dist/lib/HonoAdapter.js.map +1 -1
- package/dist/lib/HonoContextWrapper.d.ts +4 -5
- package/dist/lib/HonoContextWrapper.js +8 -14
- package/dist/lib/HonoContextWrapper.js.map +1 -1
- package/dist/lib/HonoWebsocketAdapter.d.ts +43 -14
- package/dist/lib/HonoWebsocketAdapter.js +175 -53
- package/dist/lib/HonoWebsocketAdapter.js.map +1 -1
- package/dist/lib/defaults/ConfigService.d.ts +2 -2
- package/dist/lib/defaults/Context.d.ts +3 -3
- package/dist/lib/defaults/MiddlewareService.d.ts +3 -3
- package/dist/lib/defaults/MiddlewareService.js +1 -1
- package/dist/lib/defaults/MiddlewareService.js.map +1 -1
- package/dist/lib/defaults/StaticServeService.d.ts +5 -0
- package/dist/lib/defaults/StaticServeService.js +4 -0
- package/dist/lib/defaults/StaticServeService.js.map +1 -0
- package/dist/lib/defaults/ValidationService.d.ts +3 -3
- package/dist/lib/defaults/ValidationService.js.map +1 -1
- package/dist/lib/defaults/index.d.ts +7 -4
- package/dist/lib/defaults/index.js +7 -4
- package/dist/lib/defaults/index.js.map +1 -1
- package/dist/lib/middlewares/CorsMiddleware.d.ts +148 -0
- package/dist/lib/middlewares/CorsMiddleware.js +169 -0
- package/dist/lib/middlewares/CorsMiddleware.js.map +1 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +329 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.js +301 -0
- package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -0
- package/dist/lib/types/HonoHandler.d.ts +10 -2
- package/dist/lib/types/StaticServeExtras.d.ts +6 -0
- package/dist/lib/types/StaticServeExtras.js +2 -0
- package/dist/lib/types/StaticServeExtras.js.map +1 -0
- package/dist/lib/types/index.d.ts +3 -2
- package/dist/lib/types/index.js +3 -2
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/utils/createHonoAdapter.d.ts +2 -2
- package/dist/lib/utils/createHonoAdapter.js +2 -2
- package/dist/lib/utils/createHonoAdapter.js.map +1 -1
- package/dist/lib/utils/middlewareParser.d.ts +9 -3
- package/dist/lib/utils/middlewareParser.js +30 -4
- package/dist/lib/utils/middlewareParser.js.map +1 -1
- package/package.json +17 -11
package/dist/lib/HonoAdapter.js
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
|
-
import { Hono
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { HTTPException } from 'hono/http-exception';
|
|
3
|
+
import * as bun from 'bun';
|
|
4
|
+
import { HonoContextWrapper } from './HonoContextWrapper';
|
|
5
|
+
import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
|
|
6
|
+
import { shouldApplyMiddleware } from '@asenajs/asena/utils/patternMatcher';
|
|
7
|
+
import { blue, green, red, yellow } from '@asenajs/asena/logger';
|
|
8
|
+
import { zValidator } from '@hono/zod-validator';
|
|
9
|
+
import { middlewareParser } from './utils/middlewareParser';
|
|
10
|
+
import { HttpMethod } from '@asenajs/asena/web-types';
|
|
11
|
+
import { HonoWebsocketAdapter } from './HonoWebsocketAdapter';
|
|
12
|
+
import { serveStatic } from 'hono/bun';
|
|
9
13
|
export class HonoAdapter extends AsenaAdapter {
|
|
10
|
-
name =
|
|
14
|
+
name = 'HonoAdapter';
|
|
11
15
|
app = new Hono();
|
|
12
16
|
server;
|
|
13
17
|
// @ts-ignore
|
|
14
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;
|
|
15
32
|
methodMap = {
|
|
16
33
|
[HttpMethod.GET]: (path, ...handlers) => this.app.get(path, ...handlers),
|
|
17
34
|
[HttpMethod.POST]: (path, ...handlers) => this.app.post(path, ...handlers),
|
|
@@ -23,40 +40,73 @@ export class HonoAdapter extends AsenaAdapter {
|
|
|
23
40
|
[HttpMethod.HEAD]: (path, ...handlers) => this.app.on(HttpMethod.HEAD.toUpperCase(), path, ...handlers),
|
|
24
41
|
[HttpMethod.TRACE]: (path, ...handlers) => this.app.on(HttpMethod.TRACE.toUpperCase(), path, ...handlers),
|
|
25
42
|
};
|
|
26
|
-
constructor(
|
|
27
|
-
super(
|
|
28
|
-
this.websocketAdapter
|
|
43
|
+
constructor(logger, websocketAdapter) {
|
|
44
|
+
super(logger, websocketAdapter);
|
|
45
|
+
if (!this.websocketAdapter) {
|
|
46
|
+
this.websocketAdapter = new HonoWebsocketAdapter(logger);
|
|
47
|
+
}
|
|
29
48
|
// to ensure that the logger is set
|
|
30
49
|
if (!this.websocketAdapter.logger && logger) {
|
|
31
50
|
this.websocketAdapter.logger = this.logger;
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 });
|
|
82
|
+
}
|
|
83
|
+
async registerRoute(params) {
|
|
84
|
+
// Queue the route for deferred registration
|
|
85
|
+
this.routeQueue.push(params);
|
|
86
|
+
}
|
|
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);
|
|
54
101
|
}
|
|
55
|
-
methodHandler(path, ...routeHandler);
|
|
56
|
-
this.logger.info(`${green("Successfully")} registered ${yellow(method.toUpperCase())} route for PATH: ${green(`${path}`)}`);
|
|
57
102
|
}
|
|
58
103
|
async start() {
|
|
59
|
-
|
|
104
|
+
// Register all queued routes with optimization
|
|
105
|
+
if (!this.routesRegistered) {
|
|
106
|
+
await this.optimizeAndRegisterRoutes();
|
|
107
|
+
this.routesRegistered = true;
|
|
108
|
+
}
|
|
109
|
+
this.websocketAdapter.prepareWebSocket(this.options?.wsOptions);
|
|
60
110
|
this.server = bun.serve({
|
|
61
111
|
...this.options.serveOptions,
|
|
62
112
|
port: this.port,
|
|
@@ -64,11 +114,80 @@ export class HonoAdapter extends AsenaAdapter {
|
|
|
64
114
|
websocket: this.websocketAdapter.websocket,
|
|
65
115
|
});
|
|
66
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}`);
|
|
67
122
|
return this.server;
|
|
68
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
|
+
*/
|
|
69
149
|
onError(errorHandler) {
|
|
70
150
|
this.app.onError((error, context) => {
|
|
71
|
-
|
|
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
|
+
}
|
|
72
191
|
});
|
|
73
192
|
}
|
|
74
193
|
async serveOptions(options) {
|
|
@@ -83,34 +202,536 @@ export class HonoAdapter extends AsenaAdapter {
|
|
|
83
202
|
prepareHandler(handler) {
|
|
84
203
|
return (c) => handler(new HonoContextWrapper(c));
|
|
85
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
|
+
*/
|
|
210
|
+
prepareStaticServeOptions(staticServe) {
|
|
211
|
+
let staticServeOptions = {
|
|
212
|
+
root: staticServe.root,
|
|
213
|
+
};
|
|
214
|
+
if (staticServe.rewriteRequestPath) {
|
|
215
|
+
staticServeOptions.rewriteRequestPath = staticServe.rewriteRequestPath;
|
|
216
|
+
}
|
|
217
|
+
if (staticServe.onFound) {
|
|
218
|
+
if (staticServe.onFound.override) {
|
|
219
|
+
// @ts-ignore
|
|
220
|
+
staticServeOptions.onFound = staticServe.onFound.handler;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
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));
|
|
240
|
+
};
|
|
241
|
+
}
|
|
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
|
+
}
|
|
259
|
+
if (staticServe.onNotFound) {
|
|
260
|
+
if (staticServe.onNotFound.override) {
|
|
261
|
+
// @ts-ignore
|
|
262
|
+
staticServeOptions.onNotFound = staticServe.onNotFound.handler;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
staticServeOptions.onNotFound = (path, c) => {
|
|
266
|
+
staticServe.onNotFound.handler(path, new HonoContextWrapper(c));
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (staticServe.extra) {
|
|
271
|
+
staticServeOptions.mimes = staticServe.extra.mimes;
|
|
272
|
+
staticServeOptions.precompressed = staticServe.extra.precompressed;
|
|
273
|
+
}
|
|
274
|
+
return staticServeOptions;
|
|
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
|
+
*/
|
|
86
281
|
async prepareValidator(baseValidator) {
|
|
87
282
|
if (!baseValidator) {
|
|
88
283
|
return [];
|
|
89
284
|
}
|
|
90
285
|
const validators = [];
|
|
91
286
|
for (const key of VALIDATOR_METHODS) {
|
|
92
|
-
|
|
93
|
-
if
|
|
94
|
-
|
|
287
|
+
const validatorHandler = baseValidator[key];
|
|
288
|
+
// Skip if not a valid validator
|
|
289
|
+
if (!validatorHandler || typeof validatorHandler.handle !== 'function') {
|
|
95
290
|
continue;
|
|
96
291
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
104
328
|
}
|
|
105
|
-
|
|
106
|
-
|
|
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)}`);
|
|
107
332
|
}
|
|
108
|
-
validators.push(zValidator(key, schema, hook));
|
|
109
333
|
}
|
|
110
334
|
return validators;
|
|
111
335
|
}
|
|
112
|
-
|
|
113
|
-
|
|
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');
|
|
114
735
|
}
|
|
115
736
|
}
|
|
116
737
|
//# sourceMappingURL=HonoAdapter.js.map
|