@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 +67 -0
- package/dist/__tests__/workers/entry.d.ts +9 -0
- package/dist/container/container.d.ts +17 -5
- package/dist/container/container.js +151 -34
- package/dist/container/index.d.ts +2 -2
- package/dist/container/index.js +1 -1
- package/dist/container/types.d.ts +16 -0
- package/dist/container/types.js +14 -0
- package/dist/event-emitter/event-emitter.subscriber.js +6 -1
- package/dist/factory/bootstrap.d.ts +23 -0
- package/dist/factory/bootstrap.js +76 -0
- package/dist/factory.d.ts +2 -2
- package/dist/factory.js +2 -33
- package/dist/http/request-context.d.ts +13 -0
- package/dist/http/request-context.js +22 -0
- package/dist/http/route.manager.js +2 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +6 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.js +6 -0
- package/dist/module/module-loader.d.ts +4 -0
- package/dist/module/module-loader.js +80 -23
- package/dist/plugin/plugin.d.ts +31 -0
- package/dist/plugin/plugin.js +111 -0
- package/dist/registry/metadata.registry.js +0 -1
- package/dist/schedule/schedule.registry.js +6 -1
- package/dist/schedule-node/schedule.executor.d.ts +3 -1
- package/dist/schedule-node/schedule.executor.js +21 -5
- package/package.json +6 -2
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
//
|
|
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.
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
197
|
-
|
|
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
|
-
|
|
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';
|
package/dist/container/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/container/types.js
CHANGED
|
@@ -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 {
|
|
3
|
+
import type { BootstrapOptions } from './factory/bootstrap';
|
|
4
4
|
export declare const VelaFactory: {
|
|
5
|
-
create(rootModule: Type, options?:
|
|
5
|
+
create(rootModule: Type, options?: BootstrapOptions): Promise<VelaApplication>;
|
|
6
6
|
};
|