@codefast/di 0.3.13 → 0.3.14-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +270 -234
- package/dist/binding-select.d.mts +17 -6
- package/dist/binding-select.mjs +17 -6
- package/dist/binding.d.mts +167 -34
- package/dist/binding.mjs +111 -14
- package/dist/constraints.d.mts +18 -3
- package/dist/constraints.mjs +18 -3
- package/dist/container.d.mts +85 -35
- package/dist/container.mjs +140 -6
- package/dist/decorators/inject.d.mts +40 -9
- package/dist/decorators/inject.mjs +50 -11
- package/dist/decorators/injectable.d.mts +2 -1
- package/dist/decorators/injectable.mjs +14 -2
- package/dist/decorators/lifecycle-decorators.d.mts +16 -4
- package/dist/decorators/lifecycle-decorators.mjs +16 -4
- package/dist/dependency-graph.d.mts +36 -13
- package/dist/dependency-graph.mjs +42 -8
- package/dist/errors.d.mts +132 -21
- package/dist/errors.mjs +126 -18
- package/dist/graph-adapters/cytoscape.d.mts +10 -0
- package/dist/graph-adapters/cytoscape.mjs +40 -0
- package/dist/graph-adapters/dot.d.mts +9 -0
- package/dist/graph-adapters/dot.mjs +97 -0
- package/dist/graph-adapters/reactflow.d.mts +10 -0
- package/dist/graph-adapters/reactflow.mjs +80 -0
- package/dist/graph-adapters/types.d.mts +91 -0
- package/dist/graph-adapters/types.mjs +1 -0
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +2 -2
- package/dist/inspector.d.mts +42 -40
- package/dist/inspector.mjs +18 -169
- package/dist/lifecycle.d.mts +28 -6
- package/dist/lifecycle.mjs +29 -10
- package/dist/metadata/metadata-keys.d.mts +17 -6
- package/dist/metadata/metadata-keys.mjs +17 -6
- package/dist/metadata/metadata-types.d.mts +42 -18
- package/dist/metadata/param-registry.mjs +6 -0
- package/dist/metadata/symbol-metadata-reader.d.mts +20 -3
- package/dist/metadata/symbol-metadata-reader.mjs +23 -4
- package/dist/module.d.mts +46 -2
- package/dist/module.mjs +19 -0
- package/dist/registry.d.mts +39 -8
- package/dist/registry.mjs +39 -8
- package/dist/resolver.d.mts +107 -12
- package/dist/resolver.mjs +134 -37
- package/dist/scope-validation.d.mts +3 -2
- package/dist/scope-validation.mjs +3 -2
- package/dist/scope.d.mts +38 -6
- package/dist/scope.mjs +42 -13
- package/dist/token.d.mts +9 -2
- package/dist/token.mjs +7 -1
- package/package.json +18 -2
package/dist/errors.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
//#region src/errors.ts
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Formats a resolution path array into a human-readable `"A -> B -> C"` string.
|
|
4
|
+
*/
|
|
3
5
|
function formatResolutionPath(resolutionPath) {
|
|
4
6
|
return resolutionPath.length > 0 ? resolutionPath.join(" -> ") : "(empty)";
|
|
5
7
|
}
|
|
6
|
-
|
|
8
|
+
const SCOPE_LABELS = {
|
|
9
|
+
singleton: "Singleton",
|
|
10
|
+
scoped: "Scoped",
|
|
11
|
+
transient: "Transient"
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Serializes a {@link ResolveHint} to a debug string; never throws even for exotic values.
|
|
15
|
+
*/
|
|
7
16
|
function safeSerializeHint(hint) {
|
|
8
17
|
if (hint === void 0) return "(none)";
|
|
9
18
|
try {
|
|
@@ -19,7 +28,11 @@ function safeSerializeHint(hint) {
|
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
/**
|
|
22
|
-
* Base error for all `@codefast/di` failures.
|
|
31
|
+
* Base error for all `@codefast/di` failures.
|
|
32
|
+
*
|
|
33
|
+
* Every concrete subclass exposes a stable, machine-readable {@link DiError.code} property
|
|
34
|
+
* (e.g. `"TOKEN_NOT_BOUND"`) so consumers can `switch` on error type without relying
|
|
35
|
+
* on `instanceof` across package versions.
|
|
23
36
|
*/
|
|
24
37
|
var DiError = class extends Error {
|
|
25
38
|
constructor(message, options) {
|
|
@@ -29,21 +42,43 @@ var DiError = class extends Error {
|
|
|
29
42
|
};
|
|
30
43
|
/**
|
|
31
44
|
* Raised for internal programming errors — invalid library usage or unexpected state that
|
|
32
|
-
* indicates a bug in the caller
|
|
45
|
+
* indicates a bug in the caller or the library itself.
|
|
46
|
+
*
|
|
47
|
+
* Examples: double `to*()` call on a {@link BindingBuilder}, ambiguous binding resolution
|
|
48
|
+
* with multiple candidates, or scope mutation on a constant binding.
|
|
49
|
+
*
|
|
50
|
+
* Code: `"INTERNAL_ERROR"`
|
|
33
51
|
*/
|
|
34
52
|
var InternalError = class extends DiError {
|
|
35
53
|
code = "INTERNAL_ERROR";
|
|
36
54
|
};
|
|
37
55
|
/**
|
|
38
|
-
* Raised when
|
|
56
|
+
* Raised when bindings exist for the token but none match the provided name/tag hint.
|
|
57
|
+
* Distinguishes from {@link TokenNotBoundError} (no bindings at all).
|
|
58
|
+
*
|
|
59
|
+
* Thrown when a `{ name }` or `{ tag }` hint is specified but no registered binding satisfies it
|
|
60
|
+
* (e.g. `Container.resolve`, `Container.resolveAsync`, `Container.resolveOptional`,
|
|
61
|
+
* `Container.resolveAll`, `Container.resolveAllAsync`, and binding selection helpers such as
|
|
62
|
+
* {@link selectBindingForRegistry}).
|
|
63
|
+
*
|
|
64
|
+
* Code: `"NO_MATCHING_BINDING"`
|
|
39
65
|
*/
|
|
40
66
|
var NoMatchingBindingError = class extends DiError {
|
|
41
67
|
code = "NO_MATCHING_BINDING";
|
|
68
|
+
/**
|
|
69
|
+
* The `Token.name` or `Constructor.name` that was resolved.
|
|
70
|
+
*/
|
|
42
71
|
tokenName;
|
|
72
|
+
/**
|
|
73
|
+
* The name/tag hint that failed to match any binding.
|
|
74
|
+
*/
|
|
43
75
|
hint;
|
|
76
|
+
/**
|
|
77
|
+
* Label path from the resolution root to the failing token.
|
|
78
|
+
*/
|
|
44
79
|
resolutionPath;
|
|
45
80
|
constructor(tokenName, hint, resolutionPath, options) {
|
|
46
|
-
const pathText = resolutionPath
|
|
81
|
+
const pathText = formatResolutionPath(resolutionPath);
|
|
47
82
|
const hintText = safeSerializeHint(hint);
|
|
48
83
|
super(`No binding matched resolve options ${hintText} for token "${tokenName}" (resolution path: ${pathText})`, options);
|
|
49
84
|
this.tokenName = tokenName;
|
|
@@ -52,11 +87,29 @@ var NoMatchingBindingError = class extends DiError {
|
|
|
52
87
|
}
|
|
53
88
|
};
|
|
54
89
|
/**
|
|
55
|
-
* Raised when
|
|
90
|
+
* Raised when no binding exists for the requested token or constructor.
|
|
91
|
+
*
|
|
92
|
+
* Thrown by `Container.resolve`, `Container.resolveAsync`, and during transitive
|
|
93
|
+
* dependency resolution when a required token has never been registered.
|
|
94
|
+
*
|
|
95
|
+
* Note on optional resolution:
|
|
96
|
+
* - Root-level `Container.resolveOptional` returns `undefined` when **that key** has no
|
|
97
|
+
* registry entries (it never throws this error for the root key in that case). Missing
|
|
98
|
+
* **transitive** dependencies still throw during instantiation.
|
|
99
|
+
* - `ResolutionContext.resolveOptional` (used inside factories) catches this error
|
|
100
|
+
* internally to return `undefined` for missing dependencies.
|
|
101
|
+
*
|
|
102
|
+
* Code: `"TOKEN_NOT_BOUND"`
|
|
56
103
|
*/
|
|
57
104
|
var TokenNotBoundError = class extends DiError {
|
|
58
105
|
code = "TOKEN_NOT_BOUND";
|
|
106
|
+
/**
|
|
107
|
+
* The `Token.name` or `Constructor.name` that could not be found.
|
|
108
|
+
*/
|
|
59
109
|
tokenName;
|
|
110
|
+
/**
|
|
111
|
+
* Label path from the resolution root to the missing token.
|
|
112
|
+
*/
|
|
60
113
|
resolutionPath;
|
|
61
114
|
constructor(tokenName, resolutionPath, options) {
|
|
62
115
|
const pathText = formatResolutionPath(resolutionPath);
|
|
@@ -66,11 +119,22 @@ var TokenNotBoundError = class extends DiError {
|
|
|
66
119
|
}
|
|
67
120
|
};
|
|
68
121
|
/**
|
|
69
|
-
* Raised when
|
|
122
|
+
* Raised when a token is encountered a second time on the same resolution call stack,
|
|
123
|
+
* indicating a cyclic dependency (A → B → … → A).
|
|
124
|
+
*
|
|
125
|
+
* Also raised during module loading when `import()` forms a cycle between modules.
|
|
126
|
+
*
|
|
127
|
+
* Code: `"CIRCULAR_DEPENDENCY"`
|
|
70
128
|
*/
|
|
71
129
|
var CircularDependencyError = class extends DiError {
|
|
72
130
|
code = "CIRCULAR_DEPENDENCY";
|
|
131
|
+
/**
|
|
132
|
+
* Full label path including the repeated token at the end.
|
|
133
|
+
*/
|
|
73
134
|
resolutionPath;
|
|
135
|
+
/**
|
|
136
|
+
* Mutable copy of {@link resolutionPath} for consumer convenience.
|
|
137
|
+
*/
|
|
74
138
|
cycle;
|
|
75
139
|
constructor(resolutionPath, options) {
|
|
76
140
|
const pathText = formatResolutionPath(resolutionPath);
|
|
@@ -80,11 +144,26 @@ var CircularDependencyError = class extends DiError {
|
|
|
80
144
|
}
|
|
81
145
|
};
|
|
82
146
|
/**
|
|
83
|
-
* Raised when
|
|
147
|
+
* Raised when the container's {@link MetadataReader} is **configured** and reports no
|
|
148
|
+
* constructor metadata for a `class` binding whose implementation has `arity > 0`.
|
|
149
|
+
*
|
|
150
|
+
* If `metadataReader` is `undefined`, the resolver calls `new ImplementationClass()` without
|
|
151
|
+
* this check — this error is **not** thrown in that configuration.
|
|
152
|
+
*
|
|
153
|
+
* Fix: add `@injectable([...deps])` on the class (so `getConstructorMetadata` returns params),
|
|
154
|
+
* or omit constructor parameters if you intentionally run without a reader.
|
|
155
|
+
*
|
|
156
|
+
* Code: `"MISSING_METADATA"`
|
|
84
157
|
*/
|
|
85
158
|
var MissingMetadataError = class extends DiError {
|
|
86
159
|
code = "MISSING_METADATA";
|
|
160
|
+
/**
|
|
161
|
+
* Name of the class that is missing `@injectable()` metadata.
|
|
162
|
+
*/
|
|
87
163
|
className;
|
|
164
|
+
/**
|
|
165
|
+
* Label path from the resolution root to the class binding.
|
|
166
|
+
*/
|
|
88
167
|
resolutionPath;
|
|
89
168
|
constructor(className, resolutionPath, options) {
|
|
90
169
|
const pathText = formatResolutionPath(resolutionPath);
|
|
@@ -93,9 +172,17 @@ var MissingMetadataError = class extends DiError {
|
|
|
93
172
|
this.resolutionPath = resolutionPath;
|
|
94
173
|
}
|
|
95
174
|
};
|
|
96
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Raised when the synchronous `Container.load()` is called with an {@link AsyncModule}.
|
|
177
|
+
* Use `Container.loadAsync()` or `Container.fromModulesAsync()` instead.
|
|
178
|
+
*
|
|
179
|
+
* Code: `"ASYNC_MODULE_LOAD"`
|
|
180
|
+
*/
|
|
97
181
|
var AsyncModuleLoadError = class extends DiError {
|
|
98
182
|
code = "ASYNC_MODULE_LOAD";
|
|
183
|
+
/**
|
|
184
|
+
* Name of the async module that was passed to the sync loader.
|
|
185
|
+
*/
|
|
99
186
|
moduleName;
|
|
100
187
|
constructor(moduleName, options) {
|
|
101
188
|
super(`Cannot load async module "${moduleName}" synchronously; use loadAsync() or Container.fromModulesAsync().`, options);
|
|
@@ -103,13 +190,28 @@ var AsyncModuleLoadError = class extends DiError {
|
|
|
103
190
|
}
|
|
104
191
|
};
|
|
105
192
|
/**
|
|
106
|
-
* Raised when `resolve()`
|
|
107
|
-
* an async
|
|
193
|
+
* Raised when synchronous `Container.resolve()` / `Container.resolveAll()` encounters an
|
|
194
|
+
* async operation: an `async-dynamic` factory, a `toDynamic` factory that returns a Promise,
|
|
195
|
+
* an `onActivation` handler that returns a Promise, or a `@postConstruct` method that
|
|
196
|
+
* returns a Promise.
|
|
197
|
+
*
|
|
198
|
+
* Fix: switch to `Container.resolveAsync()` / `Container.resolveAllAsync()`.
|
|
199
|
+
*
|
|
200
|
+
* Code: `"ASYNC_RESOLUTION"`
|
|
108
201
|
*/
|
|
109
202
|
var AsyncResolutionError = class extends DiError {
|
|
110
203
|
code = "ASYNC_RESOLUTION";
|
|
204
|
+
/**
|
|
205
|
+
* Token or class name that triggered the async path.
|
|
206
|
+
*/
|
|
111
207
|
tokenName;
|
|
208
|
+
/**
|
|
209
|
+
* Label path from the resolution root to the async binding.
|
|
210
|
+
*/
|
|
112
211
|
resolutionPath;
|
|
212
|
+
/**
|
|
213
|
+
* Human-readable description of why async resolution was required.
|
|
214
|
+
*/
|
|
113
215
|
reason;
|
|
114
216
|
constructor(tokenName, resolutionPath, reason, options) {
|
|
115
217
|
const pathText = formatResolutionPath(resolutionPath);
|
|
@@ -120,8 +222,16 @@ var AsyncResolutionError = class extends DiError {
|
|
|
120
222
|
}
|
|
121
223
|
};
|
|
122
224
|
/**
|
|
123
|
-
* Raised
|
|
124
|
-
*
|
|
225
|
+
* Raised for a **captive dependency**: a singleton consumer resolves (or would resolve) a
|
|
226
|
+
* non-constant binding whose lifetime is `scoped` or `transient`. Constant bindings are exempt.
|
|
227
|
+
*
|
|
228
|
+
* - **Runtime:** each resolution step checks the parent on the materialization stack, so
|
|
229
|
+
* violations are detected along the actual construction chain.
|
|
230
|
+
* - **`Container.validate()`:** {@link validateScopeRules} walks **direct** static edges from
|
|
231
|
+
* {@link listResolvedDependencies} only — it does not recursively expand the whole graph,
|
|
232
|
+
* so it may miss violations that appear only deeper in the dependency tree.
|
|
233
|
+
*
|
|
234
|
+
* Code: `"SCOPE_VIOLATION"`
|
|
125
235
|
*/
|
|
126
236
|
var ScopeViolationError = class extends DiError {
|
|
127
237
|
code = "SCOPE_VIOLATION";
|
|
@@ -136,9 +246,7 @@ var ScopeViolationError = class extends DiError {
|
|
|
136
246
|
const pathText = formatResolutionPath(details.resolutionPath);
|
|
137
247
|
const consumerLabel = details.consumerLabel ?? String(details.consumerBindingId);
|
|
138
248
|
const dependencyLabel = details.dependencyLabel ?? String(details.dependencyBindingId);
|
|
139
|
-
|
|
140
|
-
const dependencyScopeLabel = details.dependencyScope.charAt(0).toUpperCase() + details.dependencyScope.slice(1);
|
|
141
|
-
super(`Scope Violation: ${consumerScopeLabel} "${consumerLabel}" cannot depend on ${dependencyScopeLabel} "${dependencyLabel}" (resolution path: ${pathText})`, options);
|
|
249
|
+
super(`Scope Violation: ${SCOPE_LABELS[details.consumerScope]} "${consumerLabel}" cannot depend on ${SCOPE_LABELS[details.dependencyScope]} "${dependencyLabel}" (resolution path: ${pathText})`, options);
|
|
142
250
|
this.consumerBindingId = details.consumerBindingId;
|
|
143
251
|
this.consumerKind = details.consumerKind;
|
|
144
252
|
this.consumerScope = details.consumerScope;
|
|
@@ -149,4 +257,4 @@ var ScopeViolationError = class extends DiError {
|
|
|
149
257
|
}
|
|
150
258
|
};
|
|
151
259
|
//#endregion
|
|
152
|
-
export { AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, DiError, InternalError, MissingMetadataError, NoMatchingBindingError, ScopeViolationError, TokenNotBoundError };
|
|
260
|
+
export { AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, DiError, InternalError, MissingMetadataError, NoMatchingBindingError, ScopeViolationError, TokenNotBoundError, formatResolutionPath };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ContainerGraphJson } from "../inspector.mjs";
|
|
2
|
+
import { CytoscapeGraphJson } from "./types.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/graph-adapters/cytoscape.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Converts the canonical container graph JSON into Cytoscape elements format.
|
|
7
|
+
*/
|
|
8
|
+
declare function toCytoscapeGraph(graph: ContainerGraphJson): CytoscapeGraphJson;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { toCytoscapeGraph };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/graph-adapters/cytoscape.ts
|
|
2
|
+
/**
|
|
3
|
+
* Converts the canonical container graph JSON into Cytoscape elements format.
|
|
4
|
+
*/
|
|
5
|
+
function toCytoscapeGraph(graph) {
|
|
6
|
+
return { elements: {
|
|
7
|
+
nodes: graph.nodes.map((node) => ({ data: {
|
|
8
|
+
id: node.bindingId,
|
|
9
|
+
label: node.registryKeyLabel,
|
|
10
|
+
bindingId: node.bindingId,
|
|
11
|
+
kind: node.kind,
|
|
12
|
+
scope: node.scope,
|
|
13
|
+
activationStatus: node.activationStatus,
|
|
14
|
+
hasConditionalConstraint: node.hasConditionalConstraint,
|
|
15
|
+
...node.moduleId === void 0 ? {} : { moduleId: node.moduleId }
|
|
16
|
+
} })),
|
|
17
|
+
edges: graph.edges.map((edge) => ({ data: {
|
|
18
|
+
id: edgeIdForCytoscape(edge),
|
|
19
|
+
source: edge.fromBindingId,
|
|
20
|
+
target: edge.toBindingId,
|
|
21
|
+
edgeKind: edge.edgeKind,
|
|
22
|
+
...edge.injectHintLabel === void 0 ? {} : { injectHintLabel: edge.injectHintLabel },
|
|
23
|
+
toBindingConditional: edge.toBindingConditional,
|
|
24
|
+
isAliasEdge: edge.isAliasEdge,
|
|
25
|
+
resolutionPath: [...edge.resolutionPath]
|
|
26
|
+
} }))
|
|
27
|
+
} };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Produces a stable Cytoscape edge id from edge metadata.
|
|
31
|
+
*/
|
|
32
|
+
function edgeIdForCytoscape(edge) {
|
|
33
|
+
const hint = edge.injectHintLabel ?? "";
|
|
34
|
+
const conditional = edge.toBindingConditional ? "conditional" : "plain";
|
|
35
|
+
const alias = edge.isAliasEdge ? "alias" : "direct";
|
|
36
|
+
const path = edge.resolutionPath.join("->");
|
|
37
|
+
return `${edge.fromBindingId}->${edge.toBindingId}:${edge.edgeKind}:${hint}:${conditional}:${alias}:${path}`;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { toCytoscapeGraph };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ContainerGraphJson } from "../inspector.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/graph-adapters/dot.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Converts the canonical container graph JSON into a Graphviz DOT digraph string.
|
|
6
|
+
*/
|
|
7
|
+
declare function toDotGraph(graph: ContainerGraphJson): string;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { toDotGraph };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//#region src/graph-adapters/dot.ts
|
|
2
|
+
/**
|
|
3
|
+
* Converts the canonical container graph JSON into a Graphviz DOT digraph string.
|
|
4
|
+
*/
|
|
5
|
+
function toDotGraph(graph) {
|
|
6
|
+
const lines = [
|
|
7
|
+
"digraph codefast_di {",
|
|
8
|
+
" rankdir=LR;",
|
|
9
|
+
" graph [fontname=\"Arial\", fontsize=12, nodesep=0.8, ranksep=1.2];",
|
|
10
|
+
" node [fontname=\"Arial\", fontsize=12, shape=box, style=\"filled,rounded\", fillcolor=\"#F5F5F5\"];",
|
|
11
|
+
" edge [fontname=\"Arial\", fontsize=10];"
|
|
12
|
+
];
|
|
13
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const node of graph.nodes) {
|
|
15
|
+
const group = byModule.get(node.moduleId);
|
|
16
|
+
if (group === void 0) byModule.set(node.moduleId, [node]);
|
|
17
|
+
else group.push(node);
|
|
18
|
+
}
|
|
19
|
+
const moduleGroups = [...byModule.entries()].filter((entry) => entry[0] !== void 0);
|
|
20
|
+
const ungrouped = byModule.get(void 0) ?? [];
|
|
21
|
+
for (const [moduleName, nodes] of moduleGroups) {
|
|
22
|
+
const clusterId = sanitizeClusterId(moduleName);
|
|
23
|
+
lines.push(` subgraph cluster_${clusterId} {`);
|
|
24
|
+
lines.push(` label="${dotEscapeLabel(moduleName)}";`);
|
|
25
|
+
lines.push(" style=filled;");
|
|
26
|
+
lines.push(" fillcolor=lightgray;");
|
|
27
|
+
for (const node of nodes) lines.push(nodeAttributeLine(node, " "));
|
|
28
|
+
lines.push(" }");
|
|
29
|
+
}
|
|
30
|
+
for (const node of ungrouped) lines.push(nodeAttributeLine(node, " "));
|
|
31
|
+
const emittedNodeIds = new Set(graph.nodes.map((node) => node.bindingId));
|
|
32
|
+
const edgeSeen = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const edge of graph.edges) {
|
|
34
|
+
const edgeKey = `${edge.fromBindingId}->${edge.toBindingId}:${edge.edgeKind}:${edge.injectHintLabel ?? ""}`;
|
|
35
|
+
if (edgeSeen.has(edgeKey)) continue;
|
|
36
|
+
edgeSeen.add(edgeKey);
|
|
37
|
+
if (!emittedNodeIds.has(edge.fromBindingId)) {
|
|
38
|
+
emittedNodeIds.add(edge.fromBindingId);
|
|
39
|
+
lines.push(` "${dotEscapeId(edge.fromBindingId)}" [shape=box, style=dashed, label="(unlisted ${dotEscapeLabel(edge.fromBindingId)})"];`);
|
|
40
|
+
}
|
|
41
|
+
if (!emittedNodeIds.has(edge.toBindingId)) {
|
|
42
|
+
emittedNodeIds.add(edge.toBindingId);
|
|
43
|
+
lines.push(` "${dotEscapeId(edge.toBindingId)}" [shape=box, style=dashed, label="(unlisted ${dotEscapeLabel(edge.toBindingId)})"];`);
|
|
44
|
+
}
|
|
45
|
+
const labelParts = [];
|
|
46
|
+
if (edge.injectHintLabel !== void 0) labelParts.push(edge.injectHintLabel);
|
|
47
|
+
labelParts.push(edge.edgeKind);
|
|
48
|
+
if (edge.toBindingConditional) labelParts.push("conditional");
|
|
49
|
+
const edgeLabel = dotEscapeLabel(labelParts.join(" | "));
|
|
50
|
+
const pathLabel = dotEscapeLabel(edge.resolutionPath.join(" -> "));
|
|
51
|
+
const edgeStyle = edge.isAliasEdge ? ", style=dashed" : "";
|
|
52
|
+
lines.push(` "${dotEscapeId(edge.fromBindingId)}" -> "${dotEscapeId(edge.toBindingId)}" [label="${edgeLabel}", xlabel="${pathLabel}"${edgeStyle}];`);
|
|
53
|
+
}
|
|
54
|
+
lines.push("}");
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
function nodeAttributeLine(node, indent) {
|
|
58
|
+
const shape = nodeShapeForKind(node.kind);
|
|
59
|
+
const scopeAttrs = scopeVisualAttributes(node.scope);
|
|
60
|
+
const labelLines = [
|
|
61
|
+
node.kind,
|
|
62
|
+
node.registryKeyLabel,
|
|
63
|
+
`scope=${node.scope}`
|
|
64
|
+
];
|
|
65
|
+
if (node.hasConditionalConstraint) labelLines.push("when(...)");
|
|
66
|
+
return `${indent}"${dotEscapeId(node.bindingId)}" [shape=${shape}, ${scopeAttrs}, label="${dotEscapeLabel(labelLines.join("\n"))}"];`;
|
|
67
|
+
}
|
|
68
|
+
function dotEscapeLabel(text) {
|
|
69
|
+
return text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n");
|
|
70
|
+
}
|
|
71
|
+
function dotEscapeId(id) {
|
|
72
|
+
return id.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n");
|
|
73
|
+
}
|
|
74
|
+
function nodeShapeForKind(kind) {
|
|
75
|
+
switch (kind) {
|
|
76
|
+
case "constant": return "ellipse";
|
|
77
|
+
case "class": return "box";
|
|
78
|
+
case "dynamic":
|
|
79
|
+
case "async-dynamic":
|
|
80
|
+
case "resolved": return "diamond";
|
|
81
|
+
case "alias": return "octagon";
|
|
82
|
+
default: return kind;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function scopeVisualAttributes(scope) {
|
|
86
|
+
switch (scope) {
|
|
87
|
+
case "singleton": return "style=\"filled\", fillcolor=\"#FFD700\", penwidth=2";
|
|
88
|
+
case "scoped": return "style=\"filled\", fillcolor=\"#ADD8E6\"";
|
|
89
|
+
case "transient": return "style=\"dashed\"";
|
|
90
|
+
default: return scope;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function sanitizeClusterId(moduleName) {
|
|
94
|
+
return moduleName.replace(/[^0-9a-zA-Z_]/g, "_");
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { toDotGraph };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ContainerGraphJson } from "../inspector.mjs";
|
|
2
|
+
import { ReactFlowGraphJson } from "./types.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/graph-adapters/reactflow.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Converts the canonical container graph JSON into React Flow nodes/edges format.
|
|
7
|
+
*/
|
|
8
|
+
declare function toReactFlowGraph(graph: ContainerGraphJson): ReactFlowGraphJson;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { toReactFlowGraph };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/graph-adapters/reactflow.ts
|
|
2
|
+
const DEFAULT_X_GAP = 240;
|
|
3
|
+
const DEFAULT_Y_GAP = 110;
|
|
4
|
+
/**
|
|
5
|
+
* Converts the canonical container graph JSON into React Flow nodes/edges format.
|
|
6
|
+
*/
|
|
7
|
+
function toReactFlowGraph(graph) {
|
|
8
|
+
const nodes = graph.nodes.map((node, index) => ({
|
|
9
|
+
id: node.bindingId,
|
|
10
|
+
position: {
|
|
11
|
+
x: 0,
|
|
12
|
+
y: index * DEFAULT_Y_GAP
|
|
13
|
+
},
|
|
14
|
+
data: {
|
|
15
|
+
label: node.registryKeyLabel,
|
|
16
|
+
bindingId: node.bindingId,
|
|
17
|
+
kind: node.kind,
|
|
18
|
+
scope: node.scope,
|
|
19
|
+
activationStatus: node.activationStatus,
|
|
20
|
+
hasConditionalConstraint: node.hasConditionalConstraint,
|
|
21
|
+
...node.moduleId === void 0 ? {} : { moduleId: node.moduleId }
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
const edges = graph.edges.map((edge) => ({
|
|
25
|
+
id: edgeIdForReactFlow(edge),
|
|
26
|
+
source: edge.fromBindingId,
|
|
27
|
+
target: edge.toBindingId,
|
|
28
|
+
label: edgeLabelForReactFlow(edge),
|
|
29
|
+
data: {
|
|
30
|
+
edgeKind: edge.edgeKind,
|
|
31
|
+
...edge.injectHintLabel === void 0 ? {} : { injectHintLabel: edge.injectHintLabel },
|
|
32
|
+
toBindingConditional: edge.toBindingConditional,
|
|
33
|
+
isAliasEdge: edge.isAliasEdge,
|
|
34
|
+
resolutionPath: [...edge.resolutionPath]
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
const moduleIndex = /* @__PURE__ */ new Map();
|
|
38
|
+
let nextColumn = 1;
|
|
39
|
+
return {
|
|
40
|
+
nodes: nodes.map((node) => {
|
|
41
|
+
const moduleId = node.data.moduleId;
|
|
42
|
+
if (moduleId === void 0) return node;
|
|
43
|
+
const existingColumn = moduleIndex.get(moduleId);
|
|
44
|
+
if (existingColumn !== void 0) return {
|
|
45
|
+
...node,
|
|
46
|
+
position: {
|
|
47
|
+
...node.position,
|
|
48
|
+
x: existingColumn * DEFAULT_X_GAP
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const column = nextColumn;
|
|
52
|
+
nextColumn += 1;
|
|
53
|
+
moduleIndex.set(moduleId, column);
|
|
54
|
+
return {
|
|
55
|
+
...node,
|
|
56
|
+
position: {
|
|
57
|
+
...node.position,
|
|
58
|
+
x: column * DEFAULT_X_GAP
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
edges
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function edgeLabelForReactFlow(edge) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (edge.injectHintLabel !== void 0) parts.push(edge.injectHintLabel);
|
|
68
|
+
parts.push(edge.edgeKind);
|
|
69
|
+
if (edge.toBindingConditional) parts.push("conditional");
|
|
70
|
+
return parts.join(" | ");
|
|
71
|
+
}
|
|
72
|
+
function edgeIdForReactFlow(edge) {
|
|
73
|
+
const hint = edge.injectHintLabel ?? "";
|
|
74
|
+
const conditional = edge.toBindingConditional ? "conditional" : "plain";
|
|
75
|
+
const alias = edge.isAliasEdge ? "alias" : "direct";
|
|
76
|
+
const path = edge.resolutionPath.join("->");
|
|
77
|
+
return `${edge.fromBindingId}->${edge.toBindingId}:${edge.edgeKind}:${hint}:${conditional}:${alias}:${path}`;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { toReactFlowGraph };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Binding, BindingIdentifier, BindingScope } from "../binding.mjs";
|
|
2
|
+
import { StaticDependencyEdge } from "../dependency-graph.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/graph-adapters/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Cytoscape node payload emitted by graph adapters.
|
|
7
|
+
*/
|
|
8
|
+
type CytoscapeNodeData = {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly bindingId: BindingIdentifier;
|
|
12
|
+
readonly kind: Binding<unknown>["kind"];
|
|
13
|
+
readonly scope: BindingScope;
|
|
14
|
+
readonly activationStatus: "cached" | "not-cached" | "transient";
|
|
15
|
+
readonly hasConditionalConstraint: boolean;
|
|
16
|
+
readonly moduleId?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Cytoscape edge payload emitted by graph adapters.
|
|
20
|
+
*/
|
|
21
|
+
type CytoscapeEdgeData = {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly source: StaticDependencyEdge["fromBindingId"];
|
|
24
|
+
readonly target: StaticDependencyEdge["toBindingId"];
|
|
25
|
+
readonly edgeKind: StaticDependencyEdge["edgeKind"];
|
|
26
|
+
readonly injectHintLabel?: string;
|
|
27
|
+
readonly toBindingConditional: boolean;
|
|
28
|
+
readonly isAliasEdge: boolean;
|
|
29
|
+
readonly resolutionPath: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
type CytoscapeNode = {
|
|
32
|
+
readonly data: CytoscapeNodeData;
|
|
33
|
+
};
|
|
34
|
+
type CytoscapeEdge = {
|
|
35
|
+
readonly data: CytoscapeEdgeData;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Cytoscape JSON graph output shape.
|
|
39
|
+
*/
|
|
40
|
+
type CytoscapeGraphJson = {
|
|
41
|
+
readonly elements: {
|
|
42
|
+
readonly nodes: CytoscapeNode[];
|
|
43
|
+
readonly edges: CytoscapeEdge[];
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* React Flow node payload emitted by graph adapters.
|
|
48
|
+
*/
|
|
49
|
+
type ReactFlowNodeData = {
|
|
50
|
+
readonly label: string;
|
|
51
|
+
readonly bindingId: BindingIdentifier;
|
|
52
|
+
readonly kind: Binding<unknown>["kind"];
|
|
53
|
+
readonly scope: BindingScope;
|
|
54
|
+
readonly activationStatus: "cached" | "not-cached" | "transient";
|
|
55
|
+
readonly hasConditionalConstraint: boolean;
|
|
56
|
+
readonly moduleId?: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* React Flow edge payload emitted by graph adapters.
|
|
60
|
+
*/
|
|
61
|
+
type ReactFlowEdgeData = {
|
|
62
|
+
readonly edgeKind: StaticDependencyEdge["edgeKind"];
|
|
63
|
+
readonly injectHintLabel?: string;
|
|
64
|
+
readonly toBindingConditional: boolean;
|
|
65
|
+
readonly isAliasEdge: boolean;
|
|
66
|
+
readonly resolutionPath: readonly string[];
|
|
67
|
+
};
|
|
68
|
+
type ReactFlowNode = {
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly position: {
|
|
71
|
+
readonly x: number;
|
|
72
|
+
readonly y: number;
|
|
73
|
+
};
|
|
74
|
+
readonly data: ReactFlowNodeData;
|
|
75
|
+
};
|
|
76
|
+
type ReactFlowEdge = {
|
|
77
|
+
readonly id: string;
|
|
78
|
+
readonly source: StaticDependencyEdge["fromBindingId"];
|
|
79
|
+
readonly target: StaticDependencyEdge["toBindingId"];
|
|
80
|
+
readonly label: string;
|
|
81
|
+
readonly data: ReactFlowEdgeData;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* React Flow JSON graph output shape.
|
|
85
|
+
*/
|
|
86
|
+
type ReactFlowGraphJson = {
|
|
87
|
+
readonly nodes: ReactFlowNode[];
|
|
88
|
+
readonly edges: ReactFlowEdge[];
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { CytoscapeEdge, CytoscapeEdgeData, CytoscapeGraphJson, CytoscapeNode, CytoscapeNodeData, ReactFlowEdge, ReactFlowEdgeData, ReactFlowGraphJson, ReactFlowNode, ReactFlowNodeData };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Token, TokenValue, token } from "./token.mjs";
|
|
2
2
|
import { ActivationHandler, BindingBuilder, BindingIdentifier, BindingScope, ConstraintContext, Constructor, DeactivationHandler, ResolveOptions } from "./binding.mjs";
|
|
3
|
-
import { ContainerGraphJson, ContainerSnapshot } from "./inspector.mjs";
|
|
4
3
|
import { AsyncModule, AsyncModuleBuilder, Module, ModuleBuilder } from "./module.mjs";
|
|
5
4
|
import { Container } from "./container.mjs";
|
|
6
|
-
import { InjectOptions, inject, isInjectionDescriptor, optional } from "./decorators/inject.mjs";
|
|
5
|
+
import { InjectOptions, inject, injectAll, isInjectionDescriptor, optional } from "./decorators/inject.mjs";
|
|
7
6
|
import { InjectableDependency, getAutoRegistered, injectable } from "./decorators/injectable.mjs";
|
|
8
7
|
import { postConstruct, preDestroy } from "./decorators/lifecycle-decorators.mjs";
|
|
9
8
|
import { AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, DiError, InternalError, MissingMetadataError, NoMatchingBindingError, ScopeViolationDetails, ScopeViolationError, TokenNotBoundError } from "./errors.mjs";
|
|
10
|
-
export { type ActivationHandler, AsyncModule, type AsyncModuleBuilder, AsyncModuleLoadError, AsyncResolutionError, type BindingBuilder, type BindingIdentifier, type BindingScope, CircularDependencyError, type ConstraintContext, type Constructor, Container, type
|
|
9
|
+
export { type ActivationHandler, AsyncModule, type AsyncModuleBuilder, AsyncModuleLoadError, AsyncResolutionError, type BindingBuilder, type BindingIdentifier, type BindingScope, CircularDependencyError, type ConstraintContext, type Constructor, Container, type DeactivationHandler, DiError, type InjectOptions, type InjectableDependency, InternalError, MissingMetadataError, Module, type ModuleBuilder, NoMatchingBindingError, type ResolveOptions, type ScopeViolationDetails, ScopeViolationError, type Token, TokenNotBoundError, type TokenValue, getAutoRegistered, inject, injectAll, injectable, isInjectionDescriptor, optional, postConstruct, preDestroy, token };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, DiError, InternalError, MissingMetadataError, NoMatchingBindingError, ScopeViolationError, TokenNotBoundError } from "./errors.mjs";
|
|
2
|
-
import { inject, isInjectionDescriptor, optional } from "./decorators/inject.mjs";
|
|
2
|
+
import { inject, injectAll, isInjectionDescriptor, optional } from "./decorators/inject.mjs";
|
|
3
3
|
import { getAutoRegistered, injectable } from "./decorators/injectable.mjs";
|
|
4
4
|
import { AsyncModule, Module } from "./module.mjs";
|
|
5
5
|
import { Container } from "./container.mjs";
|
|
6
6
|
import { token } from "./token.mjs";
|
|
7
7
|
import { postConstruct, preDestroy } from "./decorators/lifecycle-decorators.mjs";
|
|
8
|
-
export { AsyncModule, AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, Container, DiError, InternalError, MissingMetadataError, Module, NoMatchingBindingError, ScopeViolationError, TokenNotBoundError, getAutoRegistered, inject, injectable, isInjectionDescriptor, optional, postConstruct, preDestroy, token };
|
|
8
|
+
export { AsyncModule, AsyncModuleLoadError, AsyncResolutionError, CircularDependencyError, Container, DiError, InternalError, MissingMetadataError, Module, NoMatchingBindingError, ScopeViolationError, TokenNotBoundError, getAutoRegistered, inject, injectAll, injectable, isInjectionDescriptor, optional, postConstruct, preDestroy, token };
|