@lppedd/di-wise-neo 0.5.3 → 0.7.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/dist/cjs/index.js CHANGED
@@ -129,23 +129,23 @@ function ensureInjectionContext(fn) {
129
129
  return context;
130
130
  }
131
131
 
132
- function inject(token) {
132
+ function inject(token, name) {
133
133
  const context = ensureInjectionContext(inject);
134
- return context.container.resolve(token);
134
+ return context.container.resolve(token, name);
135
135
  }
136
- function injectBy(thisArg, token) {
136
+ function injectBy(thisArg, token, name) {
137
137
  const context = ensureInjectionContext(injectBy);
138
138
  const resolution = context.resolution;
139
139
  const currentFrame = resolution.stack.peek();
140
140
  if (!currentFrame) {
141
- return inject(token);
141
+ return inject(token, name);
142
142
  }
143
143
  const currentRef = {
144
144
  current: thisArg
145
145
  };
146
146
  const cleanup = resolution.dependents.set(currentFrame.provider, currentRef);
147
147
  try {
148
- return inject(token);
148
+ return inject(token, name);
149
149
  } finally{
150
150
  cleanup();
151
151
  }
@@ -156,41 +156,59 @@ function injectAll(token) {
156
156
  return context.container.resolveAll(token);
157
157
  }
158
158
 
159
- /**
160
- * Registers a mapping between a generated (e.g., decorated or proxied) constructor
161
- * and its original, underlying constructor.
162
- *
163
- * This allows libraries or consumers that manipulate constructors, such as through
164
- * class decorators, to inform the DI system about the real "identity" of a class.
165
- *
166
- * @param transformedClass The constructor function returned by a class decorator or factory.
167
- * @param originalClass The original constructor function.
168
- *
169
- * @remarks
170
- * This API affects the core class identity resolution mechanism of the DI system.
171
- * Incorrect usage may cause metadata to be misassociated, leading to subtle errors.
172
- * Use only when manipulating constructors (e.g., via decorators or proxies),
173
- * and ensure the mapping is correct.
174
- */ function setClassIdentityMapping(transformedClass, originalClass) {
175
- classIdentityMap.set(transformedClass, originalClass);
159
+ // @internal
160
+ class Metadata {
161
+ constructor(Class){
162
+ this.dependencies = {
163
+ constructor: [],
164
+ methods: new Map()
165
+ };
166
+ this.tokensRef = {
167
+ getRefTokens: ()=>new Set()
168
+ };
169
+ this.provider = {
170
+ useClass: Class
171
+ };
172
+ }
173
+ get name() {
174
+ return this.provider.name;
175
+ }
176
+ set name(name) {
177
+ this.provider.name = name;
178
+ }
179
+ getConstructorDependency(index) {
180
+ const i = this.dependencies.constructor.findIndex((d)=>d.index === index);
181
+ if (i > -1) {
182
+ return this.dependencies.constructor[i];
183
+ }
184
+ const dependency = {
185
+ index: index
186
+ };
187
+ this.dependencies.constructor.push(dependency);
188
+ return dependency;
189
+ }
190
+ getMethodDependency(method, index) {
191
+ let methodDeps = this.dependencies.methods.get(method);
192
+ if (!methodDeps) {
193
+ this.dependencies.methods.set(method, methodDeps = []);
194
+ }
195
+ const i = methodDeps.findIndex((d)=>d.index === index);
196
+ if (i > -1) {
197
+ return methodDeps[i];
198
+ }
199
+ const dependency = {
200
+ index: index
201
+ };
202
+ methodDeps.push(dependency);
203
+ return dependency;
204
+ }
176
205
  }
177
206
  // @internal
178
207
  function getMetadata(Class) {
179
208
  const originalClass = classIdentityMap.get(Class) ?? Class;
180
209
  let metadata = metadataMap.get(originalClass);
181
210
  if (!metadata) {
182
- metadataMap.set(originalClass, metadata = {
183
- tokensRef: {
184
- getRefTokens: ()=>new Set()
185
- },
186
- provider: {
187
- useClass: originalClass
188
- },
189
- dependencies: {
190
- constructor: [],
191
- methods: new Map()
192
- }
193
- });
211
+ metadataMap.set(originalClass, metadata = new Metadata(originalClass));
194
212
  }
195
213
  if (metadata.provider.useClass !== Class) {
196
214
  // This is part of the class identity mapping API (see setClassIdentityMapping).
@@ -204,32 +222,48 @@ function getMetadata(Class) {
204
222
  // We must update useClass to be the extender class B so that instances created by the
205
223
  // DI container match the consumer's registered class. Without this update, the DI
206
224
  // system would instantiate the original class A, causing behavior inconsistencies.
207
- //
208
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
209
225
  metadata.provider.useClass = Class;
210
226
  }
211
227
  return metadata;
212
228
  }
229
+ /**
230
+ * Registers a mapping between a generated (e.g., decorated or proxied) constructor
231
+ * and its original, underlying constructor.
232
+ *
233
+ * This allows libraries or consumers that manipulate constructors, such as through
234
+ * class decorators, to inform the DI system about the real "identity" of a class.
235
+ *
236
+ * @param transformedClass The constructor function returned by a class decorator or factory.
237
+ * @param originalClass The original constructor function.
238
+ *
239
+ * @remarks
240
+ * This API affects the core class identity resolution mechanism of the DI system.
241
+ * Incorrect usage may cause metadata to be misassociated, leading to subtle errors.
242
+ * Use only when manipulating constructors (e.g., via decorators or proxies),
243
+ * and ensure the mapping is correct.
244
+ */ function setClassIdentityMapping(transformedClass, originalClass) {
245
+ classIdentityMap.set(transformedClass, originalClass);
246
+ }
213
247
  const classIdentityMap = new WeakMap();
214
248
  const metadataMap = new WeakMap();
215
249
 
216
- function optional(token) {
250
+ function optional(token, name) {
217
251
  const context = ensureInjectionContext(optional);
218
- return context.container.resolve(token, true);
252
+ return context.container.resolve(token, true, name);
219
253
  }
220
- function optionalBy(thisArg, token) {
254
+ function optionalBy(thisArg, token, name) {
221
255
  const context = ensureInjectionContext(optionalBy);
222
256
  const resolution = context.resolution;
223
257
  const currentFrame = resolution.stack.peek();
224
258
  if (!currentFrame) {
225
- return optional(token);
259
+ return optional(token, name);
226
260
  }
227
261
  const currentRef = {
228
262
  current: thisArg
229
263
  };
230
264
  const cleanup = resolution.dependents.set(currentFrame.provider, currentRef);
231
265
  try {
232
- return optional(token);
266
+ return optional(token, name);
233
267
  } finally{
234
268
  cleanup();
235
269
  }
@@ -288,28 +322,29 @@ const Scope = {
288
322
  }
289
323
  // @internal
290
324
  function isConstructor(token) {
291
- return typeof token == "function";
325
+ return typeof token === "function";
292
326
  }
293
327
 
294
328
  // @internal
295
329
  function getTypeName(value) {
296
- if (typeof value == "string") {
297
- return `"${value}"`;
298
- }
299
- if (typeof value == "function") {
300
- return value.name && `typeof ${value.name}` || "Function";
301
- }
302
- if (typeof value == "object") {
303
- if (value === null) {
304
- return "null";
305
- }
306
- const proto = Object.getPrototypeOf(value);
307
- if (proto && proto !== Object.prototype) {
308
- const constructor = proto.constructor;
309
- if (typeof constructor == "function" && constructor.name) {
310
- return constructor.name;
330
+ switch(typeof value){
331
+ case "string":
332
+ return `"${value}"`;
333
+ case "function":
334
+ return value.name && `typeof ${value.name}` || "Function";
335
+ case "object":
336
+ {
337
+ if (value === null) {
338
+ return "null";
339
+ }
340
+ const proto = Object.getPrototypeOf(value);
341
+ if (proto && proto !== Object.prototype) {
342
+ const constructor = proto.constructor;
343
+ if (typeof constructor === "function" && constructor.name) {
344
+ return constructor.name;
345
+ }
346
+ }
311
347
  }
312
- }
313
348
  }
314
349
  return typeof value;
315
350
  }
@@ -320,28 +355,46 @@ class TokenRegistry {
320
355
  this.myMap = new Map();
321
356
  this.myParent = parent;
322
357
  }
323
- get(token) {
358
+ get(token, name) {
324
359
  // To clarify, at(-1) means we take the last added registration for this token
325
- return this.getAll(token)?.at(-1);
360
+ return this.getAll(token, name).at(-1);
326
361
  }
327
- getAll(token) {
328
- const internal = internals.get(token);
362
+ getAll(token, name) {
363
+ // Internal registrations cannot have a name
364
+ const internal = name !== undefined ? undefined : internals.get(token);
329
365
  return internal && [
330
366
  internal
331
- ] || this.getAllFromParent(token);
367
+ ] || this.getAllFromParent(token, name);
332
368
  }
333
369
  set(token, registration) {
334
370
  assert(!internals.has(token), `cannot register reserved token ${token.name}`);
335
371
  let registrations = this.myMap.get(token);
336
372
  if (!registrations) {
337
373
  this.myMap.set(token, registrations = []);
374
+ } else if (registration.name !== undefined) {
375
+ const existing = registrations.filter((r)=>r.name === registration.name);
376
+ assert(existing.length === 0, `a ${token.name} token named '${registration.name}' is already registered`);
338
377
  }
339
378
  registrations.push(registration);
340
379
  }
341
- delete(token) {
380
+ delete(token, name) {
342
381
  const registrations = this.myMap.get(token);
343
- this.myMap.delete(token);
344
- return registrations;
382
+ if (registrations) {
383
+ if (name !== undefined) {
384
+ const removedRegistrations = [];
385
+ const newRegistrations = [];
386
+ for (const registration of registrations){
387
+ const array = registration.name === name ? removedRegistrations : newRegistrations;
388
+ array.push(registration);
389
+ }
390
+ if (removedRegistrations.length > 0) {
391
+ this.myMap.set(token, newRegistrations);
392
+ return removedRegistrations;
393
+ }
394
+ }
395
+ this.myMap.delete(token);
396
+ }
397
+ return registrations ?? [];
345
398
  }
346
399
  deleteAll() {
347
400
  const tokens = Array.from(this.myMap.keys());
@@ -369,9 +422,14 @@ class TokenRegistry {
369
422
  }
370
423
  return Array.from(values);
371
424
  }
372
- getAllFromParent(token) {
373
- const registrations = this.myMap.get(token);
374
- return registrations || this.myParent?.getAllFromParent(token);
425
+ getAllFromParent(token, name) {
426
+ const thisRegistrations = this.myMap.get(token);
427
+ let registrations = thisRegistrations || this.myParent?.getAllFromParent(token, name);
428
+ if (registrations && name !== undefined) {
429
+ registrations = registrations.filter((r)=>r.name === name);
430
+ assert(registrations.length < 2, `internal error: more than one registration named '${name}'`);
431
+ }
432
+ return registrations ?? [];
375
433
  }
376
434
  }
377
435
  // @internal
@@ -469,9 +527,6 @@ function isDisposable(value) {
469
527
  getAllCached(token) {
470
528
  this.checkDisposed();
471
529
  const registrations = this.myTokenRegistry.getAll(token);
472
- if (!registrations) {
473
- return [];
474
- }
475
530
  const values = new Set();
476
531
  for (const registration of registrations){
477
532
  const value = registration.value;
@@ -493,50 +548,21 @@ function isDisposable(value) {
493
548
  }
494
549
  return Array.from(values);
495
550
  }
496
- isRegistered(token) {
551
+ isRegistered(token, name) {
497
552
  this.checkDisposed();
498
- return this.myTokenRegistry.get(token) !== undefined;
499
- }
500
- registerClass(token, Class, options) {
501
- // This mess will go away once/if we remove the register method
502
- // in favor of the multiple specialized ones
503
- if (Class) {
504
- const ctor = Class ?? token;
505
- this.register(token, {
506
- useClass: ctor
507
- }, options);
508
- } else {
509
- this.register(token);
510
- }
511
- }
512
- registerFactory(token, factory, options) {
513
- this.register(token, {
514
- useFactory: factory
515
- }, options);
516
- }
517
- registerValue(token, value) {
518
- this.register(token, {
519
- useValue: value
520
- });
521
- }
522
- registerAlias(targetToken, aliasTokens) {
523
- // De-duplicate tokens
524
- for (const alias of new Set(aliasTokens)){
525
- this.register(alias, {
526
- useExisting: targetToken
527
- });
528
- }
553
+ return this.myTokenRegistry.get(token, name) !== undefined;
529
554
  }
530
555
  register(...args) {
531
556
  this.checkDisposed();
532
- if (args.length == 1) {
557
+ if (args.length === 1) {
533
558
  const Class = args[0];
534
559
  const metadata = getMetadata(Class);
535
560
  const registration = {
561
+ name: metadata.name,
536
562
  // The provider is of type ClassProvider, initialized by getMetadata
537
563
  provider: metadata.provider,
538
564
  options: {
539
- scope: metadata.scope ?? this.myOptions.defaultScope
565
+ scope: metadata.scope?.value ?? this.myOptions.defaultScope
540
566
  },
541
567
  dependencies: metadata.dependencies
542
568
  };
@@ -553,18 +579,19 @@ function isDisposable(value) {
553
579
  }
554
580
  // Eager-instantiate only if the class is container-scoped
555
581
  if (metadata.eagerInstantiate && registration.options?.scope === Scope.Container) {
556
- this.resolve(Class);
582
+ this.resolveProviderValue(registration, registration.provider);
557
583
  }
558
584
  } else {
559
585
  const [token, provider, options] = args;
560
586
  if (isClassProvider(provider)) {
561
587
  const metadata = getMetadata(provider.useClass);
562
588
  const registration = {
589
+ // An explicit provider name overrides what is specified via @Named
590
+ name: metadata.name ?? provider.name,
563
591
  provider: metadata.provider,
564
592
  options: {
565
- // The explicit registration options override what is specified
566
- // via class decorators (e.g., @Scoped)
567
- scope: metadata.scope ?? this.myOptions.defaultScope,
593
+ // Explicit registration options override what is specified via class decorators (e.g., @Scoped)
594
+ scope: metadata.scope?.value ?? this.myOptions.defaultScope,
568
595
  ...options
569
596
  },
570
597
  dependencies: metadata.dependencies
@@ -572,13 +599,15 @@ function isDisposable(value) {
572
599
  this.myTokenRegistry.set(token, registration);
573
600
  // Eager-instantiate only if the provided class is container-scoped
574
601
  if (metadata.eagerInstantiate && registration.options?.scope === Scope.Container) {
575
- this.resolve(token);
602
+ this.resolveProviderValue(registration, registration.provider);
576
603
  }
577
604
  } else {
578
- if (isExistingProvider(provider)) {
605
+ const existingProvider = isExistingProvider(provider);
606
+ if (existingProvider) {
579
607
  assert(token !== provider.useExisting, `the useExisting token ${token.name} cannot be the same as the token being registered`);
580
608
  }
581
609
  this.myTokenRegistry.set(token, {
610
+ name: existingProvider ? undefined : provider.name,
582
611
  provider: provider,
583
612
  options: options
584
613
  });
@@ -586,12 +615,9 @@ function isDisposable(value) {
586
615
  }
587
616
  return this;
588
617
  }
589
- unregister(token) {
618
+ unregister(token, name) {
590
619
  this.checkDisposed();
591
- const registrations = this.myTokenRegistry.delete(token);
592
- if (!registrations) {
593
- return [];
594
- }
620
+ const registrations = this.myTokenRegistry.delete(token, name);
595
621
  const values = new Set();
596
622
  for (const registration of registrations){
597
623
  const value = registration.value;
@@ -601,22 +627,31 @@ function isDisposable(value) {
601
627
  }
602
628
  return Array.from(values);
603
629
  }
604
- resolve(token, optional) {
630
+ resolve(token, optionalOrName, name) {
605
631
  this.checkDisposed();
606
- const registration = this.myTokenRegistry.get(token);
632
+ let localOptional;
633
+ let localName;
634
+ if (typeof optionalOrName === "string") {
635
+ localName = optionalOrName;
636
+ } else {
637
+ localOptional = optionalOrName;
638
+ localName = name;
639
+ }
640
+ const registration = this.myTokenRegistry.get(token, localName);
607
641
  if (registration) {
608
- return this.resolveRegistration(token, registration);
642
+ return this.resolveRegistration(token, registration, localName);
609
643
  }
610
644
  if (isConstructor(token)) {
611
- return this.instantiateClass(token, optional);
645
+ return this.instantiateClass(token, localOptional);
612
646
  }
613
- return optional ? undefined : throwUnregisteredError(token);
647
+ return optionalOrName ? undefined : throwUnregisteredError(token);
614
648
  }
615
649
  resolveAll(token, optional) {
616
650
  this.checkDisposed();
617
651
  const registrations = this.myTokenRegistry.getAll(token);
618
- if (registrations) {
619
- return registrations.map((registration)=>this.resolveRegistration(token, registration)).filter((value)=>value != null);
652
+ if (registrations.length > 0) {
653
+ return registrations //
654
+ .map((registration)=>this.resolveRegistration(token, registration)).filter((value)=>value != null);
620
655
  }
621
656
  if (isConstructor(token)) {
622
657
  const instance = this.instantiateClass(token, optional);
@@ -652,12 +687,12 @@ function isDisposable(value) {
652
687
  // Allow values to be GCed
653
688
  disposedRefs.clear();
654
689
  }
655
- resolveRegistration(token, registration) {
690
+ resolveRegistration(token, registration, name) {
656
691
  let currRegistration = registration;
657
692
  let currProvider = currRegistration.provider;
658
693
  while(isExistingProvider(currProvider)){
659
694
  const targetToken = currProvider.useExisting;
660
- currRegistration = this.myTokenRegistry.get(targetToken);
695
+ currRegistration = this.myTokenRegistry.get(targetToken, name);
661
696
  if (!currRegistration) {
662
697
  throwExistingUnregisteredError(token, targetToken);
663
698
  }
@@ -688,7 +723,7 @@ function isDisposable(value) {
688
723
  metadata.eagerInstantiate = eagerInstantiate;
689
724
  }
690
725
  }
691
- const scope = this.resolveScope(metadata.scope);
726
+ const scope = this.resolveScope(metadata.scope?.value);
692
727
  if (optional && scope === Scope.Container) {
693
728
  // It would not be possible to resolve the class in container scope,
694
729
  // as that would require prior registration.
@@ -788,7 +823,7 @@ function isDisposable(value) {
788
823
  }
789
824
  }
790
825
  resolveScope(scope = this.myOptions.defaultScope, context = useInjectionContext()) {
791
- if (scope == Scope.Inherited) {
826
+ if (scope === Scope.Inherited) {
792
827
  const dependentFrame = context?.resolution.stack.peek();
793
828
  return dependentFrame?.scope || Scope.Transient;
794
829
  }
@@ -798,7 +833,7 @@ function isDisposable(value) {
798
833
  const dependencies = registration.dependencies;
799
834
  if (dependencies) {
800
835
  assert(isClassProvider(registration.provider), `internal error: not a ClassProvider`);
801
- const ctorDeps = dependencies.constructor;
836
+ const ctorDeps = dependencies.constructor.filter((d)=>d.appliedBy);
802
837
  if (ctorDeps.length > 0) {
803
838
  // Let's check if all necessary constructor parameters are decorated.
804
839
  // If not, we cannot safely create an instance.
@@ -809,13 +844,13 @@ function isDisposable(value) {
809
844
  });
810
845
  return ctorDeps.sort((a, b)=>a.index - b.index).map((dep)=>{
811
846
  const token = dep.tokenRef.getRefToken();
812
- switch(dep.decorator){
847
+ switch(dep.appliedBy){
813
848
  case "Inject":
814
- return this.resolve(token);
849
+ return this.resolve(token, dep.name);
815
850
  case "InjectAll":
816
851
  return this.resolveAll(token);
817
852
  case "Optional":
818
- return this.resolve(token, true);
853
+ return this.resolve(token, true, dep.name);
819
854
  case "OptionalAll":
820
855
  return this.resolveAll(token, true);
821
856
  }
@@ -830,7 +865,9 @@ function isDisposable(value) {
830
865
  assert(isClassProvider(registration.provider), `internal error: not a ClassProvider`);
831
866
  const ctor = registration.provider.useClass;
832
867
  // Perform method injection
833
- for (const [key, methodDeps] of dependencies.methods){
868
+ for (const entry of dependencies.methods){
869
+ const key = entry[0];
870
+ const methodDeps = entry[1].filter((d)=>d.appliedBy);
834
871
  // Let's check if all necessary method parameters are decorated.
835
872
  // If not, we cannot safely invoke the method.
836
873
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@@ -841,13 +878,13 @@ function isDisposable(value) {
841
878
  });
842
879
  const args = methodDeps.sort((a, b)=>a.index - b.index).map((dep)=>{
843
880
  const token = dep.tokenRef.getRefToken();
844
- switch(dep.decorator){
881
+ switch(dep.appliedBy){
845
882
  case "Inject":
846
- return injectBy(instance, token);
883
+ return injectBy(instance, token, dep.name);
847
884
  case "InjectAll":
848
885
  return injectAll(token);
849
886
  case "Optional":
850
- return optionalBy(instance, token);
887
+ return optionalBy(instance, token, dep.name);
851
888
  case "OptionalAll":
852
889
  return optionalAll(token);
853
890
  }
@@ -878,7 +915,7 @@ function isDisposable(value) {
878
915
  *
879
916
  * @example
880
917
  * ```ts
881
- * @AutoRegister
918
+ * @AutoRegister()
882
919
  * class Wizard {}
883
920
  *
884
921
  * const wizard = container.resolve(Wizard);
@@ -886,32 +923,45 @@ function isDisposable(value) {
886
923
  * ```
887
924
  *
888
925
  * @__NO_SIDE_EFFECTS__
889
- */ function AutoRegister(Class) {
890
- const metadata = getMetadata(Class);
891
- metadata.autoRegister = true;
926
+ */ function AutoRegister() {
927
+ return function(Class) {
928
+ const metadata = getMetadata(Class);
929
+ metadata.autoRegister = true;
930
+ };
892
931
  }
893
932
 
894
933
  /**
895
- * Class decorator that enables eager instantiation of a class when it is registered
896
- * in the container with `Scope.Container`.
934
+ * Class decorator that sets the class scope to **Container** and enables
935
+ * eager instantiation when the class is registered in the container.
897
936
  *
898
937
  * This causes the container to immediately create and cache the instance of the class,
899
938
  * instead of deferring instantiation until the first resolution.
900
939
  *
901
940
  * @example
902
941
  * ```ts
903
- * @EagerInstantiate
904
- * @Scoped(Scope.Container)
942
+ * @EagerInstantiate()
905
943
  * class Wizard {}
906
944
  *
907
- * // A Wizard instance is immediately created and cached by the container
908
- * const wizard = container.register(Wizard);
945
+ * // Wizard is registered with Container scope, and an instance
946
+ * // is immediately created and cached by the container
947
+ * container.register(Wizard);
909
948
  * ```
910
949
  *
911
950
  * @__NO_SIDE_EFFECTS__
912
- */ function EagerInstantiate(Class) {
913
- const metadata = getMetadata(Class);
914
- metadata.eagerInstantiate = true;
951
+ */ function EagerInstantiate() {
952
+ return function(Class) {
953
+ const metadata = getMetadata(Class);
954
+ const currentScope = metadata.scope;
955
+ assert(!currentScope || currentScope.value === Scope.Container, ()=>{
956
+ const { value, appliedBy } = currentScope;
957
+ return `class ${Class.name}: Scope.${value} was already set by @${appliedBy},\n ` + `but @EagerInstantiate is trying to set a conflicting Scope.Container.\n ` + `Only one decorator should set the class scope, or all must agree on the same value.`;
958
+ });
959
+ metadata.eagerInstantiate = true;
960
+ metadata.scope = {
961
+ value: Scope.Container,
962
+ appliedBy: "EagerInstantiate"
963
+ };
964
+ };
915
965
  }
916
966
 
917
967
  function forwardRef(token) {
@@ -943,41 +993,31 @@ function isTokenRef(value) {
943
993
  return value && typeof value === "object" && typeof value.getRefToken === "function";
944
994
  }
945
995
 
946
- function processDecoratedParameter(decorator, token, target, propertyKey, parameterIndex) {
947
- // Error out immediately if the decorator has been applied
948
- // to a static property or a static method
996
+ // @internal
997
+ function updateParameterMetadata(decorator, target, propertyKey, parameterIndex, updateFn) {
998
+ // Error out immediately if the decorator has been applied to a static method
949
999
  if (propertyKey !== undefined && typeof target === "function") {
950
- assert(false, `@${decorator} cannot be used on static member ${target.name}.${String(propertyKey)}`);
1000
+ assert(false, `@${decorator} cannot be used on static method ${target.name}.${String(propertyKey)}`);
951
1001
  }
952
- const tokenRef = isTokenRef(token) ? token : forwardRef(()=>token);
953
1002
  if (propertyKey === undefined) {
954
1003
  // Constructor
955
1004
  const metadata = getMetadata(target);
956
- metadata.dependencies.constructor.push({
957
- decorator: decorator,
958
- tokenRef: tokenRef,
959
- index: parameterIndex
960
- });
1005
+ const dependency = metadata.getConstructorDependency(parameterIndex);
1006
+ updateFn(dependency);
961
1007
  } else {
962
- // Normal instance method
1008
+ // Instance method
963
1009
  const metadata = getMetadata(target.constructor);
964
- const methods = metadata.dependencies.methods;
965
- let dep = methods.get(propertyKey);
966
- if (dep === undefined) {
967
- dep = [];
968
- methods.set(propertyKey, dep);
969
- }
970
- dep.push({
971
- decorator: decorator,
972
- tokenRef: tokenRef,
973
- index: parameterIndex
974
- });
1010
+ const dependency = metadata.getMethodDependency(propertyKey, parameterIndex);
1011
+ updateFn(dependency);
975
1012
  }
976
1013
  }
977
1014
 
978
1015
  function Inject(token) {
979
1016
  return function(target, propertyKey, parameterIndex) {
980
- processDecoratedParameter("Inject", token, target, propertyKey, parameterIndex);
1017
+ updateParameterMetadata("Inject", target, propertyKey, parameterIndex, (dependency)=>{
1018
+ dependency.appliedBy = "Inject";
1019
+ dependency.tokenRef = isTokenRef(token) ? token : forwardRef(()=>token);
1020
+ });
981
1021
  };
982
1022
  }
983
1023
 
@@ -1003,19 +1043,66 @@ function Inject(token) {
1003
1043
 
1004
1044
  function InjectAll(token) {
1005
1045
  return function(target, propertyKey, parameterIndex) {
1006
- processDecoratedParameter("InjectAll", token, target, propertyKey, parameterIndex);
1046
+ updateParameterMetadata("InjectAll", target, propertyKey, parameterIndex, (dependency)=>{
1047
+ dependency.appliedBy = "InjectAll";
1048
+ dependency.tokenRef = isTokenRef(token) ? token : forwardRef(()=>token);
1049
+ });
1050
+ };
1051
+ }
1052
+
1053
+ /**
1054
+ * Qualifies a class or an injected parameter with a unique name.
1055
+ *
1056
+ * This allows the container to distinguish between multiple implementations
1057
+ * of the same interface or type during registration and injection.
1058
+ *
1059
+ * @example
1060
+ * ```ts
1061
+ * @Named("dumbledore")
1062
+ * class Dumbledore implements Wizard {}
1063
+ *
1064
+ * // Register Dumbledore with Type<Wizard>
1065
+ * container.register(IWizard, { useClass: Dumbledore });
1066
+ * const dumbledore = container.resolve(IWizard, "dumbledore");
1067
+ * ```
1068
+ *
1069
+ * @__NO_SIDE_EFFECTS__
1070
+ */ function Named(name) {
1071
+ if (!name.trim()) {
1072
+ assert(false, "the @Named qualifier cannot be empty or blank");
1073
+ }
1074
+ return function(target, propertyKey, parameterIndex) {
1075
+ if (parameterIndex === undefined) {
1076
+ // The decorator has been applied to the class
1077
+ const ctor = target;
1078
+ const metadata = getMetadata(ctor);
1079
+ assert(!metadata.name, `a @Named('${metadata.name}') qualifier has already been applied to ${ctor.name}`);
1080
+ metadata.name = name;
1081
+ } else {
1082
+ // The decorator has been applied to a method parameter
1083
+ updateParameterMetadata("Named", target, propertyKey, parameterIndex, (dependency)=>{
1084
+ assert(!dependency.name, `a @Named('${dependency.name}') qualifier has already been applied to the parameter`);
1085
+ dependency.name = name;
1086
+ });
1087
+ }
1007
1088
  };
1008
1089
  }
1009
1090
 
1010
1091
  function Optional(token) {
1011
1092
  return function(target, propertyKey, parameterIndex) {
1012
- processDecoratedParameter("Optional", token, target, propertyKey, parameterIndex);
1093
+ updateParameterMetadata("Optional", target, propertyKey, parameterIndex, (dependency)=>{
1094
+ dependency.appliedBy = "Optional";
1095
+ dependency.tokenRef = isTokenRef(token) ? token : forwardRef(()=>token);
1096
+ });
1013
1097
  };
1014
1098
  }
1015
1099
 
1016
1100
  function OptionalAll(token) {
1017
1101
  return function(target, propertyKey, parameterIndex) {
1018
- processDecoratedParameter("OptionalAll", token, target, propertyKey, parameterIndex);
1102
+ updateParameterMetadata("OptionalAll", target, propertyKey, parameterIndex, (dependency)=>{
1103
+ dependency.appliedBy = "OptionalAll";
1104
+ dependency.tokenRef = isTokenRef(token) ? token : forwardRef(()=>token);
1105
+ });
1019
1106
  };
1020
1107
  }
1021
1108
 
@@ -1043,7 +1130,16 @@ function OptionalAll(token) {
1043
1130
  */ function Scoped(scope) {
1044
1131
  return function(Class) {
1045
1132
  const metadata = getMetadata(Class);
1046
- metadata.scope = scope;
1133
+ const currentScope = metadata.scope;
1134
+ assert(!currentScope || currentScope.value === scope, ()=>{
1135
+ const { value, appliedBy } = currentScope;
1136
+ const by = appliedBy === "Scoped" ? `another @${appliedBy} decorator` : `@${appliedBy}`;
1137
+ return `class ${Class.name}: Scope.${value} was already set by ${by},\n ` + `but @Scoped is trying to set a conflicting Scope.${scope}.\n ` + `Only one decorator should set the class scope, or all must agree on the same value.`;
1138
+ });
1139
+ metadata.scope = {
1140
+ value: scope,
1141
+ appliedBy: "Scoped"
1142
+ };
1047
1143
  };
1048
1144
  }
1049
1145
 
@@ -1144,6 +1240,7 @@ exports.Inject = Inject;
1144
1240
  exports.InjectAll = InjectAll;
1145
1241
  exports.Injectable = Injectable;
1146
1242
  exports.Injector = Injector;
1243
+ exports.Named = Named;
1147
1244
  exports.Optional = Optional;
1148
1245
  exports.OptionalAll = OptionalAll;
1149
1246
  exports.Scope = Scope;