@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 +6 -6
- package/dist/cjs/index.d.ts +25 -5
- package/dist/cjs/index.js +125 -75
- package/dist/cjs/index.js.map +1 -1
- package/dist/es/index.d.mts +25 -5
- package/dist/es/index.mjs +125 -75
- package/dist/es/index.mjs.map +1 -1
- package/package.json +4 -4
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
|
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.
|
package/dist/cjs/index.d.ts
CHANGED
@@ -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.
|
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
|
11
|
-
throw new
|
10
|
+
function throwUnregisteredError(tokenInfo) {
|
11
|
+
throw new Error(tag(`unregistered token ${getFullTokenName(tokenInfo)}`));
|
12
12
|
}
|
13
13
|
// @internal
|
14
|
-
function
|
15
|
-
const
|
16
|
-
|
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
|
20
|
-
const
|
21
|
-
throw
|
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
|
-
})
|
31
|
+
});
|
24
32
|
}
|
25
33
|
// @internal
|
26
34
|
function throwParameterResolutionError(ctor, methodKey, dependency, cause) {
|
27
35
|
const location = getLocation(ctor, methodKey);
|
28
|
-
const tokenName =
|
29
|
-
|
30
|
-
|
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
|
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
|
-
|
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.
|
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:
|
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:
|
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
|
578
|
-
|
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 (
|
600
|
-
|
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
|
-
|
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
|
654
|
-
|
655
|
-
|
684
|
+
if (registrations.length === 0 && !optional) {
|
685
|
+
throwUnregisteredError([
|
686
|
+
token
|
687
|
+
]);
|
656
688
|
}
|
657
|
-
return
|
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
|
-
|
686
|
-
while(isExistingProvider(
|
687
|
-
const targetToken =
|
688
|
-
|
689
|
-
|
690
|
-
|
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,
|
750
|
+
return this.resolveProviderValue(token, registration);
|
695
751
|
} catch (e) {
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
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(
|
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.
|
752
|
-
|
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 =
|
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
|
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
|
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, "
|
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.
|
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(), "
|
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
|
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
|
-
|
1251
|
+
cleanup();
|
1202
1252
|
}
|
1203
1253
|
};
|
1204
1254
|
return {
|