@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/cjs/index.js CHANGED
@@ -7,27 +7,38 @@ function check(condition, message) {
7
7
  }
8
8
  }
9
9
  // @internal
10
- function expectNever(value) {
11
- throw new TypeError(tag(`unexpected value ${String(value)}`));
10
+ function throwUnregisteredError(tokenInfo) {
11
+ throw new Error(tag(`unregistered token ${getFullTokenName(tokenInfo)}`));
12
12
  }
13
13
  // @internal
14
- function throwUnregisteredError(token, name) {
15
- const spec = name !== undefined ? `[name=${name}]` : "";
16
- throw new Error(tag(`unregistered token ${getTokenName(token)}${spec}`));
14
+ function throwTargetUnregisteredError(tokenInfo, aliases) {
15
+ const path = aliases.length > 0 ? ` (alias for ${getTokenPath(aliases)})` : "";
16
+ const desc = getFullTokenName(tokenInfo) + path;
17
+ const cause = `\n [cause] useExisting points to unregistered token ${getFullTokenName(aliases.at(-1))}`;
18
+ throw new Error(tag(`failed to resolve token ${desc}`) + cause);
17
19
  }
18
20
  // @internal
19
- function throwExistingUnregisteredError(token, cause) {
20
- const msg = tag(`failed to resolve token ${getTokenName(token)}`);
21
- throw isError(cause) ? new Error(`${msg}\n [cause] ${untag(cause.message)}`, {
21
+ function throwCircularAliasError(aliases) {
22
+ const path = getTokenPath(aliases);
23
+ throw new Error(tag(`circular alias detected while resolving ${path}`));
24
+ }
25
+ // @internal
26
+ function throwResolutionError(tokenInfo, aliases, cause) {
27
+ const path = aliases.length > 0 ? ` (alias for ${getTokenPath(aliases)})` : "";
28
+ const desc = getFullTokenName(tokenInfo) + path;
29
+ throw new Error(tag(`failed to resolve token ${desc}`) + getCause(cause), {
22
30
  cause
23
- }) : new Error(`${msg}\n [cause] the aliased token ${getTokenName(cause)} is not registered`);
31
+ });
24
32
  }
25
33
  // @internal
26
34
  function throwParameterResolutionError(ctor, methodKey, dependency, cause) {
27
35
  const location = getLocation(ctor, methodKey);
28
- const tokenName = getTokenName(dependency.tokenRef.getRefToken());
36
+ const tokenName = getFullTokenName([
37
+ dependency.tokenRef.getRefToken(),
38
+ dependency.name
39
+ ]);
29
40
  const msg = tag(`failed to resolve dependency for ${location}(parameter #${dependency.index}: ${tokenName})`);
30
- throw new Error(`${msg}\n [cause] ${untag(cause.message)}`, {
41
+ throw new Error(msg + getCause(cause), {
31
42
  cause
32
43
  });
33
44
  }
@@ -37,11 +48,27 @@ function getLocation(ctor, methodKey) {
37
48
  return methodKey ? `${ctorName}.${String(methodKey)}` : ctorName;
38
49
  }
39
50
  // @internal
51
+ function getTokenPath(tokens) {
52
+ return tokens.map(getFullTokenName).join(" \u2192 ");
53
+ }
54
+ // @internal
40
55
  function getTokenName(token) {
41
56
  return token.name || "<unnamed>";
42
57
  }
58
+ function getFullTokenName(tokenInfo) {
59
+ const [token, name] = tokenInfo;
60
+ const tokenName = token.name || "<unnamed>";
61
+ return name ? `${tokenName}["${name}"]` : tokenName;
62
+ }
63
+ function getCause(error) {
64
+ if (!error) {
65
+ return "";
66
+ }
67
+ const msg = isError(error) ? error.message : String(error);
68
+ return `\n [cause] ${untag(msg)}`;
69
+ }
43
70
  function isError(value) {
44
- return value && value.stack && value.message && typeof value.message === "string";
71
+ return value?.stack && typeof value?.message === "string";
45
72
  }
46
73
  function tag(message) {
47
74
  return `[di-wise-neo] ${message}`;
@@ -303,9 +330,8 @@ const Scope = {
303
330
  Container: "Container"
304
331
  };
305
332
 
333
+ const typeSymbol = Symbol("di-wise-neo.typeToken");
306
334
  /**
307
- * Type API.
308
- */ /**
309
335
  * Creates a type token.
310
336
  *
311
337
  * @example
@@ -319,6 +345,7 @@ const Scope = {
319
345
  name: `Type<${typeName}>`,
320
346
  inter: createType,
321
347
  union: createType,
348
+ __type: typeSymbol,
322
349
  toString () {
323
350
  return type.name;
324
351
  }
@@ -326,6 +353,10 @@ const Scope = {
326
353
  return type;
327
354
  }
328
355
  // @internal
356
+ function isType(token) {
357
+ return token.__type === typeSymbol;
358
+ }
359
+ // @internal
329
360
  function isConstructor(token) {
330
361
  return typeof token === "function";
331
362
  }
@@ -431,8 +462,7 @@ class TokenRegistry {
431
462
  return Array.from(values);
432
463
  }
433
464
  getAllFromParent(token, name) {
434
- const thisRegistrations = this.myMap.get(token);
435
- let registrations = thisRegistrations || this.myParent?.getAllFromParent(token, name);
465
+ let registrations = this.myMap.get(token) || this.myParent?.getAllFromParent(token, name);
436
466
  if (registrations && name !== undefined) {
437
467
  registrations = registrations.filter((r)=>r.name === name);
438
468
  check(registrations.length < 2, `internal error: more than one registration named '${name}'`);
@@ -547,8 +577,9 @@ function isDisposable(value) {
547
577
  if (args.length === 1) {
548
578
  const Class = args[0];
549
579
  const metadata = getMetadata(Class);
580
+ const name = metadata.name;
550
581
  const registration = {
551
- name: metadata.name,
582
+ name: name,
552
583
  // The provider is of type ClassProvider, initialized by getMetadata
553
584
  provider: metadata.provider,
554
585
  options: {
@@ -562,8 +593,12 @@ function isDisposable(value) {
562
593
  // These tokens will point to the original Class token and will have the same scope.
563
594
  for (const token of metadata.tokensRef.getRefTokens()){
564
595
  this.myTokenRegistry.set(token, {
596
+ name: name,
565
597
  provider: {
566
- useExisting: Class
598
+ useExisting: {
599
+ token: Class,
600
+ name: name
601
+ }
567
602
  }
568
603
  });
569
604
  }
@@ -573,9 +608,8 @@ function isDisposable(value) {
573
608
  }
574
609
  } else {
575
610
  const [token, provider, options] = args;
576
- const existingProvider = isExistingProvider(provider);
577
- const name = existingProvider ? undefined : provider.name;
578
- check(name === undefined || name.trim(), `the name qualifier for token ${getTokenName(token)} must not be empty`);
611
+ const name = provider.name;
612
+ check(name === undefined || name.trim(), `name qualifier for token ${getTokenName(token)} must not be empty`);
579
613
  if (isClassProvider(provider)) {
580
614
  const metadata = getMetadata(provider.useClass);
581
615
  const registration = {
@@ -595,8 +629,9 @@ function isDisposable(value) {
595
629
  this.resolveProviderValue(token, registration);
596
630
  }
597
631
  } else {
598
- if (existingProvider) {
599
- check(token !== provider.useExisting, `token ${getTokenName(token)} cannot alias itself via useExisting`);
632
+ if (isExistingProvider(provider)) {
633
+ const [targetToken] = this.getTargetToken(provider);
634
+ check(token !== targetToken, `token ${getTokenName(token)} cannot alias itself via useExisting`);
600
635
  }
601
636
  this.myTokenRegistry.set(token, {
602
637
  name: name,
@@ -633,10 +668,7 @@ function isDisposable(value) {
633
668
  if (!registration && isConstructor(token)) {
634
669
  registration = this.autoRegisterClass(token, localName);
635
670
  }
636
- if (registration) {
637
- return this.resolveRegistration(token, registration, localName);
638
- }
639
- return localOptional ? undefined : throwUnregisteredError(token, localName);
671
+ return this.resolveRegistration(token, registration, localOptional, localName);
640
672
  }
641
673
  resolveAll(token, optional) {
642
674
  this.checkDisposed();
@@ -649,11 +681,13 @@ function isDisposable(value) {
649
681
  ];
650
682
  }
651
683
  }
652
- if (registrations.length > 0) {
653
- return registrations //
654
- .map((registration)=>this.resolveRegistration(token, registration)).filter((value)=>value != null);
684
+ if (registrations.length === 0 && !optional) {
685
+ throwUnregisteredError([
686
+ token
687
+ ]);
655
688
  }
656
- return optional ? [] : throwUnregisteredError(token);
689
+ return registrations //
690
+ .map((registration)=>this.resolveRegistration(token, registration, optional)).filter((value)=>value != null);
657
691
  }
658
692
  dispose() {
659
693
  if (this.myDisposed) {
@@ -680,26 +714,57 @@ function isDisposable(value) {
680
714
  // Allow values to be GCed
681
715
  disposedRefs.clear();
682
716
  }
683
- resolveRegistration(token, registration, name) {
684
- let currRegistration = registration;
685
- while(isExistingProvider(currRegistration.provider)){
686
- const targetToken = currRegistration.provider.useExisting;
687
- currRegistration = this.myTokenRegistry.get(targetToken, name);
688
- if (!currRegistration) {
689
- throwExistingUnregisteredError(token, targetToken);
717
+ resolveRegistration(token, registration, optional, name) {
718
+ const aliases = [];
719
+ while(registration && isExistingProvider(registration.provider)){
720
+ const [targetToken, targetName] = this.getTargetToken(registration.provider);
721
+ if (aliases.some(([t])=>t === targetToken)) {
722
+ throwCircularAliasError([
723
+ [
724
+ token,
725
+ name
726
+ ],
727
+ ...aliases
728
+ ]);
729
+ }
730
+ // eslint-disable-next-line no-param-reassign
731
+ registration = this.myTokenRegistry.get(targetToken, targetName);
732
+ aliases.push([
733
+ targetToken,
734
+ targetName
735
+ ]);
736
+ if (!registration && !optional) {
737
+ throwTargetUnregisteredError([
738
+ token,
739
+ name
740
+ ], aliases);
690
741
  }
691
742
  }
743
+ if (!registration) {
744
+ return optional ? undefined : throwUnregisteredError([
745
+ token,
746
+ name
747
+ ]);
748
+ }
692
749
  try {
693
- return this.resolveProviderValue(token, currRegistration);
750
+ return this.resolveProviderValue(token, registration);
694
751
  } catch (e) {
695
- // If we were trying to resolve a token registered via ExistingProvider,
696
- // we must add the cause of the error to the message
697
- if (isExistingProvider(registration.provider)) {
698
- throwExistingUnregisteredError(token, e);
699
- }
700
- throw e;
752
+ throwResolutionError([
753
+ token,
754
+ name
755
+ ], aliases, e);
701
756
  }
702
757
  }
758
+ getTargetToken(provider) {
759
+ const token = provider.useExisting;
760
+ return isType(token) || isConstructor(token) //
761
+ ? [
762
+ token
763
+ ] : [
764
+ token.token,
765
+ token.name
766
+ ];
767
+ }
703
768
  autoRegisterClass(Class, name) {
704
769
  const metadata = getMetadata(Class);
705
770
  const autoRegister = metadata.autoRegister ?? this.myOptions.autoRegister;
@@ -730,8 +795,7 @@ function isDisposable(value) {
730
795
  if (isValueProvider(provider)) {
731
796
  return provider.useValue;
732
797
  }
733
- check(!isExistingProvider(provider), "internal error: unexpected ExistingProvider");
734
- expectNever(provider);
798
+ check(false, "internal error: unexpected ExistingProvider");
735
799
  }
736
800
  resolveScopedValue(token, registration, factory) {
737
801
  let context = useInjectionContext();
@@ -746,7 +810,9 @@ function isDisposable(value) {
746
810
  if (resolution.stack.has(provider)) {
747
811
  const dependentRef = resolution.dependents.get(provider);
748
812
  check(dependentRef, ()=>{
749
- const path = resolution.tokenStack.map(getTokenName).concat(getTokenName(token)).join(" → ");
813
+ const path = getTokenPath(resolution.tokenStack.concat(token).map((t)=>[
814
+ t
815
+ ]));
750
816
  return `circular dependency detected while resolving ${path}`;
751
817
  });
752
818
  return dependentRef.current;
@@ -810,7 +876,7 @@ function isDisposable(value) {
810
876
  check(ctor.length === ctorDeps.length, ()=>{
811
877
  const location = getLocation(ctor);
812
878
  const msg = `${location} expected ${ctor.length} decorated constructor parameters`;
813
- return msg + `, but found ${ctorDeps.length}`;
879
+ return `${msg}, but found ${ctorDeps.length}`;
814
880
  });
815
881
  return this.resolveArgs(ctorDeps, ctor);
816
882
  }
@@ -832,7 +898,7 @@ function isDisposable(value) {
832
898
  check(methodDeps.length === method.length, ()=>{
833
899
  const location = getLocation(ctor, methodKey);
834
900
  const msg = `${location} expected ${method.length} decorated method parameters`;
835
- return msg + `, but found ${methodDeps.length}`;
901
+ return `${msg}, but found ${methodDeps.length}`;
836
902
  });
837
903
  const args = this.resolveArgs(methodDeps, ctor, instance, methodKey);
838
904
  method.bind(instance)(...args);
@@ -867,7 +933,7 @@ function isDisposable(value) {
867
933
  }
868
934
  }
869
935
  checkDisposed() {
870
- check(!this.myDisposed, "the container is disposed");
936
+ check(!this.myDisposed, "container is disposed");
871
937
  }
872
938
  }
873
939
 
@@ -1072,7 +1138,7 @@ function InjectAll(token) {
1072
1138
  *
1073
1139
  * @__NO_SIDE_EFFECTS__
1074
1140
  */ function Named(name) {
1075
- check(name.trim(), "the @Named qualifier must not be empty");
1141
+ check(name.trim(), "@Named qualifier must not be empty");
1076
1142
  return function(target, propertyKey, parameterIndex) {
1077
1143
  if (parameterIndex === undefined) {
1078
1144
  // The decorator has been applied to the class