@asenajs/ergenecore 1.2.0 → 1.3.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 (47) hide show
  1. package/README.md +17 -17
  2. package/dist/lib/Ergenecore.d.ts +21 -0
  3. package/dist/lib/Ergenecore.js +178 -59
  4. package/dist/lib/Ergenecore.js.map +1 -1
  5. package/dist/lib/ErgenecoreContextWrapper.d.ts +45 -4
  6. package/dist/lib/ErgenecoreContextWrapper.js +122 -7
  7. package/dist/lib/ErgenecoreContextWrapper.js.map +1 -1
  8. package/dist/lib/ErgenecoreWebsocketAdapter.d.ts +2 -16
  9. package/dist/lib/ErgenecoreWebsocketAdapter.js +20 -48
  10. package/dist/lib/ErgenecoreWebsocketAdapter.js.map +1 -1
  11. package/dist/lib/defaults/ConfigService.d.ts +5 -2
  12. package/dist/lib/defaults/ConfigService.js +1 -1
  13. package/dist/lib/defaults/ConfigService.js.map +1 -1
  14. package/dist/lib/defaults/MiddlewareService.d.ts +1 -1
  15. package/dist/lib/defaults/MiddlewareService.js +1 -1
  16. package/dist/lib/defaults/StaticServeService.d.ts +1 -1
  17. package/dist/lib/defaults/StaticServeService.js +1 -1
  18. package/dist/lib/defaults/ValidationService.d.ts +1 -1
  19. package/dist/lib/defaults/ValidationService.js +1 -1
  20. package/dist/lib/errors.d.ts +24 -5
  21. package/dist/lib/errors.js +15 -4
  22. package/dist/lib/errors.js.map +1 -1
  23. package/dist/lib/index.d.ts +5 -1
  24. package/dist/lib/index.js +6 -0
  25. package/dist/lib/index.js.map +1 -1
  26. package/dist/lib/middlewares/CorsMiddleware.d.ts +1 -1
  27. package/dist/lib/middlewares/CorsMiddleware.js +1 -1
  28. package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +1 -1
  29. package/dist/lib/middlewares/RateLimiterMiddleware.js +5 -2
  30. package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -1
  31. package/dist/lib/stream/SSEStreamWriter.d.ts +9 -0
  32. package/dist/lib/stream/SSEStreamWriter.js +18 -0
  33. package/dist/lib/stream/SSEStreamWriter.js.map +1 -0
  34. package/dist/lib/stream/StreamWriter.d.ts +24 -0
  35. package/dist/lib/stream/StreamWriter.js +72 -0
  36. package/dist/lib/stream/StreamWriter.js.map +1 -0
  37. package/dist/lib/stream/index.d.ts +2 -0
  38. package/dist/lib/stream/index.js +3 -0
  39. package/dist/lib/stream/index.js.map +1 -0
  40. package/dist/lib/types/SendPingStrategy.d.ts +4 -0
  41. package/dist/lib/types/SendPingStrategy.js +2 -0
  42. package/dist/lib/types/SendPingStrategy.js.map +1 -0
  43. package/dist/lib/types/Validation.d.ts +3 -3
  44. package/dist/lib/types/index.d.ts +1 -0
  45. package/dist/lib/types/index.js +2 -0
  46. package/dist/lib/types/index.js.map +1 -1
  47. package/package.json +12 -12
package/README.md CHANGED
@@ -70,8 +70,8 @@ await server.start();
70
70
  ### Controller Example
71
71
 
72
72
  ```typescript
73
- import { Controller } from '@asenajs/asena/server';
74
- import { Get, Post, Put, Delete } from '@asenajs/asena/web';
73
+ import { Controller } from '@asenajs/asena/decorators';
74
+ import { Get, Post, Put, Delete } from '@asenajs/asena/decorators/http';
75
75
  import type { Context } from '@asenajs/ergenecore/types';
76
76
 
77
77
  @Controller('/users')
@@ -225,9 +225,9 @@ const session = context.getCookie('session');
225
225
  #### Custom Middleware
226
226
 
227
227
  ```typescript
228
- import { Middleware } from '@asenajs/asena/server';
229
- import { Controller } from '@asenajs/asena/server';
230
- import { Get } from '@asenajs/asena/web';
228
+ import { Middleware } from '@asenajs/asena/decorators';
229
+ import { Controller } from '@asenajs/asena/decorators';
230
+ import { Get } from '@asenajs/asena/decorators/http';
231
231
  import { MiddlewareService, type Context } from '@asenajs/ergenecore';
232
232
 
233
233
  @Middleware()
@@ -262,7 +262,7 @@ export class AdminController {
262
262
  Ergenecore comes with a built-in CORS middleware for handling Cross-Origin Resource Sharing:
263
263
 
264
264
  ```typescript
265
- import { Middleware } from '@asenajs/asena/server';
265
+ import { Middleware } from '@asenajs/asena/decorators';
266
266
  import { CorsMiddleware } from '@asenajs/ergenecore';
267
267
 
268
268
  // Allow all origins (default)
@@ -303,7 +303,7 @@ export class DynamicCors extends CorsMiddleware {
303
303
  }
304
304
 
305
305
  // Use globally in server config
306
- import { Config } from '@asenajs/asena/server';
306
+ import { Config } from '@asenajs/asena/decorators';
307
307
  import { ConfigService } from '@asenajs/ergenecore';
308
308
 
309
309
  @Config()
@@ -326,7 +326,7 @@ export class ApiController {
326
326
  Ergenecore includes a Token Bucket-based rate limiter for controlling request rates and preventing abuse:
327
327
 
328
328
  ```typescript
329
- import { Middleware } from '@asenajs/asena/server';
329
+ import { Middleware } from '@asenajs/asena/decorators';
330
330
  import { RateLimiterMiddleware } from '@asenajs/ergenecore';
331
331
 
332
332
  // General API rate limiter (100 requests per minute)
@@ -381,7 +381,7 @@ export class CustomRateLimiter extends RateLimiterMiddleware {
381
381
  }
382
382
 
383
383
  // Use globally
384
- import { Config } from '@asenajs/asena/server';
384
+ import { Config } from '@asenajs/asena/decorators';
385
385
  import { ConfigService } from '@asenajs/ergenecore';
386
386
 
387
387
  @Config()
@@ -432,8 +432,8 @@ The middleware automatically sets standard rate limit headers:
432
432
  ### Validation with Zod
433
433
 
434
434
  ```typescript
435
- import { Controller, Middleware } from '@asenajs/asena/server';
436
- import { Post } from '@asenajs/asena/web';
435
+ import { Controller, Middleware } from '@asenajs/asena/decorators';
436
+ import { Post } from '@asenajs/asena/decorators/http';
437
437
  import { ValidationService, type Context, type ValidationSchema, type ValidationSchemaWithHook } from '@asenajs/ergenecore';
438
438
  import { z } from 'zod';
439
439
 
@@ -471,8 +471,8 @@ async json(): Promise<ValidationSchema | ValidationSchemaWithHook> {
471
471
  ### Combining Middleware and Validation
472
472
 
473
473
  ```typescript
474
- import { Controller, Middleware } from '@asenajs/asena/server';
475
- import { Post } from '@asenajs/asena/web';
474
+ import { Controller, Middleware } from '@asenajs/asena/decorators';
475
+ import { Post } from '@asenajs/asena/decorators/http';
476
476
  import {
477
477
  MiddlewareService,
478
478
  ValidationService,
@@ -556,8 +556,8 @@ export class ChatSocket extends AsenaWebSocketService<void> {
556
556
  ### Static File Serving
557
557
 
558
558
  ```typescript
559
- import { Controller } from '@asenajs/asena/server';
560
- import { Get } from '@asenajs/asena/web';
559
+ import { Controller } from '@asenajs/asena/decorators';
560
+ import { Get } from '@asenajs/asena/decorators/http';
561
561
  import { StaticServe, StaticServeService } from '@asenajs/asena/static';
562
562
  import type { Context } from '@asenajs/ergenecore/types';
563
563
 
@@ -587,8 +587,8 @@ export class StaticController {
587
587
  ### Error Handling
588
588
 
589
589
  ```typescript
590
- import { Config } from '@asenajs/asena/server';
591
- import { Inject } from '@asenajs/asena/ioc';
590
+ import { Config } from '@asenajs/asena/decorators';
591
+ import { Inject } from '@asenajs/asena/decorators/ioc';
592
592
  import { ConfigService, type Context } from '@asenajs/ergenecore';
593
593
 
594
594
  @Config()
@@ -56,6 +56,15 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
56
56
  * Config is optional - if not provided, middleware applies to all routes
57
57
  */
58
58
  private globalMiddlewares;
59
+ /**
60
+ * HTML routes for FrontendController pages
61
+ * Stored separately and merged into Bun.serve() routes at start time
62
+ */
63
+ private htmlRoutes;
64
+ /**
65
+ * Queue of FrontendController route metadata for deferred logging at start time
66
+ */
67
+ private frontEndRouteQueue;
59
68
  private options;
60
69
  /**
61
70
  * Creates a new CoreAdapter instance
@@ -107,6 +116,14 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
107
116
  * });
108
117
  * ```
109
118
  */
119
+ /**
120
+ * Registers an HTML route for FrontendController pages.
121
+ * HTML routes bypass the middleware chain and are served directly by Bun.serve().
122
+ *
123
+ * @param path - Full URL path (e.g., '/ui/home')
124
+ * @param htmlBundle - The HTML bundle returned by importing an .html file
125
+ */
126
+ registerHTMLRoute(path: string, htmlBundle: unknown, controllerName: string, controllerBasePath: string): void;
110
127
  use(middleware: BaseMiddleware<Context>, config?: GlobalMiddlewareConfig['routes']): void;
111
128
  /**
112
129
  * Registers a route (deferred registration)
@@ -595,4 +612,8 @@ export declare class Ergenecore extends AsenaAdapter<Context, ValidationSchemaWi
595
612
  * ```
596
613
  */
597
614
  private buildControllerBasedLog;
615
+ /**
616
+ * Groups FrontendController routes by controller name for logging
617
+ */
618
+ private groupFrontEndRoutesByController;
598
619
  }
@@ -1,11 +1,11 @@
1
1
  import { AsenaAdapter, VALIDATOR_METHODS, } from '@asenajs/asena/adapter';
2
2
  import { blue, green, red, yellow } from '@asenajs/asena/logger';
3
- import { shouldApplyMiddleware } from '@asenajs/asena/utlis';
4
3
  import { ErgenecoreWebsocketAdapter } from './ErgenecoreWebsocketAdapter';
5
4
  import { ErgenecoreContextWrapper } from './ErgenecoreContextWrapper';
6
5
  import * as Bun from 'bun';
7
6
  import * as path from 'path';
8
7
  import { HttpException, MiddlewareResponseError } from './errors';
8
+ import { shouldApplyMiddleware } from '@asenajs/asena/utils';
9
9
  /**
10
10
  * Static response headers for performance
11
11
  *
@@ -63,6 +63,15 @@ export class Ergenecore extends AsenaAdapter {
63
63
  * Config is optional - if not provided, middleware applies to all routes
64
64
  */
65
65
  globalMiddlewares = [];
66
+ /**
67
+ * HTML routes for FrontendController pages
68
+ * Stored separately and merged into Bun.serve() routes at start time
69
+ */
70
+ htmlRoutes = new Map();
71
+ /**
72
+ * Queue of FrontendController route metadata for deferred logging at start time
73
+ */
74
+ frontEndRouteQueue = [];
66
75
  options = {};
67
76
  /**
68
77
  * Creates a new CoreAdapter instance
@@ -126,6 +135,28 @@ export class Ergenecore extends AsenaAdapter {
126
135
  * });
127
136
  * ```
128
137
  */
138
+ /**
139
+ * Registers an HTML route for FrontendController pages.
140
+ * HTML routes bypass the middleware chain and are served directly by Bun.serve().
141
+ *
142
+ * @param path - Full URL path (e.g., '/ui/home')
143
+ * @param htmlBundle - The HTML bundle returned by importing an .html file
144
+ */
145
+ registerHTMLRoute(path, htmlBundle, controllerName, controllerBasePath) {
146
+ if (this.htmlRoutes.has(path)) {
147
+ throw new Error(`Duplicate HTML route: "${path}" is already registered.`);
148
+ }
149
+ this.htmlRoutes.set(path, htmlBundle);
150
+ this.frontEndRouteQueue.push({ path, controllerName, controllerBasePath });
151
+ // Register trailing slash variant for consistent routing
152
+ // e.g., /ui → also register /ui/ (or vice versa)
153
+ if (path !== '/' && !path.endsWith('/')) {
154
+ this.htmlRoutes.set(`${path}/`, htmlBundle);
155
+ }
156
+ else if (path !== '/' && path.endsWith('/')) {
157
+ this.htmlRoutes.set(path.slice(0, -1), htmlBundle);
158
+ }
159
+ }
129
160
  use(middleware, config) {
130
161
  this.globalMiddlewares.push({ middleware, config });
131
162
  }
@@ -178,6 +209,13 @@ export class Ergenecore extends AsenaAdapter {
178
209
  this.checkPathCollisions(httpRoutes, wsRoutes);
179
210
  // 4. Merge routes
180
211
  const finalRoutes = this.mergeRoutes(httpRoutes, wsRoutes);
212
+ // 4.5. Merge HTML routes (FrontendController pages)
213
+ for (const [htmlPath, htmlBundle] of this.htmlRoutes) {
214
+ if (finalRoutes[htmlPath]) {
215
+ throw new Error(`HTML route collision at "${htmlPath}": path already registered as an API or WebSocket route.`);
216
+ }
217
+ finalRoutes[htmlPath] = htmlBundle;
218
+ }
181
219
  // 5. Prepare WebSocket before starting server
182
220
  await this.websocketAdapter.prepareWebSocket(this.options.wsOptions);
183
221
  const serverPort = port ?? this.port;
@@ -193,7 +231,7 @@ export class Ergenecore extends AsenaAdapter {
193
231
  await this.websocketAdapter.startWebsocket(this.server);
194
232
  this.routesBuilt = true;
195
233
  // Log controller summary first
196
- if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0) {
234
+ if (this.routeQueue.length > 0 || this.wsRouteQueue.length > 0 || this.frontEndRouteQueue.length > 0) {
197
235
  this.logControllerSummary();
198
236
  // Then log detailed route list
199
237
  this.logger.info(this.buildControllerBasedLog());
@@ -275,7 +313,15 @@ export class Ergenecore extends AsenaAdapter {
275
313
  }
276
314
  // Build Bun router object for each path
277
315
  for (const [path, pathRoutes] of routesByPath) {
278
- routes[path] = {};
316
+ // Normalize trailing slash to support both variants
317
+ // This ensures /users and /users/ both work with query params
318
+ const normalizedPath = path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;
319
+ const pathWithSlash = normalizedPath === '/' ? '/' : `${normalizedPath}/`;
320
+ // Initialize both path variants (skip duplicate for root path)
321
+ routes[normalizedPath] = routes[normalizedPath] || {};
322
+ if (normalizedPath !== '/') {
323
+ routes[pathWithSlash] = routes[pathWithSlash] || {};
324
+ }
279
325
  for (const route of pathRoutes) {
280
326
  const method = route.method.toUpperCase();
281
327
  // Fast Path Optimization with Pattern Matching
@@ -286,19 +332,48 @@ export class Ergenecore extends AsenaAdapter {
286
332
  (!route.middlewares || route.middlewares.length === 0) &&
287
333
  !route.validator &&
288
334
  !route.staticServe;
289
- if (isSimpleRoute) {
290
- // Use fast path handler for simple routes (minimal overhead)
291
- routes[path][method] = this.createFastPathHandler(route);
335
+ // Create handler based on route complexity
336
+ const handler = isSimpleRoute
337
+ ? this.createFastPathHandler(route)
338
+ : this.createRouteHandler(route, commonMiddlewares);
339
+ // Register handler to both path variants for trailing slash support
340
+ if (method === 'ALL') {
341
+ // Bun native: direct handler function matches all HTTP methods
342
+ routes[normalizedPath] = handler;
343
+ if (normalizedPath !== '/') {
344
+ routes[pathWithSlash] = handler;
345
+ }
292
346
  }
293
347
  else {
294
- // Use full-featured handler for complex routes
295
- routes[path][method] = this.createRouteHandler(route, commonMiddlewares);
348
+ routes[normalizedPath][method] = handler;
349
+ if (normalizedPath !== '/') {
350
+ routes[pathWithSlash][method] = handler;
351
+ }
296
352
  }
297
353
  }
298
354
  }
299
355
  }
300
- // Add 404 handler with static headers
301
- routes['/*'] = () => {
356
+ // Catch-all handler for unmatched routes (including OPTIONS preflight)
357
+ // Runs global middlewares so CORS and other cross-cutting concerns work for all requests
358
+ routes['/*'] = async (req) => {
359
+ const context = new ErgenecoreContextWrapper(req, this.server);
360
+ const requestPath = new URL(req.url).pathname;
361
+ // Filter and run applicable global middlewares
362
+ const applicableMiddlewares = this.globalMiddlewares
363
+ .filter(({ config }) => shouldApplyMiddleware(requestPath, config))
364
+ .map(({ middleware }) => middleware);
365
+ if (applicableMiddlewares.length > 0) {
366
+ const result = await this.executeMiddlewares(context, applicableMiddlewares);
367
+ if (result instanceof Response)
368
+ return result;
369
+ if (result === false) {
370
+ return new Response('Forbidden', { status: 403 });
371
+ }
372
+ }
373
+ // No middleware handled the request → error handler or 404
374
+ if (this.errorHandler) {
375
+ return this.errorHandler(new Error('Not Found'), context);
376
+ }
302
377
  return new Response(JSON.stringify({ error: 'Not Found' }), {
303
378
  status: 404,
304
379
  headers: STATIC_JSON_HEADERS,
@@ -363,15 +438,24 @@ export class Ergenecore extends AsenaAdapter {
363
438
  * }
364
439
  * ```
365
440
  */
366
- async executeMiddlewares(context, middlewares, index = 0) {
441
+ async executeMiddlewares(context, middlewares, index = 0, onComplete) {
367
442
  // Base case: all middlewares executed successfully
368
443
  if (index >= middlewares.length) {
444
+ if (onComplete) {
445
+ const result = await onComplete();
446
+ if (result instanceof Response)
447
+ return result;
448
+ if (result === false)
449
+ return false;
450
+ }
369
451
  return true;
370
452
  }
371
453
  const middleware = middlewares[index];
454
+ let nextCalled = false;
372
455
  // Create next() function that executes the next middleware in chain
373
456
  const next = async () => {
374
- const result = await this.executeMiddlewares(context, middlewares, index + 1);
457
+ nextCalled = true;
458
+ const result = await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
375
459
  // If next middleware returned Response, throw MiddlewareResponseError to propagate it
376
460
  if (result instanceof Response) {
377
461
  throw new MiddlewareResponseError(result);
@@ -392,7 +476,11 @@ export class Ergenecore extends AsenaAdapter {
392
476
  if (result === false) {
393
477
  return false;
394
478
  }
395
- // Middleware executed successfully
479
+ // If middleware returned true/void without calling next(), auto-continue the chain
480
+ if (!nextCalled) {
481
+ return await this.executeMiddlewares(context, middlewares, index + 1, onComplete);
482
+ }
483
+ // Middleware called next() and completed successfully
396
484
  return true;
397
485
  }
398
486
  catch (error) {
@@ -442,7 +530,7 @@ export class Ergenecore extends AsenaAdapter {
442
530
  return async (req) => {
443
531
  try {
444
532
  // Create context wrapper
445
- const context = new ErgenecoreContextWrapper(req);
533
+ const context = new ErgenecoreContextWrapper(req, this.server);
446
534
  // Execute filtered global middlewares with real next() chain
447
535
  if (applicableGlobalMiddlewares.length > 0) {
448
536
  const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
@@ -583,7 +671,8 @@ export class Ergenecore extends AsenaAdapter {
583
671
  // If no error handler is set, use ultra-minimal version with default error handling
584
672
  if (!this.errorHandler) {
585
673
  return async (req) => {
586
- const context = new ErgenecoreContextWrapper(req);
674
+ const context = new ErgenecoreContextWrapper(req, this.server);
675
+ context.routePattern = route.path;
587
676
  try {
588
677
  // Inject Bun's native route params if present
589
678
  // @ts-expect-error - Bun adds params to Request
@@ -624,7 +713,8 @@ export class Ergenecore extends AsenaAdapter {
624
713
  }
625
714
  // With error handler (minimal try-catch)
626
715
  return async (req) => {
627
- const context = new ErgenecoreContextWrapper(req);
716
+ const context = new ErgenecoreContextWrapper(req, this.server);
717
+ context.routePattern = route.path;
628
718
  try {
629
719
  // Inject Bun's native route params if present
630
720
  // @ts-expect-error - Bun adds params to Request
@@ -676,7 +766,8 @@ export class Ergenecore extends AsenaAdapter {
676
766
  const applicableGlobalMiddlewares = this.getGlobalMiddlewaresForPath(route.path);
677
767
  return async (req) => {
678
768
  // Create context wrapper outside try block so it's accessible in catch
679
- const context = new ErgenecoreContextWrapper(req);
769
+ const context = new ErgenecoreContextWrapper(req, this.server);
770
+ context.routePattern = route.path;
680
771
  // Inject Bun's native route params
681
772
  // @ts-expect-error - Bun adds params to Request
682
773
  if (req.params) {
@@ -687,53 +778,48 @@ export class Ergenecore extends AsenaAdapter {
687
778
  }
688
779
  }
689
780
  try {
690
- // Execute filtered global middlewares with real next() chain
691
- if (applicableGlobalMiddlewares.length > 0) {
692
- const result = await this.executeMiddlewares(context, applicableGlobalMiddlewares);
693
- // If middleware returned a custom response, return it
694
- if (result instanceof Response) {
695
- return result;
781
+ // Combine global + route middlewares into a single chain
782
+ const allMiddlewares = [
783
+ ...applicableGlobalMiddlewares,
784
+ ...(route.middlewares || []),
785
+ ];
786
+ // Execute middleware chain with handler as onComplete callback
787
+ // This ensures the handler runs INSIDE the middleware async context
788
+ const result = await this.executeMiddlewares(context, allMiddlewares, 0, async () => {
789
+ // Execute validation
790
+ if (route.validator) {
791
+ const validationResult = await this.validateRequest(context, route.validator);
792
+ if (validationResult)
793
+ return validationResult;
696
794
  }
697
- // If middleware returned false, return 403
698
- if (result === false) {
699
- return new Response('Forbidden', { status: 403 });
700
- }
701
- }
702
- // Execute route middlewares with real next() chain
703
- if (route.middlewares && route.middlewares.length > 0) {
704
- const result = await this.executeMiddlewares(context, route.middlewares);
705
- // If middleware returned a custom response, return it
706
- if (result instanceof Response) {
707
- return result;
795
+ // Handle static file serving
796
+ if (route.staticServe) {
797
+ const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
798
+ if (staticResponse)
799
+ return staticResponse;
708
800
  }
709
- // If middleware returned false, return 403
710
- if (result === false) {
711
- return new Response('Forbidden', { status: 403 });
801
+ // Execute route handler
802
+ const response = await route.handler(context);
803
+ // If handler returns Response, return it directly
804
+ if (response instanceof Response) {
805
+ return response;
712
806
  }
807
+ // Otherwise, wrap in Response
808
+ return new Response(JSON.stringify(response), {
809
+ status: 200,
810
+ headers: { 'Content-Type': 'application/json' },
811
+ });
812
+ });
813
+ // If middleware chain returned a custom response, return it
814
+ if (result instanceof Response) {
815
+ return result;
713
816
  }
714
- // Execute validation
715
- if (route.validator) {
716
- const validationResult = await this.validateRequest(context, route.validator);
717
- if (validationResult)
718
- return validationResult;
719
- }
720
- // Handle static file serving
721
- if (route.staticServe) {
722
- const staticResponse = await this.serveStaticFile(req, context, route.staticServe);
723
- if (staticResponse)
724
- return staticResponse;
725
- }
726
- // Execute route handler
727
- const response = await route.handler(context);
728
- // If handler returns Response, return it directly
729
- if (response instanceof Response) {
730
- return response;
817
+ // If middleware chain returned false (stopped), return 403
818
+ if (result === false) {
819
+ return new Response('Forbidden', { status: 403 });
731
820
  }
732
- // Otherwise, wrap in Response
733
- return new Response(JSON.stringify(response), {
734
- status: 200,
735
- headers: { 'Content-Type': 'application/json' },
736
- });
821
+ // Default: result is true (should not happen with onComplete, but just in case)
822
+ return new Response(null, { status: 204 });
737
823
  }
738
824
  catch (error) {
739
825
  // If handler or middleware threw HttpException, convert to Response
@@ -1278,6 +1364,13 @@ export class Ergenecore extends AsenaAdapter {
1278
1364
  this.logger.info(`${green('✓')} Successfully registered ${yellow('WEBSOCKET')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
1279
1365
  }
1280
1366
  }
1367
+ // Log FrontendControllers
1368
+ const frontEndGroups = this.groupFrontEndRoutesByController();
1369
+ for (const [controllerName, group] of frontEndGroups) {
1370
+ const routeCount = group.routes.length;
1371
+ const routeText = routeCount === 1 ? 'route' : 'routes';
1372
+ this.logger.info(`${green('✓')} Successfully registered ${yellow('FRONTEND')} ${blue(controllerName)} ${yellow(`(${routeCount} ${routeText})`)}`);
1373
+ }
1281
1374
  }
1282
1375
  /**
1283
1376
  * Builds controller-based log output with colors
@@ -1394,7 +1487,33 @@ export class Ergenecore extends AsenaAdapter {
1394
1487
  }
1395
1488
  lines.push(''); // Empty line between namespaces
1396
1489
  }
1490
+ // 4. Frontend controllers (HTML routes)
1491
+ const frontEndGroups = this.groupFrontEndRoutesByController();
1492
+ const sortedFrontEnd = Array.from(frontEndGroups.entries()).sort(([a], [b]) => a.localeCompare(b));
1493
+ for (const [controllerName, group] of sortedFrontEnd) {
1494
+ lines.push(` ${blue(controllerName)} ${yellow(`(${group.basePath})`)}`);
1495
+ for (const route of group.routes) {
1496
+ lines.push(` ${green('HTML')} ${route.path}`);
1497
+ }
1498
+ lines.push('');
1499
+ }
1397
1500
  return lines.join('\n');
1398
1501
  }
1502
+ /**
1503
+ * Groups FrontendController routes by controller name for logging
1504
+ */
1505
+ groupFrontEndRoutesByController() {
1506
+ const groups = new Map();
1507
+ for (const route of this.frontEndRouteQueue) {
1508
+ if (!groups.has(route.controllerName)) {
1509
+ groups.set(route.controllerName, {
1510
+ basePath: route.controllerBasePath,
1511
+ routes: [],
1512
+ });
1513
+ }
1514
+ groups.get(route.controllerName).routes.push({ path: route.path });
1515
+ }
1516
+ return groups;
1517
+ }
1399
1518
  }
1400
1519
  //# sourceMappingURL=Ergenecore.js.map