@velajs/vela 1.9.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
  3. package/dist/application.d.ts +18 -0
  4. package/dist/application.js +45 -0
  5. package/dist/cache/cache.module.d.ts +5 -7
  6. package/dist/cache/cache.module.js +44 -66
  7. package/dist/cache/cache.types.d.ts +26 -0
  8. package/dist/cache/cache.types.js +1 -1
  9. package/dist/cache/index.d.ts +2 -1
  10. package/dist/cache/index.js +1 -0
  11. package/dist/cache/tiered-cache.store.d.ts +19 -0
  12. package/dist/cache/tiered-cache.store.js +40 -0
  13. package/dist/config/config.module.d.ts +6 -5
  14. package/dist/config/config.module.js +46 -49
  15. package/dist/container/container.d.ts +58 -0
  16. package/dist/container/container.js +223 -18
  17. package/dist/container/disposable.d.ts +5 -0
  18. package/dist/container/disposable.js +33 -0
  19. package/dist/container/types.d.ts +8 -0
  20. package/dist/cors/cors.module.d.ts +5 -5
  21. package/dist/cors/cors.module.js +26 -25
  22. package/dist/discovery/discoverable.decorator.d.ts +38 -0
  23. package/dist/discovery/discoverable.decorator.js +42 -0
  24. package/dist/discovery/discovery.service.d.ts +94 -0
  25. package/dist/discovery/discovery.service.js +166 -0
  26. package/dist/discovery/index.d.ts +2 -0
  27. package/dist/discovery/index.js +2 -0
  28. package/dist/entrypoint/entrypoint.registry.d.ts +40 -0
  29. package/dist/entrypoint/entrypoint.registry.js +106 -0
  30. package/dist/entrypoint/entrypoint.types.d.ts +41 -0
  31. package/dist/entrypoint/entrypoint.types.js +3 -0
  32. package/dist/entrypoint/execution-context.d.ts +15 -0
  33. package/dist/entrypoint/execution-context.js +21 -0
  34. package/dist/entrypoint/execution-scope.d.ts +21 -0
  35. package/dist/entrypoint/execution-scope.js +28 -0
  36. package/dist/entrypoint/index.d.ts +4 -0
  37. package/dist/entrypoint/index.js +4 -0
  38. package/dist/event-emitter/event-emitter.subscriber.d.ts +3 -3
  39. package/dist/event-emitter/event-emitter.subscriber.js +12 -29
  40. package/dist/factory/adapter.d.ts +36 -0
  41. package/dist/factory/adapter.js +18 -0
  42. package/dist/factory/bootstrap.js +20 -2
  43. package/dist/factory.d.ts +11 -1
  44. package/dist/factory.js +23 -2
  45. package/dist/fetch/fetch.module.d.ts +5 -6
  46. package/dist/fetch/fetch.module.js +9 -42
  47. package/dist/http/ambient.d.ts +17 -0
  48. package/dist/http/ambient.js +44 -0
  49. package/dist/http/execution-context.js +4 -1
  50. package/dist/http/handler-executor.js +22 -19
  51. package/dist/http/index.d.ts +1 -0
  52. package/dist/http/index.js +1 -0
  53. package/dist/http/route-contributor.d.ts +68 -0
  54. package/dist/http/route-contributor.js +22 -0
  55. package/dist/http/route.manager.d.ts +19 -0
  56. package/dist/http/route.manager.js +112 -19
  57. package/dist/i18n/deep-merge.d.ts +2 -0
  58. package/dist/i18n/deep-merge.js +15 -0
  59. package/dist/i18n/i18n.middleware.d.ts +15 -0
  60. package/dist/i18n/i18n.middleware.js +94 -0
  61. package/dist/i18n/i18n.module.d.ts +13 -0
  62. package/dist/i18n/i18n.module.js +64 -0
  63. package/dist/i18n/i18n.options.d.ts +30 -0
  64. package/dist/i18n/i18n.options.js +16 -0
  65. package/dist/i18n/i18n.service.d.ts +20 -0
  66. package/dist/i18n/i18n.service.js +49 -0
  67. package/dist/i18n/i18n.tokens.d.ts +6 -0
  68. package/dist/i18n/i18n.tokens.js +3 -0
  69. package/dist/i18n/i18n.types.d.ts +6 -0
  70. package/dist/i18n/i18n.types.js +1 -0
  71. package/dist/i18n/index.d.ts +10 -0
  72. package/dist/i18n/index.js +8 -0
  73. package/dist/i18n/message-loader.service.d.ts +21 -0
  74. package/dist/i18n/message-loader.service.js +90 -0
  75. package/dist/i18n/message-registry.d.ts +16 -0
  76. package/dist/i18n/message-registry.js +49 -0
  77. package/dist/index.d.ts +17 -5
  78. package/dist/index.js +14 -3
  79. package/dist/internal.d.ts +0 -2
  80. package/dist/internal.js +0 -7
  81. package/dist/module/configurable-module.builder.d.ts +56 -0
  82. package/dist/module/configurable-module.builder.js +84 -0
  83. package/dist/module/configurable-module.types.d.ts +93 -0
  84. package/dist/module/configurable-module.types.js +5 -0
  85. package/dist/module/define-module.d.ts +105 -0
  86. package/dist/module/define-module.js +227 -0
  87. package/dist/module/index.d.ts +4 -0
  88. package/dist/module/index.js +3 -0
  89. package/dist/module/lazy-provider.d.ts +65 -0
  90. package/dist/module/lazy-provider.js +111 -0
  91. package/dist/openapi/document.js +16 -15
  92. package/dist/pipeline/component.manager.d.ts +15 -9
  93. package/dist/pipeline/component.manager.js +32 -46
  94. package/dist/pipeline/index.d.ts +2 -0
  95. package/dist/pipeline/index.js +1 -0
  96. package/dist/pipeline/pipeline-runner.d.ts +31 -0
  97. package/dist/pipeline/pipeline-runner.js +40 -0
  98. package/dist/pipeline/types.d.ts +21 -4
  99. package/dist/registry/metadata.registry.d.ts +24 -18
  100. package/dist/registry/metadata.registry.js +137 -49
  101. package/dist/schedule/schedule.module.d.ts +5 -0
  102. package/dist/schedule/schedule.module.js +18 -8
  103. package/dist/schedule/schedule.registry.d.ts +3 -3
  104. package/dist/schedule/schedule.registry.js +23 -44
  105. package/dist/schedule-node/schedule-node.module.d.ts +5 -0
  106. package/dist/schedule-node/schedule-node.module.js +20 -10
  107. package/dist/seeder/index.d.ts +5 -0
  108. package/dist/seeder/index.js +4 -0
  109. package/dist/seeder/seeder.decorator.d.ts +7 -0
  110. package/dist/seeder/seeder.decorator.js +15 -0
  111. package/dist/seeder/seeder.module.d.ts +16 -0
  112. package/dist/seeder/seeder.module.js +41 -0
  113. package/dist/seeder/seeder.registry.d.ts +23 -0
  114. package/dist/seeder/seeder.registry.js +83 -0
  115. package/dist/seeder/seeder.tokens.d.ts +2 -0
  116. package/dist/seeder/seeder.tokens.js +1 -0
  117. package/dist/seeder/seeder.types.d.ts +20 -0
  118. package/dist/seeder/seeder.types.js +1 -0
  119. package/dist/storage/index.d.ts +4 -0
  120. package/dist/storage/index.js +2 -0
  121. package/dist/storage/path-template.d.ts +9 -0
  122. package/dist/storage/path-template.js +20 -0
  123. package/dist/storage/signed-url.d.ts +12 -0
  124. package/dist/storage/signed-url.js +55 -0
  125. package/dist/storage/storage.types.d.ts +41 -0
  126. package/dist/storage/storage.types.js +5 -0
  127. package/dist/throttler/throttler.module.d.ts +5 -8
  128. package/dist/throttler/throttler.module.js +27 -55
  129. package/dist/websocket/index.d.ts +12 -0
  130. package/dist/websocket/index.js +16 -0
  131. package/dist/websocket/websocket.decorators.d.ts +33 -0
  132. package/dist/websocket/websocket.decorators.js +58 -0
  133. package/dist/websocket/websocket.module.d.ts +35 -0
  134. package/dist/websocket/websocket.module.js +63 -0
  135. package/dist/websocket/websocket.tokens.d.ts +23 -0
  136. package/dist/websocket/websocket.tokens.js +21 -0
  137. package/dist/websocket/websocket.types.d.ts +86 -0
  138. package/dist/websocket/websocket.types.js +4 -0
  139. package/dist/websocket/ws-argument-resolver.d.ts +13 -0
  140. package/dist/websocket/ws-argument-resolver.js +40 -0
  141. package/dist/websocket/ws-dispatcher.d.ts +45 -0
  142. package/dist/websocket/ws-dispatcher.js +259 -0
  143. package/dist/websocket/ws-exception.d.ts +16 -0
  144. package/dist/websocket/ws-exception.js +32 -0
  145. package/dist/websocket/ws-execution-context.d.ts +9 -0
  146. package/dist/websocket/ws-execution-context.js +24 -0
  147. package/dist/websocket/ws-server.d.ts +31 -0
  148. package/dist/websocket/ws-server.js +74 -0
  149. package/dist/websocket/ws-sync.d.ts +47 -0
  150. package/dist/websocket/ws-sync.js +74 -0
  151. package/dist/websocket-node/index.d.ts +4 -0
  152. package/dist/websocket-node/index.js +5 -0
  153. package/dist/websocket-node/node-ws-client.d.ts +24 -0
  154. package/dist/websocket-node/node-ws-client.js +49 -0
  155. package/dist/websocket-node/redis-sync.d.ts +33 -0
  156. package/dist/websocket-node/redis-sync.js +56 -0
  157. package/dist/websocket-node/register-gateways.d.ts +20 -0
  158. package/dist/websocket-node/register-gateways.js +61 -0
  159. package/package.json +31 -1
  160. package/dist/http/crud-bridge.d.ts +0 -90
  161. package/dist/http/crud-bridge.js +0 -25
@@ -0,0 +1,36 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { VelaApplication } from '../application';
3
+ import type { Container } from '../container/container';
4
+ import type { DiscoveryService } from '../discovery/discovery.service';
5
+ import type { RouteManager } from '../http/route.manager';
6
+ /** The framework's resolved world, handed to adapter hooks. */
7
+ export interface AdapterContext {
8
+ app: VelaApplication;
9
+ container: Container;
10
+ routeManager: RouteManager;
11
+ discovery: DiscoveryService;
12
+ }
13
+ /**
14
+ * The contract a runtime binding implements to plug a platform into a Vela
15
+ * app — what `@velajs/cloudflare` (bindings + Durable Object WebSocket) and
16
+ * the Node/Bun WebSocket transport do, made first-class:
17
+ *
18
+ * ```ts
19
+ * const app = await VelaFactory.create(AppModule, {
20
+ * adapters: [cloudflareAdapter({ bindings })],
21
+ * });
22
+ * ```
23
+ *
24
+ * - `requestMiddleware` is prepended to the global middleware chain (runs
25
+ * before consumer middleware — e.g. capture `c.env` for binding services).
26
+ * - `onBootstrap` runs after DI + lifecycle hooks, with `app.entrypoints`
27
+ * available, BEFORE routes are built — register platform services here.
28
+ * - `onRoutesBuilt` runs after the Hono app exists — mount platform routes
29
+ * (WebSocket upgrades, health endpoints) here via `ctx.app.getHonoApp()`.
30
+ */
31
+ export interface RuntimeAdapter {
32
+ name: string;
33
+ requestMiddleware?: MiddlewareHandler[];
34
+ onBootstrap?(ctx: AdapterContext): void | Promise<void>;
35
+ onRoutesBuilt?(ctx: AdapterContext): void | Promise<void>;
36
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The contract a runtime binding implements to plug a platform into a Vela
3
+ * app — what `@velajs/cloudflare` (bindings + Durable Object WebSocket) and
4
+ * the Node/Bun WebSocket transport do, made first-class:
5
+ *
6
+ * ```ts
7
+ * const app = await VelaFactory.create(AppModule, {
8
+ * adapters: [cloudflareAdapter({ bindings })],
9
+ * });
10
+ * ```
11
+ *
12
+ * - `requestMiddleware` is prepended to the global middleware chain (runs
13
+ * before consumer middleware — e.g. capture `c.env` for binding services).
14
+ * - `onBootstrap` runs after DI + lifecycle hooks, with `app.entrypoints`
15
+ * available, BEFORE routes are built — register platform services here.
16
+ * - `onRoutesBuilt` runs after the Hono app exists — mount platform routes
17
+ * (WebSocket upgrades, health endpoints) here via `ctx.app.getHonoApp()`.
18
+ */ export { };
@@ -1,11 +1,11 @@
1
1
  import { Scope } from "../constants.js";
2
2
  import { Container } from "../container/container.js";
3
3
  import { ModuleRef } from "../container/module-ref.js";
4
+ import { DiscoveryService } from "../discovery/discovery.service.js";
4
5
  import { REQUEST_CONTEXT } from "../http/request-context.js";
5
6
  import { RouteManager } from "../http/route.manager.js";
6
7
  import { ModuleLoader } from "../module/module-loader.js";
7
8
  import { bindAppProviders } from "../pipeline/app-providers.js";
8
- import { ComponentManager } from "../pipeline/component.manager.js";
9
9
  import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
10
10
  /**
11
11
  * Wire the DI graph for `rootModule` and prepare the route manager — without
@@ -32,6 +32,15 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
32
32
  ]
33
33
  });
34
34
  container.markGlobalToken(ModuleRef);
35
+ // Decorator-driven discovery — global so any provider can inject it.
36
+ container.register({
37
+ provide: DiscoveryService,
38
+ useFactory: (c)=>new DiscoveryService(c),
39
+ inject: [
40
+ Container
41
+ ]
42
+ });
43
+ container.markGlobalToken(DiscoveryService);
35
44
  for (const t of [
36
45
  APP_GUARD,
37
46
  APP_PIPE,
@@ -54,7 +63,13 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
54
63
  });
55
64
  container.markGlobalToken(REQUEST_CONTEXT);
56
65
  const routeManager = new RouteManager(container, options);
57
- ComponentManager.init(container);
66
+ // Resolvable so non-HTTP transports (the WebSocket dispatcher) can read the
67
+ // same global-tier components (APP_* + app.useGlobalX()).
68
+ container.register({
69
+ provide: RouteManager,
70
+ useValue: routeManager
71
+ });
72
+ container.markGlobalToken(RouteManager);
58
73
  const loader = new ModuleLoader(container, routeManager);
59
74
  loader.load(rootModule);
60
75
  bindAppProviders(routeManager, container, loader);
@@ -68,6 +83,9 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
68
83
  };
69
84
  routeManager.useGlobalMiddleware(mw);
70
85
  }
86
+ // All providers are registered — compute request-scope bubbling so effective
87
+ // scopes are known before eager instantiation and request resolution.
88
+ container.computeEffectiveScopes();
71
89
  return {
72
90
  container,
73
91
  routeManager,
package/dist/factory.d.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  import { VelaApplication } from './application';
2
2
  import type { Type } from './container/types';
3
+ import type { RuntimeAdapter } from './factory/adapter';
3
4
  import type { BootstrapOptions } from './factory/bootstrap';
5
+ export interface VelaCreateOptions extends BootstrapOptions {
6
+ /**
7
+ * Runtime adapters binding this app to a platform (Cloudflare bindings,
8
+ * WebSocket transports, …). Their `requestMiddleware` is prepended to the
9
+ * global middleware chain; `onBootstrap`/`onRoutesBuilt` hooks run around
10
+ * route building. See {@link RuntimeAdapter}.
11
+ */
12
+ adapters?: RuntimeAdapter[];
13
+ }
4
14
  export declare const VelaFactory: {
5
- create(rootModule: Type, options?: BootstrapOptions): Promise<VelaApplication>;
15
+ create(rootModule: Type, options?: VelaCreateOptions): Promise<VelaApplication>;
6
16
  };
package/dist/factory.js CHANGED
@@ -1,15 +1,36 @@
1
1
  import { VelaApplication } from "./application.js";
2
+ import { DiscoveryService } from "./discovery/discovery.service.js";
2
3
  import { bootstrap } from "./factory/bootstrap.js";
3
4
  export const VelaFactory = {
4
5
  async create (rootModule, options = {}) {
5
- const { container, routeManager, loader } = await bootstrap(rootModule, options);
6
+ const { adapters = [], ...bootstrapOptions } = options;
7
+ const adapterMiddleware = adapters.flatMap((a)=>a.requestMiddleware ?? []);
8
+ if (adapterMiddleware.length > 0) {
9
+ bootstrapOptions.middleware = [
10
+ ...adapterMiddleware,
11
+ ...bootstrapOptions.middleware ?? []
12
+ ];
13
+ }
14
+ const { container, routeManager, loader } = await bootstrap(rootModule, bootstrapOptions);
6
15
  const app = new VelaApplication(container, routeManager);
7
16
  const instances = await loader.resolveAllInstances();
8
17
  app.setInstances(instances);
9
18
  await app.callOnModuleInit();
10
19
  await app.callOnApplicationBootstrap();
11
- // Build routes (async — supports CRUD integration with dynamic imports)
20
+ const adapterContext = {
21
+ app,
22
+ container,
23
+ routeManager,
24
+ discovery: container.resolve(DiscoveryService)
25
+ };
26
+ for (const adapter of adapters){
27
+ await adapter.onBootstrap?.(adapterContext);
28
+ }
29
+ // Build routes (async — supports route-contributor integration)
12
30
  await app.initRoutes();
31
+ for (const adapter of adapters){
32
+ await adapter.onRoutesBuilt?.(adapterContext);
33
+ }
13
34
  return app;
14
35
  }
15
36
  };
@@ -1,8 +1,7 @@
1
- import type { AsyncModuleOptions, DynamicModule } from '../module/types';
2
1
  import type { HttpModuleOptions } from './fetch.types';
3
- export declare class HttpModule {
4
- static forRoot(options?: HttpModuleOptions): DynamicModule;
5
- static forRootAsync(options: AsyncModuleOptions<HttpModuleOptions> & {
6
- key?: string;
7
- }): DynamicModule;
2
+ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<HttpModuleOptions, "forRoot", "create", {
3
+ isGlobal?: boolean;
4
+ }>;
5
+ export declare class HttpModule extends ConfigurableModuleClass {
8
6
  }
7
+ export {};
@@ -5,52 +5,18 @@ function _ts_decorate(decorators, target, key, desc) {
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
7
  import { Module } from "../module/decorators.js";
8
- import { stableHash } from "../module/stable-hash.js";
8
+ import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
9
9
  import { HttpService, HTTP_MODULE_OPTIONS } from "./fetch.service.js";
10
- export class HttpModule {
11
- static forRoot(options = {}) {
12
- return {
13
- module: HttpModule,
14
- key: stableHash(options),
15
- providers: [
16
- {
17
- provide: HTTP_MODULE_OPTIONS,
18
- useValue: options
19
- },
20
- HttpService
21
- ],
22
- exports: [
23
- HttpService,
24
- HTTP_MODULE_OPTIONS
25
- ]
26
- };
27
- }
28
- static forRootAsync(options) {
29
- return {
30
- module: HttpModule,
31
- key: options.key ?? stableHash({
32
- inject: options.inject,
33
- useFactory: options.useFactory
34
- }),
35
- imports: options.imports ?? [],
36
- providers: [
37
- {
38
- provide: HTTP_MODULE_OPTIONS,
39
- useFactory: options.useFactory,
40
- inject: options.inject ?? []
41
- },
42
- HttpService
43
- ],
44
- exports: [
45
- HttpService,
46
- HTTP_MODULE_OPTIONS
47
- ]
48
- };
49
- }
10
+ const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
11
+ moduleName: 'Http',
12
+ optionsInjectionToken: HTTP_MODULE_OPTIONS
13
+ }).build();
14
+ export class HttpModule extends ConfigurableModuleClass {
50
15
  }
51
16
  HttpModule = _ts_decorate([
52
17
  Module({
53
18
  providers: [
19
+ // Default for bare `imports: [HttpModule]`; forRoot(options) overrides via merge.
54
20
  {
55
21
  provide: HTTP_MODULE_OPTIONS,
56
22
  useValue: {}
@@ -58,7 +24,8 @@ HttpModule = _ts_decorate([
58
24
  HttpService
59
25
  ],
60
26
  exports: [
61
- HttpService
27
+ HttpService,
28
+ HTTP_MODULE_OPTIONS
62
29
  ]
63
30
  })
64
31
  ], HttpModule);
@@ -0,0 +1,17 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { Container } from '../container/container';
3
+ import { type RequestContext } from './request-context';
4
+ /**
5
+ * Returns Hono's `contextStorage()` middleware. Register it as the FIRST global
6
+ * middleware so `getCurrentContainer()` works everywhere downstream. When using
7
+ * `VelaFactory.create(module, { ambientContainer: true })` this is wired
8
+ * automatically — call this only for manual/raw Hono setups.
9
+ */
10
+ export declare function enableAmbientContainer(): MiddlewareHandler;
11
+ /**
12
+ * The current request's DI container. Throws with actionable guidance if
13
+ * ambient access isn't enabled or this runs outside a request.
14
+ */
15
+ export declare function getCurrentContainer(): Container;
16
+ /** The current request's {@link RequestContext} (via the ambient container). */
17
+ export declare function getCurrentRequestContext(): RequestContext;
@@ -0,0 +1,44 @@
1
+ import { contextStorage, getContext } from "hono/context-storage";
2
+ import { REQUEST_CONTEXT } from "./request-context.js";
3
+ // Opt-in ambient access to the per-request DI container / RequestContext,
4
+ // for deep code that lacks the Hono `Context` (services, standalone helpers).
5
+ //
6
+ // Portable across vela's target runtimes because the `node:async_hooks`
7
+ // dependency lives inside Hono's `contextStorage()` middleware — vela's own
8
+ // source never imports `node:*`. Uses only the workerd-safe ALS subset
9
+ // (`run()`/`getStore()`); no `enterWith`, no cross-thenable propagation.
10
+ //
11
+ // Default path is unchanged: request scope is still carried by the per-request
12
+ // child container (see RouteManager.getRequestContainer). This is a second
13
+ // READ path, active only when enabled.
14
+ /**
15
+ * Returns Hono's `contextStorage()` middleware. Register it as the FIRST global
16
+ * middleware so `getCurrentContainer()` works everywhere downstream. When using
17
+ * `VelaFactory.create(module, { ambientContainer: true })` this is wired
18
+ * automatically — call this only for manual/raw Hono setups.
19
+ */ export function enableAmbientContainer() {
20
+ return contextStorage();
21
+ }
22
+ function tryGetContext() {
23
+ try {
24
+ return getContext();
25
+ } catch {
26
+ // Thrown when called outside the contextStorage() middleware (i.e. ambient
27
+ // access not enabled, or outside a request).
28
+ return undefined;
29
+ }
30
+ }
31
+ /**
32
+ * The current request's DI container. Throws with actionable guidance if
33
+ * ambient access isn't enabled or this runs outside a request.
34
+ */ export function getCurrentContainer() {
35
+ const context = tryGetContext();
36
+ const container = context?.get('container');
37
+ if (!container) {
38
+ throw new Error('getCurrentContainer() is unavailable: enable ambient access via ' + "VelaFactory.create(module, { ambientContainer: true }) (or app.use(enableAmbientContainer())) " + 'and only call it within a request.');
39
+ }
40
+ return container;
41
+ }
42
+ /** The current request's {@link RequestContext} (via the ambient container). */ export function getCurrentRequestContext() {
43
+ return getCurrentContainer().resolve(REQUEST_CONTEXT);
44
+ }
@@ -10,7 +10,10 @@ export function buildExecutionContext(c, controller, handlerName) {
10
10
  switchToHttp: ()=>({
11
11
  getRequest: ()=>c.req.raw,
12
12
  getResponse: ()=>c
13
- })
13
+ }),
14
+ switchToWs: ()=>{
15
+ throw new Error('switchToWs() called on an HTTP ExecutionContext. This handler runs over HTTP, not a WebSocket gateway.');
16
+ }
14
17
  };
15
18
  }
16
19
  // Sentinel marker exposed via `ExecutionContext.getClass()` when the host is a
@@ -1,6 +1,7 @@
1
- import { ForbiddenException, HttpException } from "../errors/http-exception.js";
1
+ import { HttpException } from "../errors/http-exception.js";
2
2
  import { ComponentManager } from "../pipeline/component.manager.js";
3
3
  import { shouldFilterCatch } from "../pipeline/decorators.js";
4
+ import { PipelineRunner } from "../pipeline/pipeline-runner.js";
4
5
  import { getHttpCode, getRedirect, getResponseHeaders } from "./decorators.js";
5
6
  import { buildExecutionContext } from "./execution-context.js";
6
7
  import { instantiateMany } from "./instantiate.js";
@@ -25,12 +26,12 @@ export class HandlerExecutor {
25
26
  // Routed via Reflect so the polyfill funnels both src-level and dist-level
26
27
  // consumers to the same registry (matters in tests that import from dist).
27
28
  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);
29
+ const methodGuards = ComponentManager.getScopedComponents('guard', controller, route.handlerName);
30
+ const methodPipes = ComponentManager.getScopedComponents('pipe', controller, route.handlerName);
31
+ const methodInterceptors = ComponentManager.getScopedComponents('interceptor', controller, route.handlerName);
31
32
  // Filters: reverse order (handler → controller → global) — closest to handler runs first
32
33
  const methodFilters = [
33
- ...ComponentManager.getComponents('filter', controller, route.handlerName)
34
+ ...ComponentManager.getScopedComponents('filter', controller, route.handlerName)
34
35
  ].reverse();
35
36
  const httpCode = getHttpCode(controller, route.handlerName);
36
37
  const responseHeaders = getResponseHeaders(controller, route.handlerName);
@@ -58,24 +59,20 @@ export class HandlerExecutor {
58
59
  const executionContext = buildExecutionContext(c, controller, route.handlerName);
59
60
  try {
60
61
  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
62
  const method = Reflect.get(instance, route.handlerName);
72
63
  if (typeof method !== 'function') {
73
64
  throw new Error(`Method ${String(route.handlerName)} not found on controller`);
74
65
  }
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);
66
+ // Args + pipes → guards → interceptor chain → handler, via the shared
67
+ // runner. `argsBeforeGuards` preserves the deliberate vela HTTP order.
68
+ const result = await PipelineRunner.run({
69
+ context: executionContext,
70
+ guards,
71
+ interceptors,
72
+ argsBeforeGuards: true,
73
+ resolveArgs: ()=>this.argumentResolver.extract(c, paramMetadata, pipes, requestContainer, paramTypes),
74
+ invoke: async (args)=>Reflect.apply(method, instance, args)
75
+ });
79
76
  if (redirect) {
80
77
  return mapRedirect(c, result, redirect);
81
78
  }
@@ -98,6 +95,12 @@ export class HandlerExecutor {
98
95
  const status = error.getStatus();
99
96
  return c.json(response, status);
100
97
  }
98
+ // An unknown error reaching here means no filter claimed it — never
99
+ // swallow it silently: the response is a generic 500, but the cause
100
+ // must land in the logs (diagnostics 'silent' opts out).
101
+ if (requestContainer.getDiagnostics() !== 'silent') {
102
+ console.error(`[vela] unhandled error in ${controller.name}.${String(route.handlerName)}:`, error);
103
+ }
101
104
  return c.json({
102
105
  statusCode: 500,
103
106
  message: 'Internal Server Error'
@@ -4,4 +4,5 @@ export { MiddlewareBuilder } from '../module/middleware';
4
4
  export type { MiddlewareConsumer, MiddlewareConfigProxy, RouteInfo, NestModule, MiddlewareRouteDefinition } from '../module/middleware';
5
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';
6
6
  export { createLazyParamDecorator } from './lazy-param.decorator';
7
+ export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext, } from './ambient';
7
8
  export type { RouteMetadata, ControllerMetadata, ControllerOptions, ParamMetadata, ControllerRegistration, } from './types';
@@ -2,3 +2,4 @@ export { RouteManager } from "./route.manager.js";
2
2
  export { MiddlewareBuilder } from "../module/middleware.js";
3
3
  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.js";
4
4
  export { createLazyParamDecorator } from "./lazy-param.decorator.js";
5
+ export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext } from "./ambient.js";
@@ -0,0 +1,68 @@
1
+ import type { Hono } from 'hono';
2
+ import type { Container } from '../container/container';
3
+ import type { Type } from '../container/types';
4
+ import type { OpenApiPathItem } from '../openapi/types';
5
+ import type { CanActivate } from '../pipeline/types';
6
+ /**
7
+ * Context passed to {@link RouteContributor.buildRoutes} for each controller
8
+ * the contributor claims. The contributor receives the framework's resolved
9
+ * view of the world so generated routes behave identically to hand-written
10
+ * ones: same global prefix, same global guard instances, same path joining.
11
+ */
12
+ export interface RouteContributorContext {
13
+ /** The claimed controller class. */
14
+ controller: Type;
15
+ /** Controller-level prefix as registered with `@Controller(...)`. */
16
+ controllerPrefix: string;
17
+ /** The metadata value (under `claimsMetaKey`) that claimed this controller. */
18
+ meta: unknown;
19
+ /** Application-wide path prefix (e.g. `/api`), already normalized. */
20
+ globalPrefix: string;
21
+ /** Global guard instances, pre-resolved from the root container. */
22
+ globalGuards: CanActivate[];
23
+ /** The app's root container — resolve controller/services from here. */
24
+ container: Container;
25
+ /** Path joiner used by the framework — handles slash normalization. */
26
+ joinPaths: (...parts: string[]) => string;
27
+ }
28
+ /** Context for {@link RouteContributor.buildOpenApiPaths}. */
29
+ export interface RouteContributorOpenApiContext {
30
+ controller: Type;
31
+ meta: unknown;
32
+ /** Application-wide path prefix (e.g. `/api`), already normalized. */
33
+ globalPrefix: string;
34
+ /** Controller-level prefix as registered with `@Controller(...)`. */
35
+ controllerPrefix: string;
36
+ }
37
+ /**
38
+ * A route generator that claims controllers by class-level metadata and
39
+ * mounts routes for them — the public extension point behind `@Crud()` (and
40
+ * any future metadata-driven route generator: storage HTTP surfaces, admin
41
+ * panels, RPC bridges).
42
+ *
43
+ * Register once at import time (`registerRouteContributor`); the framework
44
+ * consults contributors during `RouteManager.build` — AFTER all explicit
45
+ * `@Get`/`@Post` routes, so generated `/:id` catch-alls never shadow custom
46
+ * routes — and during OpenAPI document generation.
47
+ *
48
+ * This contract exists instead of dynamic `import()` because esbuild leaves
49
+ * `await import(variable)` as a runtime import Cloudflare Workers cannot
50
+ * resolve.
51
+ */
52
+ export interface RouteContributor {
53
+ /** Stable identifier; re-registering the same id overwrites (HMR-friendly). */
54
+ id: string;
55
+ /** Class-level metadata key whose presence claims a controller. */
56
+ claimsMetaKey: string;
57
+ buildRoutes(app: Hono, ctx: RouteContributorContext): void | Promise<void>;
58
+ buildOpenApiPaths?(ctx: RouteContributorOpenApiContext): Record<string, OpenApiPathItem>;
59
+ }
60
+ /** Register a contributor. Last registration per `id` wins. */
61
+ export declare function registerRouteContributor(contributor: RouteContributor): void;
62
+ /** All registered contributors (registration order). */
63
+ export declare function getRouteContributors(): RouteContributor[];
64
+ /**
65
+ * Reset all contributor registrations. Test-only.
66
+ * @internal
67
+ */
68
+ export declare function _resetRouteContributors(): void;
@@ -0,0 +1,22 @@
1
+ // Contributors register at import time (package side effect), so the store is
2
+ // anchored on `globalThis` like MetadataRegistry state: a Vite HMR re-eval
3
+ // reuses the same store instead of minting an empty one.
4
+ const STORE_KEY = Symbol.for('vela:route-contributors:v1');
5
+ function store() {
6
+ const g = globalThis;
7
+ return g[STORE_KEY] ??= new Map();
8
+ }
9
+ /** Register a contributor. Last registration per `id` wins. */ export function registerRouteContributor(contributor) {
10
+ store().set(contributor.id, contributor);
11
+ }
12
+ /** All registered contributors (registration order). */ export function getRouteContributors() {
13
+ return [
14
+ ...store().values()
15
+ ];
16
+ }
17
+ /**
18
+ * Reset all contributor registrations. Test-only.
19
+ * @internal
20
+ */ export function _resetRouteContributors() {
21
+ store().clear();
22
+ }
@@ -9,6 +9,12 @@ export interface RouteManagerOptions {
9
9
  getClientIp?: (c: Context) => string | null;
10
10
  middleware?: MiddlewareHandler[];
11
11
  globalPrefix?: string;
12
+ /**
13
+ * Opt into ambient request-container access (`getCurrentContainer()` /
14
+ * `getCurrentRequestContext()`). Registers Hono's `contextStorage()` as the
15
+ * first middleware. Off by default — the explicit child container is unchanged.
16
+ */
17
+ ambientContainer?: boolean;
12
18
  }
13
19
  export declare class RouteManager {
14
20
  private container;
@@ -22,7 +28,20 @@ export declare class RouteManager {
22
28
  private globalPrefix;
23
29
  private consumerMiddlewareDefinitions;
24
30
  private readonly handlerExecutor;
31
+ private readonly ambientContainer;
25
32
  constructor(container: Container, options?: RouteManagerOptions);
33
+ /**
34
+ * The global-tier components (`APP_*` provider tokens + imperative
35
+ * `app.useGlobalX()` instances). Exposed so non-HTTP transports (the
36
+ * WebSocket dispatcher) can apply the same app-wide guards/pipes/interceptors/
37
+ * filters as HTTP routes, read live so late registrations propagate.
38
+ */
39
+ getGlobalComponents(): {
40
+ guards: Array<GuardType | Token<CanActivate>>;
41
+ pipes: Array<PipeType | Token<PipeTransform>>;
42
+ interceptors: Array<InterceptorType | Token<NestInterceptor>>;
43
+ filters: Array<FilterType | Token<ExceptionFilter>>;
44
+ };
26
45
  registerConsumerMiddleware(definitions: MiddlewareRouteDefinition[]): this;
27
46
  setGlobalPrefix(prefix: string): this;
28
47
  useGlobalMiddleware(...middleware: MiddlewareType[]): this;