@lppedd/di-wise-neo 0.9.4 → 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.
package/README.md CHANGED
@@ -168,14 +168,9 @@ registrar.registerCommand("my.command", () => { console.log("hey!"); });
168
168
 
169
169
  ## Container scopes
170
170
 
171
- The [Container][source-container] supports four **scope** types that determine how and when
171
+ The [Container][source-container] supports three **scope** types that determine how and when
172
172
  values are cached and reused.
173
173
 
174
- ### Inherited
175
-
176
- Inherits the scope from the requesting (dependent) token.
177
- If there is no dependent (i.e., during top-level resolution), it behaves like **Transient**.
178
-
179
174
  ### Transient
180
175
 
181
176
  Creates a new value every time the dependency is resolved, which means values are never cached.
@@ -184,6 +179,11 @@ Creates a new value every time the dependency is resolved, which means values ar
184
179
  - a factory function registered via `FactoryProvider` is invoked on each resolution
185
180
  - a value registered via `ValueProvider` is always returned as-is
186
181
 
182
+ > [!NOTE]
183
+ > When a **Transient** or **Resolution**-scoped value is injected into a **Container**-scoped
184
+ > instance, it effectively inherits the lifecycle of that instance. The value will live as long
185
+ > as the containing instance, even though it is not cached by the container itself.
186
+
187
187
  ### Resolution
188
188
 
189
189
  Creates and caches a single value per resolution graph.
@@ -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.
@@ -155,7 +174,6 @@ interface ExistingProvider<Value> {
155
174
  type Provider<Value = any> = ClassProvider<Value & object> | FactoryProvider<Value> | ValueProvider<Value> | ExistingProvider<Value>;
156
175
 
157
176
  declare const Scope: {
158
- readonly Inherited: "Inherited";
159
177
  readonly Transient: "Transient";
160
178
  readonly Resolution: "Resolution";
161
179
  readonly Container: "Container";
@@ -203,7 +221,7 @@ interface ContainerOptions {
203
221
  /**
204
222
  * The default scope for registrations.
205
223
  *
206
- * @defaultValue Scope.Inherited
224
+ * @defaultValue Scope.Transient
207
225
  */
208
226
  readonly defaultScope: Scope;
209
227
  }
@@ -325,7 +343,8 @@ interface Container {
325
343
  *
326
344
  * If the class is not registered, but it is decorated with {@link AutoRegister},
327
345
  * or {@link ContainerOptions.autoRegister} is true, the class is registered automatically.
328
- * Otherwise, resolution fails.
346
+ * Otherwise, the resolution fails.
347
+ *
329
348
  * The scope for the automatic registration is determined by either
330
349
  * the {@link Scoped} decorator on the class, or {@link ContainerOptions.defaultScope}.
331
350
  *
@@ -373,7 +392,8 @@ interface Container {
373
392
  *
374
393
  * If the class is not registered, but it is decorated with {@link AutoRegister},
375
394
  * or {@link ContainerOptions.autoRegister} is true, the class is registered automatically.
376
- * Otherwise, resolution fails.
395
+ * Otherwise, the resolution fails.
396
+ *
377
397
  * The scope for the automatic registration is determined by either
378
398
  * the {@link Scoped} decorator on the class, or {@link ContainerOptions.defaultScope}.
379
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 message = tag(`failed to resolve token ${getTokenName(token)}`);
21
- throw isError(cause) ? new Error(`${message}\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(`${message}\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());
29
- const message = tag(`failed to resolve dependency for ${location}(parameter #${dependency.index}: ${tokenName})`);
30
- throw new Error(`${message}\n [cause] ${untag(cause.message)}`, {
36
+ const tokenName = getFullTokenName([
37
+ dependency.tokenRef.getRefToken(),
38
+ dependency.name
39
+ ]);
40
+ const msg = tag(`failed to resolve dependency for ${location}(parameter #${dependency.index}: ${tokenName})`);
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}`;
@@ -298,15 +325,13 @@ function isExistingProvider(provider) {
298
325
  }
299
326
 
300
327
  const Scope = {
301
- Inherited: "Inherited",
302
328
  Transient: "Transient",
303
329
  Resolution: "Resolution",
304
330
  Container: "Container"
305
331
  };
306
332
 
333
+ const typeSymbol = Symbol("di-wise-neo.typeToken");
307
334
  /**
308
- * Type API.
309
- */ /**
310
335
  * Creates a type token.
311
336
  *
312
337
  * @example
@@ -320,6 +345,7 @@ const Scope = {
320
345
  name: `Type<${typeName}>`,
321
346
  inter: createType,
322
347
  union: createType,
348
+ __type: typeSymbol,
323
349
  toString () {
324
350
  return type.name;
325
351
  }
@@ -327,6 +353,10 @@ const Scope = {
327
353
  return type;
328
354
  }
329
355
  // @internal
356
+ function isType(token) {
357
+ return token.__type === typeSymbol;
358
+ }
359
+ // @internal
330
360
  function isConstructor(token) {
331
361
  return typeof token === "function";
332
362
  }
@@ -432,8 +462,7 @@ class TokenRegistry {
432
462
  return Array.from(values);
433
463
  }
434
464
  getAllFromParent(token, name) {
435
- const thisRegistrations = this.myMap.get(token);
436
- let registrations = thisRegistrations || this.myParent?.getAllFromParent(token, name);
465
+ let registrations = this.myMap.get(token) || this.myParent?.getAllFromParent(token, name);
437
466
  if (registrations && name !== undefined) {
438
467
  registrations = registrations.filter((r)=>r.name === name);
439
468
  check(registrations.length < 2, `internal error: more than one registration named '${name}'`);
@@ -478,7 +507,7 @@ function isDisposable(value) {
478
507
  this.myParent = parent;
479
508
  this.myOptions = {
480
509
  autoRegister: false,
481
- defaultScope: Scope.Inherited,
510
+ defaultScope: Scope.Transient,
482
511
  ...options
483
512
  };
484
513
  this.myTokenRegistry = new TokenRegistry(this.myParent?.myTokenRegistry);
@@ -548,8 +577,9 @@ function isDisposable(value) {
548
577
  if (args.length === 1) {
549
578
  const Class = args[0];
550
579
  const metadata = getMetadata(Class);
580
+ const name = metadata.name;
551
581
  const registration = {
552
- name: metadata.name,
582
+ name: name,
553
583
  // The provider is of type ClassProvider, initialized by getMetadata
554
584
  provider: metadata.provider,
555
585
  options: {
@@ -563,8 +593,12 @@ function isDisposable(value) {
563
593
  // These tokens will point to the original Class token and will have the same scope.
564
594
  for (const token of metadata.tokensRef.getRefTokens()){
565
595
  this.myTokenRegistry.set(token, {
596
+ name: name,
566
597
  provider: {
567
- useExisting: Class
598
+ useExisting: {
599
+ token: Class,
600
+ name: name
601
+ }
568
602
  }
569
603
  });
570
604
  }
@@ -574,9 +608,8 @@ function isDisposable(value) {
574
608
  }
575
609
  } else {
576
610
  const [token, provider, options] = args;
577
- const existingProvider = isExistingProvider(provider);
578
- const name = existingProvider ? undefined : provider.name;
579
- 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`);
580
613
  if (isClassProvider(provider)) {
581
614
  const metadata = getMetadata(provider.useClass);
582
615
  const registration = {
@@ -596,8 +629,9 @@ function isDisposable(value) {
596
629
  this.resolveProviderValue(token, registration);
597
630
  }
598
631
  } else {
599
- if (existingProvider) {
600
- 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`);
601
635
  }
602
636
  this.myTokenRegistry.set(token, {
603
637
  name: name,
@@ -634,10 +668,7 @@ function isDisposable(value) {
634
668
  if (!registration && isConstructor(token)) {
635
669
  registration = this.autoRegisterClass(token, localName);
636
670
  }
637
- if (registration) {
638
- return this.resolveRegistration(token, registration, localName);
639
- }
640
- return localOptional ? undefined : throwUnregisteredError(token, localName);
671
+ return this.resolveRegistration(token, registration, localOptional, localName);
641
672
  }
642
673
  resolveAll(token, optional) {
643
674
  this.checkDisposed();
@@ -650,11 +681,13 @@ function isDisposable(value) {
650
681
  ];
651
682
  }
652
683
  }
653
- if (registrations.length > 0) {
654
- return registrations //
655
- .map((registration)=>this.resolveRegistration(token, registration)).filter((value)=>value != null);
684
+ if (registrations.length === 0 && !optional) {
685
+ throwUnregisteredError([
686
+ token
687
+ ]);
656
688
  }
657
- return optional ? [] : throwUnregisteredError(token);
689
+ return registrations //
690
+ .map((registration)=>this.resolveRegistration(token, registration, optional)).filter((value)=>value != null);
658
691
  }
659
692
  dispose() {
660
693
  if (this.myDisposed) {
@@ -681,26 +714,57 @@ function isDisposable(value) {
681
714
  // Allow values to be GCed
682
715
  disposedRefs.clear();
683
716
  }
684
- resolveRegistration(token, registration, name) {
685
- let currRegistration = registration;
686
- while(isExistingProvider(currRegistration.provider)){
687
- const targetToken = currRegistration.provider.useExisting;
688
- currRegistration = this.myTokenRegistry.get(targetToken, name);
689
- if (!currRegistration) {
690
- 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);
691
741
  }
692
742
  }
743
+ if (!registration) {
744
+ return optional ? undefined : throwUnregisteredError([
745
+ token,
746
+ name
747
+ ]);
748
+ }
693
749
  try {
694
- return this.resolveProviderValue(token, currRegistration);
750
+ return this.resolveProviderValue(token, registration);
695
751
  } catch (e) {
696
- // If we were trying to resolve a token registered via ExistingProvider,
697
- // we must add the cause of the error to the message
698
- if (isExistingProvider(registration.provider)) {
699
- throwExistingUnregisteredError(token, e);
700
- }
701
- throw e;
752
+ throwResolutionError([
753
+ token,
754
+ name
755
+ ], aliases, e);
702
756
  }
703
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
+ }
704
768
  autoRegisterClass(Class, name) {
705
769
  const metadata = getMetadata(Class);
706
770
  const autoRegister = metadata.autoRegister ?? this.myOptions.autoRegister;
@@ -731,8 +795,7 @@ function isDisposable(value) {
731
795
  if (isValueProvider(provider)) {
732
796
  return provider.useValue;
733
797
  }
734
- check(!isExistingProvider(provider), "internal error: unexpected ExistingProvider");
735
- expectNever(provider);
798
+ check(false, "internal error: unexpected ExistingProvider");
736
799
  }
737
800
  resolveScopedValue(token, registration, factory) {
738
801
  let context = useInjectionContext();
@@ -744,16 +807,17 @@ function isDisposable(value) {
744
807
  }
745
808
  const resolution = context.resolution;
746
809
  const provider = registration.provider;
747
- const options = registration.options;
748
810
  if (resolution.stack.has(provider)) {
749
811
  const dependentRef = resolution.dependents.get(provider);
750
812
  check(dependentRef, ()=>{
751
- const path = resolution.tokenStack.map(getTokenName).join(" → ");
752
- return `circular dependency detected while resolving ${path} → ${getTokenName(token)}`;
813
+ const path = getTokenPath(resolution.tokenStack.concat(token).map((t)=>[
814
+ t
815
+ ]));
816
+ return `circular dependency detected while resolving ${path}`;
753
817
  });
754
818
  return dependentRef.current;
755
819
  }
756
- const scope = this.resolveScope(options?.scope, context);
820
+ const scope = registration.options?.scope ?? this.myOptions.defaultScope;
757
821
  const cleanups = [
758
822
  provideInjectionContext(context),
759
823
  resolution.tokenStack.push(token) && (()=>resolution.tokenStack.pop()),
@@ -800,13 +864,6 @@ function isDisposable(value) {
800
864
  cleanups.forEach((cleanup)=>cleanup && cleanup());
801
865
  }
802
866
  }
803
- resolveScope(scope = this.myOptions.defaultScope, context = useInjectionContext()) {
804
- if (scope === Scope.Inherited) {
805
- const dependentFrame = context?.resolution.stack.peek();
806
- return dependentFrame?.scope || Scope.Transient;
807
- }
808
- return scope;
809
- }
810
867
  resolveCtorDependencies(registration) {
811
868
  const dependencies = registration.dependencies;
812
869
  if (dependencies) {
@@ -819,7 +876,7 @@ function isDisposable(value) {
819
876
  check(ctor.length === ctorDeps.length, ()=>{
820
877
  const location = getLocation(ctor);
821
878
  const msg = `${location} expected ${ctor.length} decorated constructor parameters`;
822
- return msg + `, but found ${ctorDeps.length}`;
879
+ return `${msg}, but found ${ctorDeps.length}`;
823
880
  });
824
881
  return this.resolveArgs(ctorDeps, ctor);
825
882
  }
@@ -841,7 +898,7 @@ function isDisposable(value) {
841
898
  check(methodDeps.length === method.length, ()=>{
842
899
  const location = getLocation(ctor, methodKey);
843
900
  const msg = `${location} expected ${method.length} decorated method parameters`;
844
- return msg + `, but found ${methodDeps.length}`;
901
+ return `${msg}, but found ${methodDeps.length}`;
845
902
  });
846
903
  const args = this.resolveArgs(methodDeps, ctor, instance, methodKey);
847
904
  method.bind(instance)(...args);
@@ -876,7 +933,7 @@ function isDisposable(value) {
876
933
  }
877
934
  }
878
935
  checkDisposed() {
879
- check(!this.myDisposed, "the container is disposed");
936
+ check(!this.myDisposed, "container is disposed");
880
937
  }
881
938
  }
882
939
 
@@ -884,7 +941,7 @@ function isDisposable(value) {
884
941
  * Creates a new container.
885
942
  */ function createContainer(options = {
886
943
  autoRegister: false,
887
- defaultScope: Scope.Inherited
944
+ defaultScope: Scope.Transient
888
945
  }) {
889
946
  return new ContainerImpl(undefined, options);
890
947
  }
@@ -1081,7 +1138,7 @@ function InjectAll(token) {
1081
1138
  *
1082
1139
  * @__NO_SIDE_EFFECTS__
1083
1140
  */ function Named(name) {
1084
- check(name.trim(), "the @Named qualifier must not be empty");
1141
+ check(name.trim(), "@Named qualifier must not be empty");
1085
1142
  return function(target, propertyKey, parameterIndex) {
1086
1143
  if (parameterIndex === undefined) {
1087
1144
  // The decorator has been applied to the class
@@ -1183,22 +1240,15 @@ function OptionalAll(token) {
1183
1240
  * ```
1184
1241
  */ const Injector = /*@__PURE__*/ build(()=>{
1185
1242
  const context = ensureInjectionContext("Injector factory");
1186
- const resolution = context.resolution;
1187
- const dependentFrame = resolution.stack.peek();
1188
- const dependentRef = dependentFrame && resolution.dependents.get(dependentFrame.provider);
1189
1243
  const runInContext = (fn)=>{
1190
1244
  if (useInjectionContext()) {
1191
1245
  return fn();
1192
1246
  }
1193
- const cleanups = [
1194
- provideInjectionContext(context),
1195
- dependentFrame && resolution.stack.push(dependentFrame.provider, dependentFrame),
1196
- dependentRef && resolution.dependents.set(dependentFrame.provider, dependentRef)
1197
- ];
1247
+ const cleanup = provideInjectionContext(context);
1198
1248
  try {
1199
1249
  return fn();
1200
1250
  } finally{
1201
- cleanups.forEach((cleanup)=>cleanup?.());
1251
+ cleanup();
1202
1252
  }
1203
1253
  };
1204
1254
  return {