@lppedd/di-wise-neo 0.10.0 → 0.11.1

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.
@@ -116,11 +116,11 @@ interface FactoryProvider<Value> {
116
116
  /**
117
117
  * Provides a static - already constructed - value for a token.
118
118
  */
119
- interface ValueProvider<T> {
119
+ interface ValueProvider<Value> {
120
120
  /**
121
121
  * The static value to associate with the token.
122
122
  */
123
- readonly useValue: T;
123
+ readonly useValue: Value;
124
124
  /**
125
125
  * An optional name to qualify this provider.
126
126
  * If specified, the token must be resolved using the same name.
@@ -147,7 +147,23 @@ interface ExistingProvider<Value> {
147
147
  /**
148
148
  * The existing token to alias.
149
149
  */
150
- readonly useExisting: Token<Value>;
150
+ readonly useExisting: Token<Value> | [Token<Value>, string?];
151
+ /**
152
+ * An optional name to qualify this provider.
153
+ * If specified, the token must be resolved using the same name.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * export class ExtensionContext {
158
+ * // Decorator-based injection
159
+ * constructor(@Inject(ISecretStorage) @Named("persistent") secretStorage: SecretStorage) {}
160
+ *
161
+ * // Function-based injection
162
+ * constructor(secretStorage = inject(ISecretStorage, "persistent")) {}
163
+ * }
164
+ * ```
165
+ */
166
+ readonly name?: string;
151
167
  }
152
168
  /**
153
169
  * A token provider.
@@ -324,7 +340,8 @@ interface Container {
324
340
  *
325
341
  * If the class is not registered, but it is decorated with {@link AutoRegister},
326
342
  * or {@link ContainerOptions.autoRegister} is true, the class is registered automatically.
327
- * Otherwise, resolution fails.
343
+ * Otherwise, the resolution fails.
344
+ *
328
345
  * The scope for the automatic registration is determined by either
329
346
  * the {@link Scoped} decorator on the class, or {@link ContainerOptions.defaultScope}.
330
347
  *
@@ -372,7 +389,8 @@ interface Container {
372
389
  *
373
390
  * If the class is not registered, but it is decorated with {@link AutoRegister},
374
391
  * or {@link ContainerOptions.autoRegister} is true, the class is registered automatically.
375
- * Otherwise, resolution fails.
392
+ * Otherwise, the resolution fails.
393
+ *
376
394
  * The scope for the automatic registration is determined by either
377
395
  * the {@link Scoped} decorator on the class, or {@link ContainerOptions.defaultScope}.
378
396
  *
@@ -476,12 +494,12 @@ interface TokenRef<Value = any> {
476
494
  readonly getRefToken: () => Token<Value>;
477
495
  }
478
496
  /**
479
- * Allows referencing tokens that are declared later in the file by wrapping them
497
+ * Allows referencing tokens declared later in the file by wrapping them
480
498
  * in a lazily evaluated function.
481
499
  */
482
500
  declare function forwardRef<Value>(token: () => Tokens<Value>): TokensRef<Value>;
483
501
  /**
484
- * Allows referencing a token that is declared later in the file by wrapping it
502
+ * Allows referencing a token declared later in the file by wrapping it
485
503
  * in a lazily evaluated function.
486
504
  */
487
505
  declare function forwardRef<Value>(token: () => Token<Value>): TokenRef<Value>;
@@ -501,8 +519,8 @@ declare function Inject<Value>(token: Token<Value>): ParameterDecorator;
501
519
  /**
502
520
  * Parameter decorator that injects the value associated with the given token.
503
521
  *
504
- * Allows referencing a token that is declared later in the file by using
505
- * the {@link forwardRef} helper function.
522
+ * Allows referencing a token declared later in the file by using the
523
+ * {@link forwardRef} helper function.
506
524
  *
507
525
  * Throws an error if the token is not registered in the container.
508
526
  *
@@ -567,8 +585,8 @@ declare function InjectAll<Value>(token: Token<Value>): ParameterDecorator;
567
585
  * Parameter decorator that injects all values provided by the registrations
568
586
  * associated with the given token.
569
587
  *
570
- * Allows referencing a token that is declared later in the file by using
571
- * the {@link forwardRef} helper function.
588
+ * Allows referencing a token declared later in the file by using the
589
+ * {@link forwardRef} helper function.
572
590
  *
573
591
  * Throws an error if the token is not registered in the container.
574
592
  *
@@ -617,8 +635,8 @@ declare function Optional<Value>(token: Token<Value>): ParameterDecorator;
617
635
  * Parameter decorator that injects the value associated with the given token,
618
636
  * or `undefined` if the token is not registered in the container.
619
637
  *
620
- * Allows referencing a token that is declared later in the file by using
621
- * the {@link forwardRef} helper function.
638
+ * Allows referencing a token declared later in the file by using the
639
+ * {@link forwardRef} helper function.
622
640
  *
623
641
  * @example
624
642
  * ```ts
@@ -648,8 +666,8 @@ declare function OptionalAll<Value>(token: Token<Value>): ParameterDecorator;
648
666
  * associated with the given token, or an empty array if the token is not registered
649
667
  * in the container.
650
668
  *
651
- * Allows referencing a token that is declared later in the file by using
652
- * the {@link forwardRef} helper function.
669
+ * Allows referencing a token declared later in the file by using the
670
+ * {@link forwardRef} helper function.
653
671
  *
654
672
  * @example
655
673
  * ```ts
package/dist/es/index.mjs CHANGED
@@ -5,27 +5,38 @@ function check(condition, message) {
5
5
  }
6
6
  }
7
7
  // @internal
8
- function expectNever(value) {
9
- throw new TypeError(tag(`unexpected value ${String(value)}`));
8
+ function throwUnregisteredError(tokenInfo) {
9
+ throw new Error(tag(`unregistered token ${getFullTokenName(tokenInfo)}`));
10
10
  }
11
11
  // @internal
12
- function throwUnregisteredError(token, name) {
13
- const spec = name !== undefined ? `[name=${name}]` : "";
14
- throw new Error(tag(`unregistered token ${getTokenName(token)}${spec}`));
12
+ function throwTargetUnregisteredError(tokenInfo, aliases) {
13
+ const path = aliases.length > 0 ? ` (alias for ${getTokenPath(aliases)})` : "";
14
+ const desc = getFullTokenName(tokenInfo) + path;
15
+ const cause = `\n [cause] useExisting points to unregistered token ${getFullTokenName(aliases.at(-1))}`;
16
+ throw new Error(tag(`failed to resolve token ${desc}`) + cause);
15
17
  }
16
18
  // @internal
17
- function throwExistingUnregisteredError(token, cause) {
18
- const msg = tag(`failed to resolve token ${getTokenName(token)}`);
19
- throw isError(cause) ? new Error(`${msg}\n [cause] ${untag(cause.message)}`, {
19
+ function throwCircularAliasError(aliases) {
20
+ const path = getTokenPath(aliases);
21
+ throw new Error(tag(`circular alias detected while resolving ${path}`));
22
+ }
23
+ // @internal
24
+ function throwResolutionError(tokenInfo, aliases, cause) {
25
+ const path = aliases.length > 0 ? ` (alias for ${getTokenPath(aliases)})` : "";
26
+ const desc = getFullTokenName(tokenInfo) + path;
27
+ throw new Error(tag(`failed to resolve token ${desc}`) + getCause(cause), {
20
28
  cause
21
- }) : new Error(`${msg}\n [cause] the aliased token ${getTokenName(cause)} is not registered`);
29
+ });
22
30
  }
23
31
  // @internal
24
32
  function throwParameterResolutionError(ctor, methodKey, dependency, cause) {
25
33
  const location = getLocation(ctor, methodKey);
26
- const tokenName = getTokenName(dependency.tokenRef.getRefToken());
34
+ const tokenName = getFullTokenName([
35
+ dependency.tokenRef.getRefToken(),
36
+ dependency.name
37
+ ]);
27
38
  const msg = tag(`failed to resolve dependency for ${location}(parameter #${dependency.index}: ${tokenName})`);
28
- throw new Error(`${msg}\n [cause] ${untag(cause.message)}`, {
39
+ throw new Error(msg + getCause(cause), {
29
40
  cause
30
41
  });
31
42
  }
@@ -35,11 +46,26 @@ function getLocation(ctor, methodKey) {
35
46
  return methodKey ? `${ctorName}.${String(methodKey)}` : ctorName;
36
47
  }
37
48
  // @internal
49
+ function getTokenPath(tokens) {
50
+ return tokens.map(getFullTokenName).join(" \u2192 ");
51
+ }
52
+ // @internal
38
53
  function getTokenName(token) {
39
54
  return token.name || "<unnamed>";
40
55
  }
56
+ function getFullTokenName([token, name]) {
57
+ const tokenName = token.name || "<unnamed>";
58
+ return name ? `${tokenName}["${name}"]` : tokenName;
59
+ }
60
+ function getCause(error) {
61
+ if (!error) {
62
+ return "";
63
+ }
64
+ const msg = isError(error) ? error.message : String(error);
65
+ return `\n [cause] ${untag(msg)}`;
66
+ }
41
67
  function isError(value) {
42
- return value && value.stack && value.message && typeof value.message === "string";
68
+ return value?.stack && typeof value?.message === "string";
43
69
  }
44
70
  function tag(message) {
45
71
  return `[di-wise-neo] ${message}`;
@@ -54,8 +80,7 @@ class KeyedStack {
54
80
  return this.myKeys.has(key);
55
81
  }
56
82
  peek() {
57
- const entry = this.myEntries.at(-1);
58
- return entry?.value;
83
+ return this.myEntries.at(-1)?.value;
59
84
  }
60
85
  push(key, value) {
61
86
  check(!this.has(key), "invariant violation");
@@ -103,7 +128,7 @@ class WeakRefMap {
103
128
  // @internal
104
129
  function createResolution() {
105
130
  return {
106
- tokenStack: [],
131
+ tokens: [],
107
132
  stack: new KeyedStack(),
108
133
  values: new WeakRefMap(),
109
134
  dependents: new WeakRefMap()
@@ -385,22 +410,22 @@ class TokenRegistry {
385
410
  }
386
411
  delete(token, name) {
387
412
  const registrations = this.myMap.get(token);
388
- if (registrations) {
389
- if (name !== undefined) {
390
- const removedRegistrations = [];
391
- const newRegistrations = [];
392
- for (const registration of registrations){
393
- const array = registration.name === name ? removedRegistrations : newRegistrations;
394
- array.push(registration);
395
- }
396
- if (removedRegistrations.length > 0) {
397
- this.myMap.set(token, newRegistrations);
398
- return removedRegistrations;
399
- }
413
+ if (!registrations) {
414
+ return [];
415
+ }
416
+ if (name !== undefined) {
417
+ const removed = [];
418
+ const updated = [];
419
+ for (const registration of registrations){
420
+ (registration.name === name ? removed : updated).push(registration);
421
+ }
422
+ if (removed.length > 0) {
423
+ this.myMap.set(token, updated);
424
+ return removed;
400
425
  }
401
- this.myMap.delete(token);
402
426
  }
403
- return registrations ?? [];
427
+ this.myMap.delete(token);
428
+ return registrations;
404
429
  }
405
430
  deleteAll() {
406
431
  const tokens = Array.from(this.myMap.keys());
@@ -429,8 +454,7 @@ class TokenRegistry {
429
454
  return Array.from(values);
430
455
  }
431
456
  getAllFromParent(token, name) {
432
- const thisRegistrations = this.myMap.get(token);
433
- let registrations = thisRegistrations || this.myParent?.getAllFromParent(token, name);
457
+ let registrations = this.myMap.get(token) || this.myParent?.getAllFromParent(token, name);
434
458
  if (registrations && name !== undefined) {
435
459
  registrations = registrations.filter((r)=>r.name === name);
436
460
  check(registrations.length < 2, `internal error: more than one registration named '${name}'`);
@@ -545,8 +569,9 @@ function isDisposable(value) {
545
569
  if (args.length === 1) {
546
570
  const Class = args[0];
547
571
  const metadata = getMetadata(Class);
572
+ const name = metadata.name;
548
573
  const registration = {
549
- name: metadata.name,
574
+ name: name,
550
575
  // The provider is of type ClassProvider, initialized by getMetadata
551
576
  provider: metadata.provider,
552
577
  options: {
@@ -560,8 +585,12 @@ function isDisposable(value) {
560
585
  // These tokens will point to the original Class token and will have the same scope.
561
586
  for (const token of metadata.tokensRef.getRefTokens()){
562
587
  this.myTokenRegistry.set(token, {
588
+ name: name,
563
589
  provider: {
564
- useExisting: Class
590
+ useExisting: [
591
+ Class,
592
+ name
593
+ ]
565
594
  }
566
595
  });
567
596
  }
@@ -571,14 +600,13 @@ function isDisposable(value) {
571
600
  }
572
601
  } else {
573
602
  const [token, provider, options] = args;
574
- const existingProvider = isExistingProvider(provider);
575
- const name = existingProvider ? undefined : provider.name;
576
- check(name === undefined || name.trim(), `the name qualifier for token ${getTokenName(token)} must not be empty`);
603
+ const name = provider.name;
604
+ check(name === undefined || name.trim(), `name qualifier for token ${getTokenName(token)} must not be empty`);
577
605
  if (isClassProvider(provider)) {
578
606
  const metadata = getMetadata(provider.useClass);
579
607
  const registration = {
580
608
  // An explicit provider name overrides what is specified via @Named
581
- name: metadata.name ?? provider.name,
609
+ name: metadata.name ?? name,
582
610
  provider: metadata.provider,
583
611
  options: {
584
612
  // Explicit registration options override what is specified via class decorators (e.g., @Scoped)
@@ -593,8 +621,9 @@ function isDisposable(value) {
593
621
  this.resolveProviderValue(token, registration);
594
622
  }
595
623
  } else {
596
- if (existingProvider) {
597
- check(token !== provider.useExisting, `token ${getTokenName(token)} cannot alias itself via useExisting`);
624
+ if (isExistingProvider(provider)) {
625
+ const [targetToken] = this.getTargetToken(provider);
626
+ check(token !== targetToken, `token ${getTokenName(token)} cannot alias itself via useExisting`);
598
627
  }
599
628
  this.myTokenRegistry.set(token, {
600
629
  name: name,
@@ -631,10 +660,7 @@ function isDisposable(value) {
631
660
  if (!registration && isConstructor(token)) {
632
661
  registration = this.autoRegisterClass(token, localName);
633
662
  }
634
- if (registration) {
635
- return this.resolveRegistration(token, registration, localName);
636
- }
637
- return localOptional ? undefined : throwUnregisteredError(token, localName);
663
+ return this.resolveRegistration(token, registration, localOptional, localName);
638
664
  }
639
665
  resolveAll(token, optional) {
640
666
  this.checkDisposed();
@@ -647,11 +673,13 @@ function isDisposable(value) {
647
673
  ];
648
674
  }
649
675
  }
650
- if (registrations.length > 0) {
651
- return registrations //
652
- .map((registration)=>this.resolveRegistration(token, registration)).filter((value)=>value != null);
676
+ if (registrations.length === 0 && !optional) {
677
+ throwUnregisteredError([
678
+ token
679
+ ]);
653
680
  }
654
- return optional ? [] : throwUnregisteredError(token);
681
+ return registrations //
682
+ .map((registration)=>this.resolveRegistration(token, registration, optional)).filter((value)=>value != null);
655
683
  }
656
684
  dispose() {
657
685
  if (this.myDisposed) {
@@ -678,26 +706,53 @@ function isDisposable(value) {
678
706
  // Allow values to be GCed
679
707
  disposedRefs.clear();
680
708
  }
681
- resolveRegistration(token, registration, name) {
682
- let currRegistration = registration;
683
- while(isExistingProvider(currRegistration.provider)){
684
- const targetToken = currRegistration.provider.useExisting;
685
- currRegistration = this.myTokenRegistry.get(targetToken, name);
686
- if (!currRegistration) {
687
- throwExistingUnregisteredError(token, targetToken);
709
+ resolveRegistration(token, registration, optional, name) {
710
+ const aliases = [];
711
+ while(registration && isExistingProvider(registration.provider)){
712
+ const [targetToken, targetName] = this.getTargetToken(registration.provider);
713
+ if (aliases.some(([t])=>t === targetToken)) {
714
+ throwCircularAliasError([
715
+ [
716
+ token,
717
+ name
718
+ ],
719
+ ...aliases
720
+ ]);
688
721
  }
722
+ // eslint-disable-next-line no-param-reassign
723
+ registration = this.myTokenRegistry.get(targetToken, targetName);
724
+ aliases.push([
725
+ targetToken,
726
+ targetName
727
+ ]);
728
+ if (!registration && !optional) {
729
+ throwTargetUnregisteredError([
730
+ token,
731
+ name
732
+ ], aliases);
733
+ }
734
+ }
735
+ if (!registration) {
736
+ return optional ? undefined : throwUnregisteredError([
737
+ token,
738
+ name
739
+ ]);
689
740
  }
690
741
  try {
691
- return this.resolveProviderValue(token, currRegistration);
742
+ return this.resolveProviderValue(token, registration);
692
743
  } catch (e) {
693
- // If we were trying to resolve a token registered via ExistingProvider,
694
- // we must add the cause of the error to the message
695
- if (isExistingProvider(registration.provider)) {
696
- throwExistingUnregisteredError(token, e);
697
- }
698
- throw e;
744
+ throwResolutionError([
745
+ token,
746
+ name
747
+ ], aliases, e);
699
748
  }
700
749
  }
750
+ getTargetToken(provider) {
751
+ const token = provider.useExisting;
752
+ return Array.isArray(token) ? token : [
753
+ token
754
+ ];
755
+ }
701
756
  autoRegisterClass(Class, name) {
702
757
  const metadata = getMetadata(Class);
703
758
  const autoRegister = metadata.autoRegister ?? this.myOptions.autoRegister;
@@ -728,8 +783,7 @@ function isDisposable(value) {
728
783
  if (isValueProvider(provider)) {
729
784
  return provider.useValue;
730
785
  }
731
- check(!isExistingProvider(provider), "internal error: unexpected ExistingProvider");
732
- expectNever(provider);
786
+ check(false, "internal error: unexpected ExistingProvider");
733
787
  }
734
788
  resolveScopedValue(token, registration, factory) {
735
789
  let context = useInjectionContext();
@@ -744,7 +798,9 @@ function isDisposable(value) {
744
798
  if (resolution.stack.has(provider)) {
745
799
  const dependentRef = resolution.dependents.get(provider);
746
800
  check(dependentRef, ()=>{
747
- const path = resolution.tokenStack.map(getTokenName).concat(getTokenName(token)).join(" → ");
801
+ const path = getTokenPath(resolution.tokens.concat(token).map((t)=>[
802
+ t
803
+ ]));
748
804
  return `circular dependency detected while resolving ${path}`;
749
805
  });
750
806
  return dependentRef.current;
@@ -752,7 +808,7 @@ function isDisposable(value) {
752
808
  const scope = registration.options?.scope ?? this.myOptions.defaultScope;
753
809
  const cleanups = [
754
810
  provideInjectionContext(context),
755
- resolution.tokenStack.push(token) && (()=>resolution.tokenStack.pop()),
811
+ resolution.tokens.push(token) && (()=>resolution.tokens.pop()),
756
812
  !isBuilder(provider) && resolution.stack.push(provider, {
757
813
  provider,
758
814
  scope
@@ -808,7 +864,7 @@ function isDisposable(value) {
808
864
  check(ctor.length === ctorDeps.length, ()=>{
809
865
  const location = getLocation(ctor);
810
866
  const msg = `${location} expected ${ctor.length} decorated constructor parameters`;
811
- return msg + `, but found ${ctorDeps.length}`;
867
+ return `${msg}, but found ${ctorDeps.length}`;
812
868
  });
813
869
  return this.resolveArgs(ctorDeps, ctor);
814
870
  }
@@ -830,7 +886,7 @@ function isDisposable(value) {
830
886
  check(methodDeps.length === method.length, ()=>{
831
887
  const location = getLocation(ctor, methodKey);
832
888
  const msg = `${location} expected ${method.length} decorated method parameters`;
833
- return msg + `, but found ${methodDeps.length}`;
889
+ return `${msg}, but found ${methodDeps.length}`;
834
890
  });
835
891
  const args = this.resolveArgs(methodDeps, ctor, instance, methodKey);
836
892
  method.bind(instance)(...args);
@@ -865,7 +921,7 @@ function isDisposable(value) {
865
921
  }
866
922
  }
867
923
  checkDisposed() {
868
- check(!this.myDisposed, "the container is disposed");
924
+ check(!this.myDisposed, "container is disposed");
869
925
  }
870
926
  }
871
927
 
@@ -938,8 +994,7 @@ function isDisposable(value) {
938
994
  function forwardRef(token) {
939
995
  return {
940
996
  getRefTokens: ()=>{
941
- // Normalize the single token here, so that we don't have
942
- // to do it at every getRefTokens call site
997
+ // Normalize the single token here so that we don't have to do it at every getRefTokens call site
943
998
  const tokenOrTokens = token();
944
999
  const tokensArray = Array.isArray(tokenOrTokens) ? tokenOrTokens : [
945
1000
  tokenOrTokens
@@ -1070,7 +1125,7 @@ function InjectAll(token) {
1070
1125
  *
1071
1126
  * @__NO_SIDE_EFFECTS__
1072
1127
  */ function Named(name) {
1073
- check(name.trim(), "the @Named qualifier must not be empty");
1128
+ check(name.trim(), "@Named qualifier must not be empty");
1074
1129
  return function(target, propertyKey, parameterIndex) {
1075
1130
  if (parameterIndex === undefined) {
1076
1131
  // The decorator has been applied to the class