@velajs/vela 1.10.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 (77) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/application.d.ts +8 -0
  3. package/dist/application.js +20 -0
  4. package/dist/container/container.d.ts +25 -0
  5. package/dist/container/container.js +67 -11
  6. package/dist/container/disposable.js +1 -1
  7. package/dist/cors/cors.module.d.ts +5 -5
  8. package/dist/cors/cors.module.js +26 -25
  9. package/dist/discovery/discoverable.decorator.d.ts +38 -0
  10. package/dist/discovery/discoverable.decorator.js +42 -0
  11. package/dist/discovery/discovery.service.d.ts +94 -0
  12. package/dist/discovery/discovery.service.js +166 -0
  13. package/dist/discovery/index.d.ts +2 -0
  14. package/dist/discovery/index.js +2 -0
  15. package/dist/entrypoint/entrypoint.registry.d.ts +40 -0
  16. package/dist/entrypoint/entrypoint.registry.js +106 -0
  17. package/dist/entrypoint/entrypoint.types.d.ts +41 -0
  18. package/dist/entrypoint/entrypoint.types.js +3 -0
  19. package/dist/entrypoint/execution-context.d.ts +15 -0
  20. package/dist/entrypoint/execution-context.js +21 -0
  21. package/dist/entrypoint/execution-scope.d.ts +21 -0
  22. package/dist/entrypoint/execution-scope.js +28 -0
  23. package/dist/entrypoint/index.d.ts +4 -0
  24. package/dist/entrypoint/index.js +4 -0
  25. package/dist/event-emitter/event-emitter.subscriber.d.ts +3 -3
  26. package/dist/event-emitter/event-emitter.subscriber.js +12 -29
  27. package/dist/factory/adapter.d.ts +36 -0
  28. package/dist/factory/adapter.js +18 -0
  29. package/dist/factory/bootstrap.js +10 -2
  30. package/dist/factory.d.ts +11 -1
  31. package/dist/factory.js +23 -2
  32. package/dist/http/handler-executor.js +22 -19
  33. package/dist/http/route-contributor.d.ts +68 -0
  34. package/dist/http/route-contributor.js +22 -0
  35. package/dist/http/route.manager.js +28 -19
  36. package/dist/i18n/i18n.module.js +4 -1
  37. package/dist/index.d.ts +12 -3
  38. package/dist/index.js +9 -2
  39. package/dist/internal.d.ts +0 -2
  40. package/dist/internal.js +0 -7
  41. package/dist/module/configurable-module.builder.d.ts +12 -0
  42. package/dist/module/configurable-module.builder.js +21 -114
  43. package/dist/module/define-module.d.ts +105 -0
  44. package/dist/module/define-module.js +227 -0
  45. package/dist/module/index.d.ts +3 -1
  46. package/dist/module/index.js +3 -1
  47. package/dist/module/lazy-provider.d.ts +65 -0
  48. package/dist/module/lazy-provider.js +111 -0
  49. package/dist/openapi/document.js +16 -15
  50. package/dist/pipeline/component.manager.d.ts +15 -9
  51. package/dist/pipeline/component.manager.js +32 -46
  52. package/dist/pipeline/index.d.ts +2 -0
  53. package/dist/pipeline/index.js +1 -0
  54. package/dist/pipeline/pipeline-runner.d.ts +31 -0
  55. package/dist/pipeline/pipeline-runner.js +40 -0
  56. package/dist/registry/metadata.registry.d.ts +9 -3
  57. package/dist/registry/metadata.registry.js +55 -25
  58. package/dist/schedule/schedule.module.d.ts +5 -0
  59. package/dist/schedule/schedule.module.js +18 -8
  60. package/dist/schedule/schedule.registry.d.ts +3 -3
  61. package/dist/schedule/schedule.registry.js +23 -44
  62. package/dist/schedule-node/schedule-node.module.d.ts +5 -0
  63. package/dist/schedule-node/schedule-node.module.js +20 -10
  64. package/dist/seeder/seeder.module.d.ts +8 -10
  65. package/dist/seeder/seeder.module.js +16 -15
  66. package/dist/websocket/index.d.ts +1 -1
  67. package/dist/websocket/index.js +1 -1
  68. package/dist/websocket/websocket.module.d.ts +20 -3
  69. package/dist/websocket/websocket.module.js +50 -39
  70. package/dist/websocket/websocket.tokens.d.ts +2 -1
  71. package/dist/websocket/websocket.tokens.js +3 -2
  72. package/dist/websocket/ws-dispatcher.d.ts +21 -9
  73. package/dist/websocket/ws-dispatcher.js +44 -38
  74. package/dist/websocket-node/register-gateways.js +3 -3
  75. package/package.json +1 -1
  76. package/dist/http/crud-bridge.d.ts +0 -90
  77. package/dist/http/crud-bridge.js +0 -25
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.0 (2026-07-04)
4
+
5
+ The module-model release: one blessed authoring path plus public kernel
6
+ extension points, so feature modules (websocket, storage, queue, …) are built
7
+ entirely on the public API. See `MODULE_AUTHORING.md` for the author contract.
8
+ Contains deliberate breaking changes (no deprecation shims); coordinated
9
+ releases of `@velajs/{storage,better-auth,testing,crud,cloudflare}` accompany
10
+ this version.
11
+
12
+ ### Added
13
+
14
+ - **`defineModule`** — the single module-authoring engine: generates `forRoot`
15
+ AND `forRootAsync` (typed `inject` inference), derives deterministic
16
+ `stableHash` keys (`key(options)` override + explicit `key` passthrough),
17
+ and accepts contributions (providers/controllers/imports/exports) **as
18
+ functions of the options** plus a standardized `global:` component slot.
19
+ `ConfigurableModuleBuilder` is now a thin adapter over it (unchanged API).
20
+ - **Authoring primitives**: `lazyProvider` (memoized deferred thunk — replaces
21
+ the hand-rolled `(...deps)=>()=>fn(...deps)` closures), `provideGlobal`
22
+ (the one `APP_*` wiring idiom), `sideEffectModule` (first-class
23
+ contribution-only modules; supported form of the i18n empty-marker trick),
24
+ `moduleToken`, `moduleKey`.
25
+ - **`DiscoveryService` + `createDiscoverableDecorator`** — public
26
+ decorator-driven discovery (`providersWithMeta`, `methodsWithMeta`,
27
+ `getProviders`) backed by a reverse metadata index inside
28
+ `MetadataRegistry`; honors container diagnostics in one place;
29
+ request-scoped providers are surfaced but not materialized (opt in with
30
+ `includeRequestScoped`). The event-emitter, schedule, and websocket
31
+ bootstrap scans now all run through it.
32
+ - **Open entrypoint registry** — `registerEntrypointKind({ kind, metaKey,
33
+ level })`, the `ContributesEntrypoints` interface, and per-application
34
+ `app.entrypoints` (`ofKind`/`kinds`/`all`), built at the end of
35
+ `callOnApplicationBootstrap()` so slim bootstrap paths (Cloudflare Durable
36
+ Objects) get it too. Transports query entrypoints instead of module
37
+ internals; a new kind (queue, cron, CLI) needs **zero core changes**.
38
+ - **`RouteContributor`** — public metadata-claimed route generation
39
+ (`registerRouteContributor`), consulted after explicit routes and during
40
+ OpenAPI generation with verb-level merge. Replaces the internal CrudBridge.
41
+ - **`RuntimeAdapter`** — `VelaFactory.create(module, { adapters: [...] })`
42
+ with `requestMiddleware` (prepended to the global chain), `onBootstrap`
43
+ (after lifecycle + entrypoints, before routes) and `onRoutesBuilt` hooks.
44
+ - **`PipelineRunner`** — the shared guard → pipe → interceptor execution core
45
+ used by HTTP and WebSocket dispatch (and any custom dispatcher);
46
+ configurable args/guards order, transport-specific guard-rejection error.
47
+ - **`Container.replaceProvider(provider, { buckets })`** — supported
48
+ force-replace across module buckets (what test harnesses need).
49
+ - **`buildEntrypointExecutionContext(kind, class, handler, payload)`** — the
50
+ entrypoint sibling of the HTTP/WS execution contexts, so guards/
51
+ interceptors/filters written against `getClass()`/`getHandler()`/`getType()`
52
+ run unchanged around queue batches, scheduled ticks, and custom kinds
53
+ (`EntrypointExecutionContext.getPayload()`). `@velajs/cloudflare` dispatches
54
+ queue/scheduled handlers through `PipelineRunner` with consumer-scoped
55
+ components (HTTP-global components deliberately do not apply; unclaimed
56
+ errors rethrow to preserve platform retry semantics).
57
+ - **`runInEntrypointScope(container, fn)`** — the non-HTTP dispatch scope
58
+ primitive: runs one unit of work (queue batch, scheduled tick, RPC call) in
59
+ a fresh request-scoped child with LIFO disposal — the per-request-child
60
+ equivalent for entrypoint dispatchers. `@velajs/cloudflare`'s queue and
61
+ scheduled handlers run on it (request-scoped consumer deps rebuild per
62
+ batch/tick instead of capturing boot instances).
63
+
64
+ ### Changed
65
+
66
+ - **Factory dependency visibility**: `useFactory`/`forRootAsync` `inject`
67
+ deps now resolve from the declaring module's scope FIRST (imports and
68
+ exports are honored), with the legacy no-requester lookup kept as fallback.
69
+ - **All in-core configurable modules are on the one engine**: `CorsModule` and
70
+ `SeederModule` rebuilt on `defineModule` (Cors gains `forRootAsync`; both
71
+ keep their token/key identities), `ScheduleModule`/`ScheduleNodeModule`
72
+ normalized to zero-config `@Module` bags with parity `forRoot()` sugar.
73
+ Builder-based modules (Config/Cache/I18n/Throttler/Http) already run on it
74
+ through the `ConfigurableModuleBuilder` adapter.
75
+ - **Unhandled handler errors are logged**: an error no exception filter claims
76
+ still maps to the generic 500 response, but the cause now lands in the logs
77
+ (`console.error`, gated on container diagnostics ≠ `silent`) — closing the
78
+ silent-500 gap.
79
+ - **`WebSocketModule`** rebuilt on `defineModule`: registry → driver → server
80
+ construction moved into chained provider factories (single shared registry
81
+ preserved; everything materializes at bootstrap), deterministic key
82
+ `ws#<sync-kind>` — **two identical `forRoot()` calls now dedup**
83
+ (HMR-idempotent; pass explicit `key` for exotic multi-instance),
84
+ `forRootAsync` available. `WsDispatcher` contributes `'websocket'`
85
+ entrypoints; `registerWebSocketGateways` consumes
86
+ `app.entrypoints.ofKind('websocket')`.
87
+
88
+ ### Breaking
89
+
90
+ - **`WS_MODULE_OPTIONS`** is now a typed `InjectionToken` (was the raw string
91
+ `'vela:ws-module-options'`).
92
+ - **`ComponentManager`** is stateless: `init()` and the process-global
93
+ container are gone; `resolve*` methods require an explicit container;
94
+ `getComponents` replaced by `getScopedComponents` (controller + handler
95
+ only — app-wide components have one source: `RouteManager`).
96
+ `registerGlobal`/`MetadataRegistry.getGlobal` (a dead tier with no readers
97
+ on the request path) are removed; `MetadataRegistry.clear()` is now a
98
+ no-op.
99
+ - **CrudBridge removed** (`registerCrudBridge`/`getCrudBridge` and the
100
+ `/internal` exports): use `registerRouteContributor`. `@velajs/crud`
101
+ migrates in its coordinated release.
102
+ - `forRootAsync` structural fields (non-async keys passed alongside
103
+ `useFactory`) now merge under the resolved options.
104
+
3
105
  ## 1.10.0 (2026-07-01)
4
106
 
5
107
  ### Added
@@ -1,6 +1,7 @@
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 { EntrypointRegistry } from './entrypoint/entrypoint.registry';
4
5
  import type { RouteManager } from './http/route.manager';
5
6
  import type { MountOpenApiOptions } from './openapi/types';
6
7
  import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
@@ -9,6 +10,7 @@ export declare class VelaApplication {
9
10
  private readonly routeManager;
10
11
  private instances;
11
12
  private honoApp;
13
+ private entrypointRegistry;
12
14
  private disposed;
13
15
  constructor(container: Container, routeManager: RouteManager);
14
16
  /** Pre-build routes (handles async CRUD imports). Called by VelaFactory. */
@@ -44,6 +46,12 @@ export declare class VelaApplication {
44
46
  mountOpenApi(options: MountOpenApiOptions): this;
45
47
  callOnModuleInit(): Promise<void>;
46
48
  callOnApplicationBootstrap(): Promise<void>;
49
+ /**
50
+ * Every entrypoint contributed by the module graph, grouped by kind —
51
+ * what runtime adapters/transports query instead of re-scanning providers:
52
+ * `app.entrypoints.ofKind('websocket')`.
53
+ */
54
+ get entrypoints(): EntrypointRegistry;
47
55
  close(signal?: string): Promise<void>;
48
56
  /**
49
57
  * Full teardown: run shutdown lifecycle hooks ({@link close}) then dispose
@@ -1,3 +1,5 @@
1
+ import { DiscoveryService } from "./discovery/discovery.service.js";
2
+ import { EntrypointRegistry } from "./entrypoint/entrypoint.registry.js";
1
3
  import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
2
4
  import { renderScalarUi } from "./openapi/scalar-ui.js";
3
5
  import { renderSwaggerUi } from "./openapi/swagger-ui.js";
@@ -7,6 +9,7 @@ export class VelaApplication {
7
9
  routeManager;
8
10
  instances = [];
9
11
  honoApp = null;
12
+ entrypointRegistry = null;
10
13
  disposed = false;
11
14
  constructor(container, routeManager){
12
15
  this.container = container;
@@ -137,6 +140,23 @@ export class VelaApplication {
137
140
  await instance.onApplicationBootstrap();
138
141
  }
139
142
  }
143
+ // Build the per-app entrypoint registry AFTER the hooks: dispatchers that
144
+ // implement ContributesEntrypoints (WsDispatcher) finish their own
145
+ // discovery inside onApplicationBootstrap. Built here — not in
146
+ // VelaFactory/initRoutes — so slim bootstrap paths that never build HTTP
147
+ // routes (the Cloudflare Durable Object) still get `app.entrypoints`.
148
+ const discovery = this.container.has(DiscoveryService) ? this.container.resolve(DiscoveryService) : new DiscoveryService(this.container);
149
+ this.entrypointRegistry = await EntrypointRegistry.build(discovery, this.instances);
150
+ }
151
+ /**
152
+ * Every entrypoint contributed by the module graph, grouped by kind —
153
+ * what runtime adapters/transports query instead of re-scanning providers:
154
+ * `app.entrypoints.ofKind('websocket')`.
155
+ */ get entrypoints() {
156
+ if (!this.entrypointRegistry) {
157
+ throw new Error('Entrypoints are not built yet — they are assembled at the end of ' + 'callOnApplicationBootstrap(). Finish bootstrapping before querying them.');
158
+ }
159
+ return this.entrypointRegistry;
140
160
  }
141
161
  async close(signal) {
142
162
  const reversed = [
@@ -52,6 +52,21 @@ export declare class Container {
52
52
  has(token: Token): boolean;
53
53
  /** True if the specified module's bucket has a registration for `token`. */
54
54
  hasInScope(token: Token, moduleId: string): boolean;
55
+ /** Module buckets that hold a registration for `token` (registration order). */
56
+ getOwnerModuleIds(token: Token): string[];
57
+ /**
58
+ * Force-replace a provider across module scopes — the supported form of the
59
+ * override loop test harnesses need (module-scoped resolution consults the
60
+ * module's own bucket first, so a root-only override would never win there).
61
+ *
62
+ * - `'all-existing'` (default): every non-root bucket that already holds the
63
+ * token, plus `__root__` (so framework-internal lookups see it too).
64
+ * - `'root'`: only the `__root__` bucket.
65
+ * - `string[]`: exactly these bucket ids.
66
+ */
67
+ replaceProvider<T>(provider: Type<T> | ProviderOptions<T>, options?: {
68
+ buckets?: 'all-existing' | 'root' | string[];
69
+ }): this;
55
70
  getProviderScope(token: Token): Scope | undefined;
56
71
  getTokens(): Token[];
57
72
  /**
@@ -95,6 +110,16 @@ export declare class Container {
95
110
  dispose(): Promise<void>;
96
111
  private resolveRegistration;
97
112
  private resolveClass;
113
+ /**
114
+ * Resolve one factory `inject` dependency. Since 1.11 the declaring module's
115
+ * scope is consulted FIRST (proper visibility semantics — a `forRootAsync`
116
+ * factory can inject providers reachable through its module's imports); on
117
+ * any failure the legacy no-requester lookup (root bucket, then first
118
+ * exporter) is kept as fallback, so every previously-resolving graph still
119
+ * resolves.
120
+ */
121
+ private resolveFactoryDependency;
122
+ private resolveFactoryDependencyAsync;
98
123
  private resolveFactory;
99
124
  resolveAsync<T>(token: Token<T>, requestingModuleId?: string): Promise<T>;
100
125
  private createLazyProxy;
@@ -288,6 +288,39 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
288
288
  /** True if the specified module's bucket has a registration for `token`. */ hasInScope(token, moduleId) {
289
289
  return this.providers.get(moduleId)?.has(token) ?? false;
290
290
  }
291
+ /** Module buckets that hold a registration for `token` (registration order). */ getOwnerModuleIds(token) {
292
+ return [
293
+ ...this.exporterIndex.get(token) ?? []
294
+ ];
295
+ }
296
+ /**
297
+ * Force-replace a provider across module scopes — the supported form of the
298
+ * override loop test harnesses need (module-scoped resolution consults the
299
+ * module's own bucket first, so a root-only override would never win there).
300
+ *
301
+ * - `'all-existing'` (default): every non-root bucket that already holds the
302
+ * token, plus `__root__` (so framework-internal lookups see it too).
303
+ * - `'root'`: only the `__root__` bucket.
304
+ * - `string[]`: exactly these bucket ids.
305
+ */ replaceProvider(provider, options = {}) {
306
+ const buckets = options.buckets ?? 'all-existing';
307
+ if (buckets === 'root') {
308
+ return this.register(provider);
309
+ }
310
+ if (Array.isArray(buckets)) {
311
+ for (const moduleId of buckets)this.register(provider, moduleId);
312
+ return this;
313
+ }
314
+ const token = typeof provider === 'function' ? provider : provider.provide;
315
+ if (!token) {
316
+ throw new Error('replaceProvider requires a token (`provide`)');
317
+ }
318
+ for (const [moduleId, bucket] of this.providers){
319
+ if (moduleId === ROOT_MODULE_ID) continue;
320
+ if (bucket.has(token)) this.register(provider, moduleId);
321
+ }
322
+ return this.register(provider);
323
+ }
291
324
  getProviderScope(token) {
292
325
  const exporters = this.exporterIndex.get(token);
293
326
  if (!exporters) return undefined;
@@ -516,10 +549,9 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
516
549
  try {
517
550
  let instance;
518
551
  if (registration.useFactory) {
519
- // Factories (forRootAsync, useFactory) commonly inject deps from the
520
- // importing module's scope. Vela has no `forRootAsync({ imports })`
521
- // surface to track that, so factory inject deps resolve from the
522
- // declaring module's POV — same escape-hatch shape as ModuleRef.
552
+ // Factory inject deps resolve from the declaring module's scope first
553
+ // (visibility-correct for forRootAsync), with the legacy no-requester
554
+ // lookup kept as fallback see resolveFactoryDependency.
523
555
  instance = this.resolveFactory(registration);
524
556
  } else if (registration.useClass) {
525
557
  instance = this.resolveClass(registration.useClass, registration.declaringModuleId);
@@ -578,16 +610,40 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
578
610
  });
579
611
  return new target(...dependencies);
580
612
  }
613
+ /**
614
+ * Resolve one factory `inject` dependency. Since 1.11 the declaring module's
615
+ * scope is consulted FIRST (proper visibility semantics — a `forRootAsync`
616
+ * factory can inject providers reachable through its module's imports); on
617
+ * any failure the legacy no-requester lookup (root bucket, then first
618
+ * exporter) is kept as fallback, so every previously-resolving graph still
619
+ * resolves.
620
+ */ resolveFactoryDependency(token, declaringModuleId) {
621
+ if (declaringModuleId !== ROOT_MODULE_ID && this.scopes.has(declaringModuleId)) {
622
+ try {
623
+ return this.resolve(token, declaringModuleId);
624
+ } catch {
625
+ // Fall through to the legacy escape hatch below.
626
+ }
627
+ }
628
+ return this.resolve(token);
629
+ }
630
+ async resolveFactoryDependencyAsync(token, declaringModuleId) {
631
+ if (declaringModuleId !== ROOT_MODULE_ID && this.scopes.has(declaringModuleId)) {
632
+ try {
633
+ return await this.resolveAsync(token, declaringModuleId);
634
+ } catch {
635
+ // Fall through to the legacy escape hatch below.
636
+ }
637
+ }
638
+ return this.resolveAsync(token);
639
+ }
581
640
  resolveFactory(registration) {
582
641
  if (!registration.useFactory) {
583
642
  throw new Error('Factory function is missing');
584
643
  }
585
- // Factory inject deps resolve without a requester — Vela has no
586
- // `forRootAsync({ imports })` surface to track which module the factory
587
- // resolves from, so it uses the same escape-hatch shape as ModuleRef.
588
644
  const dependencies = (registration.inject || []).map((token)=>{
589
645
  const resolved = token instanceof ForwardRef ? token.factory() : token;
590
- return this.resolve(resolved);
646
+ return this.resolveFactoryDependency(resolved, registration.declaringModuleId);
591
647
  });
592
648
  const result = registration.useFactory(...dependencies);
593
649
  if (result instanceof Promise) {
@@ -618,11 +674,11 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
618
674
  if (scope === Scope.SINGLETON && registration.instance !== undefined) {
619
675
  return registration.instance;
620
676
  }
621
- // Factory inject deps resolve without a requester (same escape hatch as
622
- // sync resolveFactory).
677
+ // Module scope first, legacy no-requester fallback same policy as the
678
+ // sync resolveFactory path.
623
679
  const dependencies = await Promise.all((registration.inject || []).map((t)=>{
624
680
  const resolved = t instanceof ForwardRef ? t.factory() : t;
625
- return this.resolveAsync(resolved);
681
+ return this.resolveFactoryDependencyAsync(resolved, registration.declaringModuleId);
626
682
  }));
627
683
  const instance = await registration.useFactory(...dependencies);
628
684
  if (scope === Scope.SINGLETON) {
@@ -1,4 +1,4 @@
1
- // Edge-safe disposal contract (mirrors stratal's). Precedence:
1
+ // Edge-safe disposal contract. Precedence:
2
2
  // Symbol.asyncDispose > Symbol.dispose > .dispose()
3
3
  // Symbol.asyncDispose/Symbol.dispose are resolved at runtime (they are not in
4
4
  // the ES2022 lib the project targets), so nothing here depends on the
@@ -1,7 +1,7 @@
1
- import type { DynamicModule } from '../module/types';
2
1
  import type { CorsOptions } from './cors.types';
3
- export declare class CorsModule {
4
- static forRoot(options?: CorsOptions & {
5
- key?: string;
6
- }): DynamicModule;
2
+ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<CorsOptions, "forRoot", "create", {
3
+ isGlobal?: boolean;
4
+ }>;
5
+ export declare class CorsModule extends ConfigurableModuleClass {
7
6
  }
7
+ export {};
@@ -1,36 +1,37 @@
1
1
  import { cors } from "hono/cors";
2
- import { stableHash } from "../module/stable-hash.js";
2
+ import { defineModule } from "../module/define-module.js";
3
3
  import { APP_MIDDLEWARE } from "../pipeline/tokens.js";
4
4
  import { CORS_OPTIONS } from "./cors.tokens.js";
5
- export class CorsModule {
6
- static forRoot(options = {}) {
7
- const corsMiddleware = cors({
8
- origin: options.origin ?? '*',
9
- allowMethods: options.allowMethods,
10
- allowHeaders: options.allowHeaders,
11
- exposeHeaders: options.exposeHeaders,
12
- credentials: options.credentials,
13
- maxAge: options.maxAge
14
- });
15
- const middleware = {
16
- use: (c, next)=>corsMiddleware(c, next)
17
- };
18
- return {
19
- module: CorsModule,
20
- key: options.key ?? stableHash(options),
5
+ function buildCorsMiddleware(options) {
6
+ const corsMiddleware = cors({
7
+ origin: options.origin ?? '*',
8
+ allowMethods: options.allowMethods,
9
+ allowHeaders: options.allowHeaders,
10
+ exposeHeaders: options.exposeHeaders,
11
+ credentials: options.credentials,
12
+ maxAge: options.maxAge
13
+ });
14
+ return {
15
+ use: (c, next)=>corsMiddleware(c, next)
16
+ };
17
+ }
18
+ const { ConfigurableModuleClass } = defineModule({
19
+ name: 'Cors',
20
+ optionsToken: CORS_OPTIONS,
21
+ setup: ({ OPTIONS })=>({
21
22
  providers: [
22
- {
23
- provide: CORS_OPTIONS,
24
- useValue: options
25
- },
26
23
  {
27
24
  provide: APP_MIDDLEWARE,
28
- useValue: middleware
25
+ useFactory: (options)=>buildCorsMiddleware(options),
26
+ inject: [
27
+ OPTIONS
28
+ ]
29
29
  }
30
30
  ],
31
31
  exports: [
32
- CORS_OPTIONS
32
+ OPTIONS
33
33
  ]
34
- };
35
- }
34
+ })
35
+ });
36
+ export class CorsModule extends ConfigurableModuleClass {
36
37
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A decorator whose annotated classes/methods are findable through
3
+ * `DiscoveryService` by the decorator itself (no string key at the call site).
4
+ */
5
+ export interface DiscoverableDecorator<T> {
6
+ (value: T): ClassDecorator & MethodDecorator;
7
+ /** The metadata key backing this decorator — stable across HMR re-evals. */
8
+ KEY: string;
9
+ }
10
+ export interface CreateDiscoverableDecoratorOptions {
11
+ /**
12
+ * Append to a class-level list instead of overwriting a single slot —
13
+ * for stackable method decorators (`@Cron`-style: each application pushes
14
+ * `{ methodName, ...value }` onto the class's list, which
15
+ * `DiscoveryService.methodsWithMeta` flattens back to per-method entries).
16
+ */
17
+ append?: boolean;
18
+ }
19
+ /**
20
+ * Mint a discoverable decorator for a module's extension surface:
21
+ *
22
+ * ```ts
23
+ * export interface QueueConsumerMeta { queue: string }
24
+ * export const QueueConsumer = createDiscoverableDecorator<QueueConsumerMeta>('vela:queue:consumer');
25
+ *
26
+ * @QueueConsumer({ queue: 'emails' })
27
+ * @Injectable()
28
+ * class EmailConsumer { ... }
29
+ *
30
+ * // At bootstrap, anywhere:
31
+ * discovery.providersWithMeta(QueueConsumer) // typed { meta: QueueConsumerMeta }
32
+ * ```
33
+ *
34
+ * Unlike `Reflector.createDecorator`, the key is required and caller-chosen:
35
+ * a random key would mint a fresh identity on every HMR re-eval and orphan
36
+ * previously-decorated classes. Namespace it (`'<pkg>:<area>:<thing>'`).
37
+ */
38
+ export declare function createDiscoverableDecorator<T>(key: string, options?: CreateDiscoverableDecoratorOptions): DiscoverableDecorator<T>;
@@ -0,0 +1,42 @@
1
+ import { MetadataRegistry } from "../registry/metadata.registry.js";
2
+ /**
3
+ * Mint a discoverable decorator for a module's extension surface:
4
+ *
5
+ * ```ts
6
+ * export interface QueueConsumerMeta { queue: string }
7
+ * export const QueueConsumer = createDiscoverableDecorator<QueueConsumerMeta>('vela:queue:consumer');
8
+ *
9
+ * @QueueConsumer({ queue: 'emails' })
10
+ * @Injectable()
11
+ * class EmailConsumer { ... }
12
+ *
13
+ * // At bootstrap, anywhere:
14
+ * discovery.providersWithMeta(QueueConsumer) // typed { meta: QueueConsumerMeta }
15
+ * ```
16
+ *
17
+ * Unlike `Reflector.createDecorator`, the key is required and caller-chosen:
18
+ * a random key would mint a fresh identity on every HMR re-eval and orphan
19
+ * previously-decorated classes. Namespace it (`'<pkg>:<area>:<thing>'`).
20
+ */ export function createDiscoverableDecorator(key, options = {}) {
21
+ const decorator = (value)=>{
22
+ return (target, propertyKey)=>{
23
+ const ctor = propertyKey !== undefined ? target.constructor : target;
24
+ if (options.append) {
25
+ if (propertyKey !== undefined) {
26
+ MetadataRegistry.appendCustomClassMeta(ctor, key, {
27
+ methodName: propertyKey,
28
+ ...value
29
+ });
30
+ } else {
31
+ MetadataRegistry.appendCustomClassMeta(ctor, key, value);
32
+ }
33
+ } else if (propertyKey !== undefined) {
34
+ MetadataRegistry.setCustomHandlerMeta(ctor, propertyKey, key, value);
35
+ } else {
36
+ MetadataRegistry.setCustomClassMeta(ctor, key, value);
37
+ }
38
+ };
39
+ };
40
+ decorator.KEY = key;
41
+ return decorator;
42
+ }
@@ -0,0 +1,94 @@
1
+ import { Scope } from '../constants';
2
+ import { Container } from '../container/container';
3
+ import type { Token, Type } from '../container/types';
4
+ import type { Constructor } from '../registry/types';
5
+ import type { DiscoverableDecorator } from './discoverable.decorator';
6
+ /** A container-registered class provider surfaced by discovery. */
7
+ export interface DiscoveredClass<T = unknown> {
8
+ token: Token;
9
+ metatype: Type<T>;
10
+ /** Module buckets holding this token (registration order; first = primary). */
11
+ moduleIds: string[];
12
+ scope: Scope;
13
+ /**
14
+ * Resolved instance. `undefined` only when the provider is request-scoped
15
+ * and `includeRequestScoped` was not set — request-scoped providers cannot
16
+ * be materialized at bootstrap without fabricating a phantom request.
17
+ */
18
+ instance: T | undefined;
19
+ }
20
+ export interface DiscoveredMethodMeta<M = unknown> {
21
+ class: DiscoveredClass;
22
+ methodName: string | symbol;
23
+ meta: M;
24
+ }
25
+ export interface DiscoveryFilter {
26
+ /** Restrict to providers declared by these module buckets. */
27
+ moduleId?: string | string[];
28
+ /**
29
+ * Resolve request-scoped providers too (constructs an instance outside any
30
+ * request — only for callers that know what they're doing). Default false:
31
+ * request-scoped hits are skipped with a diagnostics warning.
32
+ */
33
+ includeRequestScoped?: boolean;
34
+ }
35
+ /**
36
+ * Decorator-driven provider discovery — the public replacement for the
37
+ * bootstrap scan every subsystem used to hand-roll (`container.getTokens()` +
38
+ * `MetadataRegistry.getCustomClassMeta` + `resolve` + diagnostics handling).
39
+ *
40
+ * Registered globally by `bootstrap()`, so any provider can inject it:
41
+ *
42
+ * ```ts
43
+ * @Injectable()
44
+ * class MyRegistry implements OnApplicationBootstrap {
45
+ * constructor(private readonly discovery: DiscoveryService) {}
46
+ * onApplicationBootstrap() {
47
+ * for (const { instance, meta } of this.discovery.providersWithMeta<MyMeta>(MY_KEY)) {
48
+ * this.register(instance, meta);
49
+ * }
50
+ * }
51
+ * }
52
+ * ```
53
+ *
54
+ * Discovery is kernel-level, not encapsulation-scoped: module visibility
55
+ * protects modules from *each other*, never from the framework's own
56
+ * dispatchers. Use `DiscoveryFilter.moduleId` for the rare narrowing case.
57
+ *
58
+ * The resolve-failure policy honors the container's diagnostics mode exactly
59
+ * like the legacy loops: `throw` rethrows, `log` warns and skips, `silent`
60
+ * skips — so a broken provider never silently changes discovery semantics.
61
+ */
62
+ export declare class DiscoveryService {
63
+ private readonly container;
64
+ constructor(container: Container);
65
+ /** Every class-token provider registered in the container. */
66
+ getProviders(filter?: DiscoveryFilter): DiscoveredClass[];
67
+ /**
68
+ * Providers whose class carries class-level metadata under `key` — both
69
+ * class decorators (single value) and method decorators that append
70
+ * class-level lists (`@Cron`, `@OnEvent`, `@SubscribeMessage`).
71
+ */
72
+ providersWithMeta<M>(key: string | DiscoverableDecorator<M>, filter?: DiscoveryFilter): Array<DiscoveredClass & {
73
+ meta: M;
74
+ }>;
75
+ /**
76
+ * Handler methods carrying metadata under `key`. Merges the two storage
77
+ * conventions:
78
+ * - class-level appended lists whose items carry `methodName`
79
+ * (`@Cron`/`@OnEvent`-style method decorators), and
80
+ * - true handler-level metadata (`@SetMetadata`/`createDiscoverableDecorator`
81
+ * applied to a method).
82
+ */
83
+ methodsWithMeta<M>(key: string | DiscoverableDecorator<M>, filter?: DiscoveryFilter): DiscoveredMethodMeta<M>[];
84
+ /**
85
+ * Index-first candidate walk with a legacy full-scan fallback: when the
86
+ * reverse index knows nothing about a key, an older copy of the registry
87
+ * module may have taken the writes (mixed dist/src or package versions in
88
+ * one process) — fall back to scanning container tokens the way the
89
+ * pre-DiscoveryService loops did.
90
+ */
91
+ private candidatesWithClassMeta;
92
+ private buildEntry;
93
+ }
94
+ export type { Constructor };