@codefast/di 0.10.1 → 0.12.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 +376 -0
- package/README.md +94 -17
- package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
- package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
- package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
- package/dist/container/binding-chain.js +254 -0
- package/dist/container/container.d.ts +11 -2
- package/dist/container/container.js +293 -73
- package/dist/core/binding-builders.d.ts +114 -0
- package/dist/core/binding-declaration.d.ts +127 -0
- package/dist/core/binding-declaration.js +187 -0
- package/dist/core/binding.d.ts +62 -138
- package/dist/core/binding.js +54 -17
- package/dist/core/module.d.ts +8 -5
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +17 -28
- package/dist/core/registry.js +92 -133
- package/dist/core/state-epoch.d.ts +18 -1
- package/dist/core/state-epoch.js +17 -0
- package/dist/core/tag.js +1 -1
- package/dist/decorators/inject.js +6 -5
- package/dist/decorators/injectable.js +4 -2
- package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
- package/dist/decorators/metadata-record.d.ts +9 -0
- package/dist/decorators/metadata-record.js +20 -0
- package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
- package/dist/{errors/errors.js → errors.js} +101 -17
- package/dist/index.d.ts +14 -10
- package/dist/index.js +7 -6
- package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
- package/dist/injection/descriptor.d.ts +1 -1
- package/dist/injection/descriptor.js +3 -7
- package/dist/introspection/dependency-graph.d.ts +8 -3
- package/dist/introspection/dependency-graph.js +48 -26
- package/dist/introspection/explanation.d.ts +101 -0
- package/dist/introspection/explanation.js +151 -0
- package/dist/introspection/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.d.ts +9 -2
- package/dist/introspection/inspector.js +30 -28
- package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
- package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
- package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
- package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
- package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
- package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
- package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
- package/dist/metadata/types.js +1 -0
- package/dist/metadata/verify.d.ts +43 -0
- package/dist/metadata/verify.js +133 -0
- package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
- package/dist/metadata/verifying-reader.js +53 -0
- package/dist/resolution/async-fan-out.d.ts +12 -0
- package/dist/resolution/async-fan-out.js +26 -0
- package/dist/resolution/cache/activation-need.d.ts +1 -2
- package/dist/resolution/cache/activation-need.js +16 -19
- package/dist/resolution/cache/class-introspector.d.ts +38 -36
- package/dist/resolution/cache/class-introspector.js +79 -159
- package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
- package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
- package/dist/resolution/context.d.ts +16 -24
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path.d.ts +128 -0
- package/dist/resolution/path.js +167 -0
- package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
- package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
- package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
- package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
- package/dist/resolution/resolver.d.ts +11 -12
- package/dist/resolution/resolver.js +573 -356
- package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
- package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
- package/dist/resolution/select/constraints.js +1 -1
- package/package.json +87 -67
- package/dist/container/binding-builders.js +0 -305
- package/dist/core/binding-scope.d.ts +0 -12
- package/dist/core/binding-scope.js +0 -12
- package/dist/metadata/verifying-metadata-reader.js +0 -31
- package/dist/resolution/path/resolution-path.d.ts +0 -93
- package/dist/resolution/path/resolution-path.js +0 -116
- /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
- /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
- /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
- /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
- /package/dist/{errors → introspection}/diagnostics.js +0 -0
- /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
- /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
- /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -6,26 +6,27 @@ export { coversTagKeys, NO_TAG_KEYS, slotName, tag, tagKeyMaskOf } from "#core/t
|
|
|
6
6
|
export { Container } from "#container/container";
|
|
7
7
|
// Ambient container — the context an `@inject` accessor initializer resolves from. `resolution/context`
|
|
8
8
|
// stays internal: it hands out resolver callbacks, not public values.
|
|
9
|
-
export { getActiveContainer, runWithContainer } from "#ambient
|
|
9
|
+
export { getActiveContainer, runWithContainer } from "#ambient-container";
|
|
10
10
|
// `effectiveBindingScope` is deliberately absent: it reads a `Binding`, which is internal, and no
|
|
11
11
|
// public API hands one out. `BindingSnapshot.scope` and `GraphNode.scope` are the public answers.
|
|
12
|
-
export { bindingSlotToResolveOptions, injectionSlotToResolveOptions, resolveOptionsForSlot, } from "#injection/
|
|
12
|
+
export { bindingSlotToResolveOptions, injectionSlotToResolveOptions, resolveOptionsForSlot, } from "#injection/dependency-slot";
|
|
13
13
|
// Module
|
|
14
14
|
export { AsyncModule, isSyncModule, Module, SyncModule } from "#core/module";
|
|
15
|
+
export { binding } from "#core/binding-declaration";
|
|
15
16
|
// Decorators
|
|
16
17
|
export { inject } from "#decorators/inject";
|
|
17
18
|
export { injectAll, isInjectionDescriptor, optional } from "#injection/descriptor";
|
|
18
19
|
export { injectable } from "#decorators/injectable";
|
|
19
|
-
export { postConstruct, preDestroy } from "#decorators/lifecycle
|
|
20
|
+
export { postConstruct, preDestroy } from "#decorators/lifecycle";
|
|
20
21
|
// Auto-register
|
|
21
22
|
export { createAutoRegisterRegistry } from "#decorators/injectable";
|
|
22
23
|
// MetadataReader — everything a consumer needs to write one and pass it to Container.create()
|
|
23
|
-
export { MetadataReaderToken } from "#metadata/
|
|
24
|
-
export { defaultMetadataReader, SymbolMetadataReader } from "#metadata/symbol-
|
|
24
|
+
export { MetadataReaderToken } from "#metadata/reader-token";
|
|
25
|
+
export { defaultMetadataReader, SymbolMetadataReader } from "#metadata/symbol-reader";
|
|
25
26
|
// Constraints — contextual injection predicates for .when()
|
|
26
27
|
export { whenAnyAncestorIs, whenAnyAncestorNamed, whenAnyAncestorTagged, whenAnyAncestorTaggedAll, whenNoAncestorIs, whenNoParentIs, whenParentIs, whenParentNamed, whenParentTagged, whenParentTaggedAll, } from "#resolution/select/constraints";
|
|
27
28
|
// Errors
|
|
28
|
-
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidMetadataError, MissingContainerContextError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors
|
|
29
|
+
export { AmbiguousBindingError, AsyncActivationError, AsyncDeactivationError, AsyncModuleLoadError, AsyncResolutionError, ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, CircularDependencyError, DiError, DisposedContainerError, InternalError, InvalidBindingDeclarationError, InvalidMetadataError, MissingContainerContextError, MissingDecoratorMetadataError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, RebindUnboundTokenError, ScopeViolationError, SelfBindingRequiresClassError, StaticMemberDecoratorError, SymbolKeyedLifecycleError, SyncDisposalNotSupportedError, EmptyTagCriteriaError, TokenNotBoundError, UnreachableConstraintError, UnreachableLifecycleHookError, } from "#errors";
|
|
29
30
|
// Graph adapters — render `generateDependencyGraph()` output for common viewers
|
|
30
31
|
export { toDotGraph } from "#introspection/graph-adapters/dot";
|
|
31
32
|
export { toCytoscapeGraph } from "#introspection/graph-adapters/cytoscape";
|
|
@@ -49,15 +49,17 @@ function loneNameCriterionOf(options) {
|
|
|
49
49
|
}
|
|
50
50
|
/** Shared core: build a ResolveOptions from already-normalised name + tags. */
|
|
51
51
|
function buildOptions(name, tags) {
|
|
52
|
-
|
|
52
|
+
// An empty criterion list selects nothing, so it states no criterion at all.
|
|
53
|
+
const criteria = tags !== undefined && tags.length > 0 ? tags : undefined;
|
|
54
|
+
if (name === undefined && criteria === undefined) {
|
|
53
55
|
return undefined;
|
|
54
56
|
}
|
|
55
57
|
const options = {};
|
|
56
58
|
if (name !== undefined) {
|
|
57
59
|
options.name = name;
|
|
58
60
|
}
|
|
59
|
-
if (
|
|
60
|
-
options.tags =
|
|
61
|
+
if (criteria !== undefined) {
|
|
62
|
+
options.tags = criteria;
|
|
61
63
|
}
|
|
62
64
|
return options;
|
|
63
65
|
}
|
|
@@ -85,7 +87,7 @@ const MEMOIZED_SINGLE_CRITERION = Symbol("di:single-criterion");
|
|
|
85
87
|
*/
|
|
86
88
|
export function resolveOptionsForSlot(injectionSlot) {
|
|
87
89
|
const { name, tags } = injectionSlot;
|
|
88
|
-
if (name === undefined && tags === undefined) {
|
|
90
|
+
if (name === undefined && (tags === undefined || tags.length === 0)) {
|
|
89
91
|
return undefined;
|
|
90
92
|
}
|
|
91
93
|
const slot = injectionSlot;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The one shape every declared dependency is normalised to, whatever channel declared it. */
|
|
2
2
|
import type { Token } from "#core/token";
|
|
3
3
|
import type { BindingTag, Constructor, TokenValue } from "#core/types";
|
|
4
|
-
import type { DependencySlot } from "#injection/
|
|
4
|
+
import type { DependencySlot } from "#injection/dependency-slot";
|
|
5
5
|
/**
|
|
6
6
|
* Slot-selection options — a name and tags — a declared dependency narrows its binding with.
|
|
7
7
|
*
|
|
@@ -4,12 +4,8 @@
|
|
|
4
4
|
* @since 0.3.16-canary.0
|
|
5
5
|
*/
|
|
6
6
|
export function isInjectionDescriptor(value) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
const type = typeof value;
|
|
11
|
-
// inject() returns a function (dual-role), so must check both object and function
|
|
12
|
-
if (type !== "object" && type !== "function") {
|
|
7
|
+
// inject() returns a function (dual-role), so both an object and a function qualify.
|
|
8
|
+
if ((typeof value !== "object" && typeof value !== "function") || value === null) {
|
|
13
9
|
return false;
|
|
14
10
|
}
|
|
15
11
|
return ("token" in value &&
|
|
@@ -106,7 +102,7 @@ export function buildInjectionDescriptor(token, options) {
|
|
|
106
102
|
*/
|
|
107
103
|
export function optional(token, options) {
|
|
108
104
|
return withOptions({
|
|
109
|
-
token
|
|
105
|
+
token,
|
|
110
106
|
optional: true,
|
|
111
107
|
multi: false,
|
|
112
108
|
}, options);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BindingRegistry } from "#core/registry";
|
|
2
2
|
import type { BindingKind, BindingScope } from "#core/types";
|
|
3
|
-
import type { MetadataReader } from "#metadata/
|
|
3
|
+
import type { MetadataReader } from "#metadata/types";
|
|
4
4
|
/**
|
|
5
5
|
* @remarks `kind`/`scope` are `"unbound"` for the placeholder node an optional, currently
|
|
6
6
|
* unsatisfied dependency points at.
|
|
@@ -49,8 +49,13 @@ export interface GraphOptions {
|
|
|
49
49
|
readonly includeParent?: boolean | undefined;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* Builds the JSON dependency graph of a registry's bindings, optionally including
|
|
52
|
+
* Builds the JSON dependency graph of a registry's bindings, optionally including its ancestors'.
|
|
53
|
+
*
|
|
54
|
+
* @param registry - The registry whose bindings the graph is for.
|
|
55
|
+
* @param metadataReader - The reader class dependencies are read through.
|
|
56
|
+
* @param options - Whether the ancestors' bindings join the graph.
|
|
57
|
+
* @param ancestorRegistries - The ancestor containers' registries, nearest first.
|
|
53
58
|
*
|
|
54
59
|
* @since 0.3.16-canary.0
|
|
55
60
|
*/
|
|
56
|
-
export declare function buildDependencyGraph(registry: BindingRegistry, metadataReader: MetadataReader, options: GraphOptions | undefined,
|
|
61
|
+
export declare function buildDependencyGraph(registry: BindingRegistry, metadataReader: MetadataReader, options: GraphOptions | undefined, ancestorRegistries?: ReadonlyArray<BindingRegistry>): ContainerGraphJson;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { effectiveBindingScope } from "#core/binding
|
|
1
|
+
import { effectiveBindingScope, stringifyTagValue } from "#core/binding";
|
|
2
2
|
import { slotName } from "#core/tag";
|
|
3
3
|
import { tokenName } from "#core/token";
|
|
4
|
-
import { bindingSlotToResolveOptions } from "#injection/
|
|
5
|
-
import { matchesSlot } from "#resolution/select/
|
|
4
|
+
import { bindingSlotToResolveOptions } from "#injection/dependency-slot";
|
|
5
|
+
import { matchesSlot } from "#resolution/select/candidates";
|
|
6
6
|
// ── Builder ──────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
7
7
|
// Tokens are compared by object identity, and a name is free to repeat, so the graph mints its
|
|
8
8
|
// own per-process key. Weakly held: a discarded token takes its key with it.
|
|
@@ -41,18 +41,31 @@ function edgeLabel(ref, index) {
|
|
|
41
41
|
const criterion = name !== undefined
|
|
42
42
|
? `name:${name}`
|
|
43
43
|
: firstPlainTag !== undefined
|
|
44
|
-
? `tag:${firstPlainTag.key.name}=${
|
|
44
|
+
? `tag:${firstPlainTag.key.name}=${stringifyTagValue(firstPlainTag.value)}`
|
|
45
45
|
: `[${index}]`;
|
|
46
46
|
return ref.optional ? `${criterion} optional` : criterion;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The bindings a dependency reaches, found the way resolution finds them.
|
|
50
|
+
*
|
|
51
|
+
* @remarks A single dependency stops at the nearest registry holding a binding its slot matches, and
|
|
52
|
+
* a collection gathers every registry's matches — `resolve` and `resolveAll` respectively.
|
|
53
|
+
*/
|
|
54
|
+
function dependencyTargets(chain, ref) {
|
|
55
|
+
if (ref.multi) {
|
|
56
|
+
const gathered = [];
|
|
57
|
+
for (const registry of chain) {
|
|
58
|
+
gathered.push(...matchingTargets(registry.getAll(ref.token), ref));
|
|
59
|
+
}
|
|
60
|
+
return gathered;
|
|
61
|
+
}
|
|
62
|
+
for (const registry of chain) {
|
|
63
|
+
const targets = matchingTargets(registry.getAll(ref.token), ref);
|
|
64
|
+
if (targets.length > 0) {
|
|
65
|
+
return targets;
|
|
53
66
|
}
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
56
69
|
}
|
|
57
70
|
/** The placeholder node an optional-but-unbound dependency points at, minted once per token. */
|
|
58
71
|
function unboundNodeIdFor(accumulator, dependency) {
|
|
@@ -73,8 +86,8 @@ function unboundNodeIdFor(accumulator, dependency) {
|
|
|
73
86
|
});
|
|
74
87
|
return id;
|
|
75
88
|
}
|
|
76
|
-
function addDependencyEdges(accumulator, from, ref, index,
|
|
77
|
-
const targets =
|
|
89
|
+
function addDependencyEdges(accumulator, from, ref, index, chain) {
|
|
90
|
+
const targets = dependencyTargets(chain, ref);
|
|
78
91
|
const label = edgeLabel(ref, index);
|
|
79
92
|
if (targets.length === 0) {
|
|
80
93
|
// A required-but-unbound dependency is validate()'s story, not the graph's.
|
|
@@ -108,25 +121,25 @@ function addDependencyEdges(accumulator, from, ref, index, lookup) {
|
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
123
|
/** What one binding declares up front — a class's params, a factory's descriptors, an alias's target. */
|
|
111
|
-
function addBindingEdges(accumulator, binding, metadataReader,
|
|
124
|
+
function addBindingEdges(accumulator, binding, metadataReader, chain) {
|
|
112
125
|
if (binding.kind === "class") {
|
|
113
126
|
const meta = metadataReader.getConstructorMetadata(binding.target);
|
|
114
127
|
if (meta !== undefined) {
|
|
115
128
|
for (const [index, param] of meta.params.entries()) {
|
|
116
|
-
addDependencyEdges(accumulator, String(binding.identifier), param, index,
|
|
129
|
+
addDependencyEdges(accumulator, String(binding.identifier), param, index, chain);
|
|
117
130
|
}
|
|
118
131
|
}
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
121
134
|
if (binding.kind === "resolved" || binding.kind === "resolved-async") {
|
|
122
135
|
for (const [index, dependency] of binding.deps.entries()) {
|
|
123
|
-
addDependencyEdges(accumulator, String(binding.identifier), dependency, index,
|
|
136
|
+
addDependencyEdges(accumulator, String(binding.identifier), dependency, index, chain);
|
|
124
137
|
}
|
|
125
138
|
return;
|
|
126
139
|
}
|
|
127
140
|
if (binding.kind === "alias") {
|
|
128
141
|
const aliasRef = { token: binding.target, optional: false, multi: false };
|
|
129
|
-
for (const target of
|
|
142
|
+
for (const target of dependencyTargets(chain, aliasRef)) {
|
|
130
143
|
accumulator.edges.push({
|
|
131
144
|
from: String(binding.identifier),
|
|
132
145
|
to: String(target.identifier),
|
|
@@ -136,9 +149,9 @@ function addBindingEdges(accumulator, binding, metadataReader, lookup) {
|
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (const binding of
|
|
152
|
+
/** Adds the nodes of `chain[0]`, the registry the chain starts at, and the edges each of its bindings declares. */
|
|
153
|
+
function addRegistryBindings(accumulator, chain, metadataReader, fromParent) {
|
|
154
|
+
for (const binding of chain[0].allBindings()) {
|
|
142
155
|
accumulator.nodes.push({
|
|
143
156
|
id: String(binding.identifier),
|
|
144
157
|
tokenName: tokenName(binding.token),
|
|
@@ -147,20 +160,29 @@ function addRegistryBindings(accumulator, sourceRegistry, metadataReader, fromPa
|
|
|
147
160
|
scope: effectiveBindingScope(binding),
|
|
148
161
|
fromParent,
|
|
149
162
|
});
|
|
150
|
-
addBindingEdges(accumulator, binding, metadataReader,
|
|
163
|
+
addBindingEdges(accumulator, binding, metadataReader, chain);
|
|
151
164
|
}
|
|
152
165
|
}
|
|
153
166
|
/**
|
|
154
|
-
* Builds the JSON dependency graph of a registry's bindings, optionally including
|
|
167
|
+
* Builds the JSON dependency graph of a registry's bindings, optionally including its ancestors'.
|
|
168
|
+
*
|
|
169
|
+
* @param registry - The registry whose bindings the graph is for.
|
|
170
|
+
* @param metadataReader - The reader class dependencies are read through.
|
|
171
|
+
* @param options - Whether the ancestors' bindings join the graph.
|
|
172
|
+
* @param ancestorRegistries - The ancestor containers' registries, nearest first.
|
|
155
173
|
*
|
|
156
174
|
* @since 0.3.16-canary.0
|
|
157
175
|
*/
|
|
158
|
-
export function buildDependencyGraph(registry, metadataReader, options,
|
|
176
|
+
export function buildDependencyGraph(registry, metadataReader, options, ancestorRegistries = []) {
|
|
159
177
|
const accumulator = { nodes: [], edges: [], unboundNodeIds: new Map() };
|
|
160
178
|
const includesParent = options?.includeParent === true;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
if (!includesParent) {
|
|
180
|
+
addRegistryBindings(accumulator, [registry], metadataReader, false);
|
|
181
|
+
return { nodes: accumulator.nodes, edges: accumulator.edges, includesParent };
|
|
182
|
+
}
|
|
183
|
+
const chain = [registry, ...ancestorRegistries];
|
|
184
|
+
for (let depth = 0; depth < chain.length; depth += 1) {
|
|
185
|
+
addRegistryBindings(accumulator, chain.slice(depth), metadataReader, depth > 0);
|
|
164
186
|
}
|
|
165
187
|
return { nodes: accumulator.nodes, edges: accumulator.edges, includesParent };
|
|
166
188
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { BindingRegistry } from "#core/registry";
|
|
2
|
+
import type { Token } from "#core/token";
|
|
3
|
+
import type { Constructor, ResolveOptions } from "#core/types";
|
|
4
|
+
import type { BindingSnapshot } from "#introspection/inspector";
|
|
5
|
+
import type { CandidateRule } from "#resolution/select/candidates";
|
|
6
|
+
/**
|
|
7
|
+
* A request to explain: the criteria a `resolve` would carry, and the resolutions it is nested in.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam Names - the slot names the token declares, as `ResolveOptions` narrows them
|
|
10
|
+
*
|
|
11
|
+
* @since 0.12.0
|
|
12
|
+
*/
|
|
13
|
+
export interface ExplainOptions<Names extends string = string> extends ResolveOptions<Names> {
|
|
14
|
+
/**
|
|
15
|
+
* The tokens whose resolution makes this request, outermost first, so a `when()` predicate reads the parent
|
|
16
|
+
* and ancestors it would read inside their factories.
|
|
17
|
+
*
|
|
18
|
+
* @remarks Each is selected as a request with no criteria, nested in the ones before it; one that selects
|
|
19
|
+
* nothing throws what its own `resolve` would.
|
|
20
|
+
*/
|
|
21
|
+
readonly ancestors?: ReadonlyArray<Token<unknown> | Constructor> | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What selection made of one binding: in the running, or out and why.
|
|
25
|
+
*
|
|
26
|
+
* @remarks `slot-mismatch`: the slot declares a criterion the request does not carry, or the request carries
|
|
27
|
+
* criteria and the slot is the default one. `predicate-refused`: the slot matched and the `when()` predicate
|
|
28
|
+
* returned `false`. `collection-member`: a `many()` binding, which only `resolveAll` takes.
|
|
29
|
+
*
|
|
30
|
+
* @since 0.12.0
|
|
31
|
+
*/
|
|
32
|
+
export type CandidateVerdict = "eligible" | "slot-mismatch" | "predicate-refused" | "collection-member";
|
|
33
|
+
/**
|
|
34
|
+
* The rule that settled one registry's answer.
|
|
35
|
+
*
|
|
36
|
+
* @remarks The four candidate rules of `CandidateRule`, plus `default-alias`: no slot matched, and the token's
|
|
37
|
+
* default-slot alias forwards the request's criteria to its target.
|
|
38
|
+
*
|
|
39
|
+
* @since 0.12.0
|
|
40
|
+
*/
|
|
41
|
+
export type SelectionRule = CandidateRule | "default-alias";
|
|
42
|
+
/**
|
|
43
|
+
* How an explained lookup ended, one outcome for each way `resolve` ends.
|
|
44
|
+
*
|
|
45
|
+
* @remarks `unbound` and `unmatched` are the lookups `resolve` reports as `TokenNotBoundError` and
|
|
46
|
+
* `NoMatchingBindingError`; `ambiguous` and `alias-cycle` are `AmbiguousBindingError` and `CircularDependencyError`.
|
|
47
|
+
*
|
|
48
|
+
* @since 0.12.0
|
|
49
|
+
*/
|
|
50
|
+
export type ExplanationOutcome = "selected" | "unbound" | "unmatched" | "ambiguous" | "alias-cycle";
|
|
51
|
+
/**
|
|
52
|
+
* One binding a registry offered the request, and what selection made of it.
|
|
53
|
+
*
|
|
54
|
+
* @since 0.12.0
|
|
55
|
+
*/
|
|
56
|
+
export interface CandidateExplanation {
|
|
57
|
+
readonly binding: BindingSnapshot;
|
|
58
|
+
readonly verdict: CandidateVerdict;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* One registry the lookup read, in the order `resolve` reads them.
|
|
62
|
+
*
|
|
63
|
+
* @since 0.12.0
|
|
64
|
+
*/
|
|
65
|
+
export interface ExplanationStep {
|
|
66
|
+
/** The token looked up here, which an alias hop changes. */
|
|
67
|
+
readonly tokenName: string;
|
|
68
|
+
/** How far up the chain the registry sits: `0` is the container asked, `1` its parent. */
|
|
69
|
+
readonly depth: number;
|
|
70
|
+
/** Every binding this registry holds for the token, in registration order. */
|
|
71
|
+
readonly candidates: ReadonlyArray<CandidateExplanation>;
|
|
72
|
+
/** The rule that settled this registry's answer, or `undefined` when no candidate was eligible. */
|
|
73
|
+
readonly rule: SelectionRule | undefined;
|
|
74
|
+
/** The binding this registry answered with, an alias included, or `undefined` when the lookup moved on. */
|
|
75
|
+
readonly selected: BindingSnapshot | undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Why one request selects the binding it does, or why it selects none.
|
|
79
|
+
*
|
|
80
|
+
* @since 0.12.0
|
|
81
|
+
*/
|
|
82
|
+
export interface ResolutionExplanation {
|
|
83
|
+
/** The token the request asks for. */
|
|
84
|
+
readonly tokenName: string;
|
|
85
|
+
/** Every registry that holds a token the lookup asked for, once per alias hop, in the order it read them. */
|
|
86
|
+
readonly steps: ReadonlyArray<ExplanationStep>;
|
|
87
|
+
/** The binding the request resolves to after every alias, or `undefined` when it resolves to none. */
|
|
88
|
+
readonly selected: BindingSnapshot | undefined;
|
|
89
|
+
readonly outcome: ExplanationOutcome;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Explains a request against a container chain, without instantiating anything.
|
|
93
|
+
*
|
|
94
|
+
* @param registries - the asked container's registry first, then each ancestor's up to the root
|
|
95
|
+
* @param token - the token the request asks for
|
|
96
|
+
* @param options - the request's criteria and the resolutions it is nested in
|
|
97
|
+
* @throws An error `resolve` would throw, when one of `options.ancestors` selects no binding
|
|
98
|
+
*
|
|
99
|
+
* @since 0.12.0
|
|
100
|
+
*/
|
|
101
|
+
export declare function explainRequest(registries: ReadonlyArray<BindingRegistry>, token: Token<unknown> | Constructor, options: ExplainOptions | undefined): ResolutionExplanation;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { bindingSlotToString } from "#core/binding";
|
|
2
|
+
import { tokenName } from "#core/token";
|
|
3
|
+
import { AmbiguousBindingError, CircularDependencyError, InternalError, NoMatchingBindingError, TokenNotBoundError, } from "#errors";
|
|
4
|
+
import { snapshotOf } from "#introspection/inspector";
|
|
5
|
+
import { DefaultConstraintContext } from "#resolution/context";
|
|
6
|
+
import { buildResolutionFrame } from "#resolution/path";
|
|
7
|
+
import { candidateRuleOf, chooseCandidate, matchesSlot } from "#resolution/select/candidates";
|
|
8
|
+
// ── Explanation ──────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Explains a request against a container chain, without instantiating anything.
|
|
11
|
+
*
|
|
12
|
+
* @param registries - the asked container's registry first, then each ancestor's up to the root
|
|
13
|
+
* @param token - the token the request asks for
|
|
14
|
+
* @param options - the request's criteria and the resolutions it is nested in
|
|
15
|
+
* @throws An error `resolve` would throw, when one of `options.ancestors` selects no binding
|
|
16
|
+
*
|
|
17
|
+
* @since 0.12.0
|
|
18
|
+
*/
|
|
19
|
+
export function explainRequest(registries, token, options) {
|
|
20
|
+
const stack = [];
|
|
21
|
+
for (const ancestor of options?.ancestors ?? []) {
|
|
22
|
+
const walk = walkLookup(registries, ancestor, undefined, stack);
|
|
23
|
+
if (walk.binding === undefined) {
|
|
24
|
+
throw lookupError(registries, walk);
|
|
25
|
+
}
|
|
26
|
+
stack.push(buildResolutionFrame(walk.binding, tokenName(walk.binding.token)));
|
|
27
|
+
}
|
|
28
|
+
const walk = walkLookup(registries, token, requestOf(options), stack);
|
|
29
|
+
return {
|
|
30
|
+
tokenName: tokenName(token),
|
|
31
|
+
steps: walk.steps,
|
|
32
|
+
selected: walk.binding === undefined ? undefined : snapshotOf(walk.binding),
|
|
33
|
+
outcome: walk.outcome,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// The `ancestors` key is explain's own, and a request carrying no criterion is the request with no options.
|
|
37
|
+
function requestOf(options) {
|
|
38
|
+
if (options === undefined ||
|
|
39
|
+
(options.name === undefined && options.tag === undefined && options.tags === undefined)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const { ancestors: _ancestors, ...request } = options;
|
|
43
|
+
return request;
|
|
44
|
+
}
|
|
45
|
+
function walkLookup(registries, token, request, stack) {
|
|
46
|
+
const ctx = new DefaultConstraintContext(stack, request);
|
|
47
|
+
const steps = [];
|
|
48
|
+
let current = token;
|
|
49
|
+
let visited;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const found = findInChain(registries, current, request, ctx, steps);
|
|
52
|
+
if (found.kind === "tie") {
|
|
53
|
+
return {
|
|
54
|
+
steps,
|
|
55
|
+
binding: undefined,
|
|
56
|
+
outcome: "ambiguous",
|
|
57
|
+
lastToken: current,
|
|
58
|
+
ambiguity: found.eligible,
|
|
59
|
+
cycle: undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (found.kind === "none") {
|
|
63
|
+
const outcome = registries.some((registry) => registry.getAll(current).length > 0) ? "unmatched" : "unbound";
|
|
64
|
+
return { steps, binding: undefined, outcome, lastToken: current, ambiguity: undefined, cycle: undefined };
|
|
65
|
+
}
|
|
66
|
+
const binding = found.binding;
|
|
67
|
+
if (binding.kind !== "alias") {
|
|
68
|
+
return { steps, binding, outcome: "selected", lastToken: current, ambiguity: undefined, cycle: undefined };
|
|
69
|
+
}
|
|
70
|
+
// An alias hop restarts at the container asked, as `resolve` does, and a revisited token is a cycle.
|
|
71
|
+
const target = binding.target;
|
|
72
|
+
visited ??= new Set([current]);
|
|
73
|
+
if (visited.has(target)) {
|
|
74
|
+
return {
|
|
75
|
+
steps,
|
|
76
|
+
binding: undefined,
|
|
77
|
+
outcome: "alias-cycle",
|
|
78
|
+
lastToken: current,
|
|
79
|
+
ambiguity: undefined,
|
|
80
|
+
cycle: [...visited, target],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
visited.add(target);
|
|
84
|
+
current = target;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const NO_ANSWER = { kind: "none" };
|
|
88
|
+
function findInChain(registries, token, request, ctx, steps) {
|
|
89
|
+
for (let depth = 0; depth < registries.length; depth += 1) {
|
|
90
|
+
const registry = registries[depth];
|
|
91
|
+
const bindings = registry.getAll(token);
|
|
92
|
+
if (bindings.length === 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const candidates = [];
|
|
96
|
+
const eligible = [];
|
|
97
|
+
for (const binding of bindings) {
|
|
98
|
+
const verdict = verdictOf(binding, request, ctx);
|
|
99
|
+
candidates.push({ binding: snapshotOf(binding), verdict });
|
|
100
|
+
if (verdict === "eligible") {
|
|
101
|
+
eligible.push(binding);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const name = tokenName(token);
|
|
105
|
+
if (eligible.length > 0) {
|
|
106
|
+
const selected = chooseCandidate(eligible);
|
|
107
|
+
steps.push({
|
|
108
|
+
tokenName: name,
|
|
109
|
+
depth,
|
|
110
|
+
candidates,
|
|
111
|
+
rule: candidateRuleOf(eligible, selected),
|
|
112
|
+
selected: selected === undefined ? undefined : snapshotOf(selected),
|
|
113
|
+
});
|
|
114
|
+
return selected === undefined ? { kind: "tie", eligible } : { kind: "binding", binding: selected };
|
|
115
|
+
}
|
|
116
|
+
// A request carrying criteria that no slot here matches is forwarded by the token's default-slot alias.
|
|
117
|
+
const defaultSlot = request === undefined ? undefined : registry.defaultSlotAlias(token);
|
|
118
|
+
if (defaultSlot !== undefined) {
|
|
119
|
+
steps.push({ tokenName: name, depth, candidates, rule: "default-alias", selected: snapshotOf(defaultSlot) });
|
|
120
|
+
return { kind: "binding", binding: defaultSlot };
|
|
121
|
+
}
|
|
122
|
+
steps.push({ tokenName: name, depth, candidates, rule: undefined, selected: undefined });
|
|
123
|
+
}
|
|
124
|
+
return NO_ANSWER;
|
|
125
|
+
}
|
|
126
|
+
function verdictOf(binding, request, ctx) {
|
|
127
|
+
if (binding.isMany) {
|
|
128
|
+
return "collection-member";
|
|
129
|
+
}
|
|
130
|
+
if (!matchesSlot(binding.slot, request)) {
|
|
131
|
+
return "slot-mismatch";
|
|
132
|
+
}
|
|
133
|
+
const predicate = binding.predicate;
|
|
134
|
+
return predicate === undefined || predicate(ctx) ? "eligible" : "predicate-refused";
|
|
135
|
+
}
|
|
136
|
+
/** The error `resolve` throws for a lookup that ended without a binding. */
|
|
137
|
+
function lookupError(registries, walk) {
|
|
138
|
+
const name = tokenName(walk.lastToken);
|
|
139
|
+
switch (walk.outcome) {
|
|
140
|
+
case "ambiguous":
|
|
141
|
+
return new AmbiguousBindingError(name, (walk.ambiguity ?? []).map((binding) => binding.identifier));
|
|
142
|
+
case "alias-cycle":
|
|
143
|
+
return new CircularDependencyError((walk.cycle ?? []).map((entry) => tokenName(entry)));
|
|
144
|
+
case "unmatched":
|
|
145
|
+
return new NoMatchingBindingError(name, {}, registries.flatMap((registry) => registry.getAll(walk.lastToken).map((binding) => bindingSlotToString(binding.slot))));
|
|
146
|
+
case "unbound":
|
|
147
|
+
return new TokenNotBoundError(name);
|
|
148
|
+
case "selected":
|
|
149
|
+
return new InternalError("a lookup that selected a binding has no error");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Initial grid
|
|
2
|
+
* Initial grid cell, in the pixels React Flow positions in: a starting layout viewers re-lay out,
|
|
3
|
+
* fixed by no contract.
|
|
3
4
|
*
|
|
4
5
|
* @since 0.3.16-canary.0
|
|
5
6
|
*/
|
|
6
|
-
const GRID_COLUMN_COUNT = 5;
|
|
7
7
|
const GRID_CELL_WIDTH_PX = 200;
|
|
8
8
|
const GRID_CELL_HEIGHT_PX = 100;
|
|
9
9
|
/**
|
|
@@ -12,6 +12,8 @@ const GRID_CELL_HEIGHT_PX = 100;
|
|
|
12
12
|
* @since 0.5.0-canary.7
|
|
13
13
|
*/
|
|
14
14
|
export function toReactFlowGraph(graph) {
|
|
15
|
+
// A square grid over the graph: as many columns as rows, derived from the node count.
|
|
16
|
+
const columnCount = Math.max(1, Math.ceil(Math.sqrt(graph.nodes.length)));
|
|
15
17
|
const nodes = graph.nodes.map((node, idx) => ({
|
|
16
18
|
id: node.id,
|
|
17
19
|
data: {
|
|
@@ -22,8 +24,8 @@ export function toReactFlowGraph(graph) {
|
|
|
22
24
|
fromParent: node.fromParent,
|
|
23
25
|
},
|
|
24
26
|
position: {
|
|
25
|
-
x: (idx %
|
|
26
|
-
y: Math.floor(idx /
|
|
27
|
+
x: (idx % columnCount) * GRID_CELL_WIDTH_PX,
|
|
28
|
+
y: Math.floor(idx / columnCount) * GRID_CELL_HEIGHT_PX,
|
|
27
29
|
},
|
|
28
30
|
}));
|
|
29
31
|
const edges = graph.edges.map((edge, idx) => ({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { Binding } from "#core/binding";
|
|
1
2
|
import type { BindingRegistry } from "#core/registry";
|
|
2
3
|
import type { Token } from "#core/token";
|
|
3
4
|
import type { BindingIdentifier, BindingKind, BindingScope, BindingTag, Constructor, ResolveOptions } from "#core/types";
|
|
4
|
-
import type { ScopeManager } from "#lifecycle/
|
|
5
|
+
import type { ScopeManager } from "#lifecycle/scopes";
|
|
5
6
|
/**
|
|
6
7
|
* A read-only view of one binding: token, kind, scope, slot, and id.
|
|
7
8
|
*
|
|
@@ -42,4 +43,10 @@ export declare class Inspector {
|
|
|
42
43
|
lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
|
|
43
44
|
/** Whether this container's own registry holds a binding the request could select. */
|
|
44
45
|
hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
|
|
45
|
-
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The public snapshot of one binding, the shape every introspection read reports a binding in.
|
|
49
|
+
*
|
|
50
|
+
* @since 0.12.0
|
|
51
|
+
*/
|
|
52
|
+
export declare function snapshotOf(binding: Binding): BindingSnapshot;
|