@velajs/vela 1.4.0 → 1.5.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 +13 -1
- package/README.md +52 -0
- package/dist/cache/cache.module.d.ts +1 -0
- package/dist/cache/cache.module.js +6 -0
- package/dist/config/config.module.d.ts +4 -1
- package/dist/config/config.module.js +6 -0
- package/dist/container/container.d.ts +32 -4
- package/dist/container/container.js +252 -103
- package/dist/container/decorators.d.ts +8 -1
- package/dist/container/decorators.js +7 -4
- package/dist/container/index.d.ts +1 -1
- package/dist/container/index.js +1 -1
- package/dist/container/types.d.ts +13 -0
- package/dist/container/types.js +10 -0
- package/dist/cors/cors.module.d.ts +3 -1
- package/dist/cors/cors.module.js +2 -0
- package/dist/fetch/fetch.module.d.ts +3 -1
- package/dist/fetch/fetch.module.js +8 -3
- package/dist/http/argument-resolver.d.ts +10 -0
- package/dist/http/argument-resolver.js +89 -0
- package/dist/http/handler-executor.d.ts +20 -0
- package/dist/http/handler-executor.js +108 -0
- package/dist/http/instantiate.d.ts +4 -0
- package/dist/http/instantiate.js +18 -0
- package/dist/http/response-mapper.d.ts +8 -0
- package/dist/http/response-mapper.js +42 -0
- package/dist/http/route.manager.d.ts +1 -9
- package/dist/http/route.manager.js +23 -240
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/module/decorators.d.ts +6 -2
- package/dist/module/decorators.js +7 -6
- package/dist/module/index.d.ts +2 -1
- package/dist/module/index.js +2 -1
- package/dist/module/module-loader.d.ts +6 -1
- package/dist/module/module-loader.js +127 -47
- package/dist/module/stable-hash.d.ts +15 -0
- package/dist/module/stable-hash.js +52 -0
- package/dist/registry/types.d.ts +11 -0
- package/dist/throttler/throttler.module.d.ts +6 -2
- package/dist/throttler/throttler.module.js +6 -0
- package/package.json +1 -1
|
@@ -1,46 +1,63 @@
|
|
|
1
1
|
import { Scope } from "../constants.js";
|
|
2
2
|
import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
|
|
3
|
-
import { ForwardRef, InjectionToken, ModuleVisibilityError } from "./types.js";
|
|
3
|
+
import { ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } 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
|
+
/**
|
|
6
|
+
* Sentinel comparing a token to the bare `Object` class — `design:paramtypes`
|
|
7
|
+
* emits `Object` when TypeScript erases a type-only import, so any token that
|
|
8
|
+
* is `Object` (or nullish) is the fingerprint of a stripped type.
|
|
9
|
+
*/ function isErasedTypeToken(token) {
|
|
10
|
+
return token === Object || token == null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Per-module provider buckets. Each module instance owns its providers under
|
|
14
|
+
* its `moduleId`; the same logical token can have distinct registrations in
|
|
15
|
+
* different buckets, supporting multi-instance dynamic modules.
|
|
16
|
+
*
|
|
17
|
+
* The `__root__` bucket holds bootstrap-time framework primitives (Container,
|
|
18
|
+
* ModuleRef, REQUEST_CONTEXT, etc.) and any `register()` call that doesn't
|
|
19
|
+
* supply a `declaringModuleId`.
|
|
20
|
+
*/ export class Container {
|
|
6
21
|
providers = new Map();
|
|
22
|
+
exporterIndex = new Map();
|
|
7
23
|
resolutionStack = new Set();
|
|
8
24
|
requestInstances = new Map();
|
|
9
25
|
scopes = new Map();
|
|
10
26
|
globals = new Set();
|
|
11
|
-
providerOrigin = new Map();
|
|
12
27
|
diagnostics;
|
|
13
28
|
constructor(options = {}){
|
|
14
29
|
this.diagnostics = options.diagnostics ?? 'log';
|
|
15
30
|
}
|
|
16
31
|
register(provider, declaringModuleId) {
|
|
32
|
+
const moduleId = declaringModuleId ?? ROOT_MODULE_ID;
|
|
17
33
|
if (typeof provider === 'function') {
|
|
18
|
-
this.registerClass(provider,
|
|
34
|
+
this.registerClass(provider, moduleId);
|
|
19
35
|
} else {
|
|
20
|
-
this.registerOptions(provider,
|
|
36
|
+
this.registerOptions(provider, moduleId);
|
|
21
37
|
}
|
|
22
38
|
return this;
|
|
23
39
|
}
|
|
24
|
-
registerClass(target,
|
|
40
|
+
registerClass(target, moduleId) {
|
|
25
41
|
if (!isInjectable(target)) {
|
|
26
42
|
console.warn(`Warning: ${target.name} is not decorated with @Injectable(). ` + `It will be registered but dependency resolution may not work correctly.`);
|
|
27
43
|
}
|
|
28
44
|
const scope = getScope(target);
|
|
29
|
-
this.
|
|
45
|
+
this.writeRegistration(moduleId, target, {
|
|
30
46
|
provide: target,
|
|
31
47
|
scope,
|
|
48
|
+
declaringModuleId: moduleId,
|
|
32
49
|
useClass: target
|
|
33
50
|
});
|
|
34
|
-
this.recordOrigin(target, declaringModuleId);
|
|
35
51
|
}
|
|
36
|
-
registerOptions(options,
|
|
52
|
+
registerOptions(options, moduleId) {
|
|
37
53
|
const token = options.provide;
|
|
38
54
|
if (!token) {
|
|
39
55
|
throw new Error('Provider registration requires a token');
|
|
40
56
|
}
|
|
41
57
|
const registration = {
|
|
42
58
|
provide: token,
|
|
43
|
-
scope: options.scope ?? Scope.SINGLETON
|
|
59
|
+
scope: options.scope ?? Scope.SINGLETON,
|
|
60
|
+
declaringModuleId: moduleId
|
|
44
61
|
};
|
|
45
62
|
if (options.useValue !== undefined) {
|
|
46
63
|
registration.useValue = options.useValue;
|
|
@@ -55,27 +72,27 @@ export class Container {
|
|
|
55
72
|
} else if (typeof token === 'function') {
|
|
56
73
|
registration.useClass = token;
|
|
57
74
|
}
|
|
58
|
-
this.
|
|
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
|
-
}
|
|
75
|
+
this.writeRegistration(moduleId, token, registration);
|
|
66
76
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
writeRegistration(moduleId, token, registration) {
|
|
78
|
+
let bucket = this.providers.get(moduleId);
|
|
79
|
+
if (!bucket) {
|
|
80
|
+
bucket = new Map();
|
|
81
|
+
this.providers.set(moduleId, bucket);
|
|
82
|
+
}
|
|
83
|
+
bucket.set(token, registration);
|
|
84
|
+
let exporters = this.exporterIndex.get(token);
|
|
85
|
+
if (!exporters) {
|
|
86
|
+
exporters = new Set();
|
|
87
|
+
this.exporterIndex.set(token, exporters);
|
|
75
88
|
}
|
|
89
|
+
exporters.add(moduleId);
|
|
76
90
|
}
|
|
77
91
|
registerScope(scope) {
|
|
78
92
|
this.scopes.set(scope.moduleId, scope);
|
|
93
|
+
if (!this.providers.has(scope.moduleId)) {
|
|
94
|
+
this.providers.set(scope.moduleId, new Map());
|
|
95
|
+
}
|
|
79
96
|
if (scope.isGlobal) {
|
|
80
97
|
// Match NestJS / the loader's existing globalExports semantic: only
|
|
81
98
|
// exported tokens become globally visible. Non-exported providers of
|
|
@@ -98,47 +115,188 @@ export class Container {
|
|
|
98
115
|
return this.diagnostics;
|
|
99
116
|
}
|
|
100
117
|
resolve(token, requestingModuleId) {
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
const registration = this.findRegistration(token, requestingModuleId);
|
|
119
|
+
if (registration) {
|
|
120
|
+
return this.resolveRegistration(registration, requestingModuleId);
|
|
103
121
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
// Visibility error fires first when the token exists in some bucket but
|
|
123
|
+
// isn't reachable from the requester's scope — the user wants to know
|
|
124
|
+
// their wiring is wrong, not that the token doesn't exist.
|
|
125
|
+
if (requestingModuleId !== undefined && this.scopes.has(requestingModuleId) && this.exporterIndex.has(token)) {
|
|
126
|
+
throw new ModuleVisibilityError(requestingModuleId, token);
|
|
127
|
+
}
|
|
128
|
+
// `Object`/undefined at a token position is the fingerprint of a
|
|
129
|
+
// type-only import that TypeScript stripped — emit the hint before
|
|
130
|
+
// attempting any other recovery.
|
|
131
|
+
if (isErasedTypeToken(token)) {
|
|
132
|
+
throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + IMPORT_TYPE_HINT);
|
|
133
|
+
}
|
|
134
|
+
if (typeof token === 'function') {
|
|
135
|
+
throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + `Declare it in a module's providers.`);
|
|
136
|
+
}
|
|
137
|
+
// InjectionToken default factories are explicit user declarations
|
|
138
|
+
// attached to the token at construction — self-providing singletons.
|
|
139
|
+
if (token instanceof InjectionToken && token.options?.factory) {
|
|
140
|
+
this.register({
|
|
141
|
+
provide: token,
|
|
142
|
+
useFactory: token.options.factory
|
|
143
|
+
});
|
|
144
|
+
return this.resolve(token, requestingModuleId);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
|
|
147
|
+
}
|
|
148
|
+
resolveAll(token, requestingModuleId) {
|
|
149
|
+
const registrations = this.findAllRegistrations(token, requestingModuleId);
|
|
150
|
+
return registrations.map((r)=>this.resolveRegistration(r, requestingModuleId));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Find a single reachable registration for a token, applying the visibility
|
|
154
|
+
* walk: requester's bucket → globals → requester's importedModules
|
|
155
|
+
* (transitively, following re-exports).
|
|
156
|
+
*
|
|
157
|
+
* Returns `undefined` if no candidate exists. Throws
|
|
158
|
+
* `MultipleProvidersFoundError` if the imports walk yields more than one.
|
|
159
|
+
*/ findRegistration(token, requestingModuleId) {
|
|
160
|
+
// No requester OR no scope for this requester: framework-internal /
|
|
161
|
+
// sandbox lookup. Prefer the `__root__` bucket so sandbox-registered
|
|
162
|
+
// transients (ModuleRef.create) and bootstrap primitives win over module
|
|
163
|
+
// buckets that may hold the same token under SINGLETON scope.
|
|
164
|
+
if (requestingModuleId === undefined || !this.scopes.has(requestingModuleId)) {
|
|
165
|
+
const rootHit = this.lookupInBucket(ROOT_MODULE_ID, token);
|
|
166
|
+
if (rootHit) return rootHit;
|
|
167
|
+
const exporters = this.exporterIndex.get(token);
|
|
168
|
+
if (!exporters) return undefined;
|
|
169
|
+
for (const owner of exporters){
|
|
170
|
+
const hit = this.lookupInBucket(owner, token);
|
|
171
|
+
if (hit) return hit;
|
|
111
172
|
}
|
|
112
|
-
|
|
113
|
-
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
// 1. Local bucket.
|
|
176
|
+
const local = this.lookupInBucket(requestingModuleId, token);
|
|
177
|
+
if (local) return local;
|
|
178
|
+
// 2. Global tokens — registration still lives in some module's bucket.
|
|
179
|
+
if (this.globals.has(token)) {
|
|
180
|
+
const exporters = this.exporterIndex.get(token);
|
|
181
|
+
if (exporters && exporters.size > 0) {
|
|
182
|
+
if (exporters.size > 1) {
|
|
183
|
+
throw new MultipleProvidersFoundError(requestingModuleId, token, [
|
|
184
|
+
...exporters
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
const [owner] = [
|
|
188
|
+
...exporters
|
|
189
|
+
];
|
|
190
|
+
return this.lookupInBucket(owner, token);
|
|
114
191
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
// 3. InjectionToken with default factory — self-providing from any
|
|
195
|
+
// scope. If already auto-registered (in `__root__`), return that;
|
|
196
|
+
// otherwise let the caller materialize it.
|
|
197
|
+
if (token instanceof InjectionToken && token.options?.factory) {
|
|
198
|
+
return this.lookupInBucket(ROOT_MODULE_ID, token);
|
|
199
|
+
}
|
|
200
|
+
// 4. Imported modules' exports — walk transitively so re-exports work.
|
|
201
|
+
const scope = this.scopes.get(requestingModuleId);
|
|
202
|
+
if (!scope) return undefined;
|
|
203
|
+
const candidates = [];
|
|
204
|
+
const candidateModuleIds = [];
|
|
205
|
+
const visited = new Set([
|
|
206
|
+
requestingModuleId
|
|
207
|
+
]);
|
|
208
|
+
this.collectFromImports(scope, token, visited, candidates, candidateModuleIds);
|
|
209
|
+
if (candidates.length === 0) return undefined;
|
|
210
|
+
if (candidates.length > 1) {
|
|
211
|
+
throw new MultipleProvidersFoundError(requestingModuleId, token, candidateModuleIds);
|
|
212
|
+
}
|
|
213
|
+
return candidates[0];
|
|
214
|
+
}
|
|
215
|
+
/** Typed bucket lookup — single seam where the Map<Token, Registration> erases T. */ lookupInBucket(moduleId, token) {
|
|
216
|
+
return this.providers.get(moduleId)?.get(token);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Walk a scope's imports (and their imports' imports, …) collecting every
|
|
220
|
+
* registration reachable through re-export chains. A child only contributes
|
|
221
|
+
* if it explicitly exports the token — non-exported providers stay private.
|
|
222
|
+
*/ collectFromImports(scope, token, visited, candidates, candidateModuleIds) {
|
|
223
|
+
for (const importedId of scope.importedModules){
|
|
224
|
+
if (visited.has(importedId)) continue;
|
|
225
|
+
visited.add(importedId);
|
|
226
|
+
const imported = this.scopes.get(importedId);
|
|
227
|
+
if (!imported?.exportedTokens.has(token)) continue;
|
|
228
|
+
const direct = this.lookupInBucket(importedId, token);
|
|
229
|
+
if (direct) {
|
|
230
|
+
candidates.push(direct);
|
|
231
|
+
candidateModuleIds.push(importedId);
|
|
232
|
+
continue;
|
|
123
233
|
}
|
|
124
|
-
|
|
234
|
+
// Imported module re-exports the token without owning it — recurse
|
|
235
|
+
// into ITS imports.
|
|
236
|
+
this.collectFromImports(imported, token, visited, candidates, candidateModuleIds);
|
|
125
237
|
}
|
|
126
|
-
return this.resolveRegistration(registration, requestingModuleId);
|
|
127
238
|
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
239
|
+
findAllRegistrations(token, requestingModuleId) {
|
|
240
|
+
if (requestingModuleId === undefined) {
|
|
241
|
+
const exporters = this.exporterIndex.get(token);
|
|
242
|
+
if (!exporters) return [];
|
|
243
|
+
return [
|
|
244
|
+
...exporters
|
|
245
|
+
].map((id)=>this.lookupInBucket(id, token)).filter((r)=>r !== undefined);
|
|
246
|
+
}
|
|
247
|
+
const out = [];
|
|
248
|
+
const seen = new Set();
|
|
249
|
+
const local = this.lookupInBucket(requestingModuleId, token);
|
|
250
|
+
if (local && !seen.has(local)) {
|
|
251
|
+
out.push(local);
|
|
252
|
+
seen.add(local);
|
|
253
|
+
}
|
|
254
|
+
if (this.globals.has(token)) {
|
|
255
|
+
for (const owner of this.exporterIndex.get(token) ?? []){
|
|
256
|
+
const reg = this.lookupInBucket(owner, token);
|
|
257
|
+
if (reg && !seen.has(reg)) {
|
|
258
|
+
out.push(reg);
|
|
259
|
+
seen.add(reg);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const scope = this.scopes.get(requestingModuleId);
|
|
264
|
+
if (scope) {
|
|
265
|
+
for (const importedId of scope.importedModules){
|
|
266
|
+
const imported = this.scopes.get(importedId);
|
|
267
|
+
if (!imported?.exportedTokens.has(token)) continue;
|
|
268
|
+
const reg = this.lookupInBucket(importedId, token);
|
|
269
|
+
if (reg && !seen.has(reg)) {
|
|
270
|
+
out.push(reg);
|
|
271
|
+
seen.add(reg);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
/** True if any bucket has a registration for `token`. Sandbox-friendly. */ has(token) {
|
|
278
|
+
return this.exporterIndex.has(token);
|
|
133
279
|
}
|
|
134
|
-
has(token) {
|
|
135
|
-
return this.providers.has(token);
|
|
280
|
+
/** True if the specified module's bucket has a registration for `token`. */ hasInScope(token, moduleId) {
|
|
281
|
+
return this.providers.get(moduleId)?.has(token) ?? false;
|
|
136
282
|
}
|
|
137
283
|
getProviderScope(token) {
|
|
138
|
-
|
|
284
|
+
const exporters = this.exporterIndex.get(token);
|
|
285
|
+
if (!exporters) return undefined;
|
|
286
|
+
for (const owner of exporters){
|
|
287
|
+
const reg = this.providers.get(owner)?.get(token);
|
|
288
|
+
if (reg) return reg.scope;
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
139
291
|
}
|
|
140
292
|
getTokens() {
|
|
141
|
-
|
|
293
|
+
const out = new Set();
|
|
294
|
+
for (const bucket of this.providers.values()){
|
|
295
|
+
for (const token of bucket.keys())out.add(token);
|
|
296
|
+
}
|
|
297
|
+
return [
|
|
298
|
+
...out
|
|
299
|
+
];
|
|
142
300
|
}
|
|
143
301
|
/**
|
|
144
302
|
* Create a child container for request-scoped resolution.
|
|
@@ -151,30 +309,40 @@ export class Container {
|
|
|
151
309
|
// Share state by reference — request-scope children must see the same
|
|
152
310
|
// module graph as the root.
|
|
153
311
|
child.providers = this.providers;
|
|
312
|
+
child.exporterIndex = this.exporterIndex;
|
|
154
313
|
child.scopes = this.scopes;
|
|
155
314
|
child.globals = this.globals;
|
|
156
|
-
child.providerOrigin = this.providerOrigin;
|
|
157
315
|
return child;
|
|
158
316
|
}
|
|
159
317
|
createDetached() {
|
|
160
318
|
const child = new Container({
|
|
161
319
|
diagnostics: this.diagnostics
|
|
162
320
|
});
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
321
|
+
// Deep-clone provider buckets so sandbox writes don't leak back to the
|
|
322
|
+
// parent. Inner ProviderRegistration objects are shallow-shared (we only
|
|
323
|
+
// mutate `instance` cache, and per-bucket clones already give per-sandbox
|
|
324
|
+
// singleton isolation when needed).
|
|
325
|
+
const clonedProviders = new Map();
|
|
326
|
+
for (const [moduleId, bucket] of this.providers){
|
|
327
|
+
clonedProviders.set(moduleId, new Map(bucket));
|
|
328
|
+
}
|
|
329
|
+
child.providers = clonedProviders;
|
|
330
|
+
const clonedIndex = new Map();
|
|
331
|
+
for (const [token, owners] of this.exporterIndex){
|
|
332
|
+
clonedIndex.set(token, new Set(owners));
|
|
333
|
+
}
|
|
334
|
+
child.exporterIndex = clonedIndex;
|
|
167
335
|
child.scopes = this.scopes;
|
|
168
336
|
child.globals = this.globals;
|
|
169
337
|
return child;
|
|
170
338
|
}
|
|
171
339
|
clear() {
|
|
172
340
|
this.providers.clear();
|
|
341
|
+
this.exporterIndex.clear();
|
|
173
342
|
this.resolutionStack.clear();
|
|
174
343
|
this.requestInstances.clear();
|
|
175
344
|
this.scopes.clear();
|
|
176
345
|
this.globals.clear();
|
|
177
|
-
this.providerOrigin.clear();
|
|
178
346
|
}
|
|
179
347
|
resolveRegistration(registration, requestingModuleId) {
|
|
180
348
|
if (registration.useValue !== undefined) {
|
|
@@ -208,11 +376,11 @@ export class Container {
|
|
|
208
376
|
if (registration.useFactory) {
|
|
209
377
|
// Factories (forRootAsync, useFactory) commonly inject deps from the
|
|
210
378
|
// importing module's scope. Vela has no `forRootAsync({ imports })`
|
|
211
|
-
// surface to track that, so factory inject deps resolve
|
|
212
|
-
//
|
|
379
|
+
// surface to track that, so factory inject deps resolve from the
|
|
380
|
+
// declaring module's POV — same escape-hatch shape as ModuleRef.
|
|
213
381
|
instance = this.resolveFactory(registration);
|
|
214
382
|
} else if (registration.useClass) {
|
|
215
|
-
instance = this.resolveClass(registration.useClass);
|
|
383
|
+
instance = this.resolveClass(registration.useClass, registration.declaringModuleId);
|
|
216
384
|
} else {
|
|
217
385
|
throw new Error(`Invalid provider registration for: ${this.tokenToString(registration.provide)}`);
|
|
218
386
|
}
|
|
@@ -226,9 +394,7 @@ export class Container {
|
|
|
226
394
|
this.resolutionStack.delete(registration.provide);
|
|
227
395
|
}
|
|
228
396
|
}
|
|
229
|
-
resolveClass(target,
|
|
230
|
-
// The class's dependencies resolve from ITS module's POV, not the caller's.
|
|
231
|
-
const ownerModuleId = this.providerOrigin.get(target) ?? callerModuleId;
|
|
397
|
+
resolveClass(target, ownerModuleId) {
|
|
232
398
|
const paramTypes = getConstructorDependencies(target);
|
|
233
399
|
const injectMetadata = getInjectMetadata(target);
|
|
234
400
|
const injectMap = new Map(injectMetadata.map((m)=>[
|
|
@@ -240,7 +406,7 @@ export class Container {
|
|
|
240
406
|
const rawToken = meta?.token;
|
|
241
407
|
const isForwardRef = rawToken instanceof ForwardRef;
|
|
242
408
|
const token = isForwardRef ? rawToken.factory() : rawToken ?? paramType;
|
|
243
|
-
if (!token || token
|
|
409
|
+
if (!token || isErasedTypeToken(token)) {
|
|
244
410
|
if (meta?.optional) return undefined;
|
|
245
411
|
throw new Error(`Cannot resolve dependency at index ${index} for ${target.name}. ` + `Parameter type is undefined or Object. ` + IMPORT_TYPE_HINT + ` Alternatively, use \`@Inject()\` to specify the token explicitly.`);
|
|
246
412
|
}
|
|
@@ -255,13 +421,16 @@ export class Container {
|
|
|
255
421
|
});
|
|
256
422
|
return new target(...dependencies);
|
|
257
423
|
}
|
|
258
|
-
resolveFactory(registration
|
|
424
|
+
resolveFactory(registration) {
|
|
259
425
|
if (!registration.useFactory) {
|
|
260
426
|
throw new Error('Factory function is missing');
|
|
261
427
|
}
|
|
428
|
+
// Factory inject deps resolve without a requester — Vela has no
|
|
429
|
+
// `forRootAsync({ imports })` surface to track which module the factory
|
|
430
|
+
// resolves from, so it uses the same escape-hatch shape as ModuleRef.
|
|
262
431
|
const dependencies = (registration.inject || []).map((token)=>{
|
|
263
432
|
const resolved = token instanceof ForwardRef ? token.factory() : token;
|
|
264
|
-
return this.resolve(resolved
|
|
433
|
+
return this.resolve(resolved);
|
|
265
434
|
});
|
|
266
435
|
const result = registration.useFactory(...dependencies);
|
|
267
436
|
if (result instanceof Promise) {
|
|
@@ -270,16 +439,15 @@ export class Container {
|
|
|
270
439
|
return result;
|
|
271
440
|
}
|
|
272
441
|
async resolveAsync(token, requestingModuleId) {
|
|
273
|
-
|
|
274
|
-
this.assertVisible(requestingModuleId, token);
|
|
275
|
-
}
|
|
276
|
-
const registration = this.providers.get(token);
|
|
442
|
+
const registration = this.findRegistration(token, requestingModuleId);
|
|
277
443
|
if (!registration) {
|
|
444
|
+
if (requestingModuleId !== undefined && this.scopes.has(requestingModuleId) && this.exporterIndex.has(token)) {
|
|
445
|
+
throw new ModuleVisibilityError(requestingModuleId, token);
|
|
446
|
+
}
|
|
278
447
|
if (typeof token === 'function') {
|
|
279
448
|
throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + `Declare it in a module's providers.`);
|
|
280
449
|
}
|
|
281
450
|
if (token instanceof InjectionToken && token.options?.factory) {
|
|
282
|
-
// Self-providing token — register on demand from its declared factory.
|
|
283
451
|
this.register({
|
|
284
452
|
provide: token,
|
|
285
453
|
useFactory: token.options.factory
|
|
@@ -292,8 +460,8 @@ export class Container {
|
|
|
292
460
|
if (registration.scope === Scope.SINGLETON && registration.instance !== undefined) {
|
|
293
461
|
return registration.instance;
|
|
294
462
|
}
|
|
295
|
-
// Factory inject deps resolve without a requester (escape hatch
|
|
296
|
-
//
|
|
463
|
+
// Factory inject deps resolve without a requester (same escape hatch as
|
|
464
|
+
// sync resolveFactory).
|
|
297
465
|
const dependencies = await Promise.all((registration.inject || []).map((t)=>{
|
|
298
466
|
const resolved = t instanceof ForwardRef ? t.factory() : t;
|
|
299
467
|
return this.resolveAsync(resolved);
|
|
@@ -312,38 +480,19 @@ export class Container {
|
|
|
312
480
|
return new Proxy(target, {
|
|
313
481
|
get (_target, prop) {
|
|
314
482
|
const instance = container.resolve(token, requestingModuleId);
|
|
315
|
-
|
|
483
|
+
// Annotate `unknown` to contain `Reflect.get`'s `any` return so the
|
|
484
|
+
// typeof-narrows-to-Function guard below stays honest.
|
|
485
|
+
const value = Reflect.get(instance, prop, instance);
|
|
316
486
|
return typeof value === 'function' ? value.bind(instance) : value;
|
|
317
487
|
},
|
|
318
488
|
set (_target, prop, value) {
|
|
319
489
|
const instance = container.resolve(token, requestingModuleId);
|
|
320
|
-
|
|
321
|
-
return true
|
|
490
|
+
// `Reflect.set` returns whether the assignment succeeded — that's the
|
|
491
|
+
// spec-correct Proxy `set` return, vs returning `true` unconditionally.
|
|
492
|
+
return Reflect.set(instance, prop, value);
|
|
322
493
|
}
|
|
323
494
|
});
|
|
324
495
|
}
|
|
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
|
-
}
|
|
347
496
|
tokenToString(token) {
|
|
348
497
|
if (token instanceof InjectionToken) {
|
|
349
498
|
return token.toString();
|
|
@@ -6,5 +6,12 @@ export declare function Optional(): ParameterDecorator;
|
|
|
6
6
|
export declare function Inject(token: Token | ForwardRef): ParameterDecorator;
|
|
7
7
|
export declare function isInjectable(target: object): boolean;
|
|
8
8
|
export declare function getScope(target: object): Scope;
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Read through Reflect so dist/src coexistence in tests routes through the
|
|
11
|
+
* single polyfill-installed registry — SWC writes `design:paramtypes` via
|
|
12
|
+
* Reflect, so reads must too. The runtime contract is that each entry is a
|
|
13
|
+
* constructor reference (a `Token`); we surface that contract directly so
|
|
14
|
+
* callers don't need to re-cast.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getConstructorDependencies(target: object): Array<Token | undefined>;
|
|
10
17
|
export declare function getInjectMetadata(target: object): InjectMetadata[];
|
|
@@ -43,10 +43,13 @@ export function isInjectable(target) {
|
|
|
43
43
|
export function getScope(target) {
|
|
44
44
|
return MetadataRegistry.getScope(target) ?? Scope.SINGLETON;
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Read through Reflect so dist/src coexistence in tests routes through the
|
|
48
|
+
* single polyfill-installed registry — SWC writes `design:paramtypes` via
|
|
49
|
+
* Reflect, so reads must too. The runtime contract is that each entry is a
|
|
50
|
+
* constructor reference (a `Token`); we surface that contract directly so
|
|
51
|
+
* callers don't need to re-cast.
|
|
52
|
+
*/ export function getConstructorDependencies(target) {
|
|
50
53
|
return Reflect.getMetadata('design:paramtypes', target) ?? [];
|
|
51
54
|
}
|
|
52
55
|
export function getInjectMetadata(target) {
|
|
@@ -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, ModuleVisibilityError } from './types';
|
|
3
|
+
export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, } from './types';
|
|
4
4
|
export { ModuleRef } from './module-ref';
|
|
5
5
|
export { mixin } from './mixin';
|
|
6
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, ModuleVisibilityError } from "./types.js";
|
|
3
|
+
export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
|
|
4
4
|
export { ModuleRef } from "./module-ref.js";
|
|
5
5
|
export { mixin } from "./mixin.js";
|
|
@@ -36,6 +36,12 @@ export interface ProviderOptions<T = unknown> {
|
|
|
36
36
|
export interface ProviderRegistration<T = unknown> {
|
|
37
37
|
provide: Token<T>;
|
|
38
38
|
scope: Scope;
|
|
39
|
+
/**
|
|
40
|
+
* Module that owns this registration. Used by `resolveClass` to determine
|
|
41
|
+
* the POV from which the class's dependencies resolve. Sandbox/bootstrap
|
|
42
|
+
* registrations land in the `"__root__"` bucket.
|
|
43
|
+
*/
|
|
44
|
+
declaringModuleId: string;
|
|
39
45
|
instance?: T;
|
|
40
46
|
useValue?: T;
|
|
41
47
|
useFactory?: (...args: any[]) => T | Promise<T>;
|
|
@@ -59,3 +65,10 @@ export declare class ModuleVisibilityError extends Error {
|
|
|
59
65
|
readonly token: Token;
|
|
60
66
|
constructor(moduleId: string, token: Token);
|
|
61
67
|
}
|
|
68
|
+
export declare class MultipleProvidersFoundError extends Error {
|
|
69
|
+
readonly moduleId: string;
|
|
70
|
+
readonly token: Token;
|
|
71
|
+
readonly candidates: string[];
|
|
72
|
+
constructor(moduleId: string, token: Token, candidates: string[]);
|
|
73
|
+
}
|
|
74
|
+
export declare const ROOT_MODULE_ID = "__root__";
|
package/dist/container/types.js
CHANGED
|
@@ -32,3 +32,13 @@ export class ModuleVisibilityError extends Error {
|
|
|
32
32
|
this.name = 'ModuleVisibilityError';
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
export class MultipleProvidersFoundError extends Error {
|
|
36
|
+
moduleId;
|
|
37
|
+
token;
|
|
38
|
+
candidates;
|
|
39
|
+
constructor(moduleId, token, candidates){
|
|
40
|
+
super(`Multiple providers found for '${describeToken(token)}' in module '${moduleId}':\n` + candidates.map((c)=>` - ${c}`).join('\n') + `\nResolve ambiguity by importing only one instance, or by using a per-instance ` + `accessor exposed by the module (e.g., Module.tokenFor(key)).`), this.moduleId = moduleId, this.token = token, this.candidates = candidates;
|
|
41
|
+
this.name = 'MultipleProvidersFoundError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const ROOT_MODULE_ID = '__root__';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { DynamicModule } from '../module/types';
|
|
2
2
|
import type { CorsOptions } from './cors.types';
|
|
3
3
|
export declare class CorsModule {
|
|
4
|
-
static forRoot(options?: CorsOptions
|
|
4
|
+
static forRoot(options?: CorsOptions & {
|
|
5
|
+
key?: string;
|
|
6
|
+
}): DynamicModule;
|
|
5
7
|
}
|
package/dist/cors/cors.module.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cors } from "hono/cors";
|
|
2
|
+
import { stableHash } from "../module/stable-hash.js";
|
|
2
3
|
import { APP_MIDDLEWARE } from "../pipeline/tokens.js";
|
|
3
4
|
import { CORS_OPTIONS } from "./cors.tokens.js";
|
|
4
5
|
export class CorsModule {
|
|
@@ -16,6 +17,7 @@ export class CorsModule {
|
|
|
16
17
|
};
|
|
17
18
|
return {
|
|
18
19
|
module: CorsModule,
|
|
20
|
+
key: options.key ?? stableHash(options),
|
|
19
21
|
providers: [
|
|
20
22
|
{
|
|
21
23
|
provide: CORS_OPTIONS,
|
|
@@ -2,5 +2,7 @@ import type { AsyncModuleOptions, DynamicModule } from '../module/types';
|
|
|
2
2
|
import type { HttpModuleOptions } from './fetch.types';
|
|
3
3
|
export declare class HttpModule {
|
|
4
4
|
static forRoot(options?: HttpModuleOptions): DynamicModule;
|
|
5
|
-
static forRootAsync(options: AsyncModuleOptions<HttpModuleOptions>
|
|
5
|
+
static forRootAsync(options: AsyncModuleOptions<HttpModuleOptions> & {
|
|
6
|
+
key?: string;
|
|
7
|
+
}): DynamicModule;
|
|
6
8
|
}
|