@lppedd/di-wise-neo 0.10.0 → 0.11.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.
@@ -147,7 +147,26 @@ interface ExistingProvider<Value> {
147
147
  /**
148
148
  * The existing token to alias.
149
149
  */
150
- readonly useExisting: Token<Value>;
150
+ readonly useExisting: Token<Value> | {
151
+ readonly token: Token<Value>;
152
+ readonly name?: string;
153
+ };
154
+ /**
155
+ * An optional name to qualify this provider.
156
+ * If specified, the token must be resolved using the same name.
157
+ *
158
+ * @example
159
+ * ```ts
160
+ * export class ExtensionContext {
161
+ * // Decorator-based injection
162
+ * constructor(@Inject(ISecretStorage) @Named("persistent") secretStorage: SecretStorage) {}
163
+ *
164
+ * // Function-based injection
165
+ * constructor(secretStorage = inject(ISecretStorage, "persistent")) {}
166
+ * }
167
+ * ```
168
+ */
169
+ readonly name?: string;
151
170
  }
152
171
  /**
153
172
  * A token provider.
@@ -324,7 +343,8 @@ interface Container {
324
343
  *
325
344
  * If the class is not registered, but it is decorated with {@link AutoRegister},
326
345
  * or {@link ContainerOptions.autoRegister} is true, the class is registered automatically.
327
- * Otherwise, resolution fails.
346
+ * Otherwise, the resolution fails.
347
+ *
328
348
  * The scope for the automatic registration is determined by either
329
349
  * the {@link Scoped} decorator on the class, or {@link ContainerOptions.defaultScope}.
330
350
  *
@@ -372,7 +392,8 @@ interface Container {
372
392
  *
373
393
  * If the class is not registered, but it is decorated with {@link AutoRegister},
374
394
  * or {@link ContainerOptions.autoRegister} is true, the class is registered automatically.
375
- * Otherwise, resolution fails.
395
+ * Otherwise, the resolution fails.
396
+ *
376
397
  * The scope for the automatic registration is determined by either
377
398
  * the {@link Scoped} decorator on the class, or {@link ContainerOptions.defaultScope}.
378
399
  *
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,27 @@ 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(tokenInfo) {
57
+ const [token, name] = tokenInfo;
58
+ const tokenName = token.name || "<unnamed>";
59
+ return name ? `${tokenName}["${name}"]` : tokenName;
60
+ }
61
+ function getCause(error) {
62
+ if (!error) {
63
+ return "";
64
+ }
65
+ const msg = isError(error) ? error.message : String(error);
66
+ return `\n [cause] ${untag(msg)}`;
67
+ }
41
68
  function isError(value) {
42
- return value && value.stack && value.message && typeof value.message === "string";
69
+ return value?.stack && typeof value?.message === "string";
43
70
  }
44
71
  function tag(message) {
45
72
  return `[di-wise-neo] ${message}`;
@@ -301,9 +328,8 @@ const Scope = {
301
328
  Container: "Container"
302
329
  };
303
330
 
331
+ const typeSymbol = Symbol("di-wise-neo.typeToken");
304
332
  /**
305
- * Type API.
306
- */ /**
307
333
  * Creates a type token.
308
334
  *
309
335
  * @example
@@ -317,6 +343,7 @@ const Scope = {
317
343
  name: `Type<${typeName}>`,
318
344
  inter: createType,
319
345
  union: createType,
346
+ __type: typeSymbol,
320
347
  toString () {
321
348
  return type.name;
322
349
  }
@@ -324,6 +351,10 @@ const Scope = {
324
351
  return type;
325
352
  }
326
353
  // @internal
354
+ function isType(token) {
355
+ return token.__type === typeSymbol;
356
+ }
357
+ // @internal
327
358
  function isConstructor(token) {
328
359
  return typeof token === "function";
329
360
  }
@@ -429,8 +460,7 @@ class TokenRegistry {
429
460
  return Array.from(values);
430
461
  }
431
462
  getAllFromParent(token, name) {
432
- const thisRegistrations = this.myMap.get(token);
433
- let registrations = thisRegistrations || this.myParent?.getAllFromParent(token, name);
463
+ let registrations = this.myMap.get(token) || this.myParent?.getAllFromParent(token, name);
434
464
  if (registrations && name !== undefined) {
435
465
  registrations = registrations.filter((r)=>r.name === name);
436
466
  check(registrations.length < 2, `internal error: more than one registration named '${name}'`);
@@ -545,8 +575,9 @@ function isDisposable(value) {
545
575
  if (args.length === 1) {
546
576
  const Class = args[0];
547
577
  const metadata = getMetadata(Class);
578
+ const name = metadata.name;
548
579
  const registration = {
549
- name: metadata.name,
580
+ name: name,
550
581
  // The provider is of type ClassProvider, initialized by getMetadata
551
582
  provider: metadata.provider,
552
583
  options: {
@@ -560,8 +591,12 @@ function isDisposable(value) {
560
591
  // These tokens will point to the original Class token and will have the same scope.
561
592
  for (const token of metadata.tokensRef.getRefTokens()){
562
593
  this.myTokenRegistry.set(token, {
594
+ name: name,
563
595
  provider: {
564
- useExisting: Class
596
+ useExisting: {
597
+ token: Class,
598
+ name: name
599
+ }
565
600
  }
566
601
  });
567
602
  }
@@ -571,9 +606,8 @@ function isDisposable(value) {
571
606
  }
572
607
  } else {
573
608
  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`);
609
+ const name = provider.name;
610
+ check(name === undefined || name.trim(), `name qualifier for token ${getTokenName(token)} must not be empty`);
577
611
  if (isClassProvider(provider)) {
578
612
  const metadata = getMetadata(provider.useClass);
579
613
  const registration = {
@@ -593,8 +627,9 @@ function isDisposable(value) {
593
627
  this.resolveProviderValue(token, registration);
594
628
  }
595
629
  } else {
596
- if (existingProvider) {
597
- check(token !== provider.useExisting, `token ${getTokenName(token)} cannot alias itself via useExisting`);
630
+ if (isExistingProvider(provider)) {
631
+ const [targetToken] = this.getTargetToken(provider);
632
+ check(token !== targetToken, `token ${getTokenName(token)} cannot alias itself via useExisting`);
598
633
  }
599
634
  this.myTokenRegistry.set(token, {
600
635
  name: name,
@@ -631,10 +666,7 @@ function isDisposable(value) {
631
666
  if (!registration && isConstructor(token)) {
632
667
  registration = this.autoRegisterClass(token, localName);
633
668
  }
634
- if (registration) {
635
- return this.resolveRegistration(token, registration, localName);
636
- }
637
- return localOptional ? undefined : throwUnregisteredError(token, localName);
669
+ return this.resolveRegistration(token, registration, localOptional, localName);
638
670
  }
639
671
  resolveAll(token, optional) {
640
672
  this.checkDisposed();
@@ -647,11 +679,13 @@ function isDisposable(value) {
647
679
  ];
648
680
  }
649
681
  }
650
- if (registrations.length > 0) {
651
- return registrations //
652
- .map((registration)=>this.resolveRegistration(token, registration)).filter((value)=>value != null);
682
+ if (registrations.length === 0 && !optional) {
683
+ throwUnregisteredError([
684
+ token
685
+ ]);
653
686
  }
654
- return optional ? [] : throwUnregisteredError(token);
687
+ return registrations //
688
+ .map((registration)=>this.resolveRegistration(token, registration, optional)).filter((value)=>value != null);
655
689
  }
656
690
  dispose() {
657
691
  if (this.myDisposed) {
@@ -678,26 +712,57 @@ function isDisposable(value) {
678
712
  // Allow values to be GCed
679
713
  disposedRefs.clear();
680
714
  }
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);
715
+ resolveRegistration(token, registration, optional, name) {
716
+ const aliases = [];
717
+ while(registration && isExistingProvider(registration.provider)){
718
+ const [targetToken, targetName] = this.getTargetToken(registration.provider);
719
+ if (aliases.some(([t])=>t === targetToken)) {
720
+ throwCircularAliasError([
721
+ [
722
+ token,
723
+ name
724
+ ],
725
+ ...aliases
726
+ ]);
727
+ }
728
+ // eslint-disable-next-line no-param-reassign
729
+ registration = this.myTokenRegistry.get(targetToken, targetName);
730
+ aliases.push([
731
+ targetToken,
732
+ targetName
733
+ ]);
734
+ if (!registration && !optional) {
735
+ throwTargetUnregisteredError([
736
+ token,
737
+ name
738
+ ], aliases);
688
739
  }
689
740
  }
741
+ if (!registration) {
742
+ return optional ? undefined : throwUnregisteredError([
743
+ token,
744
+ name
745
+ ]);
746
+ }
690
747
  try {
691
- return this.resolveProviderValue(token, currRegistration);
748
+ return this.resolveProviderValue(token, registration);
692
749
  } 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;
750
+ throwResolutionError([
751
+ token,
752
+ name
753
+ ], aliases, e);
699
754
  }
700
755
  }
756
+ getTargetToken(provider) {
757
+ const token = provider.useExisting;
758
+ return isType(token) || isConstructor(token) //
759
+ ? [
760
+ token
761
+ ] : [
762
+ token.token,
763
+ token.name
764
+ ];
765
+ }
701
766
  autoRegisterClass(Class, name) {
702
767
  const metadata = getMetadata(Class);
703
768
  const autoRegister = metadata.autoRegister ?? this.myOptions.autoRegister;
@@ -728,8 +793,7 @@ function isDisposable(value) {
728
793
  if (isValueProvider(provider)) {
729
794
  return provider.useValue;
730
795
  }
731
- check(!isExistingProvider(provider), "internal error: unexpected ExistingProvider");
732
- expectNever(provider);
796
+ check(false, "internal error: unexpected ExistingProvider");
733
797
  }
734
798
  resolveScopedValue(token, registration, factory) {
735
799
  let context = useInjectionContext();
@@ -744,7 +808,9 @@ function isDisposable(value) {
744
808
  if (resolution.stack.has(provider)) {
745
809
  const dependentRef = resolution.dependents.get(provider);
746
810
  check(dependentRef, ()=>{
747
- const path = resolution.tokenStack.map(getTokenName).concat(getTokenName(token)).join(" → ");
811
+ const path = getTokenPath(resolution.tokenStack.concat(token).map((t)=>[
812
+ t
813
+ ]));
748
814
  return `circular dependency detected while resolving ${path}`;
749
815
  });
750
816
  return dependentRef.current;
@@ -808,7 +874,7 @@ function isDisposable(value) {
808
874
  check(ctor.length === ctorDeps.length, ()=>{
809
875
  const location = getLocation(ctor);
810
876
  const msg = `${location} expected ${ctor.length} decorated constructor parameters`;
811
- return msg + `, but found ${ctorDeps.length}`;
877
+ return `${msg}, but found ${ctorDeps.length}`;
812
878
  });
813
879
  return this.resolveArgs(ctorDeps, ctor);
814
880
  }
@@ -830,7 +896,7 @@ function isDisposable(value) {
830
896
  check(methodDeps.length === method.length, ()=>{
831
897
  const location = getLocation(ctor, methodKey);
832
898
  const msg = `${location} expected ${method.length} decorated method parameters`;
833
- return msg + `, but found ${methodDeps.length}`;
899
+ return `${msg}, but found ${methodDeps.length}`;
834
900
  });
835
901
  const args = this.resolveArgs(methodDeps, ctor, instance, methodKey);
836
902
  method.bind(instance)(...args);
@@ -865,7 +931,7 @@ function isDisposable(value) {
865
931
  }
866
932
  }
867
933
  checkDisposed() {
868
- check(!this.myDisposed, "the container is disposed");
934
+ check(!this.myDisposed, "container is disposed");
869
935
  }
870
936
  }
871
937
 
@@ -1070,7 +1136,7 @@ function InjectAll(token) {
1070
1136
  *
1071
1137
  * @__NO_SIDE_EFFECTS__
1072
1138
  */ function Named(name) {
1073
- check(name.trim(), "the @Named qualifier must not be empty");
1139
+ check(name.trim(), "@Named qualifier must not be empty");
1074
1140
  return function(target, propertyKey, parameterIndex) {
1075
1141
  if (parameterIndex === undefined) {
1076
1142
  // The decorator has been applied to the class