@velajs/vela 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -24,6 +24,9 @@ A sanctioned per-request injectable lands as a framework primitive, and the meta
24
24
  ### Internal cleanup
25
25
 
26
26
  - **Metadata stacking + funnel coverage** (audit #8 follow-up). New `metadata-stacking.test.ts` asserts `appendCustomHandlerMeta` is order-deterministic, `Reflect.defineMetadata` round-trips through `MetadataRegistry`'s typed slots, `MetadataRegistry.reset()` clears `classMeta`/`handlerMeta`, and class+handler `@SetMetadata` on the same key remain independent.
27
+ - **`NestModule.configure()` regression coverage** (audit #4 follow-up). New `configure-resolution.test.ts` asserts configure-time DI works (modules can constructor-inject providers from their own scope), synchronous errors thrown from `configure()` propagate, and unresolvable constructor deps fail loudly rather than silently skipping middleware setup.
28
+ - **Edge-safe contract documented** (audit #7 follow-up). README now states the contract explicitly: the main export is edge-safe and audited in CI by `src/__tests__/edge-runtime-audit.test.ts`; `@velajs/vela/schedule-node` is the one opt-in Node/Bun carve-out.
29
+ - **`RouteManager` split into focused units** (audit #9 follow-up). New files `argument-resolver.ts`, `handler-executor.ts`, `response-mapper.ts`, and `instantiate.ts` carry parameter extraction, the per-request orchestration closure, response/redirect mapping, and the container-aware factory. `RouteManager` is now focused on Hono route registration and path composition. Internal-only refactor — no public API change, no behavior change.
27
30
  - **Stale `WeakMap` comment removed** from `MetadataRegistry.reset()` — the WeakMap fallback was retired in 1.1.0; the comment was documentation drift.
28
31
  - **`Container.setRequestInstance(token, value)`** — public method to pre-seed the per-request cache. Used by `RouteManager` to populate `REQUEST_CONTEXT` before any handler resolution runs.
29
32
 
package/README.md CHANGED
@@ -73,6 +73,19 @@ Vela runs on any runtime that supports the Web Standards API:
73
73
 
74
74
  No Node.js-specific APIs (`node:fs`, `Buffer`, `process`) are used.
75
75
 
76
+ ### Edge-safe contract
77
+
78
+ The **main export** (`@velajs/vela`) is edge-safe by contract — no `node:*` imports, no `Buffer`, no `process`, no `setInterval`, no `Bun.serve`. This is enforced in CI by [`src/__tests__/edge-runtime-audit.test.ts`](src/__tests__/edge-runtime-audit.test.ts), which fails the build if any file under `src/` references a forbidden API.
79
+
80
+ One subpath, **`@velajs/vela/schedule-node`**, is an opt-in Node/Bun adapter for `setInterval`-based job execution. It uses runtime-specific APIs by design and is **excluded from the edge-runtime audit**. Edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge) should not import it — use platform cron triggers instead (e.g., `@velajs/cloudflare` ≥ 0.2.0 dispatches `@Cron` jobs via the Workers `scheduled()` handler).
81
+
82
+ ```ts
83
+ // Node / Bun only — opt-in
84
+ import { ScheduleNodeModule } from '@velajs/vela/schedule-node';
85
+ ```
86
+
87
+ The other subpaths (`@velajs/vela/internal`, `@velajs/vela/streaming`) follow the main export's edge-safe contract.
88
+
76
89
  ## Dynamic modules
77
90
 
78
91
  Configurable modules use `forRoot` (sync) and `forRootAsync` (DI-resolved):
@@ -0,0 +1,10 @@
1
+ import type { Context } from 'hono';
2
+ import type { Container } from '../container/container';
3
+ import type { PipeTransform } from '../pipeline/types';
4
+ import type { ParamMetadata } from './types';
5
+ export declare class ArgumentResolver {
6
+ private readonly ipExtractor;
7
+ constructor(ipExtractor: (c: Context) => string | null);
8
+ extract(c: Context, paramMetadata: ParamMetadata[], pipes: PipeTransform[], requestContainer: Container, paramTypes?: unknown[]): Promise<unknown[]>;
9
+ private extractParam;
10
+ }
@@ -0,0 +1,89 @@
1
+ import { getCookie } from "hono/cookie";
2
+ import { ParamType } from "../constants.js";
3
+ import { instantiate } from "./instantiate.js";
4
+ const PARAM_EXTRACTORS = new Map([
5
+ [
6
+ ParamType.PARAM,
7
+ (c, p)=>p.name ? c.req.param(p.name) : c.req.param()
8
+ ],
9
+ [
10
+ ParamType.QUERY,
11
+ (c, p)=>p.name ? c.req.query(p.name) : c.req.query()
12
+ ],
13
+ [
14
+ ParamType.BODY,
15
+ async (c, p)=>{
16
+ let body;
17
+ try {
18
+ body = await c.req.json();
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ return p.name && body !== null && typeof body === 'object' ? body[p.name] : body;
23
+ }
24
+ ],
25
+ [
26
+ ParamType.HEADERS,
27
+ (c, p)=>p.name ? c.req.header(p.name) : c.req.header()
28
+ ],
29
+ [
30
+ ParamType.REQUEST,
31
+ (c)=>c
32
+ ],
33
+ [
34
+ ParamType.RESPONSE,
35
+ (c)=>c
36
+ ],
37
+ [
38
+ ParamType.COOKIE,
39
+ (c, p)=>p.name ? getCookie(c, p.name) : getCookie(c)
40
+ ],
41
+ [
42
+ ParamType.RAW_BODY,
43
+ async (c)=>new Uint8Array(await c.req.arrayBuffer())
44
+ ]
45
+ ]);
46
+ // Pulls handler arguments from the request, applies shared pipes (global +
47
+ // controller + method) and then per-param pipes. Empty paramMetadata returns
48
+ // `[c]` to match the old direct-Hono-handler shape — a back-compat behavior
49
+ // the framework's tests rely on.
50
+ export class ArgumentResolver {
51
+ ipExtractor;
52
+ constructor(ipExtractor){
53
+ this.ipExtractor = ipExtractor;
54
+ }
55
+ async extract(c, paramMetadata, pipes, requestContainer, paramTypes) {
56
+ if (paramMetadata.length === 0) {
57
+ return [
58
+ c
59
+ ];
60
+ }
61
+ const maxIndex = paramMetadata.at(-1).index;
62
+ const args = new Array(maxIndex + 1).fill(undefined);
63
+ for (const param of paramMetadata){
64
+ let value = await this.extractParam(c, param);
65
+ const metadata = {
66
+ type: param.type,
67
+ data: param.name,
68
+ metatype: paramTypes?.[param.index]
69
+ };
70
+ for (const pipe of pipes){
71
+ value = await pipe.transform(value, metadata);
72
+ }
73
+ if (param.pipes && param.pipes.length > 0) {
74
+ for (const paramPipe of param.pipes){
75
+ const pipeInstance = instantiate(paramPipe, requestContainer);
76
+ value = await pipeInstance.transform(value, metadata);
77
+ }
78
+ }
79
+ args[param.index] = value;
80
+ }
81
+ return args;
82
+ }
83
+ extractParam(c, param) {
84
+ if (param.type === ParamType.IP) return this.ipExtractor(c);
85
+ const extractor = PARAM_EXTRACTORS.get(param.type);
86
+ if (extractor) return extractor(c, param);
87
+ return param.factory ? param.factory(param.name, c) : undefined;
88
+ }
89
+ }
@@ -0,0 +1,20 @@
1
+ import type { Context } from 'hono';
2
+ import type { Container } from '../container/container';
3
+ import type { Token, Type } from '../container/types';
4
+ import type { CanActivate, ExceptionFilter, NestInterceptor, PipeTransform } from '../pipeline/types';
5
+ import type { FilterType, GuardType, InterceptorType, ParameterMetadata, PipeType } from '../registry/types';
6
+ import type { ArgumentResolver } from './argument-resolver';
7
+ import type { RouteMetadata } from './types';
8
+ export interface HandlerGlobals {
9
+ guards: Array<GuardType | Token<CanActivate>>;
10
+ pipes: Array<PipeType | Token<PipeTransform>>;
11
+ interceptors: Array<InterceptorType | Token<NestInterceptor>>;
12
+ filters: Array<FilterType | Token<ExceptionFilter>>;
13
+ }
14
+ export declare class HandlerExecutor {
15
+ private readonly argumentResolver;
16
+ private readonly getGlobals;
17
+ private readonly getRequestContainer;
18
+ constructor(argumentResolver: ArgumentResolver, getGlobals: () => HandlerGlobals, getRequestContainer: (c: Context) => Container);
19
+ create(route: RouteMetadata, controller: Type, allParamMetadata: Map<string | symbol, ParameterMetadata[]>): (c: Context) => Promise<Response>;
20
+ }
@@ -0,0 +1,108 @@
1
+ import { ForbiddenException, HttpException } from "../errors/http-exception.js";
2
+ import { ComponentManager } from "../pipeline/component.manager.js";
3
+ import { shouldFilterCatch } from "../pipeline/decorators.js";
4
+ import { getHttpCode, getRedirect, getResponseHeaders } from "./decorators.js";
5
+ import { buildExecutionContext } from "./execution-context.js";
6
+ import { instantiateMany } from "./instantiate.js";
7
+ import { applyResponseHeaders, mapRedirect, mapResponse } from "./response-mapper.js";
8
+ // Builds the per-request closure that runs guards, pipes, interceptors, the
9
+ // handler itself, and exception filters. Pure orchestration — Hono route
10
+ // registration stays in RouteManager. Globals are read via a callback so
11
+ // post-create additions (`useGlobalGuards`, etc.) propagate to subsequent
12
+ // requests without rebuilding routes.
13
+ export class HandlerExecutor {
14
+ argumentResolver;
15
+ getGlobals;
16
+ getRequestContainer;
17
+ constructor(argumentResolver, getGlobals, getRequestContainer){
18
+ this.argumentResolver = argumentResolver;
19
+ this.getGlobals = getGlobals;
20
+ this.getRequestContainer = getRequestContainer;
21
+ }
22
+ create(route, controller, allParamMetadata) {
23
+ const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
24
+ // Read param types once at build time for metatype population.
25
+ // Routed via Reflect so the polyfill funnels both src-level and dist-level
26
+ // consumers to the same registry (matters in tests that import from dist).
27
+ const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, route.handlerName);
28
+ const methodGuards = ComponentManager.getComponents('guard', controller, route.handlerName);
29
+ const methodPipes = ComponentManager.getComponents('pipe', controller, route.handlerName);
30
+ const methodInterceptors = ComponentManager.getComponents('interceptor', controller, route.handlerName);
31
+ // Filters: reverse order (handler → controller → global) — closest to handler runs first
32
+ const methodFilters = [
33
+ ...ComponentManager.getComponents('filter', controller, route.handlerName)
34
+ ].reverse();
35
+ const httpCode = getHttpCode(controller, route.handlerName);
36
+ const responseHeaders = getResponseHeaders(controller, route.handlerName);
37
+ const redirect = getRedirect(controller, route.handlerName);
38
+ return async (c)=>{
39
+ // Combine global + method at request time so post-create registrations propagate.
40
+ const requestContainer = this.getRequestContainer(c);
41
+ const globals = this.getGlobals();
42
+ const guards = [
43
+ ...instantiateMany(globals.guards, requestContainer),
44
+ ...instantiateMany(methodGuards, requestContainer)
45
+ ];
46
+ const pipes = [
47
+ ...instantiateMany(globals.pipes, requestContainer),
48
+ ...instantiateMany(methodPipes, requestContainer)
49
+ ];
50
+ const interceptors = [
51
+ ...instantiateMany(globals.interceptors, requestContainer),
52
+ ...instantiateMany(methodInterceptors, requestContainer)
53
+ ];
54
+ const filters = [
55
+ ...instantiateMany(methodFilters, requestContainer),
56
+ ...instantiateMany(globals.filters, requestContainer)
57
+ ];
58
+ const executionContext = buildExecutionContext(c, controller, route.handlerName);
59
+ try {
60
+ const instance = requestContainer.resolve(controller);
61
+ // 1. Extract args + run pipes (vela order: args before guards).
62
+ const args = await this.argumentResolver.extract(c, paramMetadata, pipes, requestContainer, paramTypes);
63
+ // 2. Guards (fail-fast).
64
+ for (const guard of guards){
65
+ const canActivate = await guard.canActivate(executionContext);
66
+ if (!canActivate) {
67
+ throw new ForbiddenException();
68
+ }
69
+ }
70
+ // 3. Get handler method.
71
+ const method = Reflect.get(instance, route.handlerName);
72
+ if (typeof method !== 'function') {
73
+ throw new Error(`Method ${String(route.handlerName)} not found on controller`);
74
+ }
75
+ // 4. Build core handler.
76
+ const coreHandler = async ()=>Reflect.apply(method, instance, args);
77
+ // 5. Run interceptor chain.
78
+ const result = await ComponentManager.runInterceptorChain(interceptors, executionContext, coreHandler);
79
+ if (redirect) {
80
+ return mapRedirect(c, result, redirect);
81
+ }
82
+ const response = mapResponse(c, result, httpCode);
83
+ applyResponseHeaders(response, responseHeaders);
84
+ return response;
85
+ } catch (error) {
86
+ for (const filter of filters){
87
+ if (shouldFilterCatch(filter, error)) {
88
+ try {
89
+ const filtered = await filter.catch(error, executionContext);
90
+ return mapResponse(c, filtered);
91
+ } catch {
92
+ break;
93
+ }
94
+ }
95
+ }
96
+ if (error instanceof HttpException) {
97
+ const response = error.getResponse();
98
+ const status = error.getStatus();
99
+ return c.json(response, status);
100
+ }
101
+ return c.json({
102
+ statusCode: 500,
103
+ message: 'Internal Server Error'
104
+ }, 500);
105
+ }
106
+ };
107
+ }
108
+ }
@@ -0,0 +1,4 @@
1
+ import type { Container } from '../container/container';
2
+ import type { Token, Type } from '../container/types';
3
+ export declare function instantiate<T>(classOrInstance: Type<T> | Token<T> | T, container: Container): T;
4
+ export declare function instantiateMany<T>(items: Array<Type<T> | Token<T> | T>, container: Container): T[];
@@ -0,0 +1,18 @@
1
+ // Resolve a class/token through the container if registered, otherwise treat
2
+ // the input as a plain instance. Used by RouteManager and HandlerExecutor to
3
+ // materialize middleware, guards, pipes, interceptors, and filters per request.
4
+ export function instantiate(classOrInstance, container) {
5
+ if (typeof classOrInstance === 'function') {
6
+ if (container.has(classOrInstance)) {
7
+ return container.resolve(classOrInstance);
8
+ }
9
+ return new classOrInstance();
10
+ }
11
+ if (container.has(classOrInstance)) {
12
+ return container.resolve(classOrInstance);
13
+ }
14
+ return classOrInstance;
15
+ }
16
+ export function instantiateMany(items, container) {
17
+ return items.map((item)=>instantiate(item, container));
18
+ }
@@ -0,0 +1,8 @@
1
+ import type { Context } from 'hono';
2
+ export interface ResponseRedirect {
3
+ url: string;
4
+ statusCode: number;
5
+ }
6
+ export declare function mapResponse(c: Context, result: unknown, statusCode?: number): Response;
7
+ export declare function mapRedirect(c: Context, result: unknown, redirect: ResponseRedirect): Response;
8
+ export declare function applyResponseHeaders(response: Response, headers: Iterable<readonly [string, string]>): void;
@@ -0,0 +1,42 @@
1
+ // A handler returning `{ url, statusCode? }` may override the @Redirect-decorator
2
+ // destination — kept identical to the previous inline behavior.
3
+ function parseRedirectResult(result) {
4
+ if (typeof result !== 'object' || result === null) return undefined;
5
+ if (!('url' in result)) return undefined;
6
+ const obj = result;
7
+ if (typeof obj.url !== 'string') return undefined;
8
+ return {
9
+ url: obj.url,
10
+ ...typeof obj.statusCode === 'number' ? {
11
+ statusCode: obj.statusCode
12
+ } : {}
13
+ };
14
+ }
15
+ // Maps a controller return value to a Hono Response. `null`/`undefined` → 204
16
+ // (empty body); strings → text; anything else → JSON. A pre-built Response
17
+ // passes through unchanged.
18
+ export function mapResponse(c, result, statusCode) {
19
+ if (result instanceof Response) {
20
+ return result;
21
+ }
22
+ if (result === null || result === undefined) {
23
+ return c.body(null, statusCode ?? 204);
24
+ }
25
+ if (typeof result === 'string') {
26
+ return c.text(result, statusCode ?? 200);
27
+ }
28
+ return c.json(result, statusCode ?? 200);
29
+ }
30
+ // Honors @Redirect on the handler. If the handler returned `{ url, statusCode? }`,
31
+ // those override the decorator's defaults; otherwise the decorator wins.
32
+ export function mapRedirect(c, result, redirect) {
33
+ const overrides = parseRedirectResult(result);
34
+ const finalUrl = overrides?.url ?? redirect.url;
35
+ const finalStatus = overrides?.statusCode ?? redirect.statusCode;
36
+ return c.redirect(finalUrl, finalStatus);
37
+ }
38
+ export function applyResponseHeaders(response, headers) {
39
+ for (const [name, value] of headers){
40
+ response.headers.set(name, value);
41
+ }
42
+ }
@@ -13,7 +13,6 @@ export interface RouteManagerOptions {
13
13
  export declare class RouteManager {
14
14
  private container;
15
15
  private static readonly METHOD_REGISTRAR;
16
- private static readonly PARAM_EXTRACTORS;
17
16
  private controllers;
18
17
  private globalMiddleware;
19
18
  private globalPipes;
@@ -22,7 +21,7 @@ export declare class RouteManager {
22
21
  private globalFilters;
23
22
  private globalPrefix;
24
23
  private consumerMiddlewareDefinitions;
25
- private readonly ipExtractor;
24
+ private readonly handlerExecutor;
26
25
  constructor(container: Container, options?: RouteManagerOptions);
27
26
  registerConsumerMiddleware(definitions: MiddlewareRouteDefinition[]): this;
28
27
  setGlobalPrefix(prefix: string): this;
@@ -36,17 +35,10 @@ export declare class RouteManager {
36
35
  useGlobalInterceptorTokens(...interceptorTokens: Array<Token<NestInterceptor>>): this;
37
36
  useGlobalFilters(...filters: FilterType[]): this;
38
37
  useGlobalFilterTokens(...filterTokens: Array<Token<ExceptionFilter>>): this;
39
- private instantiate;
40
- private instantiateMany;
41
38
  private getMiddlewarePriority;
42
39
  private getRequestContainer;
43
40
  registerController(controller: Type): this;
44
41
  build(): Promise<Hono>;
45
- private createExecutionContext;
46
- private createHandler;
47
- private extractArguments;
48
- private extractParam;
49
- private createResponse;
50
42
  private registerRoute;
51
43
  private buildVersionedPaths;
52
44
  private compilePathMatcher;
@@ -1,28 +1,14 @@
1
1
  import { Hono } from "hono";
2
- import { getCookie } from "hono/cookie";
3
- import { HttpMethod, ParamType } from "../constants.js";
2
+ import { HttpMethod } from "../constants.js";
4
3
  import { getMetadata } from "../metadata.js";
5
4
  import { joinPaths } from "../registry/paths.js";
6
- import { buildExecutionContext } from "./execution-context.js";
5
+ import { ArgumentResolver } from "./argument-resolver.js";
6
+ import { HandlerExecutor } from "./handler-executor.js";
7
+ import { instantiate, instantiateMany } from "./instantiate.js";
7
8
  import { REQUEST_CONTEXT, createRequestContext } from "./request-context.js";
8
- import { ForbiddenException, HttpException } from "../errors/http-exception.js";
9
9
  import { ComponentManager } from "../pipeline/component.manager.js";
10
- import { shouldFilterCatch } from "../pipeline/decorators.js";
11
10
  import { MetadataRegistry } from "../registry/metadata.registry.js";
12
- import { getHttpCode, getRedirect, getResponseHeaders } from "./decorators.js";
13
11
  const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
14
- function parseRedirectResult(result) {
15
- if (typeof result !== 'object' || result === null) return undefined;
16
- if (!('url' in result)) return undefined;
17
- const obj = result;
18
- if (typeof obj.url !== 'string') return undefined;
19
- return {
20
- url: obj.url,
21
- ...typeof obj.statusCode === 'number' ? {
22
- statusCode: obj.statusCode
23
- } : {}
24
- };
25
- }
26
12
  export class RouteManager {
27
13
  container;
28
14
  static METHOD_REGISTRAR = new Map([
@@ -59,48 +45,6 @@ export class RouteManager {
59
45
  (app, p, h)=>app.all(p, h)
60
46
  ]
61
47
  ]);
62
- static PARAM_EXTRACTORS = new Map([
63
- [
64
- ParamType.PARAM,
65
- (c, p)=>p.name ? c.req.param(p.name) : c.req.param()
66
- ],
67
- [
68
- ParamType.QUERY,
69
- (c, p)=>p.name ? c.req.query(p.name) : c.req.query()
70
- ],
71
- [
72
- ParamType.BODY,
73
- async (c, p)=>{
74
- let body;
75
- try {
76
- body = await c.req.json();
77
- } catch {
78
- return undefined;
79
- }
80
- return p.name && body !== null && typeof body === 'object' ? body[p.name] : body;
81
- }
82
- ],
83
- [
84
- ParamType.HEADERS,
85
- (c, p)=>p.name ? c.req.header(p.name) : c.req.header()
86
- ],
87
- [
88
- ParamType.REQUEST,
89
- (c)=>c
90
- ],
91
- [
92
- ParamType.RESPONSE,
93
- (c)=>c
94
- ],
95
- [
96
- ParamType.COOKIE,
97
- (c, p)=>p.name ? getCookie(c, p.name) : getCookie(c)
98
- ],
99
- [
100
- ParamType.RAW_BODY,
101
- async (c)=>new Uint8Array(await c.req.arrayBuffer())
102
- ]
103
- ]);
104
48
  controllers = [];
105
49
  globalMiddleware = [];
106
50
  globalPipes = [];
@@ -109,10 +53,16 @@ export class RouteManager {
109
53
  globalFilters = [];
110
54
  globalPrefix = '';
111
55
  consumerMiddlewareDefinitions = [];
112
- ipExtractor;
56
+ handlerExecutor;
113
57
  constructor(container, options = {}){
114
58
  this.container = container;
115
- this.ipExtractor = options.getClientIp ?? defaultGetClientIp;
59
+ const argumentResolver = new ArgumentResolver(options.getClientIp ?? defaultGetClientIp);
60
+ this.handlerExecutor = new HandlerExecutor(argumentResolver, ()=>({
61
+ guards: this.globalGuards,
62
+ pipes: this.globalPipes,
63
+ interceptors: this.globalInterceptors,
64
+ filters: this.globalFilters
65
+ }), (c)=>this.getRequestContainer(c));
116
66
  }
117
67
  registerConsumerMiddleware(definitions) {
118
68
  this.consumerMiddlewareDefinitions.push(...definitions);
@@ -162,21 +112,6 @@ export class RouteManager {
162
112
  this.globalFilters.push(...filterTokens);
163
113
  return this;
164
114
  }
165
- instantiate(classOrInstance, container) {
166
- if (typeof classOrInstance === 'function') {
167
- if (container.has(classOrInstance)) {
168
- return container.resolve(classOrInstance);
169
- }
170
- return new classOrInstance();
171
- }
172
- if (container.has(classOrInstance)) {
173
- return container.resolve(classOrInstance);
174
- }
175
- return classOrInstance;
176
- }
177
- instantiateMany(items, container) {
178
- return items.map((item)=>this.instantiate(item, container));
179
- }
180
115
  getMiddlewarePriority(entry) {
181
116
  if (entry == null) return 0;
182
117
  if (typeof entry === 'function') {
@@ -189,7 +124,7 @@ export class RouteManager {
189
124
  if (typeof inst.constructor?.priority === 'number') return inst.constructor.priority;
190
125
  }
191
126
  try {
192
- const resolved = this.instantiate(entry, this.container);
127
+ const resolved = instantiate(entry, this.container);
193
128
  if (resolved && typeof resolved === 'object') {
194
129
  const p = resolved.priority;
195
130
  if (typeof p === 'number') return p;
@@ -237,21 +172,18 @@ export class RouteManager {
237
172
  index,
238
173
  priority: this.getMiddlewarePriority(entry)
239
174
  })).sort((a, b)=>a.priority - b.priority || a.index - b.index);
240
- // Register global middleware on the Hono app
241
175
  for (const { entry } of sortedGlobal){
242
176
  app.use('*', (c, next)=>{
243
177
  const requestContainer = this.getRequestContainer(c);
244
- const resolved = this.instantiate(entry, requestContainer);
178
+ const resolved = instantiate(entry, requestContainer);
245
179
  return resolved.use(c, next);
246
180
  });
247
181
  }
248
- // Sort consumer middleware with the same stable-by-index rule.
249
182
  const sortedConsumer = this.consumerMiddlewareDefinitions.map((def, index)=>({
250
183
  def,
251
184
  index,
252
185
  priority: def.priority ?? 0
253
186
  })).sort((a, b)=>a.priority - b.priority || a.index - b.index);
254
- // Register MiddlewareConsumer-configured middleware
255
187
  for (const { def } of sortedConsumer){
256
188
  const matchRoute = this.compileRouteMatcher(def.routes);
257
189
  const matchExclude = this.compileRouteMatcher(def.excludes);
@@ -262,30 +194,27 @@ export class RouteManager {
262
194
  const requestContainer = this.getRequestContainer(c);
263
195
  const runChain = (index)=>{
264
196
  if (index >= def.middleware.length) return next();
265
- const instance = this.instantiate(def.middleware[index], requestContainer);
197
+ const instance = instantiate(def.middleware[index], requestContainer);
266
198
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
267
199
  return instance.use(c, ()=>runChain(index + 1)).then(()=>{});
268
200
  };
269
201
  return runChain(0);
270
202
  });
271
203
  }
272
- // First pass: register all custom routes (must come before CRUD /:id routes)
204
+ // First pass: register all custom routes (must come before CRUD /:id routes).
273
205
  for (const { controller, metadata, routes } of this.controllers){
274
206
  if (routes.length > 0) {
275
207
  const allParamMetadata = MetadataRegistry.getParameters(controller);
276
208
  for (const route of routes){
277
- // Determine effective version: route-level overrides controller-level
278
209
  const effectiveVersion = route.version ?? metadata.version;
279
- // Build versioned paths
280
210
  const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
281
- // Register per-route middleware as Hono middleware before the handler
282
211
  const middlewareItems = ComponentManager.getComponents('middleware', controller, route.handlerName);
283
- const handler = this.createHandler(route, controller, allParamMetadata);
212
+ const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
284
213
  for (const fullPath of versionedPaths){
285
214
  for (const middlewareItem of middlewareItems){
286
215
  app.use(fullPath, (c, next)=>{
287
216
  const requestContainer = this.getRequestContainer(c);
288
- const resolved = this.instantiate(middlewareItem, requestContainer);
217
+ const resolved = instantiate(middlewareItem, requestContainer);
289
218
  return resolved.use(c, next);
290
219
  });
291
220
  }
@@ -294,7 +223,7 @@ export class RouteManager {
294
223
  }
295
224
  }
296
225
  }
297
- // Second pass: mount CRUD sub-apps (/:id routes registered last)
226
+ // Second pass: mount CRUD sub-apps (/:id routes registered last).
298
227
  for (const { controller, metadata } of this.controllers){
299
228
  const crudConfig = getMetadata('vela:crud', controller);
300
229
  if (crudConfig) {
@@ -303,7 +232,7 @@ export class RouteManager {
303
232
  const { buildCrudRoutes } = await import(pkg);
304
233
  await buildCrudRoutes(app, controller, metadata.prefix, crudConfig, {
305
234
  globalPrefix: this.globalPrefix,
306
- globalGuards: this.instantiateMany(this.globalGuards, this.container),
235
+ globalGuards: instantiateMany(this.globalGuards, this.container),
307
236
  joinPaths
308
237
  });
309
238
  } catch (err) {
@@ -315,155 +244,6 @@ export class RouteManager {
315
244
  }
316
245
  return app;
317
246
  }
318
- createExecutionContext(c, controller, route) {
319
- return buildExecutionContext(c, controller, route.handlerName);
320
- }
321
- createHandler(route, controller, allParamMetadata) {
322
- const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
323
- // Read param types once at build time for metatype population.
324
- // Routed via Reflect so the polyfill funnels both src-level and dist-level
325
- // consumers to the same registry (matters in tests that import from dist).
326
- const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, route.handlerName);
327
- // Collect controller + method level components at build time
328
- const methodGuards = ComponentManager.getComponents('guard', controller, route.handlerName);
329
- const methodPipes = ComponentManager.getComponents('pipe', controller, route.handlerName);
330
- const methodInterceptors = ComponentManager.getComponents('interceptor', controller, route.handlerName);
331
- // Filters: reverse order (handler → controller → global) — closest to handler runs first
332
- const methodFilters = [
333
- ...ComponentManager.getComponents('filter', controller, route.handlerName)
334
- ].reverse();
335
- // Read response decorators at build time
336
- const httpCode = getHttpCode(controller, route.handlerName);
337
- const responseHeaders = getResponseHeaders(controller, route.handlerName);
338
- const redirect = getRedirect(controller, route.handlerName);
339
- return async (c)=>{
340
- // Combine global + method at request time (allows post-create registration)
341
- const requestContainer = this.getRequestContainer(c);
342
- const guards = [
343
- ...this.instantiateMany(this.globalGuards, requestContainer),
344
- ...this.instantiateMany(methodGuards, requestContainer)
345
- ];
346
- const pipes = [
347
- ...this.instantiateMany(this.globalPipes, requestContainer),
348
- ...this.instantiateMany(methodPipes, requestContainer)
349
- ];
350
- const interceptors = [
351
- ...this.instantiateMany(this.globalInterceptors, requestContainer),
352
- ...this.instantiateMany(methodInterceptors, requestContainer)
353
- ];
354
- const filters = [
355
- ...this.instantiateMany(methodFilters, requestContainer),
356
- ...this.instantiateMany(this.globalFilters, requestContainer)
357
- ];
358
- const executionContext = this.createExecutionContext(c, controller, route);
359
- try {
360
- const instance = requestContainer.resolve(controller);
361
- // 1. Extract args + run pipes
362
- const args = await this.extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes);
363
- // 2. Guards (fail-fast)
364
- for (const guard of guards){
365
- const canActivate = await guard.canActivate(executionContext);
366
- if (!canActivate) {
367
- throw new ForbiddenException();
368
- }
369
- }
370
- // 3. Get handler method
371
- const method = Reflect.get(instance, route.handlerName);
372
- if (typeof method !== 'function') {
373
- throw new Error(`Method ${String(route.handlerName)} not found on controller`);
374
- }
375
- // 4. Build core handler
376
- const coreHandler = async ()=>Reflect.apply(method, instance, args);
377
- // 5. Run interceptor chain
378
- const result = await ComponentManager.runInterceptorChain(interceptors, executionContext, coreHandler);
379
- // Handle @Redirect
380
- if (redirect) {
381
- const overrides = parseRedirectResult(result);
382
- const finalUrl = overrides?.url ?? redirect.url;
383
- const finalStatus = overrides?.statusCode ?? redirect.statusCode;
384
- return c.redirect(finalUrl, finalStatus);
385
- }
386
- // Build response with @HttpCode and @Header support
387
- const response = this.createResponse(c, result, httpCode);
388
- // Apply @Header decorators
389
- for (const [headerName, headerValue] of responseHeaders){
390
- response.headers.set(headerName, headerValue);
391
- }
392
- return response;
393
- } catch (error) {
394
- // Run exception filters
395
- for (const filter of filters){
396
- if (shouldFilterCatch(filter, error)) {
397
- try {
398
- const result = await filter.catch(error, executionContext);
399
- return this.createResponse(c, result);
400
- } catch {
401
- break;
402
- }
403
- }
404
- }
405
- // Default HttpException handling
406
- if (error instanceof HttpException) {
407
- const response = error.getResponse();
408
- const status = error.getStatus();
409
- return c.json(response, status);
410
- }
411
- // Default 500
412
- return c.json({
413
- statusCode: 500,
414
- message: 'Internal Server Error'
415
- }, 500);
416
- }
417
- };
418
- }
419
- async extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes) {
420
- if (paramMetadata.length === 0) {
421
- return [
422
- c
423
- ];
424
- }
425
- const maxIndex = paramMetadata.at(-1).index;
426
- const args = new Array(maxIndex + 1).fill(undefined);
427
- for (const param of paramMetadata){
428
- let value = await this.extractParam(c, param);
429
- const metadata = {
430
- type: param.type,
431
- data: param.name,
432
- metatype: paramTypes?.[param.index]
433
- };
434
- // Run shared pipes (global + controller + method)
435
- for (const pipe of pipes){
436
- value = await pipe.transform(value, metadata);
437
- }
438
- // Run param-level pipes
439
- if (param.pipes && param.pipes.length > 0) {
440
- for (const paramPipe of param.pipes){
441
- const pipeInstance = this.instantiate(paramPipe, requestContainer);
442
- value = await pipeInstance.transform(value, metadata);
443
- }
444
- }
445
- args[param.index] = value;
446
- }
447
- return args;
448
- }
449
- extractParam(c, param) {
450
- if (param.type === ParamType.IP) return this.ipExtractor(c);
451
- const extractor = RouteManager.PARAM_EXTRACTORS.get(param.type);
452
- if (extractor) return extractor(c, param);
453
- return param.factory ? param.factory(param.name, c) : undefined;
454
- }
455
- createResponse(c, result, statusCode) {
456
- if (result instanceof Response) {
457
- return result;
458
- }
459
- if (result === null || result === undefined) {
460
- return c.body(null, statusCode ?? 204);
461
- }
462
- if (typeof result === 'string') {
463
- return c.text(result, statusCode ?? 200);
464
- }
465
- return c.json(result, statusCode ?? 200);
466
- }
467
247
  registerRoute(app, method, path, handler) {
468
248
  const normalizedPath = path || '/';
469
249
  const registrar = RouteManager.METHOD_REGISTRAR.get(method);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",