@velajs/vela 0.5.2 → 0.6.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.
@@ -1,45 +1,27 @@
1
1
  import type { Hono } from 'hono';
2
2
  import type { Container } from './container/container';
3
3
  import type { Token } from './container/types';
4
- import type { CorsOptions } from './cors/cors.types';
5
4
  import type { RouteManager } from './http/route.manager';
6
- import type { FilterType, GuardType, InterceptorType, MiddlewareType, PipeType } from './registry/types';
5
+ import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
7
6
  export declare class VelaApplication {
8
7
  private readonly container;
9
8
  private readonly routeManager;
10
9
  private instances;
11
10
  private honoApp;
12
- private routesBuilt;
13
11
  constructor(container: Container, routeManager: RouteManager);
14
12
  /** Pre-build routes (handles async CRUD imports). Called by VelaFactory. */
15
13
  initRoutes(): Promise<void>;
16
- /**
17
- * Rebuild routes. Call after registering global middleware/guards/etc.
18
- * Not needed if you don't use useGlobalMiddleware() post-create.
19
- */
20
- rebuild(): Promise<void>;
21
14
  private getApp;
22
15
  get fetch(): Hono['fetch'];
23
16
  getInstances(): unknown[];
24
17
  getContainer(): Container;
25
18
  setInstances(instances: unknown[]): void;
26
- /**
27
- * Set a global prefix for all routes (e.g. '/api').
28
- * Must be called before getHonoApp()/fetch, or call rebuild() after.
29
- */
30
- setGlobalPrefix(prefix: string): this;
31
- /**
32
- * Register global middleware. Call rebuild() after if called post-create.
33
- * Controller/method-level @UseMiddleware works without rebuild.
34
- */
35
- useGlobalMiddleware(...middleware: MiddlewareType[]): this;
19
+ get<T>(token: Token<T>): T;
20
+ getHonoApp(): Hono;
36
21
  useGlobalPipes(...pipes: PipeType[]): this;
37
22
  useGlobalGuards(...guards: GuardType[]): this;
38
23
  useGlobalInterceptors(...interceptors: InterceptorType[]): this;
39
24
  useGlobalFilters(...filters: FilterType[]): this;
40
- enableCors(options?: CorsOptions): this;
41
- get<T>(token: Token<T>): T;
42
- getHonoApp(): Hono;
43
25
  callOnModuleInit(): Promise<void>;
44
26
  callOnApplicationBootstrap(): Promise<void>;
45
27
  close(signal?: string): Promise<void>;
@@ -1,25 +1,15 @@
1
- import { cors } from "hono/cors";
2
1
  import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
3
2
  export class VelaApplication {
4
3
  container;
5
4
  routeManager;
6
5
  instances = [];
7
6
  honoApp = null;
8
- routesBuilt = false;
9
7
  constructor(container, routeManager){
10
8
  this.container = container;
11
9
  this.routeManager = routeManager;
12
10
  }
13
11
  /** Pre-build routes (handles async CRUD imports). Called by VelaFactory. */ async initRoutes() {
14
12
  this.honoApp = await this.routeManager.build();
15
- this.routesBuilt = true;
16
- }
17
- /**
18
- * Rebuild routes. Call after registering global middleware/guards/etc.
19
- * Not needed if you don't use useGlobalMiddleware() post-create.
20
- */ async rebuild() {
21
- this.honoApp = await this.routeManager.build();
22
- this.routesBuilt = true;
23
13
  }
24
14
  getApp() {
25
15
  if (!this.honoApp) {
@@ -39,20 +29,13 @@ export class VelaApplication {
39
29
  setInstances(instances) {
40
30
  this.instances = instances;
41
31
  }
42
- /**
43
- * Set a global prefix for all routes (e.g. '/api').
44
- * Must be called before getHonoApp()/fetch, or call rebuild() after.
45
- */ setGlobalPrefix(prefix) {
46
- this.routeManager.setGlobalPrefix(prefix);
47
- return this;
32
+ get(token) {
33
+ return this.container.resolve(token);
48
34
  }
49
- /**
50
- * Register global middleware. Call rebuild() after if called post-create.
51
- * Controller/method-level @UseMiddleware works without rebuild.
52
- */ useGlobalMiddleware(...middleware) {
53
- this.routeManager.useGlobalMiddleware(...middleware);
54
- return this;
35
+ getHonoApp() {
36
+ return this.getApp();
55
37
  }
38
+ // Pipeline components — applied at request time, no rebuild needed
56
39
  useGlobalPipes(...pipes) {
57
40
  this.routeManager.useGlobalPipes(...pipes);
58
41
  return this;
@@ -69,27 +52,6 @@ export class VelaApplication {
69
52
  this.routeManager.useGlobalFilters(...filters);
70
53
  return this;
71
54
  }
72
- enableCors(options = {}) {
73
- const corsMiddleware = cors({
74
- origin: options.origin ?? '*',
75
- allowMethods: options.allowMethods,
76
- allowHeaders: options.allowHeaders,
77
- exposeHeaders: options.exposeHeaders,
78
- credentials: options.credentials,
79
- maxAge: options.maxAge
80
- });
81
- const mw = {
82
- use: (c, next)=>corsMiddleware(c, next)
83
- };
84
- this.routeManager.useGlobalMiddleware(mw);
85
- return this;
86
- }
87
- get(token) {
88
- return this.container.resolve(token);
89
- }
90
- getHonoApp() {
91
- return this.getApp();
92
- }
93
55
  // Lifecycle hooks
94
56
  async callOnModuleInit() {
95
57
  for (const instance of this.instances){
@@ -1,13 +1,13 @@
1
1
  import type { EventHandler } from './event-emitter.types';
2
2
  export declare class EventEmitter {
3
- private listeners;
4
- private onceListeners;
3
+ private exactListeners;
4
+ private exactOnceListeners;
5
+ private wildcardListeners;
5
6
  on(event: string, handler: EventHandler): this;
6
7
  once(event: string, handler: EventHandler): this;
7
8
  off(event: string, handler: EventHandler): this;
8
9
  emit(event: string, ...args: unknown[]): Promise<void>;
9
10
  removeAllListeners(event?: string): this;
10
11
  listenerCount(event: string): number;
11
- private getMatchingHandlers;
12
- private matchPattern;
12
+ private getOrCreateWildcard;
13
13
  }
@@ -5,86 +5,121 @@ function _ts_decorate(decorators, target, key, desc) {
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
7
  import { Injectable } from "../container/index.js";
8
+ function isWildcard(pattern) {
9
+ return pattern.includes('*');
10
+ }
11
+ function compilePattern(pattern) {
12
+ const regexStr = pattern.split('.').map((segment)=>{
13
+ if (segment === '**') return '.*';
14
+ if (segment === '*') return '[^.]+';
15
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+ }).join('\\.');
17
+ return new RegExp(`^${regexStr}$`);
18
+ }
8
19
  export class EventEmitter {
9
- listeners = new Map();
10
- onceListeners = new Set();
20
+ exactListeners = new Map();
21
+ exactOnceListeners = new Map();
22
+ wildcardListeners = new Map();
11
23
  on(event, handler) {
12
- const handlers = this.listeners.get(event) ?? [];
13
- handlers.push(handler);
14
- this.listeners.set(event, handlers);
24
+ if (isWildcard(event)) {
25
+ this.getOrCreateWildcard(event).handlers.add(handler);
26
+ } else {
27
+ const handlers = this.exactListeners.get(event) ?? new Set();
28
+ handlers.add(handler);
29
+ this.exactListeners.set(event, handlers);
30
+ }
15
31
  return this;
16
32
  }
17
33
  once(event, handler) {
18
- this.onceListeners.add(handler);
19
- return this.on(event, handler);
34
+ this.on(event, handler);
35
+ if (isWildcard(event)) {
36
+ this.getOrCreateWildcard(event).onceHandlers.add(handler);
37
+ } else {
38
+ const onceSet = this.exactOnceListeners.get(event) ?? new Set();
39
+ onceSet.add(handler);
40
+ this.exactOnceListeners.set(event, onceSet);
41
+ }
42
+ return this;
20
43
  }
21
44
  off(event, handler) {
22
- const handlers = this.listeners.get(event);
23
- if (handlers) {
24
- const index = handlers.indexOf(handler);
25
- if (index !== -1) handlers.splice(index, 1);
26
- if (handlers.length === 0) this.listeners.delete(event);
45
+ if (isWildcard(event)) {
46
+ const entry = this.wildcardListeners.get(event);
47
+ if (entry) {
48
+ entry.handlers.delete(handler);
49
+ entry.onceHandlers.delete(handler);
50
+ if (entry.handlers.size === 0) this.wildcardListeners.delete(event);
51
+ }
52
+ } else {
53
+ const handlers = this.exactListeners.get(event);
54
+ if (handlers) {
55
+ handlers.delete(handler);
56
+ this.exactOnceListeners.get(event)?.delete(handler);
57
+ if (handlers.size === 0) this.exactListeners.delete(event);
58
+ }
27
59
  }
28
- this.onceListeners.delete(handler);
29
60
  return this;
30
61
  }
31
62
  async emit(event, ...args) {
32
- const handlers = this.getMatchingHandlers(event);
33
63
  const toRemove = [];
34
- await Promise.all(handlers.map(async ({ event: registeredEvent, handler })=>{
35
- await handler(...args);
36
- if (this.onceListeners.has(handler)) {
37
- toRemove.push({
38
- event: registeredEvent,
39
- handler
40
- });
41
- }
42
- }));
43
- for (const { event: registeredEvent, handler } of toRemove){
44
- this.off(registeredEvent, handler);
64
+ // Exact match
65
+ const exactHandlers = this.exactListeners.get(event);
66
+ if (exactHandlers) {
67
+ const onceSet = this.exactOnceListeners.get(event);
68
+ await Promise.all([
69
+ ...exactHandlers
70
+ ].map(async (handler)=>{
71
+ await handler(...args);
72
+ if (onceSet?.has(handler)) {
73
+ toRemove.push(()=>this.off(event, handler));
74
+ }
75
+ }));
76
+ }
77
+ // Wildcard match (regex pre-compiled at registration)
78
+ for (const [pattern, entry] of this.wildcardListeners){
79
+ if (!entry.regex.test(event)) continue;
80
+ await Promise.all([
81
+ ...entry.handlers
82
+ ].map(async (handler)=>{
83
+ await handler(...args);
84
+ if (entry.onceHandlers.has(handler)) {
85
+ toRemove.push(()=>this.off(pattern, handler));
86
+ }
87
+ }));
45
88
  }
89
+ for (const remove of toRemove)remove();
46
90
  }
47
91
  removeAllListeners(event) {
48
92
  if (event) {
49
- const handlers = this.listeners.get(event);
50
- if (handlers) {
51
- for (const h of handlers)this.onceListeners.delete(h);
93
+ if (isWildcard(event)) {
94
+ this.wildcardListeners.delete(event);
95
+ } else {
96
+ this.exactListeners.delete(event);
97
+ this.exactOnceListeners.delete(event);
52
98
  }
53
- this.listeners.delete(event);
54
99
  } else {
55
- this.listeners.clear();
56
- this.onceListeners.clear();
100
+ this.exactListeners.clear();
101
+ this.exactOnceListeners.clear();
102
+ this.wildcardListeners.clear();
57
103
  }
58
104
  return this;
59
105
  }
60
106
  listenerCount(event) {
61
- return this.getMatchingHandlers(event).length;
62
- }
63
- getMatchingHandlers(event) {
64
- const result = [];
65
- for (const [pattern, handlers] of this.listeners){
66
- if (this.matchPattern(pattern, event)) {
67
- for (const handler of handlers){
68
- result.push({
69
- event: pattern,
70
- handler
71
- });
72
- }
73
- }
107
+ let count = this.exactListeners.get(event)?.size ?? 0;
108
+ for (const entry of this.wildcardListeners.values()){
109
+ if (entry.regex.test(event)) count += entry.handlers.size;
74
110
  }
75
- return result;
111
+ return count;
76
112
  }
77
- matchPattern(pattern, event) {
78
- if (pattern === event) return true;
79
- // Convert glob pattern to regex
80
- // ** matches any depth (any number of segments)
81
- // * matches single segment (no dots)
82
- const regexStr = pattern.split('.').map((segment)=>{
83
- if (segment === '**') return '.*';
84
- if (segment === '*') return '[^.]+';
85
- return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
86
- }).join('\\.');
87
- return new RegExp(`^${regexStr}$`).test(event);
113
+ getOrCreateWildcard(pattern) {
114
+ const existing = this.wildcardListeners.get(pattern);
115
+ if (existing) return existing;
116
+ const entry = {
117
+ regex: compilePattern(pattern),
118
+ handlers: new Set(),
119
+ onceHandlers: new Set()
120
+ };
121
+ this.wildcardListeners.set(pattern, entry);
122
+ return entry;
88
123
  }
89
124
  }
90
125
  EventEmitter = _ts_decorate([
package/dist/factory.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { VelaApplication } from './application';
2
2
  import type { Type } from './container/types';
3
+ import type { RouteManagerOptions } from './http/route.manager';
3
4
  export declare const VelaFactory: {
4
- create(rootModule: Type): Promise<VelaApplication>;
5
+ create(rootModule: Type, options?: RouteManagerOptions): Promise<VelaApplication>;
5
6
  };
package/dist/factory.js CHANGED
@@ -6,7 +6,7 @@ import { ModuleLoader } from "./module/module-loader.js";
6
6
  import { ComponentManager } from "./pipeline/component.manager.js";
7
7
  import { APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE } from "./pipeline/tokens.js";
8
8
  export const VelaFactory = {
9
- async create (rootModule) {
9
+ async create (rootModule, options = {}) {
10
10
  const container = new Container();
11
11
  container.register({
12
12
  provide: Container,
@@ -19,7 +19,7 @@ export const VelaFactory = {
19
19
  Container
20
20
  ]
21
21
  });
22
- const routeManager = new RouteManager(container);
22
+ const routeManager = new RouteManager(container, options);
23
23
  ComponentManager.init(container);
24
24
  const loader = new ModuleLoader(container, routeManager);
25
25
  loader.load(rootModule);
@@ -54,6 +54,15 @@ export const VelaFactory = {
54
54
  routeManager.useGlobalMiddlewareTokens(APP_MIDDLEWARE);
55
55
  }
56
56
  routeManager.registerConsumerMiddleware(loader.getConsumerMiddlewareDefinitions());
57
+ if (options.globalPrefix) {
58
+ routeManager.setGlobalPrefix(options.globalPrefix);
59
+ }
60
+ for (const handler of options.middleware ?? []){
61
+ const mw = {
62
+ use: handler
63
+ };
64
+ routeManager.useGlobalMiddleware(mw);
65
+ }
57
66
  const app = new VelaApplication(container, routeManager);
58
67
  const instances = await loader.resolveAllInstances();
59
68
  app.setInstances(instances);
@@ -1,4 +1,5 @@
1
1
  export { RouteManager } from './route.manager';
2
+ export type { RouteManagerOptions } from './route.manager';
2
3
  export { MiddlewareBuilder, RequestMethod } from './middleware-consumer';
3
4
  export type { MiddlewareConsumer, MiddlewareConfigProxy, RouteInfo, NestModule, MiddlewareRouteDefinition } from './middleware-consumer';
4
5
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, isController, } from './decorators';
@@ -1,12 +1,19 @@
1
- import { Hono } from 'hono';
1
+ import { type Context, type MiddlewareHandler, Hono } from 'hono';
2
2
  import type { Container } from '../container/container';
3
3
  import type { Token, Type } from '../container/types';
4
4
  import type { MiddlewareRouteDefinition } from './middleware-consumer';
5
5
  import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, PipeTransform } from '../pipeline/types';
6
6
  import type { FilterType, GuardType, InterceptorType, MiddlewareType, PipeType } from '../registry/types';
7
7
  import type { ControllerRegistration } from './types';
8
+ export interface RouteManagerOptions {
9
+ getClientIp?: (c: Context) => string | null;
10
+ middleware?: MiddlewareHandler[];
11
+ globalPrefix?: string;
12
+ }
8
13
  export declare class RouteManager {
9
14
  private container;
15
+ private static readonly METHOD_REGISTRAR;
16
+ private static readonly PARAM_EXTRACTORS;
10
17
  private controllers;
11
18
  private globalMiddleware;
12
19
  private globalPipes;
@@ -15,7 +22,8 @@ export declare class RouteManager {
15
22
  private globalFilters;
16
23
  private globalPrefix;
17
24
  private consumerMiddlewareDefinitions;
18
- constructor(container: Container);
25
+ private readonly ipExtractor;
26
+ constructor(container: Container, options?: RouteManagerOptions);
19
27
  registerConsumerMiddleware(definitions: MiddlewareRouteDefinition[]): this;
20
28
  setGlobalPrefix(prefix: string): this;
21
29
  useGlobalMiddleware(...middleware: MiddlewareType[]): this;
@@ -41,6 +49,7 @@ export declare class RouteManager {
41
49
  private registerRoute;
42
50
  private buildVersionedPaths;
43
51
  private joinPaths;
44
- private matchesConsumerPath;
52
+ private compilePathMatcher;
53
+ private compileRouteMatcher;
45
54
  getControllers(): ControllerRegistration[];
46
55
  }
@@ -1,4 +1,5 @@
1
1
  import { Hono } from "hono";
2
+ import { getCookie } from "hono/cookie";
2
3
  import { HttpMethod, ParamType } from "../constants.js";
3
4
  import { getMetadata } from "../metadata.js";
4
5
  import { RequestMethod } from "./middleware-consumer.js";
@@ -7,6 +8,7 @@ import { ComponentManager } from "../pipeline/component.manager.js";
7
8
  import { shouldFilterCatch } from "../pipeline/decorators.js";
8
9
  import { MetadataRegistry } from "../registry/metadata.registry.js";
9
10
  import { getHttpCode, getRedirect, getResponseHeaders } from "./decorators.js";
11
+ const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
10
12
  function parseRedirectResult(result) {
11
13
  if (typeof result !== 'object' || result === null) return undefined;
12
14
  if (!('url' in result)) return undefined;
@@ -21,6 +23,82 @@ function parseRedirectResult(result) {
21
23
  }
22
24
  export class RouteManager {
23
25
  container;
26
+ static METHOD_REGISTRAR = new Map([
27
+ [
28
+ HttpMethod.GET,
29
+ (app, p, h)=>app.get(p, h)
30
+ ],
31
+ [
32
+ HttpMethod.POST,
33
+ (app, p, h)=>app.post(p, h)
34
+ ],
35
+ [
36
+ HttpMethod.PUT,
37
+ (app, p, h)=>app.put(p, h)
38
+ ],
39
+ [
40
+ HttpMethod.PATCH,
41
+ (app, p, h)=>app.patch(p, h)
42
+ ],
43
+ [
44
+ HttpMethod.DELETE,
45
+ (app, p, h)=>app.delete(p, h)
46
+ ],
47
+ [
48
+ HttpMethod.OPTIONS,
49
+ (app, p, h)=>app.options(p, h)
50
+ ],
51
+ [
52
+ HttpMethod.HEAD,
53
+ (app, p, h)=>app.get(p, h)
54
+ ],
55
+ [
56
+ HttpMethod.ALL,
57
+ (app, p, h)=>app.all(p, h)
58
+ ]
59
+ ]);
60
+ static PARAM_EXTRACTORS = new Map([
61
+ [
62
+ ParamType.PARAM,
63
+ (c, p)=>p.name ? c.req.param(p.name) : c.req.param()
64
+ ],
65
+ [
66
+ ParamType.QUERY,
67
+ (c, p)=>p.name ? c.req.query(p.name) : c.req.query()
68
+ ],
69
+ [
70
+ ParamType.BODY,
71
+ async (c, p)=>{
72
+ let body;
73
+ try {
74
+ body = await c.req.json();
75
+ } catch {
76
+ return undefined;
77
+ }
78
+ return p.name && body !== null && typeof body === 'object' ? body[p.name] : body;
79
+ }
80
+ ],
81
+ [
82
+ ParamType.HEADERS,
83
+ (c, p)=>p.name ? c.req.header(p.name) : c.req.header()
84
+ ],
85
+ [
86
+ ParamType.REQUEST,
87
+ (c)=>c
88
+ ],
89
+ [
90
+ ParamType.RESPONSE,
91
+ (c)=>c
92
+ ],
93
+ [
94
+ ParamType.COOKIE,
95
+ (c, p)=>p.name ? getCookie(c, p.name) : getCookie(c)
96
+ ],
97
+ [
98
+ ParamType.RAW_BODY,
99
+ async (c)=>new Uint8Array(await c.req.arrayBuffer())
100
+ ]
101
+ ]);
24
102
  controllers = [];
25
103
  globalMiddleware = [];
26
104
  globalPipes = [];
@@ -29,8 +107,10 @@ export class RouteManager {
29
107
  globalFilters = [];
30
108
  globalPrefix = '';
31
109
  consumerMiddlewareDefinitions = [];
32
- constructor(container){
110
+ ipExtractor;
111
+ constructor(container, options = {}){
33
112
  this.container = container;
113
+ this.ipExtractor = options.getClientIp ?? defaultGetClientIp;
34
114
  }
35
115
  registerConsumerMiddleware(definitions) {
36
116
  this.consumerMiddlewareDefinitions.push(...definitions);
@@ -133,25 +213,12 @@ export class RouteManager {
133
213
  }
134
214
  // Register MiddlewareConsumer-configured middleware
135
215
  for (const def of this.consumerMiddlewareDefinitions){
216
+ const matchRoute = this.compileRouteMatcher(def.routes);
217
+ const matchExclude = this.compileRouteMatcher(def.excludes);
136
218
  app.use('*', (c, next)=>{
137
- const reqPath = c.req.path;
138
- const reqMethod = c.req.method;
139
- const matches = def.routes.some((route)=>{
140
- if (!this.matchesConsumerPath(reqPath, route.path)) return false;
141
- if (route.method && route.method !== RequestMethod.ALL) {
142
- if (reqMethod !== route.method) return false;
143
- }
144
- return true;
145
- });
146
- if (!matches) return next();
147
- const excluded = def.excludes.some((exclude)=>{
148
- if (!this.matchesConsumerPath(reqPath, exclude.path)) return false;
149
- if (exclude.method && exclude.method !== RequestMethod.ALL) {
150
- if (reqMethod !== exclude.method) return false;
151
- }
152
- return true;
153
- });
154
- if (excluded) return next();
219
+ const path = c.req.path;
220
+ const method = c.req.method;
221
+ if (!matchRoute(path, method) || matchExclude(path, method)) return next();
155
222
  const requestContainer = this.getRequestContainer(c);
156
223
  const runChain = (index)=>{
157
224
  if (index >= def.middleware.length) return next();
@@ -321,7 +388,7 @@ export class RouteManager {
321
388
  c
322
389
  ];
323
390
  }
324
- const maxIndex = Math.max(...paramMetadata.map((p)=>p.index));
391
+ const maxIndex = paramMetadata.at(-1).index;
325
392
  const args = new Array(maxIndex + 1).fill(undefined);
326
393
  for (const param of paramMetadata){
327
394
  let value = await this.extractParam(c, param);
@@ -345,65 +412,11 @@ export class RouteManager {
345
412
  }
346
413
  return args;
347
414
  }
348
- async extractParam(c, param) {
349
- switch(param.type){
350
- case ParamType.PARAM:
351
- return param.name ? c.req.param(param.name) : c.req.param();
352
- case ParamType.QUERY:
353
- return param.name ? c.req.query(param.name) : c.req.query();
354
- case ParamType.BODY:
355
- {
356
- let body;
357
- try {
358
- body = await c.req.json();
359
- } catch {
360
- return undefined;
361
- }
362
- if (param.name && body !== null && typeof body === 'object') {
363
- return body[param.name];
364
- }
365
- return body;
366
- }
367
- case ParamType.HEADERS:
368
- if (param.name) {
369
- return c.req.header(param.name);
370
- }
371
- const headers = {};
372
- c.req.raw.headers.forEach((value, key)=>{
373
- headers[key] = value;
374
- });
375
- return headers;
376
- case ParamType.REQUEST:
377
- return c;
378
- case ParamType.RESPONSE:
379
- return c;
380
- case ParamType.IP:
381
- return c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
382
- case ParamType.COOKIE:
383
- {
384
- const cookieHeader = c.req.raw.headers.get('cookie') ?? '';
385
- const cookies = {};
386
- for (const pair of cookieHeader.split(';')){
387
- const eqIdx = pair.indexOf('=');
388
- if (eqIdx === -1) continue;
389
- const name = pair.slice(0, eqIdx).trim();
390
- const value = pair.slice(eqIdx + 1).trim();
391
- if (name) cookies[name] = decodeURIComponent(value);
392
- }
393
- return param.name ? cookies[param.name] ?? undefined : cookies;
394
- }
395
- case ParamType.RAW_BODY:
396
- {
397
- const buffer = await c.req.arrayBuffer();
398
- return new Uint8Array(buffer);
399
- }
400
- default:
401
- // Custom param decorator — use factory if available
402
- if (param.factory) {
403
- return param.factory(param.name, c);
404
- }
405
- return undefined;
406
- }
415
+ extractParam(c, param) {
416
+ if (param.type === ParamType.IP) return this.ipExtractor(c);
417
+ const extractor = RouteManager.PARAM_EXTRACTORS.get(param.type);
418
+ if (extractor) return extractor(c, param);
419
+ return param.factory ? param.factory(param.name, c) : undefined;
407
420
  }
408
421
  createResponse(c, result, statusCode) {
409
422
  if (result instanceof Response) {
@@ -419,35 +432,8 @@ export class RouteManager {
419
432
  }
420
433
  registerRoute(app, method, path, handler) {
421
434
  const normalizedPath = path || '/';
422
- switch(method){
423
- case HttpMethod.GET:
424
- app.get(normalizedPath, handler);
425
- break;
426
- case HttpMethod.POST:
427
- app.post(normalizedPath, handler);
428
- break;
429
- case HttpMethod.PUT:
430
- app.put(normalizedPath, handler);
431
- break;
432
- case HttpMethod.PATCH:
433
- app.patch(normalizedPath, handler);
434
- break;
435
- case HttpMethod.DELETE:
436
- app.delete(normalizedPath, handler);
437
- break;
438
- case HttpMethod.OPTIONS:
439
- app.options(normalizedPath, handler);
440
- break;
441
- case HttpMethod.HEAD:
442
- // Hono converts HEAD to GET internally, so register as GET
443
- app.get(normalizedPath, handler);
444
- break;
445
- case HttpMethod.ALL:
446
- app.all(normalizedPath, handler);
447
- break;
448
- default:
449
- app.on(method.toUpperCase(), normalizedPath, handler);
450
- }
435
+ const registrar = RouteManager.METHOD_REGISTRAR.get(method);
436
+ registrar ? registrar(app, normalizedPath, handler) : app.on(method.toUpperCase(), normalizedPath, handler);
451
437
  }
452
438
  buildVersionedPaths(globalPrefix, controllerPrefix, routePath, version) {
453
439
  if (version === undefined) {
@@ -468,15 +454,27 @@ export class RouteManager {
468
454
  const cleanPath = path && !path.startsWith('/') ? `/${path}` : path;
469
455
  return `${cleanPrefix}${cleanPath}` || '/';
470
456
  }
471
- matchesConsumerPath(reqPath, routePath) {
472
- if (routePath === '*') return true;
473
- const normalized = routePath.startsWith('/') ? routePath : `/${routePath}`;
474
- if (reqPath === normalized) return true;
475
- if (reqPath.startsWith(`${normalized}/`)) return true;
476
- if (normalized.endsWith('*')) {
477
- return reqPath.startsWith(normalized.slice(0, -1));
457
+ compilePathMatcher(routePath) {
458
+ if (routePath === '*') return ()=>true;
459
+ const norm = routePath.startsWith('/') ? routePath : `/${routePath}`;
460
+ if (norm.endsWith('*')) {
461
+ const prefix = norm.slice(0, -1);
462
+ return (p)=>p.startsWith(prefix);
478
463
  }
479
- return false;
464
+ return (p)=>p === norm || p.startsWith(`${norm}/`);
465
+ }
466
+ compileRouteMatcher(routes) {
467
+ const matchers = routes.map((route)=>{
468
+ const matchPath = this.compilePathMatcher(route.path);
469
+ return (path, method)=>{
470
+ if (!matchPath(path)) return false;
471
+ if (route.method && route.method !== RequestMethod.ALL) return method === route.method;
472
+ return true;
473
+ };
474
+ });
475
+ if (matchers.length === 0) return ()=>false;
476
+ if (matchers.length === 1) return matchers[0];
477
+ return (path, method)=>matchers.some((m)=>m(path, method));
480
478
  }
481
479
  getControllers() {
482
480
  return [
package/dist/index.d.ts CHANGED
@@ -37,8 +37,10 @@ export { HttpException, BadRequestException, UnauthorizedException, ForbiddenExc
37
37
  export type { OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, OnApplicationShutdown, BeforeApplicationShutdown, } from './lifecycle/index';
38
38
  export { MetadataRegistry } from './registry/index';
39
39
  export { RouteManager } from './http/index';
40
+ export type { RouteManagerOptions } from './http/index';
40
41
  export { ModuleLoader } from './module/index';
41
42
  export { ComponentManager } from './pipeline/index';
42
43
  export { createZodDto, ValidationPipe } from './validation/index';
43
44
  export { Serialize, SerializerInterceptor, SERIALIZE_METADATA } from './serialization/index';
44
45
  export { Test, TestingModule, TestingModuleBuilder } from './testing/index';
46
+ export { getRuntimeKey, env } from 'hono/adapter';
package/dist/index.js CHANGED
@@ -49,3 +49,5 @@ export { createZodDto, ValidationPipe } from "./validation/index.js";
49
49
  export { Serialize, SerializerInterceptor, SERIALIZE_METADATA } from "./serialization/index.js";
50
50
  // Testing
51
51
  export { Test, TestingModule, TestingModuleBuilder } from "./testing/index.js";
52
+ // Hono Adapter Utilities
53
+ export { getRuntimeKey, env } from "hono/adapter";
@@ -1,7 +1,7 @@
1
- import type { Container } from '../container/container';
2
- import type { Token, Type } from '../container/types';
3
- import type { MiddlewareRouteDefinition } from '../http/middleware-consumer';
4
- import type { RouteManager } from '../http/route.manager';
1
+ import type { Container } from "../container/container";
2
+ import type { Token, Type } from "../container/types";
3
+ import type { MiddlewareRouteDefinition } from "../http/middleware-consumer";
4
+ import type { RouteManager } from "../http/route.manager";
5
5
  export declare class ModuleLoader {
6
6
  private container;
7
7
  private router;
@@ -2,17 +2,24 @@ import { Scope } from "../constants.js";
2
2
  import { ForwardRef, InjectionToken } from "../container/types.js";
3
3
  import { MiddlewareBuilder } from "../http/middleware-consumer.js";
4
4
  import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
5
+ const APP_TOKENS = new Set([
6
+ APP_GUARD,
7
+ APP_PIPE,
8
+ APP_INTERCEPTOR,
9
+ APP_FILTER,
10
+ APP_MIDDLEWARE
11
+ ]);
5
12
  import { MetadataRegistry } from "../registry/metadata.registry.js";
6
13
  import { getModuleMetadata, isModule } from "./decorators.js";
7
14
  function isDynamicModule(value) {
8
- return typeof value === 'object' && value !== null && 'module' in value && typeof value.module === 'function';
15
+ return typeof value === "object" && value !== null && "module" in value && typeof value.module === "function";
9
16
  }
10
17
  export class ModuleLoader {
11
18
  container;
12
19
  router;
13
20
  processedModules = new Set();
14
21
  processingStack = new Set();
15
- collectedControllers = [];
22
+ collectedControllers = new Set();
16
23
  registeredProviders = [];
17
24
  moduleExportsCache = new Map();
18
25
  globalExports = new Set();
@@ -67,9 +74,7 @@ export class ModuleLoader {
67
74
  if (this.processedModules.has(moduleClass)) {
68
75
  // Even if already processed, still collect extra controllers from dynamic module
69
76
  for (const controller of extraControllers){
70
- if (!this.collectedControllers.includes(controller)) {
71
- this.collectedControllers.push(controller);
72
- }
77
+ this.collectedControllers.add(controller);
73
78
  }
74
79
  return this.moduleExportsCache.get(moduleClass) ?? new Set();
75
80
  }
@@ -77,7 +82,7 @@ export class ModuleLoader {
77
82
  const chain = [
78
83
  ...this.processingStack,
79
84
  moduleClass
80
- ].map((m)=>m.name).join(' -> ');
85
+ ].map((m)=>m.name).join(" -> ");
81
86
  throw new Error(`Circular module dependency detected: ${chain}`);
82
87
  }
83
88
  if (!isModule(moduleClass)) {
@@ -122,9 +127,7 @@ export class ModuleLoader {
122
127
  ...extraControllers
123
128
  ];
124
129
  for (const controller of allControllers){
125
- if (!this.collectedControllers.includes(controller)) {
126
- this.collectedControllers.push(controller);
127
- }
130
+ this.collectedControllers.add(controller);
128
131
  }
129
132
  // Propagate module-level Use* decorators (@UseGuards, @UseInterceptors, etc.) to each controller
130
133
  for (const controller of allControllers){
@@ -140,7 +143,7 @@ export class ModuleLoader {
140
143
  }
141
144
  }
142
145
  // Call configure() if the module implements NestModule
143
- if (typeof moduleClass.prototype?.configure === 'function') {
146
+ if (typeof moduleClass.prototype?.configure === "function") {
144
147
  try {
145
148
  const instance = new moduleClass();
146
149
  const builder = new MiddlewareBuilder();
@@ -156,7 +159,7 @@ export class ModuleLoader {
156
159
  }
157
160
  }
158
161
  registerProvider(provider) {
159
- if (typeof provider === 'function') {
162
+ if (typeof provider === "function") {
160
163
  if (!this.container.has(provider)) {
161
164
  this.container.register(provider);
162
165
  this.registeredProviders.push(provider);
@@ -184,17 +187,17 @@ export class ModuleLoader {
184
187
  }
185
188
  }
186
189
  isAppToken(token) {
187
- return token === APP_GUARD || token === APP_PIPE || token === APP_INTERCEPTOR || token === APP_FILTER || token === APP_MIDDLEWARE;
190
+ return APP_TOKENS.has(token);
188
191
  }
189
192
  buildExportSet(exports, providers, importedProviders) {
190
193
  const exportSet = new Set();
194
+ const providerTokens = new Set();
195
+ for (const p of providers){
196
+ const token = typeof p === "function" ? p : p.provide;
197
+ if (token !== undefined) providerTokens.add(token);
198
+ }
191
199
  for (const exported of exports){
192
- const isLocalProvider = providers.some((p)=>{
193
- if (typeof p === 'function') {
194
- return p === exported;
195
- }
196
- return p.provide === exported;
197
- });
200
+ const isLocalProvider = providerTokens.has(exported);
198
201
  const isImportedProvider = importedProviders.has(exported);
199
202
  if (!isLocalProvider && !isImportedProvider) {
200
203
  const name = exported.name ?? String(exported);
@@ -225,14 +228,14 @@ export class ModuleLoader {
225
228
  ];
226
229
  }
227
230
  async resolveAllInstances() {
228
- const instances = [];
231
+ const instanceSet = new Set();
229
232
  for (const token of this.registeredProviders){
230
233
  try {
231
234
  if (this.container.getProviderScope(token) === Scope.REQUEST) {
232
235
  continue;
233
236
  }
234
237
  const instance = await this.container.resolveAsync(token);
235
- instances.push(instance);
238
+ instanceSet.add(instance);
236
239
  } catch {
237
240
  // Skip unresolvable tokens
238
241
  }
@@ -243,13 +246,13 @@ export class ModuleLoader {
243
246
  continue;
244
247
  }
245
248
  const instance = await this.container.resolveAsync(controller);
246
- if (!instances.includes(instance)) {
247
- instances.push(instance);
248
- }
249
+ instanceSet.add(instance);
249
250
  } catch {
250
251
  // Skip unresolvable controllers
251
252
  }
252
253
  }
253
- return instances;
254
+ return [
255
+ ...instanceSet
256
+ ];
254
257
  }
255
258
  }
@@ -27,11 +27,10 @@ export class ComponentManager {
27
27
  // 3-level resolution: global → controller → handler
28
28
  static getComponents(type, controller, handlerName) {
29
29
  const handlerKey = `${controller.name}:${String(handlerName)}`;
30
- const globalComponents = Array.from(MetadataRegistry.getGlobal(type));
31
30
  const controllerComponents = MetadataRegistry.getController(type, controller);
32
31
  const handlerComponents = MetadataRegistry.getHandler(type, handlerKey);
33
32
  return [
34
- ...globalComponents,
33
+ ...MetadataRegistry.getGlobal(type),
35
34
  ...controllerComponents,
36
35
  ...handlerComponents
37
36
  ];
@@ -32,7 +32,8 @@ export declare class ParseUUIDPipe implements PipeTransform<string, string> {
32
32
  transform(value: string, metadata: ArgumentMetadata): string;
33
33
  }
34
34
  export declare class ParseEnumPipe<T extends Record<string, string | number>> implements PipeTransform<string, T[keyof T]> {
35
- private readonly enumType;
35
+ private readonly allowedValues;
36
+ private readonly valuesLabel;
36
37
  constructor(enumType: T);
37
38
  transform(value: string, metadata: ArgumentMetadata): T[keyof T];
38
39
  }
@@ -70,14 +70,16 @@ export class ParseUUIDPipe {
70
70
  }
71
71
  }
72
72
  export class ParseEnumPipe {
73
- enumType;
73
+ allowedValues;
74
+ valuesLabel;
74
75
  constructor(enumType){
75
- this.enumType = enumType;
76
+ const values = Object.values(enumType);
77
+ this.allowedValues = new Set(values);
78
+ this.valuesLabel = values.join(', ');
76
79
  }
77
80
  transform(value, metadata) {
78
- const enumValues = Object.values(this.enumType);
79
- if (!enumValues.includes(value)) {
80
- throw new BadRequestException(`Validation failed (${enumValues.join(', ')} expected)${metadata.data ? ` for parameter '${metadata.data}'` : ''}`);
81
+ if (!this.allowedValues.has(value)) {
82
+ throw new BadRequestException(`Validation failed (${this.valuesLabel} expected)${metadata.data ? ` for parameter '${metadata.data}'` : ''}`);
81
83
  }
82
84
  return value;
83
85
  }
@@ -1,3 +1,4 @@
1
+ import type { Context } from 'hono';
1
2
  import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, PipeTransform } from '../pipeline/types';
2
3
  export type Type<T = any> = new (...args: any[]) => T;
3
4
  export type Constructor = Function;
@@ -26,7 +27,7 @@ export interface ParameterMetadata {
26
27
  type: string;
27
28
  name?: string;
28
29
  pipes?: PipeType[];
29
- factory?: (data: unknown, ctx: unknown) => unknown;
30
+ factory?: (data: unknown, ctx: Context) => unknown;
30
31
  }
31
32
  export interface HttpHandlerMeta {
32
33
  httpCode?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",