@velajs/vela 1.2.0 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ A sanctioned per-request injectable lands as a framework primitive, and the metadata-store unification finally has its regression tests.
6
+
7
+ ### New
8
+
9
+ - **`REQUEST_CONTEXT` injectable.** A request-scoped primitive carrying a stable `id` (mirrored from inbound `x-request-id` if present, else `crypto.randomUUID()`), `receivedAt`, the raw `Request`, the Hono `Context`, and a typed `set/get/has` bag for cross-cutting metadata. Seeded by `RouteManager` into each per-request child container; resolves through `@Inject(REQUEST_CONTEXT)` from any request-scoped service. No `AsyncLocalStorage` — edge-runtime contract intact (verified live under workerd via `pnpm test:workers`).
10
+
11
+ ```ts
12
+ import { Inject, Injectable, Scope, REQUEST_CONTEXT } from '@velajs/vela';
13
+ import type { RequestContext } from '@velajs/vela';
14
+
15
+ @Injectable({ scope: Scope.REQUEST })
16
+ class TenantResolver {
17
+ constructor(@Inject(REQUEST_CONTEXT) private readonly ctx: RequestContext) {}
18
+ resolve() {
19
+ return this.ctx.hono.req.header('x-tenant') ?? 'default';
20
+ }
21
+ }
22
+ ```
23
+
24
+ ### Internal cleanup
25
+
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
+ - **Stale `WeakMap` comment removed** from `MetadataRegistry.reset()` — the WeakMap fallback was retired in 1.1.0; the comment was documentation drift.
28
+ - **`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
+
30
+ ## 1.3.0
31
+
32
+ Module boundaries are enforced. NestJS-shape: a service cannot resolve dependencies from a module it didn't import. Bootstrap is consolidated into a single primitive, discovery failures are diagnostically routed, and a generic plugin composer is included.
33
+
34
+ ### New
35
+
36
+ - **Module visibility enforcement.** `VelaFactory.create(Mod)` checks every constructor injection: the token must be declared locally, exported by an imported module, marked `@Global`, or be an `InjectionToken` with a default factory. Throws `ModuleVisibilityError` with an actionable message on violation. Auto-registration of unknown class tokens is gone — declare every dependency in a module's `providers`. There is no opt-out flag; `ModuleRef.create()` is the sandbox escape hatch for transient instantiation.
37
+
38
+ - **`bootstrap(rootModule, options)`** — the wiring primitive shared by `VelaFactory.create`, `@velajs/testing`, and any non-HTTP consumer (CLI tools, custom runtimes). Returns `{ container, routeManager, loader }` without running lifecycle hooks or building the Hono app. `VelaFactory.create` is now a thin wrapper that calls `bootstrap()` then runs `OnModuleInit` / `OnApplicationBootstrap` and builds routes. Exported from `@velajs/vela` and `@velajs/vela/internal`.
39
+
40
+ - **Module visibility primitives.** `Container({ diagnostics })`, `ModuleScope`, `Container.registerScope`, `Container.markGlobalToken`, and `ModuleVisibilityError`. `useExisting` aliases honor visibility (alias targets the caller can't see are rejected).
41
+
42
+ - **Self-providing tokens stay visible.** `new InjectionToken('X', { factory: () => Y })` is implicitly globally visible — the token's factory IS its provider, so no explicit declaration is required.
43
+
44
+ - **Factory inject is a framework-level escape hatch.** `useFactory` provider deps (including `forRootAsync`, `registerAsync`) resolve without a module-visibility requester, so factory `inject: [...]` arrays can pull from the importing module's scope. A future `forRootAsync({ imports })` will tighten this; for now it's permissive by design.
45
+
46
+ - **Discovery diagnostics: `{ diagnostics: 'silent' | 'log' | 'throw' }`** (default `'log'`). Failed provider/controller resolution at `loader.resolveAllInstances`, schedule discovery, event-emitter discovery, and runtime job execution route through one dispatcher. `ModuleVisibilityError` always propagates regardless of mode.
47
+
48
+ - **Live Cloudflare Workers smoke tests.** `pnpm test:workers` runs vela inside workerd via `@cloudflare/vitest-pool-workers` (driven by a test-only `wrangler.toml`). Validates the edge-runtime contract end-to-end — boot, request lifecycle, per-request DI without `AsyncLocalStorage`, handler-chain order, OpenAPI mount — beyond what the static edge-audit can catch. Wired into CI alongside `pnpm test`.
49
+
50
+ - **Framework primitives are globally visible.** `Container`, `ModuleRef`, and `APP_GUARD`/`APP_PIPE`/`APP_INTERCEPTOR`/`APP_FILTER`/`APP_MIDDLEWARE` are marked global at boot — resolvable from any module without explicit imports, in both strict and non-strict mode. `ModuleRef.create()` continues to be the sandbox escape hatch (visibility check skipped for transient instantiation).
51
+
52
+ - **Plugin manifest + composer** — `definePlugin({ id, version, module, dependsOn?, metadata? })` and `composePlugins(plugins): DynamicModule` with topological sort, cycle detection, and missing-dep detection. Produces a global module that exposes a queryable `PluginRegistry` via `PLUGIN_REGISTRY_TOKEN` (`list`, `get(id)`, `dependents(id)`).
53
+
54
+ - **New types**: `ModuleScope`, `ContainerOptions`, `BootstrapOptions`, `BootstrapResult`, `Diagnostics`, `Plugin` — exported from both root and `/internal`.
55
+
56
+ ### Internal cleanup
57
+
58
+ - **`Container` constructor accepts `ContainerOptions`** (`{ diagnostics? }`). Threads `requestingModuleId` through `resolve` / `resolveAsync` / `resolveAll`. Per-module scopes are tracked via `registerScope`; framework-internal globals via `markGlobalToken`. `providerOrigin: Map<Token, string>` records each provider's declaring module so constructor injections resolve from the *class's* module, not the caller's. Visibility enforcement runs whenever a `requestingModuleId` is supplied — there is no on/off switch.
59
+
60
+ - **`ModuleLoader` registers a `ModuleScope` per module** before recursing into imports — `localProviders` includes the module class itself (so `NestModule.configure()` resolution stays inside its own scope), controllers, and every provider token. Synthetic `APP_*` tokens are marked global at mint time so RouteManager's request-time resolutions (no requester) keep working.
61
+
62
+ - **`createChild()` shares container state by reference** (providers, scopes, globals, providerOrigin); `createDetached()` copies providers + providerOrigin and shares scopes + globals. Re-registering a token on a detached container without a moduleId clears any stale `providerOrigin` so sandbox-local registrations don't inherit a misleading owner.
63
+
64
+ - **`resolveAsync` factory branch now unwraps `ForwardRef` in `inject`** (mirroring the sync `resolveFactory`). Previously asymmetric — sync path worked, async path silently failed during `loader.resolveAllInstances` and was swallowed by the discovery `try/catch`.
65
+
66
+ - **Dropped unused `Container.parent` field** (audit #10). Was assigned in `createChild()` but never read.
67
+
68
+ - **Bootstrap consolidated into `src/factory/bootstrap.ts`** — `VelaFactory.create` no longer hand-rolls the APP_* / consumer-middleware / global-prefix wiring sequence. Net code reduction in `factory.ts`.
69
+
3
70
  ## 1.1.0 (2026-04-30)
4
71
 
5
72
  Architectural remodel: one metadata model, one storage, public surface trimmed, internal primitives exposed via a stable subpath.
@@ -0,0 +1,9 @@
1
+ declare const _default: {
2
+ fetch(request: Request, _env: unknown, _ctx: unknown): Promise<Response>;
3
+ };
4
+ export default _default;
5
+ declare global {
6
+ interface ExportedHandler<Env = unknown> {
7
+ fetch?(request: Request, env: Env, ctx: unknown): Response | Promise<Response>;
8
+ }
9
+ }
@@ -1,14 +1,24 @@
1
1
  import { Scope } from '../constants';
2
- import type { ProviderOptions, Token, Type } from './types';
2
+ import type { ContainerOptions, Diagnostics, ModuleScope, ProviderOptions, Token, Type } from './types';
3
3
  export declare class Container {
4
4
  private providers;
5
5
  private resolutionStack;
6
- private parent;
7
6
  private requestInstances;
8
- register<T>(provider: Type<T> | ProviderOptions<T>): this;
7
+ private scopes;
8
+ private globals;
9
+ private providerOrigin;
10
+ private diagnostics;
11
+ constructor(options?: ContainerOptions);
12
+ register<T>(provider: Type<T> | ProviderOptions<T>, declaringModuleId?: string): this;
9
13
  private registerClass;
10
14
  private registerOptions;
11
- resolve<T>(token: Token<T>): T;
15
+ private recordOrigin;
16
+ registerScope(scope: ModuleScope): void;
17
+ markGlobalToken(token: Token): void;
18
+ setRequestInstance(token: Token, value: unknown): void;
19
+ getDiagnostics(): Diagnostics;
20
+ resolve<T>(token: Token<T>, requestingModuleId?: string): T;
21
+ resolveAll<T>(token: Token<T>, requestingModuleId?: string): T[];
12
22
  has(token: Token): boolean;
13
23
  getProviderScope(token: Token): Scope | undefined;
14
24
  getTokens(): Token[];
@@ -23,7 +33,9 @@ export declare class Container {
23
33
  private resolveRegistration;
24
34
  private resolveClass;
25
35
  private resolveFactory;
26
- resolveAsync<T>(token: Token<T>): Promise<T>;
36
+ resolveAsync<T>(token: Token<T>, requestingModuleId?: string): Promise<T>;
27
37
  private createLazyProxy;
38
+ private assertVisible;
39
+ private isExportedFromImports;
28
40
  private tokenToString;
29
41
  }
@@ -1,21 +1,27 @@
1
1
  import { Scope } from "../constants.js";
2
2
  import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
3
- import { ForwardRef, InjectionToken } from "./types.js";
3
+ import { ForwardRef, InjectionToken, ModuleVisibilityError } from "./types.js";
4
4
  const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips type-only imports at ' + 'runtime and `design:paramtypes` emits `Object`/`undefined` for their ' + 'positions. Use a runtime `import { X }` for DI tokens.';
5
5
  export class Container {
6
6
  providers = new Map();
7
7
  resolutionStack = new Set();
8
- parent = null;
9
8
  requestInstances = new Map();
10
- register(provider) {
9
+ scopes = new Map();
10
+ globals = new Set();
11
+ providerOrigin = new Map();
12
+ diagnostics;
13
+ constructor(options = {}){
14
+ this.diagnostics = options.diagnostics ?? 'log';
15
+ }
16
+ register(provider, declaringModuleId) {
11
17
  if (typeof provider === 'function') {
12
- this.registerClass(provider);
18
+ this.registerClass(provider, declaringModuleId);
13
19
  } else {
14
- this.registerOptions(provider);
20
+ this.registerOptions(provider, declaringModuleId);
15
21
  }
16
22
  return this;
17
23
  }
18
- registerClass(target) {
24
+ registerClass(target, declaringModuleId) {
19
25
  if (!isInjectable(target)) {
20
26
  console.warn(`Warning: ${target.name} is not decorated with @Injectable(). ` + `It will be registered but dependency resolution may not work correctly.`);
21
27
  }
@@ -25,8 +31,9 @@ export class Container {
25
31
  scope,
26
32
  useClass: target
27
33
  });
34
+ this.recordOrigin(target, declaringModuleId);
28
35
  }
29
- registerOptions(options) {
36
+ registerOptions(options, declaringModuleId) {
30
37
  const token = options.provide;
31
38
  if (!token) {
32
39
  throw new Error('Provider registration requires a token');
@@ -49,30 +56,80 @@ export class Container {
49
56
  registration.useClass = token;
50
57
  }
51
58
  this.providers.set(token, registration);
59
+ this.recordOrigin(token, declaringModuleId);
60
+ // For aliases like `{ provide: Foo, useClass: Bar }`, also record Bar's
61
+ // origin so `resolveClass(Bar)` resolves Bar's deps from the alias's
62
+ // declaring module. Idempotent if Bar === Foo.
63
+ if (options.useClass !== undefined) {
64
+ this.recordOrigin(options.useClass, declaringModuleId);
65
+ }
66
+ }
67
+ recordOrigin(token, moduleId) {
68
+ if (moduleId !== undefined) {
69
+ this.providerOrigin.set(token, moduleId);
70
+ } else {
71
+ // Sandbox containers (createDetached) re-register tokens with no
72
+ // moduleId; clearing keeps the token "module-less" so it's only
73
+ // resolvable when the requester is also undefined.
74
+ this.providerOrigin.delete(token);
75
+ }
76
+ }
77
+ registerScope(scope) {
78
+ this.scopes.set(scope.moduleId, scope);
79
+ if (scope.isGlobal) {
80
+ // Match NestJS / the loader's existing globalExports semantic: only
81
+ // exported tokens become globally visible. Non-exported providers of
82
+ // a @Global module still need explicit imports.
83
+ for (const token of scope.exportedTokens)this.globals.add(token);
84
+ }
52
85
  }
53
- resolve(token) {
86
+ markGlobalToken(token) {
87
+ this.globals.add(token);
88
+ }
89
+ // Pre-seed the per-request cache. Only meaningful on a child container
90
+ // produced by createChild() — the root's requestInstances map is unused.
91
+ // Used by RouteManager to populate framework-provided request-scope
92
+ // values (REQUEST_CONTEXT) before any handler resolution runs, so the
93
+ // provider's factory never fires on the request path.
94
+ setRequestInstance(token, value) {
95
+ this.requestInstances.set(token, value);
96
+ }
97
+ getDiagnostics() {
98
+ return this.diagnostics;
99
+ }
100
+ resolve(token, requestingModuleId) {
101
+ if (requestingModuleId !== undefined) {
102
+ this.assertVisible(requestingModuleId, token);
103
+ }
54
104
  const registration = this.providers.get(token);
55
105
  if (!registration) {
56
106
  // `Object`/undefined at a token position is the fingerprint of a
57
- // type-only import that TypeScript stripped — emit the hint BEFORE
58
- // auto-registering Object as a provider (which would silently "succeed").
107
+ // type-only import that TypeScript stripped — emit the hint before
108
+ // attempting any other recovery.
59
109
  if (token === Object || token == null) {
60
110
  throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + IMPORT_TYPE_HINT);
61
111
  }
62
112
  if (typeof token === 'function') {
63
- this.register(token);
64
- return this.resolve(token);
113
+ throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + `Declare it in a module's providers.`);
65
114
  }
115
+ // InjectionToken default factories are explicit user declarations
116
+ // attached to the token at construction — self-providing singletons.
66
117
  if (token instanceof InjectionToken && token.options?.factory) {
67
118
  this.register({
68
119
  provide: token,
69
120
  useFactory: token.options.factory
70
121
  });
71
- return this.resolve(token);
122
+ return this.resolve(token, requestingModuleId);
72
123
  }
73
124
  throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
74
125
  }
75
- return this.resolveRegistration(registration);
126
+ return this.resolveRegistration(registration, requestingModuleId);
127
+ }
128
+ resolveAll(token, requestingModuleId) {
129
+ if (!this.has(token)) return [];
130
+ return [
131
+ this.resolve(token, requestingModuleId)
132
+ ];
76
133
  }
77
134
  has(token) {
78
135
  return this.providers.has(token);
@@ -88,27 +145,44 @@ export class Container {
88
145
  * The child shares the parent's providers but caches REQUEST-scoped
89
146
  * instances separately per child (per request).
90
147
  */ createChild() {
91
- const child = new Container();
92
- child.parent = this;
93
- child.providers = this.providers; // share provider registrations
148
+ const child = new Container({
149
+ diagnostics: this.diagnostics
150
+ });
151
+ // Share state by reference — request-scope children must see the same
152
+ // module graph as the root.
153
+ child.providers = this.providers;
154
+ child.scopes = this.scopes;
155
+ child.globals = this.globals;
156
+ child.providerOrigin = this.providerOrigin;
94
157
  return child;
95
158
  }
96
159
  createDetached() {
97
- const child = new Container();
98
- child.providers = new Map(this.providers); // copy, not share
160
+ const child = new Container({
161
+ diagnostics: this.diagnostics
162
+ });
163
+ // Copy mutable per-resolution state; share static module-graph metadata
164
+ // so sandbox resolutions can still see exported providers.
165
+ child.providers = new Map(this.providers);
166
+ child.providerOrigin = new Map(this.providerOrigin);
167
+ child.scopes = this.scopes;
168
+ child.globals = this.globals;
99
169
  return child;
100
170
  }
101
171
  clear() {
102
172
  this.providers.clear();
103
173
  this.resolutionStack.clear();
104
174
  this.requestInstances.clear();
175
+ this.scopes.clear();
176
+ this.globals.clear();
177
+ this.providerOrigin.clear();
105
178
  }
106
- resolveRegistration(registration) {
179
+ resolveRegistration(registration, requestingModuleId) {
107
180
  if (registration.useValue !== undefined) {
108
181
  return registration.useValue;
109
182
  }
110
183
  if (registration.useExisting) {
111
- return this.resolve(registration.useExisting);
184
+ // Pass through the original requester to catch alias leaks
185
+ return this.resolve(registration.useExisting, requestingModuleId);
112
186
  }
113
187
  // Singleton: return cached from registration (shared across all containers)
114
188
  if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
@@ -132,6 +206,10 @@ export class Container {
132
206
  try {
133
207
  let instance;
134
208
  if (registration.useFactory) {
209
+ // Factories (forRootAsync, useFactory) commonly inject deps from the
210
+ // importing module's scope. Vela has no `forRootAsync({ imports })`
211
+ // surface to track that, so factory inject deps resolve without a
212
+ // requester — same escape-hatch shape as ModuleRef.
135
213
  instance = this.resolveFactory(registration);
136
214
  } else if (registration.useClass) {
137
215
  instance = this.resolveClass(registration.useClass);
@@ -148,7 +226,9 @@ export class Container {
148
226
  this.resolutionStack.delete(registration.provide);
149
227
  }
150
228
  }
151
- resolveClass(target) {
229
+ resolveClass(target, callerModuleId) {
230
+ // The class's dependencies resolve from ITS module's POV, not the caller's.
231
+ const ownerModuleId = this.providerOrigin.get(target) ?? callerModuleId;
152
232
  const paramTypes = getConstructorDependencies(target);
153
233
  const injectMetadata = getInjectMetadata(target);
154
234
  const injectMap = new Map(injectMetadata.map((m)=>[
@@ -169,19 +249,19 @@ export class Container {
169
249
  }
170
250
  // forwardRef with circular dep — break the cycle with a lazy Proxy
171
251
  if (isForwardRef && this.resolutionStack.has(token)) {
172
- return this.createLazyProxy(token);
252
+ return this.createLazyProxy(token, ownerModuleId);
173
253
  }
174
- return this.resolve(token);
254
+ return this.resolve(token, ownerModuleId);
175
255
  });
176
256
  return new target(...dependencies);
177
257
  }
178
- resolveFactory(registration) {
258
+ resolveFactory(registration, requestingModuleId) {
179
259
  if (!registration.useFactory) {
180
260
  throw new Error('Factory function is missing');
181
261
  }
182
262
  const dependencies = (registration.inject || []).map((token)=>{
183
263
  const resolved = token instanceof ForwardRef ? token.factory() : token;
184
- return this.resolve(resolved);
264
+ return this.resolve(resolved, requestingModuleId);
185
265
  });
186
266
  const result = registration.useFactory(...dependencies);
187
267
  if (result instanceof Promise) {
@@ -189,12 +269,22 @@ export class Container {
189
269
  }
190
270
  return result;
191
271
  }
192
- async resolveAsync(token) {
272
+ async resolveAsync(token, requestingModuleId) {
273
+ if (requestingModuleId !== undefined) {
274
+ this.assertVisible(requestingModuleId, token);
275
+ }
193
276
  const registration = this.providers.get(token);
194
277
  if (!registration) {
195
278
  if (typeof token === 'function') {
196
- this.register(token);
197
- return this.resolveAsync(token);
279
+ throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + `Declare it in a module's providers.`);
280
+ }
281
+ if (token instanceof InjectionToken && token.options?.factory) {
282
+ // Self-providing token — register on demand from its declared factory.
283
+ this.register({
284
+ provide: token,
285
+ useFactory: token.options.factory
286
+ });
287
+ return this.resolveAsync(token, requestingModuleId);
198
288
  }
199
289
  throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
200
290
  }
@@ -202,31 +292,58 @@ export class Container {
202
292
  if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
203
293
  return registration.instance;
204
294
  }
205
- const dependencies = await Promise.all((registration.inject || []).map((t)=>this.resolveAsync(t)));
295
+ // Factory inject deps resolve without a requester (escape hatch — see
296
+ // resolveRegistration's useFactory branch).
297
+ const dependencies = await Promise.all((registration.inject || []).map((t)=>{
298
+ const resolved = t instanceof ForwardRef ? t.factory() : t;
299
+ return this.resolveAsync(resolved);
300
+ }));
206
301
  const instance = await registration.useFactory(...dependencies);
207
302
  if (registration.scope === Scope.SINGLETON) {
208
303
  registration.instance = instance;
209
304
  }
210
305
  return instance;
211
306
  }
212
- return this.resolve(token);
307
+ return this.resolve(token, requestingModuleId);
213
308
  }
214
- createLazyProxy(token) {
309
+ createLazyProxy(token, requestingModuleId) {
215
310
  const container = this;
216
311
  const target = Object.create(null);
217
312
  return new Proxy(target, {
218
313
  get (_target, prop) {
219
- const instance = container.resolve(token);
314
+ const instance = container.resolve(token, requestingModuleId);
220
315
  const value = instance[prop];
221
316
  return typeof value === 'function' ? value.bind(instance) : value;
222
317
  },
223
318
  set (_target, prop, value) {
224
- const instance = container.resolve(token);
319
+ const instance = container.resolve(token, requestingModuleId);
225
320
  instance[prop] = value;
226
321
  return true;
227
322
  }
228
323
  });
229
324
  }
325
+ assertVisible(moduleId, token) {
326
+ if (this.globals.has(token)) return;
327
+ // InjectionTokens with a default factory are self-providing singletons —
328
+ // visible from any module without requiring explicit declaration.
329
+ if (token instanceof InjectionToken && token.options?.factory) return;
330
+ const scope = this.scopes.get(moduleId);
331
+ if (!scope) {
332
+ // No scope registered for this moduleId — typically synthetic /
333
+ // framework-internal callers. Allow rather than break primitives.
334
+ return;
335
+ }
336
+ if (scope.localProviders.has(token)) return;
337
+ if (this.isExportedFromImports(scope, token)) return;
338
+ throw new ModuleVisibilityError(moduleId, token);
339
+ }
340
+ isExportedFromImports(scope, token) {
341
+ for (const importedId of scope.importedModules){
342
+ const imported = this.scopes.get(importedId);
343
+ if (imported?.exportedTokens.has(token)) return true;
344
+ }
345
+ return false;
346
+ }
230
347
  tokenToString(token) {
231
348
  if (token instanceof InjectionToken) {
232
349
  return token.toString();
@@ -1,6 +1,6 @@
1
1
  export { Container } from './container';
2
2
  export { Injectable, Inject, Optional, isInjectable, getScope } from './decorators';
3
- export { InjectionToken, ForwardRef, forwardRef } from './types';
3
+ export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError } from './types';
4
4
  export { ModuleRef } from './module-ref';
5
5
  export { mixin } from './mixin';
6
- export type { Type, Token, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, } from './types';
6
+ export type { Type, Token, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, ModuleScope, ContainerOptions, Diagnostics, } from './types';
@@ -1,5 +1,5 @@
1
1
  export { Container } from "./container.js";
2
2
  export { Injectable, Inject, Optional, isInjectable, getScope } from "./decorators.js";
3
- export { InjectionToken, ForwardRef, forwardRef } from "./types.js";
3
+ export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError } from "./types.js";
4
4
  export { ModuleRef } from "./module-ref.js";
5
5
  export { mixin } from "./mixin.js";
@@ -43,3 +43,19 @@ export interface ProviderRegistration<T = unknown> {
43
43
  inject?: Token[];
44
44
  useExisting?: Token<T>;
45
45
  }
46
+ export interface ModuleScope {
47
+ moduleId: string;
48
+ localProviders: Set<Token>;
49
+ importedModules: Set<string>;
50
+ exportedTokens: Set<Token>;
51
+ isGlobal: boolean;
52
+ }
53
+ export type Diagnostics = 'silent' | 'log' | 'throw';
54
+ export interface ContainerOptions {
55
+ diagnostics?: Diagnostics;
56
+ }
57
+ export declare class ModuleVisibilityError extends Error {
58
+ readonly moduleId: string;
59
+ readonly token: Token;
60
+ constructor(moduleId: string, token: Token);
61
+ }
@@ -18,3 +18,17 @@ export class ForwardRef {
18
18
  export function forwardRef(factory) {
19
19
  return new ForwardRef(factory);
20
20
  }
21
+ function describeToken(token) {
22
+ if (token instanceof InjectionToken) return token.toString();
23
+ if (typeof token === 'function') return token.name;
24
+ if (typeof token === 'symbol') return token.toString();
25
+ return String(token);
26
+ }
27
+ export class ModuleVisibilityError extends Error {
28
+ moduleId;
29
+ token;
30
+ constructor(moduleId, token){
31
+ super(`Module '${moduleId}' cannot resolve '${describeToken(token)}': ` + `not declared in providers, not imported from another module's exports, not @Global. ` + `Either add to imports/exports or mark as @Global.`), this.moduleId = moduleId, this.token = token;
32
+ this.name = 'ModuleVisibilityError';
33
+ }
34
+ }
@@ -34,7 +34,12 @@ export class EventEmitterSubscriber {
34
34
  let instance;
35
35
  try {
36
36
  instance = this.container.resolve(token);
37
- } catch {
37
+ } catch (err) {
38
+ const mode = this.container.getDiagnostics();
39
+ if (mode === 'throw') throw err;
40
+ if (mode === 'log') {
41
+ console.warn(`[vela] event subscriber discovery: cannot resolve ${token.name || String(token)}:`, err);
42
+ }
38
43
  continue;
39
44
  }
40
45
  for (const { event, methodName } of metadata){
@@ -0,0 +1,23 @@
1
+ import { Container } from '../container/container';
2
+ import type { Diagnostics, Type } from '../container/types';
3
+ import { RouteManager } from '../http/route.manager';
4
+ import type { RouteManagerOptions } from '../http/route.manager';
5
+ import { ModuleLoader } from '../module/module-loader';
6
+ export interface BootstrapOptions extends RouteManagerOptions {
7
+ diagnostics?: Diagnostics;
8
+ }
9
+ export interface BootstrapResult {
10
+ container: Container;
11
+ routeManager: RouteManager;
12
+ loader: ModuleLoader;
13
+ }
14
+ /**
15
+ * Wire the DI graph for `rootModule` and prepare the route manager — without
16
+ * running lifecycle hooks or building the Hono app. The single primitive
17
+ * shared by `VelaFactory.create` (HTTP), `@velajs/testing` (test), and any
18
+ * non-HTTP consumer (CLI tools, custom runtimes).
19
+ *
20
+ * Framework-internal tokens (`Container`, `ModuleRef`, `APP_*`) are marked
21
+ * global so they are resolvable from any module.
22
+ */
23
+ export declare function bootstrap(rootModule: Type, options?: BootstrapOptions): Promise<BootstrapResult>;
@@ -0,0 +1,76 @@
1
+ import { Scope } from "../constants.js";
2
+ import { Container } from "../container/container.js";
3
+ import { ModuleRef } from "../container/module-ref.js";
4
+ import { REQUEST_CONTEXT } from "../http/request-context.js";
5
+ import { RouteManager } from "../http/route.manager.js";
6
+ import { ModuleLoader } from "../module/module-loader.js";
7
+ import { bindAppProviders } from "../pipeline/app-providers.js";
8
+ import { ComponentManager } from "../pipeline/component.manager.js";
9
+ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
10
+ /**
11
+ * Wire the DI graph for `rootModule` and prepare the route manager — without
12
+ * running lifecycle hooks or building the Hono app. The single primitive
13
+ * shared by `VelaFactory.create` (HTTP), `@velajs/testing` (test), and any
14
+ * non-HTTP consumer (CLI tools, custom runtimes).
15
+ *
16
+ * Framework-internal tokens (`Container`, `ModuleRef`, `APP_*`) are marked
17
+ * global so they are resolvable from any module.
18
+ */ export async function bootstrap(rootModule, options = {}) {
19
+ const container = new Container({
20
+ diagnostics: options.diagnostics
21
+ });
22
+ container.register({
23
+ provide: Container,
24
+ useValue: container
25
+ });
26
+ container.markGlobalToken(Container);
27
+ container.register({
28
+ provide: ModuleRef,
29
+ useFactory: (c)=>new ModuleRef(c),
30
+ inject: [
31
+ Container
32
+ ]
33
+ });
34
+ container.markGlobalToken(ModuleRef);
35
+ for (const t of [
36
+ APP_GUARD,
37
+ APP_PIPE,
38
+ APP_INTERCEPTOR,
39
+ APP_FILTER,
40
+ APP_MIDDLEWARE
41
+ ]){
42
+ container.markGlobalToken(t);
43
+ }
44
+ // REQUEST_CONTEXT is seeded into each request-scoped child by RouteManager
45
+ // before any handler resolves it (see route.manager.ts:getRequestContainer).
46
+ // The factory throws so misuse outside the request path surfaces immediately
47
+ // instead of materializing a phantom context.
48
+ container.register({
49
+ provide: REQUEST_CONTEXT,
50
+ scope: Scope.REQUEST,
51
+ useFactory: ()=>{
52
+ throw new Error('REQUEST_CONTEXT can only be resolved inside a request — ' + 'it is seeded by RouteManager when the request enters the pipeline.');
53
+ }
54
+ });
55
+ container.markGlobalToken(REQUEST_CONTEXT);
56
+ const routeManager = new RouteManager(container, options);
57
+ ComponentManager.init(container);
58
+ const loader = new ModuleLoader(container, routeManager);
59
+ loader.load(rootModule);
60
+ bindAppProviders(routeManager, container, loader);
61
+ routeManager.registerConsumerMiddleware(loader.getConsumerMiddlewareDefinitions());
62
+ if (options.globalPrefix) {
63
+ routeManager.setGlobalPrefix(options.globalPrefix);
64
+ }
65
+ for (const handler of options.middleware ?? []){
66
+ const mw = {
67
+ use: handler
68
+ };
69
+ routeManager.useGlobalMiddleware(mw);
70
+ }
71
+ return {
72
+ container,
73
+ routeManager,
74
+ loader
75
+ };
76
+ }
package/dist/factory.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { VelaApplication } from './application';
2
2
  import type { Type } from './container/types';
3
- import type { RouteManagerOptions } from './http/route.manager';
3
+ import type { BootstrapOptions } from './factory/bootstrap';
4
4
  export declare const VelaFactory: {
5
- create(rootModule: Type, options?: RouteManagerOptions): Promise<VelaApplication>;
5
+ create(rootModule: Type, options?: BootstrapOptions): Promise<VelaApplication>;
6
6
  };