@codefast/di 0.8.1 → 0.9.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/CHANGELOG.md +27 -0
- package/LICENSE +1 -1
- package/README.md +26 -17
- package/dist/container/binding-builders.d.ts +11 -11
- package/dist/container/container.d.ts +10 -10
- package/dist/container/container.js +22 -7
- package/dist/core/binding.d.ts +19 -14
- package/dist/core/constraint-requirement.d.ts +19 -4
- package/dist/core/constraint-requirement.js +19 -9
- package/dist/core/tag.d.ts +2 -2
- package/dist/core/tag.js +2 -2
- package/dist/core/token.d.ts +24 -4
- package/dist/core/token.js +1 -1
- package/dist/core/types.d.ts +8 -2
- package/dist/decorators/inject.d.ts +1 -1
- package/dist/decorators/inject.js +6 -4
- package/dist/errors/errors.d.ts +4 -1
- package/dist/errors/errors.js +9 -3
- package/dist/index.d.ts +1 -1
- package/dist/injection/descriptor.d.ts +11 -5
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/resolution/select/constraints.d.ts +7 -4
- package/dist/resolution/select/constraints.js +34 -13
- package/package.json +6 -40
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @codefast/di
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#847](https://github.com/codefastlabs/codefast/pull/847) [`d0b794c`](https://github.com/codefastlabs/codefast/commit/d0b794c047344c4040b5641202c259d72a0ea48c) Thanks [@thevuong](https://github.com/thevuong)! - `Token` gains a second type parameter, `Names extends string = string`, declaring the slot names its bindings may use:
|
|
8
|
+
`token<Logger, "console" | "file">("Logger")`. `whenNamed`, and `name` in `ResolveOptions` and `InjectOptions`, narrow
|
|
9
|
+
to it, so a misspelt name is a compile error and the IDE completes the declared names at every bind and request site.
|
|
10
|
+
`Names` is a covariant phantom that defaults to `string`, so existing tokens, class keys and internal `Token<unknown>`
|
|
11
|
+
lanes are unchanged; a new `SlotNamesOf<Key>` type reads the set back.
|
|
12
|
+
|
|
13
|
+
**Breaking:** `whenParentNamed` and `whenAnyAncestorNamed` now take the parent token first —
|
|
14
|
+
`whenParentNamed(Database, "primary")` — and match only when that frame resolves that token at that slot. A slot name is
|
|
15
|
+
a label on one token's bindings, so the token is part of the question and is what types the name; a label shared across
|
|
16
|
+
tokens is what a tag key is for. `validate()` checks the name on that token's bindings and `UnreachableConstraintError`
|
|
17
|
+
carries the new `requiredTokenName`. The reserved criterion handed to a `…Tagged` helper
|
|
18
|
+
(`whenParentTagged(slotName.of("x"))`) is now validated too, as it is the same bare string.
|
|
19
|
+
|
|
20
|
+
Display names follow one rule everywhere the package speaks — spelled like the TS symbol they stand for, under the
|
|
21
|
+
owner's namespace: `token<Logger>("app:Logger")`, `Module.create("app:Infra", …)`, `tag("app:cacheTier")`. The package's
|
|
22
|
+
own `MetadataReaderToken` now prints as `di:MetadataReader`, beside the reserved `di:name` key. SPEC gains a normative
|
|
23
|
+
"Display names" section stating the rule and its enforcement.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- [#827](https://github.com/codefastlabs/codefast/pull/827) [`0984174`](https://github.com/codefastlabs/codefast/commit/0984174df148a7cffcd09b837bdde1922f38f24e) Thanks [@thevuong](https://github.com/thevuong)! - `package.json` now carries `homepage` and `bugs`, so npm links the package README and the issue tracker the way the
|
|
28
|
+
other `@codefast/*` packages already do.
|
|
29
|
+
|
|
3
30
|
## 0.8.1
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ interface Logger {
|
|
|
48
48
|
info(message: string): void;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const LoggerToken = token<Logger>("Logger");
|
|
51
|
+
const LoggerToken = token<Logger>("app:Logger");
|
|
52
52
|
|
|
53
53
|
@injectable([LoggerToken])
|
|
54
54
|
class CheckoutService {
|
|
@@ -86,7 +86,7 @@ and resolve against. Tokens compare by reference, so declare each one once and r
|
|
|
86
86
|
```ts
|
|
87
87
|
import { token } from "@codefast/di";
|
|
88
88
|
|
|
89
|
-
const DbToken = token<Database>("Database");
|
|
89
|
+
const DbToken = token<Database>("app:Database");
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
A class constructor works as a key too: `container.bind(UserService).toSelf()`, then `container.resolve(UserService)`.
|
|
@@ -170,6 +170,15 @@ container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
|
|
|
170
170
|
container.resolve(LoggerToken, { name: "file" }); // → fileLogger
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
Declare the names on the token and they become checked, completable literals at every bind and request site:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const LoggerToken = token<Logger, "console" | "file">("app:Logger");
|
|
177
|
+
|
|
178
|
+
container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
|
|
179
|
+
container.resolve(LoggerToken, { name: "file" }); // { name: "fiel" } is a compile error
|
|
180
|
+
```
|
|
181
|
+
|
|
173
182
|
**Tagged — for typed, collision-proof keys.** A criterion is a `[key, value]` pair. Declare the key once with
|
|
174
183
|
`tag<Value>(name)`, then mint a criterion with `key.of(value)`. The bind site and the resolve site share the same typed
|
|
175
184
|
key: a key declared `tag<"s3" | "gcs">` refuses any other value, so the two sites can't drift apart.
|
|
@@ -177,7 +186,7 @@ key: a key declared `tag<"s3" | "gcs">` refuses any other value, so the two site
|
|
|
177
186
|
```ts
|
|
178
187
|
import { tag } from "@codefast/di";
|
|
179
188
|
|
|
180
|
-
const Provider = tag<"s3" | "gcs">("provider");
|
|
189
|
+
const Provider = tag<"s3" | "gcs">("app:provider");
|
|
181
190
|
|
|
182
191
|
container.bind(StorageToken).to(S3Storage).whenTagged(Provider.of("s3"));
|
|
183
192
|
container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
|
|
@@ -202,18 +211,18 @@ container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
|
|
|
202
211
|
resolving — pass a predicate to `.when(ctx => boolean)`. It runs at resolve time, after slot matching. These ready-made
|
|
203
212
|
predicates ship from the root entry:
|
|
204
213
|
|
|
205
|
-
| Predicate | Matches when
|
|
206
|
-
| ------------------------------------ |
|
|
207
|
-
| `whenParentIs(token)` | the direct parent resolves `token`
|
|
208
|
-
| `whenNoParentIs(token)` | there is no parent, or it resolves a different token
|
|
209
|
-
| `whenAnyAncestorIs(token)` | some ancestor resolves `token`
|
|
210
|
-
| `whenNoAncestorIs(token)` | no ancestor resolves `token`
|
|
211
|
-
| `whenParentNamed(name)`
|
|
212
|
-
| `whenAnyAncestorNamed(name)`
|
|
213
|
-
| `whenParentTagged(criterion)` | the parent's slot carries that criterion
|
|
214
|
-
| `whenAnyAncestorTagged(criterion)` | some ancestor's slot carries that criterion
|
|
215
|
-
| `whenParentTaggedAll(criteria)` | the parent's slot carries all criteria in the array
|
|
216
|
-
| `whenAnyAncestorTaggedAll(criteria)` | some ancestor's slot carries all criteria in the array
|
|
214
|
+
| Predicate | Matches when |
|
|
215
|
+
| ------------------------------------ | ------------------------------------------------------- |
|
|
216
|
+
| `whenParentIs(token)` | the direct parent resolves `token` |
|
|
217
|
+
| `whenNoParentIs(token)` | there is no parent, or it resolves a different token |
|
|
218
|
+
| `whenAnyAncestorIs(token)` | some ancestor resolves `token` |
|
|
219
|
+
| `whenNoAncestorIs(token)` | no ancestor resolves `token` |
|
|
220
|
+
| `whenParentNamed(token, name)` | the parent resolves `token` at the slot named `name` |
|
|
221
|
+
| `whenAnyAncestorNamed(token, name)` | some ancestor resolves `token` at the slot named `name` |
|
|
222
|
+
| `whenParentTagged(criterion)` | the parent's slot carries that criterion |
|
|
223
|
+
| `whenAnyAncestorTagged(criterion)` | some ancestor's slot carries that criterion |
|
|
224
|
+
| `whenParentTaggedAll(criteria)` | the parent's slot carries all criteria in the array |
|
|
225
|
+
| `whenAnyAncestorTaggedAll(criteria)` | some ancestor's slot carries all criteria in the array |
|
|
217
226
|
|
|
218
227
|
For the exact matching and most-specific-wins rules, see [`SPEC.md` → Slots and last-wins](./SPEC.md#slot-matching).
|
|
219
228
|
|
|
@@ -358,11 +367,11 @@ A module is a reusable, stateless bundle of related bindings. Group them once, t
|
|
|
358
367
|
```ts
|
|
359
368
|
import { Container, Module } from "@codefast/di";
|
|
360
369
|
|
|
361
|
-
const InfrastructureModule = Module.create("Infra", (api) => {
|
|
370
|
+
const InfrastructureModule = Module.create("app:Infra", (api) => {
|
|
362
371
|
api.bind(LoggerToken).toConstantValue(console);
|
|
363
372
|
});
|
|
364
373
|
|
|
365
|
-
const AppModule = Module.create("
|
|
374
|
+
const AppModule = Module.create("app:Root", (api) => {
|
|
366
375
|
api.import(InfrastructureModule);
|
|
367
376
|
api.bind(UserRepository).toSelf().singleton();
|
|
368
377
|
});
|
|
@@ -31,23 +31,23 @@ export interface BindingRegistration {
|
|
|
31
31
|
*
|
|
32
32
|
* @since 0.5.0-canary.8
|
|
33
33
|
*/
|
|
34
|
-
export declare class BindingChain<Value> implements AliasBindingBuilder
|
|
34
|
+
export declare class BindingChain<Value, Names extends string = string> implements AliasBindingBuilder<Names>, BindingBuilder<Value, Names>, BindToBuilder<Value, Names>, ConstantBindingBuilder<Value, Names>, ScopedBindingBuilder<Value>, SingletonBindingBuilder<Value>, SingletonLifecycleBuilder<Value>, TransientBindingBuilder<Value> {
|
|
35
35
|
#private;
|
|
36
|
-
constructor(token: Token<Value> | Constructor<Value>, registration: BindingRegistration);
|
|
37
|
-
to(type: Constructor<Value>): BindingBuilder<Value>;
|
|
38
|
-
toSelf(): BindingBuilder<Value>;
|
|
39
|
-
toConstantValue(value: Value): ConstantBindingBuilder<Value>;
|
|
40
|
-
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
|
|
41
|
-
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
|
|
36
|
+
constructor(token: Token<Value, Names> | Constructor<Value>, registration: BindingRegistration);
|
|
37
|
+
to(type: Constructor<Value>): BindingBuilder<Value, Names>;
|
|
38
|
+
toSelf(): BindingBuilder<Value, Names>;
|
|
39
|
+
toConstantValue(value: Value): ConstantBindingBuilder<Value, Names>;
|
|
40
|
+
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value, Names>;
|
|
41
|
+
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value, Names>;
|
|
42
42
|
toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
|
|
43
43
|
[K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
|
|
44
|
-
}) => Value, deps: Deps): BindingBuilder<Value>;
|
|
44
|
+
}) => Value, deps: Deps): BindingBuilder<Value, Names>;
|
|
45
45
|
toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
|
|
46
46
|
[K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
|
|
47
|
-
}) => Promise<Value>, deps: Deps): BindingBuilder<Value>;
|
|
48
|
-
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder
|
|
47
|
+
}) => Promise<Value>, deps: Deps): BindingBuilder<Value, Names>;
|
|
48
|
+
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder<Names>;
|
|
49
49
|
when(predicate: BindingConstraint): this;
|
|
50
|
-
whenNamed(name:
|
|
50
|
+
whenNamed(name: Names): this;
|
|
51
51
|
whenTagged(criterion: BindingTag): this;
|
|
52
52
|
whenDefault(): this;
|
|
53
53
|
singleton(): SingletonBindingBuilder<Value>;
|
|
@@ -13,12 +13,12 @@ import type { MetadataReader } from "#/metadata/metadata-types";
|
|
|
13
13
|
*/
|
|
14
14
|
export interface Container {
|
|
15
15
|
readonly isDisposed: boolean;
|
|
16
|
-
bind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
16
|
+
bind<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>): BindToBuilder<Value, Names>;
|
|
17
17
|
unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void;
|
|
18
18
|
unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void>;
|
|
19
19
|
unbindAll(): void;
|
|
20
20
|
unbindAllAsync(): Promise<void>;
|
|
21
|
-
rebind<Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
|
|
21
|
+
rebind<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>): BindToBuilder<Value, Names>;
|
|
22
22
|
load(...modules: Array<SyncModule>): void;
|
|
23
23
|
loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
|
|
24
24
|
unload(...modules: Array<SyncModule>): void;
|
|
@@ -26,20 +26,20 @@ export interface Container {
|
|
|
26
26
|
loadAutoRegistered(registry: AutoRegisterRegistry): number;
|
|
27
27
|
onActivation<Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void;
|
|
28
28
|
onDeactivation<Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
|
|
29
|
-
resolve<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
|
|
30
|
-
resolveAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
|
|
31
|
-
resolveOptional<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
|
|
32
|
-
resolveOptionalAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value | undefined>;
|
|
33
|
-
resolveAll<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value>;
|
|
34
|
-
resolveAllAsync<Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Array<Value>>;
|
|
29
|
+
resolve<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Value;
|
|
30
|
+
resolveAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Value>;
|
|
31
|
+
resolveOptional<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Value | undefined;
|
|
32
|
+
resolveOptionalAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Value | undefined>;
|
|
33
|
+
resolveAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Array<Value>;
|
|
34
|
+
resolveAllAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Array<Value>>;
|
|
35
35
|
createChild(): Container;
|
|
36
36
|
dispose(): Promise<void>;
|
|
37
37
|
[Symbol.asyncDispose](): Promise<void>;
|
|
38
38
|
[Symbol.dispose](): never;
|
|
39
39
|
initializeAsync(): Promise<void>;
|
|
40
40
|
validate(): void;
|
|
41
|
-
has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
|
|
42
|
-
hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
|
|
41
|
+
has<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ResolveOptions<Names>>): boolean;
|
|
42
|
+
hasOwn<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ResolveOptions<Names>>): boolean;
|
|
43
43
|
lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
|
|
44
44
|
inspect(): ContainerSnapshot;
|
|
45
45
|
generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
|
|
@@ -2,7 +2,7 @@ import { BindingChain } from "#/container/binding-builders";
|
|
|
2
2
|
import { NO_INSTANCE } from "#/core/binding";
|
|
3
3
|
import { effectiveBindingScope } from "#/core/binding-scope";
|
|
4
4
|
import { constraintRequirementsOf } from "#/core/constraint-requirement";
|
|
5
|
-
import { getOrInsert } from "#/core/map-upsert";
|
|
5
|
+
import { getOrInsert, getOrInsertComputed } from "#/core/map-upsert";
|
|
6
6
|
import { isSyncModule, MODULE_SETUP } from "#/core/module";
|
|
7
7
|
import { BindingRegistry } from "#/core/registry";
|
|
8
8
|
import { tokenName } from "#/core/token";
|
|
@@ -18,6 +18,20 @@ import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
|
|
|
18
18
|
import { verifyingMetadataReader } from "#/metadata/verifying-metadata-reader";
|
|
19
19
|
import { ROOT_BRANCH } from "#/resolution/path/resolution-path";
|
|
20
20
|
import { DependencyResolver } from "#/resolution/resolver";
|
|
21
|
+
/** Whether a requirement's name is declared — on its token when it names one, on any token otherwise. */
|
|
22
|
+
function isSlotNameDeclared(declared, requirement) {
|
|
23
|
+
if (requirement.tokenName !== undefined) {
|
|
24
|
+
return declared.get(requirement.tokenName)?.has(requirement.name) ?? false;
|
|
25
|
+
}
|
|
26
|
+
for (const names of declared.values()) {
|
|
27
|
+
if (names.has(requirement.name)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/** Hoisted so the slot-name index's `getOrInsertComputed` allocates only on a miss, no closure per call. */
|
|
34
|
+
const newSlotNameSet = () => new Set();
|
|
21
35
|
// A Record rather than an if-chain, so a new `BindingScope` is a compile error here instead of
|
|
22
36
|
// silently landing in whichever branch happened to be last.
|
|
23
37
|
const APPLY_BINDING_SCOPE = {
|
|
@@ -552,18 +566,19 @@ class DefaultContainer {
|
|
|
552
566
|
}
|
|
553
567
|
declaredSlotNames ??= this.#slotNamesInChain();
|
|
554
568
|
for (const requirement of requirements) {
|
|
555
|
-
if (!declaredSlotNames
|
|
556
|
-
throw new UnreachableConstraintError(tokenName(binding.token), requirement
|
|
569
|
+
if (!isSlotNameDeclared(declaredSlotNames, requirement)) {
|
|
570
|
+
throw new UnreachableConstraintError(tokenName(binding.token), requirement);
|
|
557
571
|
}
|
|
558
572
|
}
|
|
559
573
|
}
|
|
560
574
|
}
|
|
561
|
-
/** Every slot name declared anywhere a resolve through this container could reach. */
|
|
575
|
+
/** Every slot name declared anywhere a resolve through this container could reach, by declaring token. */
|
|
562
576
|
#slotNamesInChain() {
|
|
563
|
-
const names = this.#parent === undefined ? new
|
|
577
|
+
const names = this.#parent === undefined ? new Map() : this.#parent.#slotNamesInChain();
|
|
564
578
|
for (const binding of this.#registry.allBindings()) {
|
|
565
|
-
|
|
566
|
-
|
|
579
|
+
const name = binding.slot.name;
|
|
580
|
+
if (name !== undefined) {
|
|
581
|
+
getOrInsertComputed(names, tokenName(binding.token), newSlotNameSet).add(name);
|
|
567
582
|
}
|
|
568
583
|
}
|
|
569
584
|
return names;
|
package/dist/core/binding.d.ts
CHANGED
|
@@ -248,11 +248,16 @@ export declare function clearBindingFrame<Value>(binding: Binding<Value>): void;
|
|
|
248
248
|
*
|
|
249
249
|
* @since 0.3.16-canary.0
|
|
250
250
|
*/
|
|
251
|
-
export interface SlotConstrainedBuilder {
|
|
251
|
+
export interface SlotConstrainedBuilder<Names extends string = string> {
|
|
252
|
+
/** Narrows the binding to requests the predicate accepts, evaluated on every resolve. */
|
|
252
253
|
when(predicate: BindingConstraint): this;
|
|
253
|
-
|
|
254
|
+
/** Declares the binding's slot name, one of the names the token declares. */
|
|
255
|
+
whenNamed(name: Names): this;
|
|
256
|
+
/** Declares one criterion of the binding's slot, replacing any earlier criterion of the same key. */
|
|
254
257
|
whenTagged(criterion: BindingTag): this;
|
|
258
|
+
/** Keeps the binding on the default slot, the one an unconstrained request selects. */
|
|
255
259
|
whenDefault(): this;
|
|
260
|
+
/** The identifier this binding is registered under. */
|
|
256
261
|
id(): BindingIdentifier;
|
|
257
262
|
}
|
|
258
263
|
/**
|
|
@@ -260,26 +265,26 @@ export interface SlotConstrainedBuilder {
|
|
|
260
265
|
*
|
|
261
266
|
* @since 0.3.16-canary.0
|
|
262
267
|
*/
|
|
263
|
-
export interface BindToBuilder<Value> {
|
|
264
|
-
to(type: Constructor<Value>): BindingBuilder<Value>;
|
|
265
|
-
toSelf(): BindingBuilder<Value>;
|
|
266
|
-
toConstantValue(value: Value): ConstantBindingBuilder<Value>;
|
|
267
|
-
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
|
|
268
|
-
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
|
|
268
|
+
export interface BindToBuilder<Value, Names extends string = string> {
|
|
269
|
+
to(type: Constructor<Value>): BindingBuilder<Value, Names>;
|
|
270
|
+
toSelf(): BindingBuilder<Value, Names>;
|
|
271
|
+
toConstantValue(value: Value): ConstantBindingBuilder<Value, Names>;
|
|
272
|
+
toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value, Names>;
|
|
273
|
+
toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value, Names>;
|
|
269
274
|
toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
|
|
270
275
|
[K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
|
|
271
|
-
}) => Value, deps: Deps): BindingBuilder<Value>;
|
|
276
|
+
}) => Value, deps: Deps): BindingBuilder<Value, Names>;
|
|
272
277
|
toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
|
|
273
278
|
[K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
|
|
274
|
-
}) => Promise<Value>, deps: Deps): BindingBuilder<Value>;
|
|
275
|
-
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder
|
|
279
|
+
}) => Promise<Value>, deps: Deps): BindingBuilder<Value, Names>;
|
|
280
|
+
toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder<Names>;
|
|
276
281
|
}
|
|
277
282
|
/**
|
|
278
283
|
* The scope-selection step of the fluent chain.
|
|
279
284
|
*
|
|
280
285
|
* @since 0.3.16-canary.0
|
|
281
286
|
*/
|
|
282
|
-
export interface BindingBuilder<Value> extends SlotConstrainedBuilder {
|
|
287
|
+
export interface BindingBuilder<Value, Names extends string = string> extends SlotConstrainedBuilder<Names> {
|
|
283
288
|
singleton(): SingletonBindingBuilder<Value>;
|
|
284
289
|
transient(): TransientBindingBuilder<Value>;
|
|
285
290
|
scoped(): ScopedBindingBuilder<Value>;
|
|
@@ -289,7 +294,7 @@ export interface BindingBuilder<Value> extends SlotConstrainedBuilder {
|
|
|
289
294
|
*
|
|
290
295
|
* @since 0.3.16-canary.0
|
|
291
296
|
*/
|
|
292
|
-
export interface ConstantBindingBuilder<Value> extends SlotConstrainedBuilder {
|
|
297
|
+
export interface ConstantBindingBuilder<Value, Names extends string = string> extends SlotConstrainedBuilder<Names> {
|
|
293
298
|
onActivation(fn: ActivationHandler<Value>): SingletonLifecycleBuilder<Value>;
|
|
294
299
|
onDeactivation(fn: DeactivationHandler<Value>): SingletonLifecycleBuilder<Value>;
|
|
295
300
|
}
|
|
@@ -298,7 +303,7 @@ export interface ConstantBindingBuilder<Value> extends SlotConstrainedBuilder {
|
|
|
298
303
|
*
|
|
299
304
|
* @since 0.3.16-canary.0
|
|
300
305
|
*/
|
|
301
|
-
export interface AliasBindingBuilder extends SlotConstrainedBuilder {
|
|
306
|
+
export interface AliasBindingBuilder<Names extends string = string> extends SlotConstrainedBuilder<Names> {
|
|
302
307
|
}
|
|
303
308
|
/**
|
|
304
309
|
* The fluent chain after `singleton()`, where both lifecycle hooks stay available.
|
|
@@ -12,23 +12,38 @@ export declare const CONSTRAINT_REQUIREMENT: unique symbol;
|
|
|
12
12
|
/**
|
|
13
13
|
* The slot name a constraint waits for on an ancestor.
|
|
14
14
|
*
|
|
15
|
-
* @remarks Only names are described
|
|
16
|
-
* looks valid, while a name is a bare string that
|
|
15
|
+
* @remarks Only names are described, however spelled: a tag criterion of any other key is minted from
|
|
16
|
+
* a typed key, so a typo cannot produce one that looks valid, while a name is a bare string that
|
|
17
|
+
* nothing checks — through `whenNamed`'s reserved key included.
|
|
17
18
|
*
|
|
18
19
|
* @since 0.6.0
|
|
19
20
|
*/
|
|
20
21
|
export interface ConstraintRequirement {
|
|
21
22
|
readonly requires: "ancestorSlotName";
|
|
23
|
+
/** The token whose slot must carry the name, or `undefined` when the spelling named no token. */
|
|
24
|
+
readonly tokenName: string | undefined;
|
|
22
25
|
readonly name: string;
|
|
23
26
|
/** The helper that built the predicate, so a report can name what the caller wrote. */
|
|
24
27
|
readonly helperName: string;
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
30
|
+
* What a helper records about one waited-for slot name, before the discriminant is stamped on.
|
|
31
|
+
*
|
|
32
|
+
* @since 0.9.0
|
|
33
|
+
*/
|
|
34
|
+
export type SlotNameRequirement = Omit<ConstraintRequirement, "requires">;
|
|
35
|
+
/**
|
|
36
|
+
* Records the one slot name a predicate waits for. Called once, where the predicate is built.
|
|
28
37
|
*
|
|
29
38
|
* @since 0.6.0
|
|
30
39
|
*/
|
|
31
|
-
export declare function requiringAncestorSlotName(predicate: BindingConstraint,
|
|
40
|
+
export declare function requiringAncestorSlotName(predicate: BindingConstraint, requirement: SlotNameRequirement): BindingConstraint;
|
|
41
|
+
/**
|
|
42
|
+
* Records every slot name a predicate waits for — one per reserved criterion in a `…TaggedAll` list.
|
|
43
|
+
*
|
|
44
|
+
* @since 0.9.0
|
|
45
|
+
*/
|
|
46
|
+
export declare function requiringAncestorSlotNames(predicate: BindingConstraint, requirements: ReadonlyArray<SlotNameRequirement>): BindingConstraint;
|
|
32
47
|
/**
|
|
33
48
|
* The requirement a predicate carries, if it was built by a helper that records one.
|
|
34
49
|
*
|
|
@@ -8,13 +8,27 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export const CONSTRAINT_REQUIREMENT = Symbol("di:constraint-requirement");
|
|
10
10
|
/**
|
|
11
|
-
* Records
|
|
11
|
+
* Records the one slot name a predicate waits for. Called once, where the predicate is built.
|
|
12
12
|
*
|
|
13
13
|
* @since 0.6.0
|
|
14
14
|
*/
|
|
15
|
-
export function requiringAncestorSlotName(predicate,
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
export function requiringAncestorSlotName(predicate, requirement) {
|
|
16
|
+
return requiringAncestorSlotNames(predicate, [requirement]);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Records every slot name a predicate waits for — one per reserved criterion in a `…TaggedAll` list.
|
|
20
|
+
*
|
|
21
|
+
* @since 0.9.0
|
|
22
|
+
*/
|
|
23
|
+
export function requiringAncestorSlotNames(predicate, requirements) {
|
|
24
|
+
return attachRequirements(predicate, requirements.map((requirement) => ({ requires: "ancestorSlotName", ...requirement })));
|
|
25
|
+
}
|
|
26
|
+
/** One non-enumerable write per predicate: a second define on the same key would throw. */
|
|
27
|
+
function attachRequirements(predicate, requirements) {
|
|
28
|
+
Object.defineProperty(predicate, CONSTRAINT_REQUIREMENT, {
|
|
29
|
+
value: requirements.length === 1 ? requirements[0] : requirements,
|
|
30
|
+
enumerable: false,
|
|
31
|
+
});
|
|
18
32
|
return predicate;
|
|
19
33
|
}
|
|
20
34
|
/**
|
|
@@ -53,9 +67,5 @@ export function mergingConstraintRequirements(composite, left, right) {
|
|
|
53
67
|
if (merged.length === 0) {
|
|
54
68
|
return composite;
|
|
55
69
|
}
|
|
56
|
-
|
|
57
|
-
value: merged.length === 1 ? merged[0] : merged,
|
|
58
|
-
enumerable: false,
|
|
59
|
-
});
|
|
60
|
-
return composite;
|
|
70
|
+
return attachRequirements(composite, merged);
|
|
61
71
|
}
|
package/dist/core/tag.d.ts
CHANGED
|
@@ -59,12 +59,12 @@ export interface TagKey<Value = unknown> {
|
|
|
59
59
|
/**
|
|
60
60
|
* Declares a tag key, whose `of()` builds the criteria a `whenTagged` and a resolve both take.
|
|
61
61
|
*
|
|
62
|
-
* @remarks The value type is checked at both ends: a key declared `tag<Region>("region")` refuses a
|
|
62
|
+
* @remarks The value type is checked at both ends: a key declared `tag<Region>("di:region")` refuses a
|
|
63
63
|
* value that is not a `Region`, so a bind site and a resolve site cannot drift apart silently.
|
|
64
64
|
*
|
|
65
65
|
* @example
|
|
66
66
|
* ```ts
|
|
67
|
-
* const Region = tag<"eu" | "us">("region");
|
|
67
|
+
* const Region = tag<"eu" | "us">("di:region");
|
|
68
68
|
* container.bind(Storage).to(S3).whenTagged(Region.of("eu"));
|
|
69
69
|
* container.resolve(Storage, { tag: Region.of("eu") });
|
|
70
70
|
* ```
|
package/dist/core/tag.js
CHANGED
|
@@ -27,12 +27,12 @@ function internKeyFor(value) {
|
|
|
27
27
|
/**
|
|
28
28
|
* Declares a tag key, whose `of()` builds the criteria a `whenTagged` and a resolve both take.
|
|
29
29
|
*
|
|
30
|
-
* @remarks The value type is checked at both ends: a key declared `tag<Region>("region")` refuses a
|
|
30
|
+
* @remarks The value type is checked at both ends: a key declared `tag<Region>("di:region")` refuses a
|
|
31
31
|
* value that is not a `Region`, so a bind site and a resolve site cannot drift apart silently.
|
|
32
32
|
*
|
|
33
33
|
* @example
|
|
34
34
|
* ```ts
|
|
35
|
-
* const Region = tag<"eu" | "us">("region");
|
|
35
|
+
* const Region = tag<"eu" | "us">("di:region");
|
|
36
36
|
* container.bind(Storage).to(S3).whenTagged(Region.of("eu"));
|
|
37
37
|
* container.resolve(Storage, { tag: Region.of("eu") });
|
|
38
38
|
* ```
|
package/dist/core/token.d.ts
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
import type { Constructor } from "#/core/constructor-type";
|
|
2
2
|
declare const TOKEN_BRAND: unique symbol;
|
|
3
|
+
declare const TOKEN_NAMES_BRAND: unique symbol;
|
|
3
4
|
/**
|
|
4
|
-
* A branded identifier carrying the value type its bindings resolve to.
|
|
5
|
+
* A branded identifier carrying the value type its bindings resolve to, and the slot names they may declare.
|
|
6
|
+
*
|
|
7
|
+
* @remarks `Names` exists at the type level only: `whenNamed`, and `name` in `ResolveOptions` and
|
|
8
|
+
* `InjectOptions`, narrow to it, so a bind site and a request site cannot drift apart silently. It
|
|
9
|
+
* is covariant, so a token declaring names is still a `Token<unknown>` wherever the engine erases
|
|
10
|
+
* the value type; the default `string` leaves a token that declares none unconstrained.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const Logger = token<Logger, "console" | "file">("di:Logger");
|
|
15
|
+
* container.bind(Logger).to(FileLogger).whenNamed("file");
|
|
16
|
+
* container.resolve(Logger, { name: "file" });
|
|
17
|
+
* ```
|
|
5
18
|
*
|
|
6
19
|
* @since 0.3.16-canary.0
|
|
7
20
|
*/
|
|
8
|
-
export interface Token<out Value> {
|
|
21
|
+
export interface Token<out Value, out Names extends string = string> {
|
|
9
22
|
readonly name: string;
|
|
10
23
|
readonly [TOKEN_BRAND]: Value;
|
|
24
|
+
readonly [TOKEN_NAMES_BRAND]?: Names;
|
|
11
25
|
}
|
|
12
26
|
/**
|
|
13
|
-
*
|
|
27
|
+
* The slot names a dependency key declares — `string` for a class, or a token that declares none.
|
|
28
|
+
*
|
|
29
|
+
* @since 0.9.0
|
|
30
|
+
*/
|
|
31
|
+
export type SlotNamesOf<Key> = Key extends Token<unknown, infer Names extends string> ? Names : string;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a named `Token` for the given value type, optionally declaring the slot names its bindings may use.
|
|
14
34
|
*
|
|
15
35
|
* @since 0.3.16-canary.0
|
|
16
36
|
*/
|
|
17
|
-
export declare function token<Value>(name: string): Token<Value>;
|
|
37
|
+
export declare function token<Value, Names extends string = string>(name: string): Token<Value, Names>;
|
|
18
38
|
/**
|
|
19
39
|
* Returns the display name of a token or class used as a dependency key.
|
|
20
40
|
*
|
package/dist/core/token.js
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -47,8 +47,14 @@ export type DeactivationHandler<Value> = (instance: Value) => void | Promise<voi
|
|
|
47
47
|
*
|
|
48
48
|
* @since 0.3.16-canary.0
|
|
49
49
|
*/
|
|
50
|
-
export interface ResolveOptions {
|
|
51
|
-
|
|
50
|
+
export interface ResolveOptions<Names extends string = string> {
|
|
51
|
+
/**
|
|
52
|
+
* The slot name a binding declared with `whenNamed`.
|
|
53
|
+
*
|
|
54
|
+
* @remarks Narrowed to the names the token declares, so a request cannot ask for a name no
|
|
55
|
+
* binding could carry; a token declaring none takes any string.
|
|
56
|
+
*/
|
|
57
|
+
name?: Names | undefined;
|
|
52
58
|
/**
|
|
53
59
|
* Single-tag shorthand, equivalent to listing the one pair in `tags`.
|
|
54
60
|
*
|
|
@@ -8,5 +8,5 @@ type ClassAccessorDecorator<This, Value> = (target: ClassAccessorDecoratorTarget
|
|
|
8
8
|
*
|
|
9
9
|
* @since 0.3.16-canary.0
|
|
10
10
|
*/
|
|
11
|
-
export declare function inject<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
|
|
11
|
+
export declare function inject<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
|
|
12
12
|
export {};
|
|
@@ -30,6 +30,8 @@ export function inject(token, options) {
|
|
|
30
30
|
// Derived from the descriptor, not from `options`: the descriptor is where the tag shorthand has
|
|
31
31
|
// already been folded. Built once here rather than per constructed instance.
|
|
32
32
|
const resolveOptions = injectionSlotToResolveOptions(descriptor);
|
|
33
|
+
// The names are checked above; the container lane only needs the value type.
|
|
34
|
+
const resolveKey = token;
|
|
33
35
|
const decoratorFn = (_target, context) => {
|
|
34
36
|
if (context.static) {
|
|
35
37
|
throw new StaticMemberDecoratorError("inject", String(context.name));
|
|
@@ -50,8 +52,8 @@ export function inject(token, options) {
|
|
|
50
52
|
const ambient = getAmbientResolution();
|
|
51
53
|
if (ambient !== undefined) {
|
|
52
54
|
const value = descriptor.optional
|
|
53
|
-
? ambient.resolveOptional(
|
|
54
|
-
: ambient.resolve(
|
|
55
|
+
? ambient.resolveOptional(resolveKey, resolveOptions)
|
|
56
|
+
: ambient.resolve(resolveKey, resolveOptions);
|
|
55
57
|
context.access.set(this, value);
|
|
56
58
|
return;
|
|
57
59
|
}
|
|
@@ -60,8 +62,8 @@ export function inject(token, options) {
|
|
|
60
62
|
throw new MissingContainerContextError(classNameOf(this), context.name);
|
|
61
63
|
}
|
|
62
64
|
const value = descriptor.optional
|
|
63
|
-
? container.resolveOptional(
|
|
64
|
-
: container.resolve(
|
|
65
|
+
? container.resolveOptional(resolveKey, resolveOptions)
|
|
66
|
+
: container.resolve(resolveKey, resolveOptions);
|
|
65
67
|
context.access.set(this, value);
|
|
66
68
|
});
|
|
67
69
|
return {};
|
package/dist/errors/errors.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConstraintRequirement } from "#/core/constraint-requirement";
|
|
1
2
|
import type { BindingIdentifier, BindingScope, ResolveOptions } from "#/core/types";
|
|
2
3
|
/**
|
|
3
4
|
* Base class for every error the library throws, each carrying a machine-readable `code`.
|
|
@@ -132,8 +133,10 @@ export declare class UnreachableConstraintError extends DiError {
|
|
|
132
133
|
readonly code = "UNREACHABLE_CONSTRAINT";
|
|
133
134
|
readonly tokenName: string;
|
|
134
135
|
readonly requiredName: string;
|
|
136
|
+
/** The token the name was required on, or `undefined` when the constraint named no token. */
|
|
137
|
+
readonly requiredTokenName: string | undefined;
|
|
135
138
|
readonly helperName: string;
|
|
136
|
-
constructor(tokenName: string,
|
|
139
|
+
constructor(tokenName: string, requirement: ConstraintRequirement);
|
|
137
140
|
}
|
|
138
141
|
/**
|
|
139
142
|
* A container-level lifecycle hook whose token nothing is bound to, so it can never run.
|
package/dist/errors/errors.js
CHANGED
|
@@ -191,11 +191,17 @@ export class UnreachableConstraintError extends DiError {
|
|
|
191
191
|
code = "UNREACHABLE_CONSTRAINT";
|
|
192
192
|
tokenName;
|
|
193
193
|
requiredName;
|
|
194
|
+
/** The token the name was required on, or `undefined` when the constraint named no token. */
|
|
195
|
+
requiredTokenName;
|
|
194
196
|
helperName;
|
|
195
|
-
constructor(tokenName,
|
|
196
|
-
|
|
197
|
+
constructor(tokenName, requirement) {
|
|
198
|
+
const { name, helperName, tokenName: requiredTokenName } = requirement;
|
|
199
|
+
const scope = requiredTokenName === undefined ? "no binding" : `no binding for '${requiredTokenName}'`;
|
|
200
|
+
const target = requiredTokenName === undefined ? "the binding it should match" : `a '${requiredTokenName}' binding`;
|
|
201
|
+
super(`The binding for '${tokenName}' is constrained by ${helperName} waiting on the slot name '${name}', but ${scope} in this container or its ancestors declares it, so the constraint can never hold. Name the slot with .whenNamed('${name}') on ${target}, or correct the name here.`);
|
|
197
202
|
this.tokenName = tokenName;
|
|
198
|
-
this.requiredName =
|
|
203
|
+
this.requiredName = name;
|
|
204
|
+
this.requiredTokenName = requiredTokenName;
|
|
199
205
|
this.helperName = helperName;
|
|
200
206
|
}
|
|
201
207
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { ActivationHandler, BindingConstraint, BindingIdentifier, BindingKind, BindingScope, BindingTag, ConstraintContext, Constructor, DependencyKey, DeactivationHandler, ResolutionFrame, ResolveOptions, ResolutionContext, TokenValue, } from "#/core/types";
|
|
2
2
|
export { token, tokenName } from "#/core/token";
|
|
3
|
-
export type { Token } from "#/core/token";
|
|
3
|
+
export type { SlotNamesOf, Token } from "#/core/token";
|
|
4
4
|
export { coversTagKeys, NO_TAG_KEYS, slotName, tag, tagKeyMaskOf } from "#/core/tag";
|
|
5
5
|
export type { TagKey, TagKeyMask } from "#/core/tag";
|
|
6
6
|
export type { AliasBindingBuilder, BindToBuilder, BindingBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, SlotConstrainedBuilder, TransientBindingBuilder, } from "#/core/binding";
|
|
@@ -7,8 +7,14 @@ import type { DependencySlot } from "#/injection/resolve-options";
|
|
|
7
7
|
*
|
|
8
8
|
* @since 0.3.16-canary.0
|
|
9
9
|
*/
|
|
10
|
-
export interface InjectOptions {
|
|
11
|
-
|
|
10
|
+
export interface InjectOptions<Names extends string = string> {
|
|
11
|
+
/**
|
|
12
|
+
* The slot name a binding declared with `whenNamed`.
|
|
13
|
+
*
|
|
14
|
+
* @remarks Narrowed to the names the token declares, so a dependency cannot ask for a name no
|
|
15
|
+
* binding could carry; a token declaring none takes any string.
|
|
16
|
+
*/
|
|
17
|
+
name?: Names | undefined;
|
|
12
18
|
/**
|
|
13
19
|
* Single-tag shorthand, equivalent to listing the one pair in `tags`.
|
|
14
20
|
*
|
|
@@ -74,17 +80,17 @@ export declare function normalizeToDescriptor(dependency: InjectableDependency):
|
|
|
74
80
|
*
|
|
75
81
|
* @since 0.6.0
|
|
76
82
|
*/
|
|
77
|
-
export declare function buildInjectionDescriptor<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Value>;
|
|
83
|
+
export declare function buildInjectionDescriptor<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Value>;
|
|
78
84
|
/**
|
|
79
85
|
* Creates a descriptor that resolves to `undefined` instead of throwing when no binding matches.
|
|
80
86
|
*
|
|
81
87
|
* @since 0.3.16-canary.0
|
|
82
88
|
*/
|
|
83
|
-
export declare function optional<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Value | undefined>;
|
|
89
|
+
export declare function optional<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Value | undefined>;
|
|
84
90
|
/**
|
|
85
91
|
* Creates a descriptor that resolves every matching binding for the token into an array.
|
|
86
92
|
*
|
|
87
93
|
* @since 0.3.16-canary.0
|
|
88
94
|
*/
|
|
89
|
-
export declare function injectAll<Value>(token: Token<Value> | Constructor<Value>, options?: InjectOptions): InjectionDescriptor<Array<Value>>;
|
|
95
|
+
export declare function injectAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<InjectOptions<Names>>): InjectionDescriptor<Array<Value>>;
|
|
90
96
|
export {};
|
|
@@ -26,17 +26,20 @@ export declare function whenAnyAncestorIs(token: Token<unknown> | Constructor):
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function whenNoAncestorIs(token: Token<unknown> | Constructor): BindingConstraint;
|
|
28
28
|
/**
|
|
29
|
-
* Matches when the direct parent slot
|
|
29
|
+
* Matches when the direct parent resolves the given token at the slot carrying the given name.
|
|
30
|
+
*
|
|
31
|
+
* @remarks A slot name is a label on one token's bindings, so the token is part of the question — and
|
|
32
|
+
* what types `name` to the names that token declares.
|
|
30
33
|
*
|
|
31
34
|
* @since 0.3.16-canary.0
|
|
32
35
|
*/
|
|
33
|
-
export declare function whenParentNamed(name:
|
|
36
|
+
export declare function whenParentNamed<Names extends string>(token: Token<unknown, Names> | Constructor, name: NoInfer<Names>): BindingConstraint;
|
|
34
37
|
/**
|
|
35
|
-
* Matches when at least one ancestor slot
|
|
38
|
+
* Matches when at least one ancestor resolves the given token at the slot carrying the given name.
|
|
36
39
|
*
|
|
37
40
|
* @since 0.3.16-canary.0
|
|
38
41
|
*/
|
|
39
|
-
export declare function whenAnyAncestorNamed(name:
|
|
42
|
+
export declare function whenAnyAncestorNamed<Names extends string>(token: Token<unknown, Names> | Constructor, name: NoInfer<Names>): BindingConstraint;
|
|
40
43
|
/**
|
|
41
44
|
* Matches when the direct parent slot carries the given tag pair.
|
|
42
45
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { requiringAncestorSlotName } from "#/core/constraint-requirement";
|
|
2
|
-
import { coversTagKeys, tagKeyMaskOf } from "#/core/tag";
|
|
1
|
+
import { requiringAncestorSlotName, requiringAncestorSlotNames } from "#/core/constraint-requirement";
|
|
2
|
+
import { coversTagKeys, slotName, tagKeyMaskOf } from "#/core/tag";
|
|
3
3
|
import { tokenName } from "#/core/token";
|
|
4
4
|
import { EmptyTagCriteriaError } from "#/errors/errors";
|
|
5
5
|
/**
|
|
@@ -39,20 +39,27 @@ export function whenNoAncestorIs(token) {
|
|
|
39
39
|
return (constraintContext) => constraintContext.ancestors.every((ancestorFrame) => ancestorFrame.tokenName !== tokenDisplayName);
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Matches when the direct parent slot
|
|
42
|
+
* Matches when the direct parent resolves the given token at the slot carrying the given name.
|
|
43
|
+
*
|
|
44
|
+
* @remarks A slot name is a label on one token's bindings, so the token is part of the question — and
|
|
45
|
+
* what types `name` to the names that token declares.
|
|
43
46
|
*
|
|
44
47
|
* @since 0.3.16-canary.0
|
|
45
48
|
*/
|
|
46
|
-
export function whenParentNamed(name) {
|
|
47
|
-
|
|
49
|
+
export function whenParentNamed(token, name) {
|
|
50
|
+
const parentTokenName = tokenName(token);
|
|
51
|
+
return requiringAncestorSlotName((constraintContext) => constraintContext.parent !== undefined &&
|
|
52
|
+
constraintContext.parent.tokenName === parentTokenName &&
|
|
53
|
+
constraintContext.parent.slot.name === name, { tokenName: parentTokenName, name, helperName: "whenParentNamed" });
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
|
-
* Matches when at least one ancestor slot
|
|
56
|
+
* Matches when at least one ancestor resolves the given token at the slot carrying the given name.
|
|
51
57
|
*
|
|
52
58
|
* @since 0.3.16-canary.0
|
|
53
59
|
*/
|
|
54
|
-
export function whenAnyAncestorNamed(name) {
|
|
55
|
-
|
|
60
|
+
export function whenAnyAncestorNamed(token, name) {
|
|
61
|
+
const ancestorTokenName = tokenName(token);
|
|
62
|
+
return requiringAncestorSlotName((constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.tokenName === ancestorTokenName && ancestorFrame.slot.name === name), { tokenName: ancestorTokenName, name, helperName: "whenAnyAncestorNamed" });
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
58
65
|
* Matches when the direct parent slot carries the given tag pair.
|
|
@@ -60,7 +67,7 @@ export function whenAnyAncestorNamed(name) {
|
|
|
60
67
|
* @since 0.3.16-canary.0
|
|
61
68
|
*/
|
|
62
69
|
export function whenParentTagged(criterion) {
|
|
63
|
-
return (constraintContext) => constraintContext.parent !== undefined && constraintContext.parent.slot.tags.includes(criterion);
|
|
70
|
+
return requiringReservedNamesAmong((constraintContext) => constraintContext.parent !== undefined && constraintContext.parent.slot.tags.includes(criterion), [criterion], "whenParentTagged");
|
|
64
71
|
}
|
|
65
72
|
/**
|
|
66
73
|
* Matches when at least one ancestor slot carries the given tag pair.
|
|
@@ -68,7 +75,7 @@ export function whenParentTagged(criterion) {
|
|
|
68
75
|
* @since 0.3.16-canary.0
|
|
69
76
|
*/
|
|
70
77
|
export function whenAnyAncestorTagged(criterion) {
|
|
71
|
-
return (constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.slot.tags.includes(criterion));
|
|
78
|
+
return requiringReservedNamesAmong((constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.slot.tags.includes(criterion)), [criterion], "whenAnyAncestorTagged");
|
|
72
79
|
}
|
|
73
80
|
/**
|
|
74
81
|
* Matches when the direct parent slot carries **all** of the given tag pairs.
|
|
@@ -80,13 +87,13 @@ export function whenAnyAncestorTagged(criterion) {
|
|
|
80
87
|
export function whenParentTaggedAll(tags) {
|
|
81
88
|
assertHasCriteria(tags, "whenParentTaggedAll");
|
|
82
89
|
const wanted = tagKeyMaskOf(tags);
|
|
83
|
-
return (constraintContext) => {
|
|
90
|
+
return requiringReservedNamesAmong((constraintContext) => {
|
|
84
91
|
const { parent } = constraintContext;
|
|
85
92
|
if (parent === undefined || !coversTagKeys(parent.slot.keyMask, wanted)) {
|
|
86
93
|
return false;
|
|
87
94
|
}
|
|
88
95
|
return tags.every((criterion) => parent.slot.tags.includes(criterion));
|
|
89
|
-
};
|
|
96
|
+
}, tags, "whenParentTaggedAll");
|
|
90
97
|
}
|
|
91
98
|
/**
|
|
92
99
|
* Matches when at least one ancestor slot carries **all** of the given tag pairs.
|
|
@@ -98,7 +105,21 @@ export function whenParentTaggedAll(tags) {
|
|
|
98
105
|
export function whenAnyAncestorTaggedAll(tags) {
|
|
99
106
|
assertHasCriteria(tags, "whenAnyAncestorTaggedAll");
|
|
100
107
|
const wanted = tagKeyMaskOf(tags);
|
|
101
|
-
return (constraintContext) => constraintContext.ancestors.some((frame) => coversTagKeys(frame.slot.keyMask, wanted) && tags.every((criterion) => frame.slot.tags.includes(criterion)));
|
|
108
|
+
return requiringReservedNamesAmong((constraintContext) => constraintContext.ancestors.some((frame) => coversTagKeys(frame.slot.keyMask, wanted) && tags.every((criterion) => frame.slot.tags.includes(criterion))), tags, "whenAnyAncestorTaggedAll");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Records a requirement for every reserved-key criterion in a list, so a name spelled through the tag
|
|
112
|
+
* lane is validated exactly as one spelled through `whenNamed`.
|
|
113
|
+
*/
|
|
114
|
+
function requiringReservedNamesAmong(predicate, tags, helperName) {
|
|
115
|
+
let requirements;
|
|
116
|
+
for (const criterion of tags) {
|
|
117
|
+
if (criterion.key === slotName) {
|
|
118
|
+
requirements ??= [];
|
|
119
|
+
requirements.push({ tokenName: undefined, name: criterion.value, helperName });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return requirements === undefined ? predicate : requiringAncestorSlotNames(predicate, requirements);
|
|
102
123
|
}
|
|
103
124
|
/**
|
|
104
125
|
* Refuses a criteria list with nothing in it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codefast/di",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Lightweight dependency injection primitives for Codefast",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codefast",
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
"inversion-of-control",
|
|
10
10
|
"typescript"
|
|
11
11
|
],
|
|
12
|
+
"homepage": "https://github.com/codefastlabs/codefast/tree/main/packages/di#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/codefastlabs/codefast/issues"
|
|
15
|
+
},
|
|
12
16
|
"license": "MIT",
|
|
13
|
-
"author": "Vuong Phan <mr.thevuong@gmail.com>",
|
|
17
|
+
"author": "Vuong Phan <mr.thevuong@gmail.com> (https://github.com/thevuong)",
|
|
14
18
|
"repository": {
|
|
15
19
|
"type": "git",
|
|
16
20
|
"url": "git+https://github.com/codefastlabs/codefast.git",
|
|
@@ -28,20 +32,6 @@
|
|
|
28
32
|
"module": "./dist/index.js",
|
|
29
33
|
"types": "./dist/index.d.ts",
|
|
30
34
|
"imports": {
|
|
31
|
-
"#/tests/*": [
|
|
32
|
-
"./tests/*",
|
|
33
|
-
"./tests/*.ts",
|
|
34
|
-
"./tests/*.tsx",
|
|
35
|
-
"./tests/*/index.ts",
|
|
36
|
-
"./tests/*/index.tsx"
|
|
37
|
-
],
|
|
38
|
-
"#/examples/*": [
|
|
39
|
-
"./examples/*",
|
|
40
|
-
"./examples/*.ts",
|
|
41
|
-
"./examples/*.tsx",
|
|
42
|
-
"./examples/*/index.ts",
|
|
43
|
-
"./examples/*/index.tsx"
|
|
44
|
-
],
|
|
45
35
|
"#/*": {
|
|
46
36
|
"types": "./dist/*.d.ts",
|
|
47
37
|
"default": "./dist/*.js"
|
|
@@ -225,31 +215,7 @@
|
|
|
225
215
|
"publishConfig": {
|
|
226
216
|
"access": "public"
|
|
227
217
|
},
|
|
228
|
-
"devDependencies": {
|
|
229
|
-
"@babel/core": "^8.0.1",
|
|
230
|
-
"@babel/plugin-proposal-decorators": "8.0.2",
|
|
231
|
-
"@rolldown/plugin-babel": "^0.2.3",
|
|
232
|
-
"@types/node": "^26.4.1",
|
|
233
|
-
"@vitest/coverage-v8": "^5.0.0",
|
|
234
|
-
"tsx": "^4.23.13",
|
|
235
|
-
"typescript": "^7.0.2",
|
|
236
|
-
"vitest": "^5.0.0",
|
|
237
|
-
"@codefast/typescript-config": "0.9.0"
|
|
238
|
-
},
|
|
239
218
|
"engines": {
|
|
240
219
|
"node": ">=24.0.0"
|
|
241
|
-
},
|
|
242
|
-
"scripts": {
|
|
243
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
244
|
-
"check-types": "tsc --noEmit",
|
|
245
|
-
"clean": "rm -rf dist",
|
|
246
|
-
"examples": "node examples/run.mjs",
|
|
247
|
-
"test": "vitest run",
|
|
248
|
-
"test:coverage": "vitest run --coverage",
|
|
249
|
-
"test:e2e": "vitest run tests/e2e",
|
|
250
|
-
"test:integration": "vitest run tests/integration",
|
|
251
|
-
"test:type": "vitest run tests/types",
|
|
252
|
-
"test:unit": "vitest run tests/unit",
|
|
253
|
-
"test:watch": "vitest"
|
|
254
220
|
}
|
|
255
221
|
}
|