@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/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,26 @@ 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([token, name]) {
59
+ const tokenName = token.name || "<unnamed>";
60
+ return name ? `${tokenName}["${name}"]` : tokenName;
61
+ }
62
+ function getCause(error) {
63
+ if (!error) {
64
+ return "";
65
+ }
66
+ const msg = isError(error) ? error.message : String(error);
67
+ return `\n [cause] ${untag(msg)}`;
68
+ }
43
69
  function isError(value) {
44
- return value && value.stack && value.message && typeof value.message === "string";
70
+ return value?.stack && typeof value?.message === "string";
45
71
  }
46
72
  function tag(message) {
47
73
  return `[di-wise-neo] ${message}`;
@@ -56,8 +82,7 @@ class KeyedStack {
56
82
  return this.myKeys.has(key);
57
83
  }
58
84
  peek() {
59
- const entry = this.myEntries.at(-1);
60
- return entry?.value;
85
+ return this.myEntries.at(-1)?.value;
61
86
  }
62
87
  push(key, value) {
63
88
  check(!this.has(key), "invariant violation");
@@ -105,7 +130,7 @@ class WeakRefMap {
105
130
  // @internal
106
131
  function createResolution() {
107
132
  return {
108
- tokenStack: [],
133
+ tokens: [],
109
134
  stack: new KeyedStack(),
110
135
  values: new WeakRefMap(),
111
136
  dependents: new WeakRefMap()
@@ -387,22 +412,22 @@ class TokenRegistry {
387
412
  }
388
413
  delete(token, name) {
389
414
  const registrations = this.myMap.get(token);
390
- if (registrations) {
391
- if (name !== undefined) {
392
- const removedRegistrations = [];
393
- const newRegistrations = [];
394
- for (const registration of registrations){
395
- const array = registration.name === name ? removedRegistrations : newRegistrations;
396
- array.push(registration);
397
- }
398
- if (removedRegistrations.length > 0) {
399
- this.myMap.set(token, newRegistrations);
400
- return removedRegistrations;
401
- }
415
+ if (!registrations) {
416
+ return [];
417
+ }
418
+ if (name !== undefined) {
419
+ const removed = [];
420
+ const updated = [];
421
+ for (const registration of registrations){
422
+ (registration.name === name ? removed : updated).push(registration);
423
+ }
424
+ if (removed.length > 0) {
425
+ this.myMap.set(token, updated);
426
+ return removed;
402
427
  }
403
- this.myMap.delete(token);
404
428
  }
405
- return registrations ?? [];
429
+ this.myMap.delete(token);
430
+ return registrations;
406
431
  }
407
432
  deleteAll() {
408
433
  const tokens = Array.from(this.myMap.keys());
@@ -431,8 +456,7 @@ class TokenRegistry {
431
456
  return Array.from(values);
432
457
  }
433
458
  getAllFromParent(token, name) {
434
- const thisRegistrations = this.myMap.get(token);
435
- let registrations = thisRegistrations || this.myParent?.getAllFromParent(token, name);
459
+ let registrations = this.myMap.get(token) || this.myParent?.getAllFromParent(token, name);
436
460
  if (registrations && name !== undefined) {
437
461
  registrations = registrations.filter((r)=>r.name === name);
438
462
  check(registrations.length < 2, `internal error: more than one registration named '${name}'`);
@@ -547,8 +571,9 @@ function isDisposable(value) {
547
571
  if (args.length === 1) {
548
572
  const Class = args[0];
549
573
  const metadata = getMetadata(Class);
574
+ const name = metadata.name;
550
575
  const registration = {
551
- name: metadata.name,
576
+ name: name,
552
577
  // The provider is of type ClassProvider, initialized by getMetadata
553
578
  provider: metadata.provider,
554
579
  options: {
@@ -562,8 +587,12 @@ function isDisposable(value) {
562
587
  // These tokens will point to the original Class token and will have the same scope.
563
588
  for (const token of metadata.tokensRef.getRefTokens()){
564
589
  this.myTokenRegistry.set(token, {
590
+ name: name,
565
591
  provider: {
566
- useExisting: Class
592
+ useExisting: [
593
+ Class,
594
+ name
595
+ ]
567
596
  }
568
597
  });
569
598
  }
@@ -573,14 +602,13 @@ function isDisposable(value) {
573
602
  }
574
603
  } else {
575
604
  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`);
605
+ const name = provider.name;
606
+ check(name === undefined || name.trim(), `name qualifier for token ${getTokenName(token)} must not be empty`);
579
607
  if (isClassProvider(provider)) {
580
608
  const metadata = getMetadata(provider.useClass);
581
609
  const registration = {
582
610
  // An explicit provider name overrides what is specified via @Named
583
- name: metadata.name ?? provider.name,
611
+ name: metadata.name ?? name,
584
612
  provider: metadata.provider,
585
613
  options: {
586
614
  // Explicit registration options override what is specified via class decorators (e.g., @Scoped)
@@ -595,8 +623,9 @@ function isDisposable(value) {
595
623
  this.resolveProviderValue(token, registration);
596
624
  }
597
625
  } else {
598
- if (existingProvider) {
599
- check(token !== provider.useExisting, `token ${getTokenName(token)} cannot alias itself via useExisting`);
626
+ if (isExistingProvider(provider)) {
627
+ const [targetToken] = this.getTargetToken(provider);
628
+ check(token !== targetToken, `token ${getTokenName(token)} cannot alias itself via useExisting`);
600
629
  }
601
630
  this.myTokenRegistry.set(token, {
602
631
  name: name,
@@ -633,10 +662,7 @@ function isDisposable(value) {
633
662
  if (!registration && isConstructor(token)) {
634
663
  registration = this.autoRegisterClass(token, localName);
635
664
  }
636
- if (registration) {
637
- return this.resolveRegistration(token, registration, localName);
638
- }
639
- return localOptional ? undefined : throwUnregisteredError(token, localName);
665
+ return this.resolveRegistration(token, registration, localOptional, localName);
640
666
  }
641
667
  resolveAll(token, optional) {
642
668
  this.checkDisposed();
@@ -649,11 +675,13 @@ function isDisposable(value) {
649
675
  ];
650
676
  }
651
677
  }
652
- if (registrations.length > 0) {
653
- return registrations //
654
- .map((registration)=>this.resolveRegistration(token, registration)).filter((value)=>value != null);
678
+ if (registrations.length === 0 && !optional) {
679
+ throwUnregisteredError([
680
+ token
681
+ ]);
655
682
  }
656
- return optional ? [] : throwUnregisteredError(token);
683
+ return registrations //
684
+ .map((registration)=>this.resolveRegistration(token, registration, optional)).filter((value)=>value != null);
657
685
  }
658
686
  dispose() {
659
687
  if (this.myDisposed) {
@@ -680,26 +708,53 @@ function isDisposable(value) {
680
708
  // Allow values to be GCed
681
709
  disposedRefs.clear();
682
710
  }
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);
711
+ resolveRegistration(token, registration, optional, name) {
712
+ const aliases = [];
713
+ while(registration && isExistingProvider(registration.provider)){
714
+ const [targetToken, targetName] = this.getTargetToken(registration.provider);
715
+ if (aliases.some(([t])=>t === targetToken)) {
716
+ throwCircularAliasError([
717
+ [
718
+ token,
719
+ name
720
+ ],
721
+ ...aliases
722
+ ]);
690
723
  }
724
+ // eslint-disable-next-line no-param-reassign
725
+ registration = this.myTokenRegistry.get(targetToken, targetName);
726
+ aliases.push([
727
+ targetToken,
728
+ targetName
729
+ ]);
730
+ if (!registration && !optional) {
731
+ throwTargetUnregisteredError([
732
+ token,
733
+ name
734
+ ], aliases);
735
+ }
736
+ }
737
+ if (!registration) {
738
+ return optional ? undefined : throwUnregisteredError([
739
+ token,
740
+ name
741
+ ]);
691
742
  }
692
743
  try {
693
- return this.resolveProviderValue(token, currRegistration);
744
+ return this.resolveProviderValue(token, registration);
694
745
  } 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;
746
+ throwResolutionError([
747
+ token,
748
+ name
749
+ ], aliases, e);
701
750
  }
702
751
  }
752
+ getTargetToken(provider) {
753
+ const token = provider.useExisting;
754
+ return Array.isArray(token) ? token : [
755
+ token
756
+ ];
757
+ }
703
758
  autoRegisterClass(Class, name) {
704
759
  const metadata = getMetadata(Class);
705
760
  const autoRegister = metadata.autoRegister ?? this.myOptions.autoRegister;
@@ -730,8 +785,7 @@ function isDisposable(value) {
730
785
  if (isValueProvider(provider)) {
731
786
  return provider.useValue;
732
787
  }
733
- check(!isExistingProvider(provider), "internal error: unexpected ExistingProvider");
734
- expectNever(provider);
788
+ check(false, "internal error: unexpected ExistingProvider");
735
789
  }
736
790
  resolveScopedValue(token, registration, factory) {
737
791
  let context = useInjectionContext();
@@ -746,7 +800,9 @@ function isDisposable(value) {
746
800
  if (resolution.stack.has(provider)) {
747
801
  const dependentRef = resolution.dependents.get(provider);
748
802
  check(dependentRef, ()=>{
749
- const path = resolution.tokenStack.map(getTokenName).concat(getTokenName(token)).join(" → ");
803
+ const path = getTokenPath(resolution.tokens.concat(token).map((t)=>[
804
+ t
805
+ ]));
750
806
  return `circular dependency detected while resolving ${path}`;
751
807
  });
752
808
  return dependentRef.current;
@@ -754,7 +810,7 @@ function isDisposable(value) {
754
810
  const scope = registration.options?.scope ?? this.myOptions.defaultScope;
755
811
  const cleanups = [
756
812
  provideInjectionContext(context),
757
- resolution.tokenStack.push(token) && (()=>resolution.tokenStack.pop()),
813
+ resolution.tokens.push(token) && (()=>resolution.tokens.pop()),
758
814
  !isBuilder(provider) && resolution.stack.push(provider, {
759
815
  provider,
760
816
  scope
@@ -810,7 +866,7 @@ function isDisposable(value) {
810
866
  check(ctor.length === ctorDeps.length, ()=>{
811
867
  const location = getLocation(ctor);
812
868
  const msg = `${location} expected ${ctor.length} decorated constructor parameters`;
813
- return msg + `, but found ${ctorDeps.length}`;
869
+ return `${msg}, but found ${ctorDeps.length}`;
814
870
  });
815
871
  return this.resolveArgs(ctorDeps, ctor);
816
872
  }
@@ -832,7 +888,7 @@ function isDisposable(value) {
832
888
  check(methodDeps.length === method.length, ()=>{
833
889
  const location = getLocation(ctor, methodKey);
834
890
  const msg = `${location} expected ${method.length} decorated method parameters`;
835
- return msg + `, but found ${methodDeps.length}`;
891
+ return `${msg}, but found ${methodDeps.length}`;
836
892
  });
837
893
  const args = this.resolveArgs(methodDeps, ctor, instance, methodKey);
838
894
  method.bind(instance)(...args);
@@ -867,7 +923,7 @@ function isDisposable(value) {
867
923
  }
868
924
  }
869
925
  checkDisposed() {
870
- check(!this.myDisposed, "the container is disposed");
926
+ check(!this.myDisposed, "container is disposed");
871
927
  }
872
928
  }
873
929
 
@@ -940,8 +996,7 @@ function isDisposable(value) {
940
996
  function forwardRef(token) {
941
997
  return {
942
998
  getRefTokens: ()=>{
943
- // Normalize the single token here, so that we don't have
944
- // to do it at every getRefTokens call site
999
+ // Normalize the single token here so that we don't have to do it at every getRefTokens call site
945
1000
  const tokenOrTokens = token();
946
1001
  const tokensArray = Array.isArray(tokenOrTokens) ? tokenOrTokens : [
947
1002
  tokenOrTokens
@@ -1072,7 +1127,7 @@ function InjectAll(token) {
1072
1127
  *
1073
1128
  * @__NO_SIDE_EFFECTS__
1074
1129
  */ function Named(name) {
1075
- check(name.trim(), "the @Named qualifier must not be empty");
1130
+ check(name.trim(), "@Named qualifier must not be empty");
1076
1131
  return function(target, propertyKey, parameterIndex) {
1077
1132
  if (parameterIndex === undefined) {
1078
1133
  // The decorator has been applied to the class