@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +52 -0
  3. package/dist/cache/cache.module.d.ts +1 -0
  4. package/dist/cache/cache.module.js +6 -0
  5. package/dist/config/config.module.d.ts +4 -1
  6. package/dist/config/config.module.js +6 -0
  7. package/dist/container/container.d.ts +32 -4
  8. package/dist/container/container.js +252 -103
  9. package/dist/container/decorators.d.ts +8 -1
  10. package/dist/container/decorators.js +7 -4
  11. package/dist/container/index.d.ts +1 -1
  12. package/dist/container/index.js +1 -1
  13. package/dist/container/types.d.ts +13 -0
  14. package/dist/container/types.js +10 -0
  15. package/dist/cors/cors.module.d.ts +3 -1
  16. package/dist/cors/cors.module.js +2 -0
  17. package/dist/fetch/fetch.module.d.ts +3 -1
  18. package/dist/fetch/fetch.module.js +8 -3
  19. package/dist/http/argument-resolver.d.ts +10 -0
  20. package/dist/http/argument-resolver.js +89 -0
  21. package/dist/http/handler-executor.d.ts +20 -0
  22. package/dist/http/handler-executor.js +108 -0
  23. package/dist/http/instantiate.d.ts +4 -0
  24. package/dist/http/instantiate.js +18 -0
  25. package/dist/http/response-mapper.d.ts +8 -0
  26. package/dist/http/response-mapper.js +42 -0
  27. package/dist/http/route.manager.d.ts +1 -9
  28. package/dist/http/route.manager.js +23 -240
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +2 -2
  31. package/dist/module/decorators.d.ts +6 -2
  32. package/dist/module/decorators.js +7 -6
  33. package/dist/module/index.d.ts +2 -1
  34. package/dist/module/index.js +2 -1
  35. package/dist/module/module-loader.d.ts +6 -1
  36. package/dist/module/module-loader.js +127 -47
  37. package/dist/module/stable-hash.d.ts +15 -0
  38. package/dist/module/stable-hash.js +52 -0
  39. package/dist/registry/types.d.ts +11 -0
  40. package/dist/throttler/throttler.module.d.ts +6 -2
  41. package/dist/throttler/throttler.module.js +6 -0
  42. 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
- export class Container {
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, declaringModuleId);
34
+ this.registerClass(provider, moduleId);
19
35
  } else {
20
- this.registerOptions(provider, declaringModuleId);
36
+ this.registerOptions(provider, moduleId);
21
37
  }
22
38
  return this;
23
39
  }
24
- registerClass(target, declaringModuleId) {
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.providers.set(target, {
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, declaringModuleId) {
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.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
- }
75
+ this.writeRegistration(moduleId, token, registration);
66
76
  }
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);
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
- if (requestingModuleId !== undefined) {
102
- this.assertVisible(requestingModuleId, token);
118
+ const registration = this.findRegistration(token, requestingModuleId);
119
+ if (registration) {
120
+ return this.resolveRegistration(registration, requestingModuleId);
103
121
  }
104
- const registration = this.providers.get(token);
105
- if (!registration) {
106
- // `Object`/undefined at a token position is the fingerprint of a
107
- // type-only import that TypeScript stripped emit the hint before
108
- // attempting any other recovery.
109
- if (token === Object || token == null) {
110
- throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + IMPORT_TYPE_HINT);
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
- if (typeof token === 'function') {
113
- throw new Error(`No provider found for token: ${this.tokenToString(token)}. ` + `Declare it in a module's providers.`);
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
- // InjectionToken default factories are explicit user declarations
116
- // attached to the token at construction — self-providing singletons.
117
- if (token instanceof InjectionToken && token.options?.factory) {
118
- this.register({
119
- provide: token,
120
- useFactory: token.options.factory
121
- });
122
- return this.resolve(token, requestingModuleId);
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
- throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
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
- resolveAll(token, requestingModuleId) {
129
- if (!this.has(token)) return [];
130
- return [
131
- this.resolve(token, requestingModuleId)
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
- return this.providers.get(token)?.scope;
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
- return Array.from(this.providers.keys());
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
- // 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);
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 without a
212
- // requester — same escape-hatch shape as ModuleRef.
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, callerModuleId) {
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 === Object) {
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, requestingModuleId) {
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, requestingModuleId);
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
- if (requestingModuleId !== undefined) {
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 — see
296
- // resolveRegistration's useFactory branch).
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
- const value = instance[prop];
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
- instance[prop] = value;
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
- export declare function getConstructorDependencies(target: object): unknown[];
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
- export function getConstructorDependencies(target) {
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.
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';
@@ -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__";
@@ -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): DynamicModule;
4
+ static forRoot(options?: CorsOptions & {
5
+ key?: string;
6
+ }): DynamicModule;
5
7
  }
@@ -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>): DynamicModule;
5
+ static forRootAsync(options: AsyncModuleOptions<HttpModuleOptions> & {
6
+ key?: string;
7
+ }): DynamicModule;
6
8
  }