@codefast/di 0.3.14-canary.0 → 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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.3.14-canary.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`2097bf6`](https://github.com/codefastlabs/codefast/commit/2097bf6c81639506c4c7f3f8a9a0f72bdb49ea49) Thanks [@thevuong](https://github.com/thevuong)! - refactor(di): update generateDependencyGraph method for improved clarity and functionality
8
+
9
+ - [`0720553`](https://github.com/codefastlabs/codefast/commit/0720553f01bfae88725c1688efaf608e2cf45493) Thanks [@thevuong](https://github.com/thevuong)! - feat(di): add graph adapters for visualization formats
10
+
11
+ - [`ab92dc7`](https://github.com/codefastlabs/codefast/commit/ab92dc7e5d864727bb5dcb1f2a3d08660c4112e8) Thanks [@thevuong](https://github.com/thevuong)! - refactor(di): enhance type definitions and documentation for clarity
12
+
3
13
  ## 0.3.14-canary.0
4
14
 
5
15
  ### Patch Changes
package/README.md CHANGED
@@ -435,12 +435,21 @@ Control the environment heuristic via `NODE_ENV` — see `isDevelopmentOrTestEnv
435
435
  ### Introspection
436
436
 
437
437
  ```typescript
438
+ import { toCytoscapeGraph } from "@codefast/di/graph-adapters/cytoscape";
439
+ import { toDotGraph } from "@codefast/di/graph-adapters/dot";
440
+ import { toReactFlowGraph } from "@codefast/di/graph-adapters/reactflow";
441
+
438
442
  const snapshot = container.inspect();
439
- const dot = container.generateDependencyGraph({ hideInternals: true });
440
- const json = container.generateDependencyGraph({ format: "json" });
443
+ const json = container.generateDependencyGraph({ hideInternals: true });
444
+ const dot = toDotGraph(json);
445
+
446
+ // Adapters are pure converters from the canonical JSON graph.
447
+ const cytoscape = toCytoscapeGraph(json);
448
+ const reactflow = toReactFlowGraph(json);
441
449
  ```
442
450
 
443
- `generateDependencyGraph` returns a Graphviz DOT string by default, or a typed `ContainerGraphJson` when `format: "json"` is passed.
451
+ `generateDependencyGraph` always returns the canonical typed `ContainerGraphJson` (`nodes` + `edges`).
452
+ Keep visualization adapters (`toDotGraph`, `toCytoscapeGraph`, `toReactFlowGraph`, or your own converters) outside container/inspector core APIs and import them from direct subpaths under `@codefast/di/graph-adapters/*`.
444
453
 
445
454
  ### Disposal
446
455
 
@@ -25,7 +25,7 @@ type BindingScope = "singleton" | "transient" | "scoped";
25
25
  * Hint for disambiguating multi-bindings registered against the same token or constructor.
26
26
  */
27
27
  type ResolveHint = {
28
- readonly name?: string;
28
+ /** Matches bindings configured with `.whenNamed(name)`. */readonly name?: string; /** Matches bindings configured with `.whenTagged(tagKey, value)`. */
29
29
  readonly tag?: readonly [tag: string, value: unknown];
30
30
  };
31
31
  /**
@@ -44,10 +44,10 @@ type ConstraintBindingKind = "constant" | "class" | "dynamic" | "async-dynamic"
44
44
  * ({@link whenParentIs}, {@link whenAnyAncestorIs}) and captive-dependency detection.
45
45
  */
46
46
  type ConstraintParentFrame = {
47
- readonly registryKey: RegistryKey;
48
- readonly bindingId: BindingIdentifier;
49
- readonly bindingKind: ConstraintBindingKind;
50
- readonly tags: ReadonlyMap<string, unknown>;
47
+ /** Registry key (token/constructor) that selected this binding. */readonly registryKey: RegistryKey; /** Stable identifier of the materialized binding. */
48
+ readonly bindingId: BindingIdentifier; /** Discriminant of the selected binding strategy. */
49
+ readonly bindingKind: ConstraintBindingKind; /** Immutable tag map present on the selected binding. */
50
+ readonly tags: ReadonlyMap<string, unknown>; /** Effective scope of the selected binding. */
51
51
  readonly scope: BindingScope;
52
52
  };
53
53
  /**
@@ -58,10 +58,10 @@ type MaterializationFrame = ConstraintParentFrame;
58
58
  * Context for {@link BindingBuilder.when} predicates: path, ancestor metadata, and the current resolve hint.
59
59
  */
60
60
  type ConstraintContext = {
61
- readonly resolutionPath: readonly string[];
62
- readonly materializationStack: readonly ConstraintParentFrame[];
63
- readonly parent: ConstraintParentFrame | undefined;
64
- readonly ancestors: readonly ConstraintParentFrame[];
61
+ /** Resolution labels from root request to current key. */readonly resolutionPath: readonly string[]; /** Full chain of materialized parent frames (oldest → newest). */
62
+ readonly materializationStack: readonly ConstraintParentFrame[]; /** Immediate parent frame, if the current resolution has one. */
63
+ readonly parent: ConstraintParentFrame | undefined; /** Parent chain excluding the immediate parent frame. */
64
+ readonly ancestors: readonly ConstraintParentFrame[]; /** Name/tag hint used for the current lookup, if provided. */
65
65
  readonly currentResolveHint: ResolveHint | undefined;
66
66
  };
67
67
  /**
@@ -72,8 +72,8 @@ type ConstraintContext = {
72
72
  * context-sensitive bindings such as `whenParentIs` or `whenAnyAncestorIs`.
73
73
  */
74
74
  type ResolutionContext = {
75
- readonly resolve: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value;
76
- readonly resolveAsync: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Promise<Value>;
75
+ /** Resolves one binding synchronously using current path/stack context. */readonly resolve: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value; /** Async variant of {@link ResolutionContext.resolve}. */
76
+ readonly resolveAsync: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Promise<Value>; /** Returns `undefined` when the requested root key is unbound. */
77
77
  readonly resolveOptional: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value | undefined; /** Resolves every binding registered for `token` (multi-binding). */
78
78
  readonly resolveAll: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Value[]; /** Async variant of {@link ResolutionContext.resolveAll}. */
79
79
  readonly resolveAllAsync: <Value>(token: Token<Value> | Constructor<Value>, hint?: ResolveOptions) => Promise<Value[]>;
@@ -213,7 +213,15 @@ declare class BindingBuilder<Value> {
213
213
  * Custom constraint predicates accumulated by `.when()`; all must pass for this binding to be selected.
214
214
  */
215
215
  private readonly constraintPredicates;
216
+ /**
217
+ * Latest `onActivation` hook provided by `.onActivation(...)`.
218
+ * Applied to emitted binding snapshots until replaced.
219
+ */
216
220
  private onActivationHandler;
221
+ /**
222
+ * Latest `onDeactivation` hook provided by `.onDeactivation(...)`.
223
+ * Emitted only on builder variants that expose deactivation support.
224
+ */
217
225
  private onDeactivationHandler;
218
226
  /**
219
227
  * Set when this binding was created inside a {@link Module} / {@link AsyncModule} setup callback.
package/dist/binding.mjs CHANGED
@@ -50,7 +50,15 @@ var BindingBuilder = class {
50
50
  * Custom constraint predicates accumulated by `.when()`; all must pass for this binding to be selected.
51
51
  */
52
52
  constraintPredicates = [];
53
+ /**
54
+ * Latest `onActivation` hook provided by `.onActivation(...)`.
55
+ * Applied to emitted binding snapshots until replaced.
56
+ */
53
57
  onActivationHandler;
58
+ /**
59
+ * Latest `onDeactivation` hook provided by `.onDeactivation(...)`.
60
+ * Emitted only on builder variants that expose deactivation support.
61
+ */
54
62
  onDeactivationHandler;
55
63
  /**
56
64
  * Set when this binding was created inside a {@link Module} / {@link AsyncModule} setup callback.
@@ -1,7 +1,7 @@
1
1
  import { Token } from "./token.mjs";
2
2
  import { RegistryKey } from "./registry.mjs";
3
- import { Binding, BindingBuilder, BindingIdentifier, Constructor, ResolveHint, ResolveOptions } from "./binding.mjs";
4
- import { ContainerGraphJson, ContainerSnapshot, DotGraphOptions } from "./inspector.mjs";
3
+ import { Binding, BindingBuilder, BindingIdentifier, Constructor, ResolveHint } from "./binding.mjs";
4
+ import { ContainerGraphJson, ContainerSnapshot, GraphOptions } from "./inspector.mjs";
5
5
  import { AsyncModule, Module } from "./module.mjs";
6
6
 
7
7
  //#region src/container.d.ts
@@ -90,14 +90,9 @@ interface Container extends AsyncDisposable {
90
90
  */
91
91
  inspect(): ContainerSnapshot;
92
92
  /**
93
- * Renders the dependency graph as a Graphviz DOT string (default) or a typed JSON object.
93
+ * Returns the canonical dependency graph as typed JSON (`nodes` + `edges`).
94
94
  */
95
- generateDependencyGraph(options?: DotGraphOptions & {
96
- format?: "dot";
97
- }): string;
98
- generateDependencyGraph(options: DotGraphOptions & {
99
- format: "json";
100
- }): ContainerGraphJson;
95
+ generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
101
96
  /**
102
97
  * Creates a child container that inherits bindings from this container without polluting its registry.
103
98
  */
@@ -134,4 +129,4 @@ declare namespace Container {
134
129
  function fromModulesAsync(...modules: (Module | AsyncModule)[]): Promise<Container>;
135
130
  }
136
131
  //#endregion
137
- export { type BindingIdentifier, Container, type ContainerGraphJson, type ContainerSnapshot, type ResolveOptions };
132
+ export { Container };
@@ -53,6 +53,10 @@ var DefaultContainer = class DefaultContainer {
53
53
  * Reset to `false` by {@link invalidateDevValidationState} on every registry mutation.
54
54
  */
55
55
  devValidationRan = false;
56
+ /**
57
+ * Internal constructor for root/child instances.
58
+ * Use {@link Container.create}, {@link Container.fromModules}, or {@link createChild}.
59
+ */
56
60
  constructor(ownRegistry, ownScopeManager, parent, resolver, metadataReader) {
57
61
  this.ownRegistry = ownRegistry;
58
62
  this.ownScopeManager = ownScopeManager;
@@ -98,6 +102,12 @@ var DefaultContainer = class DefaultContainer {
98
102
  }
99
103
  });
100
104
  }
105
+ /**
106
+ * Fast registry presence check without instantiation.
107
+ *
108
+ * When `hint` is provided, this only verifies that at least one binding matches
109
+ * the name/tag discriminator; it does not evaluate runtime `when()` predicates.
110
+ */
101
111
  has(token, hint) {
102
112
  const list = this.lookupBindings(token);
103
113
  if (list === void 0 || list.length === 0) return false;
@@ -111,6 +121,12 @@ var DefaultContainer = class DefaultContainer {
111
121
  return true;
112
122
  });
113
123
  }
124
+ /**
125
+ * Removes bindings by token or by binding id and synchronously releases cached instances.
126
+ *
127
+ * - `binding id` path removes one binding and its cache entry.
128
+ * - `token` path removes all owned bindings for that key at once.
129
+ */
114
130
  unbind(tokenOrId) {
115
131
  this.invalidateDevValidationState();
116
132
  if (typeof tokenOrId === "string") {
@@ -122,6 +138,9 @@ var DefaultContainer = class DefaultContainer {
122
138
  if (owned !== void 0) for (const binding of owned) this.ownScopeManager.releaseBinding(binding);
123
139
  this.ownRegistry.remove(tokenOrId);
124
140
  }
141
+ /**
142
+ * Async counterpart of {@link unbind}; awaits deactivation hooks before registry removal.
143
+ */
125
144
  async unbindAsync(tokenOrId) {
126
145
  this.invalidateDevValidationState();
127
146
  if (typeof tokenOrId === "string") {
@@ -133,6 +152,11 @@ var DefaultContainer = class DefaultContainer {
133
152
  if (owned !== void 0) for (const binding of owned) await this.ownScopeManager.releaseBindingAsync(binding);
134
153
  this.ownRegistry.remove(tokenOrId);
135
154
  }
155
+ /**
156
+ * Replaces all owned bindings for `token` and returns a fresh builder.
157
+ *
158
+ * Existing cached instances for the removed bindings are synchronously released first.
159
+ */
136
160
  rebind(token) {
137
161
  this.invalidateDevValidationState();
138
162
  const owned = this.ownRegistry.get(token);
@@ -206,6 +230,10 @@ var DefaultContainer = class DefaultContainer {
206
230
  this.maybeRunDevValidationOnce();
207
231
  });
208
232
  }
233
+ /**
234
+ * Optional root resolution: returns `undefined` when the requested key is absent (or filtered out
235
+ * without a name/tag hint), while preserving normal errors for nested required dependencies.
236
+ */
209
237
  resolveOptional(key, hint) {
210
238
  try {
211
239
  return this.resolver.resolveOptionalRoot(key, hint);
@@ -213,6 +241,10 @@ var DefaultContainer = class DefaultContainer {
213
241
  this.maybeRunDevValidationOnce();
214
242
  }
215
243
  }
244
+ /**
245
+ * Synchronously resolves every matching binding for a key.
246
+ * Returns an empty array when no binding exists.
247
+ */
216
248
  resolveAll(key, hint) {
217
249
  try {
218
250
  return this.resolver.resolveAllRoot(key, hint);
@@ -277,11 +309,16 @@ var DefaultContainer = class DefaultContainer {
277
309
  inspect() {
278
310
  return this.createInspector().getSnapshot();
279
311
  }
312
+ /**
313
+ * Delegates canonical dependency-graph generation to {@link ContainerInspector}.
314
+ */
280
315
  generateDependencyGraph(options) {
281
- const inspector = this.createInspector();
282
- if (options?.format === "json") return inspector.generateDependencyGraph(options);
283
- return inspector.generateDotGraph(options);
316
+ return this.createInspector().generateDependencyGraph(options);
284
317
  }
318
+ /**
319
+ * Registers every class collected by `@injectable({ autoRegister: true })`.
320
+ * Returns how many entries were processed.
321
+ */
285
322
  loadAutoRegistered() {
286
323
  const entries = getAutoRegistered();
287
324
  let count = 0;
@@ -352,14 +389,23 @@ var DefaultContainer = class DefaultContainer {
352
389
  holder.current = child;
353
390
  return child;
354
391
  }
392
+ /**
393
+ * Lookup helper with parent fallback: own bindings take precedence over parent bindings.
394
+ */
355
395
  lookupBindings(token) {
356
396
  const own = this.ownRegistry.get(token);
357
397
  if (own !== void 0 && own.length > 0) return own;
358
398
  return this.parent?.lookupBindings(token);
359
399
  }
400
+ /**
401
+ * Disposes this container's scope manager and runs async deactivation hooks.
402
+ */
360
403
  async dispose() {
361
404
  await this.ownScopeManager.disposeAsync();
362
405
  }
406
+ /**
407
+ * Async-dispose protocol hook used by `await using`.
408
+ */
363
409
  [Symbol.asyncDispose]() {
364
410
  return this.dispose();
365
411
  }
@@ -7,9 +7,9 @@ import { MetadataReader } from "./metadata/metadata-types.mjs";
7
7
  * A directed edge in the static dependency graph produced by {@link collectStaticDependencyEdges}.
8
8
  */
9
9
  type StaticDependencyEdge = {
10
- readonly fromBindingId: BindingIdentifier;
11
- readonly toBindingId: BindingIdentifier;
12
- readonly resolutionPath: readonly string[];
10
+ /** Binding id of the consumer node (edge source). */readonly fromBindingId: BindingIdentifier; /** Binding id of the dependency node (edge target). */
11
+ readonly toBindingId: BindingIdentifier; /** Resolution labels leading to this edge. */
12
+ readonly resolutionPath: readonly string[]; /** Edge execution kind inferred from binding strategies. */
13
13
  readonly edgeKind: "sync" | "async";
14
14
  /**
15
15
  * True when the resolved target binding carries a {@link BindingBuilder.when} predicate (runtime may skip this edge).
@@ -28,8 +28,8 @@ type StaticDependencyEdge = {
28
28
  * A single resolved dependency entry produced by {@link listResolvedDependencies}.
29
29
  */
30
30
  type ResolvedDependency = {
31
- readonly binding: Binding<unknown>;
32
- readonly path: readonly string[];
31
+ /** Effective dependency binding selected for this edge. */readonly binding: Binding<unknown>; /** Resolution labels from consumer to dependency. */
32
+ readonly path: readonly string[]; /** Optional name/tag label shown in graph outputs. */
33
33
  readonly injectHintLabel?: string;
34
34
  };
35
35
  /**
package/dist/errors.d.mts CHANGED
@@ -174,14 +174,14 @@ declare class AsyncResolutionError extends DiError {
174
174
  * Carries identities and scopes of both the long-lived consumer and the shorter-lived dependency.
175
175
  */
176
176
  type ScopeViolationDetails = {
177
- readonly consumerBindingId: BindingIdentifier;
178
- readonly consumerKind: Binding<unknown>["kind"];
179
- readonly consumerScope: BindingScope;
180
- readonly consumerLabel?: string;
181
- readonly dependencyBindingId: BindingIdentifier;
182
- readonly dependencyKind: Binding<unknown>["kind"];
183
- readonly dependencyScope: BindingScope;
184
- readonly dependencyLabel?: string;
177
+ /** Binding id of the long-lived consumer (typically singleton). */readonly consumerBindingId: BindingIdentifier; /** Binding strategy kind of the consumer. */
178
+ readonly consumerKind: Binding<unknown>["kind"]; /** Scope of the consumer binding. */
179
+ readonly consumerScope: BindingScope; /** Optional display label for consumer in error messages. */
180
+ readonly consumerLabel?: string; /** Binding id of the shorter-lived dependency. */
181
+ readonly dependencyBindingId: BindingIdentifier; /** Binding strategy kind of the dependency. */
182
+ readonly dependencyKind: Binding<unknown>["kind"]; /** Scope of the dependency binding. */
183
+ readonly dependencyScope: BindingScope; /** Optional display label for dependency in error messages. */
184
+ readonly dependencyLabel?: string; /** Resolution path captured at the violation point. */
185
185
  readonly resolutionPath: readonly string[];
186
186
  };
187
187
  /**
@@ -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
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 ContainerGraphJson, type ContainerSnapshot, 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 };
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 };
@@ -12,10 +12,10 @@ type BindingActivationStatus = "cached" | "not-cached" | "transient";
12
12
  * Per-binding row inside a {@link ContainerSnapshot}.
13
13
  */
14
14
  type ContainerBindingSnapshot = {
15
- readonly registryKeyLabel: string;
16
- readonly bindingId: BindingIdentifier;
17
- readonly kind: Binding<unknown>["kind"];
18
- readonly scope: BindingScope;
15
+ /** Human-readable token/constructor label that owns this binding row. */readonly registryKeyLabel: string; /** Stable binding identifier. */
16
+ readonly bindingId: BindingIdentifier; /** Binding strategy kind. */
17
+ readonly kind: Binding<unknown>["kind"]; /** Declared binding scope. */
18
+ readonly scope: BindingScope; /** Cache/materialization status at snapshot time. */
19
19
  readonly activationStatus: BindingActivationStatus;
20
20
  /**
21
21
  * True when {@link BindingBuilder.when} was used (runtime predicate; static graph may still show edges).
@@ -27,28 +27,28 @@ type ContainerBindingSnapshot = {
27
27
  * Full debug snapshot returned by {@link Container.inspect}.
28
28
  */
29
29
  type ContainerSnapshot = {
30
- readonly bindings: readonly ContainerBindingSnapshot[];
30
+ /** Flat list of every visible binding row in the container hierarchy. */readonly bindings: readonly ContainerBindingSnapshot[];
31
31
  };
32
32
  /**
33
- * Structured dependency graph returned by {@link Container.generateDependencyGraph} with `format: "json"`.
33
+ * Canonical structured dependency graph returned by {@link Container.generateDependencyGraph}.
34
34
  */
35
35
  type ContainerGraphJson = {
36
- nodes: ContainerBindingSnapshot[];
36
+ /** Graph nodes (same shape as snapshot rows). */nodes: ContainerBindingSnapshot[]; /** Directed dependency edges between node binding ids. */
37
37
  edges: ReturnType<typeof collectStaticDependencyEdges>[number][];
38
38
  };
39
39
  /**
40
40
  * Read-only view of the container internals exposed to {@link ContainerInspector}.
41
41
  */
42
42
  type ContainerInspectorContext = {
43
- collectAllRegistryKeys(): readonly RegistryKey[];
44
- lookupBindings(key: RegistryKey): readonly Binding<unknown>[] | undefined;
45
- isBindingCached(binding: Binding<unknown>): boolean;
43
+ /** Enumerates every registry key visible to the inspector. */collectAllRegistryKeys(): readonly RegistryKey[]; /** Returns bindings for a given key, including hierarchy lookup behavior. */
44
+ lookupBindings(key: RegistryKey): readonly Binding<unknown>[] | undefined; /** Reports whether a binding currently has a cached scoped/singleton instance. */
45
+ isBindingCached(binding: Binding<unknown>): boolean; /** Metadata reader used for static constructor/lifecycle analysis. */
46
46
  metadataReader: MetadataReader | undefined;
47
47
  };
48
48
  /**
49
49
  * Options for {@link Container.generateDependencyGraph}.
50
50
  */
51
- type DotGraphOptions = {
51
+ type GraphOptions = {
52
52
  /**
53
53
  * When true, omit registry keys whose label starts with `CODEFAST_DI_` (framework-style tokens)
54
54
  * and any edges that would only connect hidden nodes.
@@ -57,7 +57,7 @@ type DotGraphOptions = {
57
57
  };
58
58
  /**
59
59
  * Reads the container's registry and scope-cache state to produce debug snapshots
60
- * and dependency-graph output (Graphviz DOT / typed JSON).
60
+ * and canonical dependency-graph output (`nodes` + `edges`).
61
61
  *
62
62
  * Constructed internally by the container; advanced consumers can also construct it
63
63
  * directly via the `@codefast/di/inspector` subpath export.
@@ -70,31 +70,9 @@ declare class ContainerInspector {
70
70
  */
71
71
  getSnapshot(): ContainerSnapshot;
72
72
  /**
73
- * Produces a Graphviz `digraph` string with HTML-label nodes and styled edges.
74
- * Cycles are represented as ordinary edges (Graphviz renders them correctly).
75
- * Pass `hideInternals: true` to suppress `CODEFAST_DI_`-prefixed tokens.
73
+ * Builds the canonical JSON graph (`nodes` + `edges`).
76
74
  */
77
- generateDotGraph(options?: DotGraphOptions): string;
78
- /**
79
- * Overloaded entry point: returns DOT format by default, or a typed {@link ContainerGraphJson}
80
- * when `format: "json"` is specified.
81
- */
82
- generateDependencyGraph(options?: DotGraphOptions & {
83
- format?: "dot";
84
- }): string;
85
- generateDependencyGraph(options: DotGraphOptions & {
86
- format: "json";
87
- }): ContainerGraphJson;
88
- /**
89
- * Builds the typed JSON graph (nodes + edges) used by the `"json"` format path.
90
- * Applies the same `hideInternals` / deduplication logic as {@link generateDotGraph}.
91
- */
92
- generateDependencyGraphJsonTyped(options?: DotGraphOptions): ContainerGraphJson;
93
- /**
94
- * Produces a JSON graph representation with `nodes` and `edges`.
95
- * @internal Use `generateDependencyGraph({ format: "json" })` for typed output.
96
- */
97
- generateDependencyGraphJson(options?: DotGraphOptions): string;
75
+ generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
98
76
  }
99
77
  //#endregion
100
- export { BindingActivationStatus, ContainerBindingSnapshot, ContainerGraphJson, ContainerInspector, ContainerInspectorContext, ContainerSnapshot, DotGraphOptions };
78
+ export { BindingActivationStatus, ContainerBindingSnapshot, ContainerGraphJson, ContainerInspector, ContainerInspectorContext, ContainerSnapshot, GraphOptions };
@@ -9,49 +9,6 @@ function activationStatusFor(binding, isCached) {
9
9
  return isCached(binding) ? "cached" : "not-cached";
10
10
  }
11
11
  /**
12
- * Escapes a string for use as a DOT `label="..."` attribute value.
13
- */
14
- function dotEscapeLabel(text) {
15
- return text.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n");
16
- }
17
- /**
18
- * Escapes a string for safe embedding inside a DOT HTML-label (`<...>`) table cell.
19
- */
20
- function dotEscapeHtml(text) {
21
- return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
22
- }
23
- /**
24
- * Returns the Graphviz node shape name for a given binding kind.
25
- */
26
- function nodeShapeForKind(kind) {
27
- switch (kind) {
28
- case "constant": return "ellipse";
29
- case "class": return "box";
30
- case "dynamic":
31
- case "async-dynamic":
32
- case "resolved": return "diamond";
33
- case "alias": return "octagon";
34
- default: return kind;
35
- }
36
- }
37
- /**
38
- * Returns DOT fill-color and style attributes that visually distinguish binding scopes.
39
- */
40
- function scopeVisualAttributes(scope) {
41
- switch (scope) {
42
- case "singleton": return "style=\"filled\", fillcolor=\"#FFD700\", penwidth=2";
43
- case "scoped": return "style=\"filled\", fillcolor=\"#ADD8E6\"";
44
- case "transient": return "style=\"dashed\"";
45
- default: return scope;
46
- }
47
- }
48
- /**
49
- * Strips non-alphanumeric characters from a module name to produce a valid DOT subgraph identifier.
50
- */
51
- function sanitizeClusterId(moduleName) {
52
- return moduleName.replace(/[^0-9a-zA-Z_]/g, "_");
53
- }
54
- /**
55
12
  * Returns `true` when the label string belongs to a framework-internal registry key.
56
13
  */
57
14
  function registryKeyLabelIsInternal(label) {
@@ -65,7 +22,7 @@ function isInternalRegistryKey(key) {
65
22
  }
66
23
  /**
67
24
  * Reads the container's registry and scope-cache state to produce debug snapshots
68
- * and dependency-graph output (Graphviz DOT / typed JSON).
25
+ * and canonical dependency-graph output (`nodes` + `edges`).
69
26
  *
70
27
  * Constructed internally by the container; advanced consumers can also construct it
71
28
  * directly via the `@codefast/di/inspector` subpath export.
@@ -104,106 +61,9 @@ var ContainerInspector = class {
104
61
  return { bindings };
105
62
  }
106
63
  /**
107
- * Produces a Graphviz `digraph` string with HTML-label nodes and styled edges.
108
- * Cycles are represented as ordinary edges (Graphviz renders them correctly).
109
- * Pass `hideInternals: true` to suppress `CODEFAST_DI_`-prefixed tokens.
64
+ * Builds the canonical JSON graph (`nodes` + `edges`).
110
65
  */
111
- generateDotGraph(options) {
112
- const hideInternals = options?.hideInternals === true;
113
- const fullSnapshot = this.getSnapshot();
114
- const visibleRows = hideInternals ? fullSnapshot.bindings.filter((row) => !registryKeyLabelIsInternal(row.registryKeyLabel)) : fullSnapshot.bindings;
115
- const allowedBindingIds = new Set(visibleRows.map((row) => row.bindingId));
116
- const lines = [
117
- "digraph codefast_di {",
118
- " rankdir=LR;",
119
- " graph [fontname=\"Arial\", fontsize=12, nodesep=0.8, ranksep=1.2];",
120
- " node [fontname=\"Arial\", fontsize=12, shape=box, style=\"filled,rounded\", fillcolor=\"#F5F5F5\"];",
121
- " edge [fontname=\"Arial\", fontsize=10];"
122
- ];
123
- const byModule = /* @__PURE__ */ new Map();
124
- for (const row of visibleRows) {
125
- const key = row.moduleId;
126
- const moduleGroup = byModule.get(key);
127
- if (moduleGroup === void 0) byModule.set(key, [row]);
128
- else moduleGroup.push(row);
129
- }
130
- const clusteredEntries = [...byModule.entries()].filter((entry) => entry[0] !== void 0);
131
- const unclustered = byModule.get(void 0) ?? [];
132
- const nodeAttributeLine = (row, indent) => {
133
- const shape = nodeShapeForKind(row.kind);
134
- const scopeAttrs = scopeVisualAttributes(row.scope);
135
- const kindText = dotEscapeHtml(row.kind);
136
- const nameText = dotEscapeHtml(row.registryKeyLabel);
137
- const scopeText = dotEscapeHtml(`scope=${row.scope}`);
138
- const whenRow = row.hasConditionalConstraint ? ` <TR><TD ALIGN="LEFT"><FONT POINT-SIZE="9" COLOR="#666666">when(...)</FONT></TD></TR>\n` : "";
139
- const htmlLabel = [
140
- "<",
141
- " <TABLE BORDER=\"0\" CELLPADDING=\"4\" CELLSPACING=\"0\">",
142
- ` <TR><TD ALIGN="LEFT"><FONT POINT-SIZE="9" COLOR="#666666">${kindText}</FONT></TD></TR>`,
143
- ` <TR><TD ALIGN="LEFT"><B>${nameText}</B></TD></TR>`,
144
- ` <TR><TD ALIGN="LEFT"><FONT POINT-SIZE="9">${scopeText}</FONT></TD></TR>`,
145
- whenRow.trimEnd(),
146
- " </TABLE>",
147
- " >"
148
- ].filter((line) => line.length > 0).join("\n");
149
- return `${indent}"${row.bindingId}" [shape=${shape}, ${scopeAttrs}, label=${htmlLabel}];`;
150
- };
151
- for (const [moduleName, rows] of clusteredEntries) {
152
- const clusterId = sanitizeClusterId(moduleName);
153
- lines.push(` subgraph cluster_${clusterId} {`);
154
- lines.push(` label="${dotEscapeLabel(moduleName)}";`);
155
- lines.push(` style=filled;`);
156
- lines.push(` fillcolor=lightgray;`);
157
- for (const row of rows) lines.push(nodeAttributeLine(row, " "));
158
- lines.push(` }`);
159
- }
160
- for (const row of unclustered) lines.push(nodeAttributeLine(row, " "));
161
- const emittedNodeIds = new Set(visibleRows.map((row) => row.bindingId));
162
- const edgeSeen = /* @__PURE__ */ new Set();
163
- for (const registryKey of this.ctx.collectAllRegistryKeys()) {
164
- if (hideInternals && isInternalRegistryKey(registryKey)) continue;
165
- const list = this.ctx.lookupBindings(registryKey);
166
- if (list === void 0) continue;
167
- const pathStart = [registryKeyLabel(registryKey)];
168
- for (const consumerBinding of list) {
169
- if (hideInternals && !allowedBindingIds.has(consumerBinding.id)) continue;
170
- const edges = collectStaticDependencyEdges(consumerBinding, (dependencyKey) => this.ctx.lookupBindings(dependencyKey), this.ctx.metadataReader, pathStart);
171
- for (const edge of edges) {
172
- if (hideInternals && (!allowedBindingIds.has(edge.fromBindingId) || !allowedBindingIds.has(edge.toBindingId))) continue;
173
- const edgeKey = `${edge.fromBindingId}->${edge.toBindingId}:${edge.edgeKind}:${edge.injectHintLabel ?? ""}`;
174
- if (edgeSeen.has(edgeKey)) continue;
175
- edgeSeen.add(edgeKey);
176
- if (!emittedNodeIds.has(edge.fromBindingId)) {
177
- emittedNodeIds.add(edge.fromBindingId);
178
- lines.push(` "${edge.fromBindingId}" [shape=box, style=dashed, label="(unlisted ${edge.fromBindingId})"];`);
179
- }
180
- if (!emittedNodeIds.has(edge.toBindingId)) {
181
- emittedNodeIds.add(edge.toBindingId);
182
- lines.push(` "${edge.toBindingId}" [shape=box, style=dashed, label="(unlisted ${edge.toBindingId})"];`);
183
- }
184
- const labelParts = [];
185
- if (edge.injectHintLabel !== void 0) labelParts.push(edge.injectHintLabel);
186
- labelParts.push(edge.edgeKind);
187
- if (edge.toBindingConditional) labelParts.push("conditional");
188
- const edgeLabel = dotEscapeLabel(labelParts.join(" | "));
189
- const pathLabel = dotEscapeLabel(edge.resolutionPath.join(" -> "));
190
- const edgeStyle = edge.isAliasEdge ? ", style=dashed" : "";
191
- lines.push(` "${edge.fromBindingId}" -> "${edge.toBindingId}" [label="${edgeLabel}", xlabel="${pathLabel}"${edgeStyle}];`);
192
- }
193
- }
194
- }
195
- lines.push("}");
196
- return lines.join("\n");
197
- }
198
66
  generateDependencyGraph(options) {
199
- if (options?.format === "json") return this.generateDependencyGraphJsonTyped(options);
200
- return this.generateDotGraph(options);
201
- }
202
- /**
203
- * Builds the typed JSON graph (nodes + edges) used by the `"json"` format path.
204
- * Applies the same `hideInternals` / deduplication logic as {@link generateDotGraph}.
205
- */
206
- generateDependencyGraphJsonTyped(options) {
207
67
  const hideInternals = options?.hideInternals === true;
208
68
  const snapshot = this.getSnapshot();
209
69
  const visibleNodes = hideInternals ? snapshot.bindings.filter((row) => !registryKeyLabelIsInternal(row.registryKeyLabel)) : [...snapshot.bindings];
@@ -231,38 +91,6 @@ var ContainerInspector = class {
231
91
  edges
232
92
  };
233
93
  }
234
- /**
235
- * Produces a JSON graph representation with `nodes` and `edges`.
236
- * @internal Use `generateDependencyGraph({ format: "json" })` for typed output.
237
- */
238
- generateDependencyGraphJson(options) {
239
- const hideInternals = options?.hideInternals === true;
240
- const snapshot = this.getSnapshot();
241
- const visibleNodes = hideInternals ? snapshot.bindings.filter((row) => !registryKeyLabelIsInternal(row.registryKeyLabel)) : snapshot.bindings;
242
- const allowedBindingIds = new Set(visibleNodes.map((row) => row.bindingId));
243
- const edges = [];
244
- const edgeSeen = /* @__PURE__ */ new Set();
245
- for (const registryKey of this.ctx.collectAllRegistryKeys()) {
246
- if (hideInternals && isInternalRegistryKey(registryKey)) continue;
247
- const list = this.ctx.lookupBindings(registryKey);
248
- if (list === void 0) continue;
249
- const pathStart = [registryKeyLabel(registryKey)];
250
- for (const consumerBinding of list) {
251
- if (hideInternals && !allowedBindingIds.has(consumerBinding.id)) continue;
252
- for (const edge of collectStaticDependencyEdges(consumerBinding, (dependencyKey) => this.ctx.lookupBindings(dependencyKey), this.ctx.metadataReader, pathStart)) {
253
- if (hideInternals && (!allowedBindingIds.has(edge.fromBindingId) || !allowedBindingIds.has(edge.toBindingId))) continue;
254
- const edgeKey = `${edge.fromBindingId}->${edge.toBindingId}:${edge.edgeKind}:${edge.injectHintLabel ?? ""}`;
255
- if (edgeSeen.has(edgeKey)) continue;
256
- edgeSeen.add(edgeKey);
257
- edges.push(edge);
258
- }
259
- }
260
- }
261
- return JSON.stringify({
262
- nodes: visibleNodes,
263
- edges
264
- });
265
- }
266
94
  };
267
95
  //#endregion
268
96
  export { ContainerInspector };
@@ -6,11 +6,11 @@ import { Constructor } from "../binding.mjs";
6
6
  * Metadata written per `accessor` field decorated with `@inject`; collected into `Symbol.metadata`.
7
7
  */
8
8
  type AccessorInjectionMetadata = {
9
- readonly name: string;
10
- readonly token: Token<unknown> | Constructor<unknown>;
11
- readonly optional: boolean;
9
+ /** Accessor property name to inject after construction. */readonly name: string; /** Token/constructor resolved for this accessor. */
10
+ readonly token: Token<unknown> | Constructor<unknown>; /** Whether missing binding resolves to `undefined` instead of throwing. */
11
+ readonly optional: boolean; /** Optional name/tag filter forwarded to binding selection. */
12
12
  readonly resolveHint?: {
13
- readonly name?: string;
13
+ /** Named-binding discriminator (`whenNamed`). */readonly name?: string; /** Tagged-binding discriminator (`whenTagged`). */
14
14
  readonly tag?: readonly [tag: string, value: unknown];
15
15
  };
16
16
  };
@@ -18,17 +18,17 @@ type AccessorInjectionMetadata = {
18
18
  * Lifecycle method names written by `@postConstruct()` / `@preDestroy()` into `Symbol.metadata`.
19
19
  */
20
20
  type LifecycleMetadata = {
21
- readonly postConstruct?: string;
21
+ /** Method name marked with `@postConstruct()`. */readonly postConstruct?: string; /** Method name marked with `@preDestroy()`. */
22
22
  readonly preDestroy?: string;
23
23
  };
24
24
  /**
25
25
  * Per-parameter injection description collected by `@injectable()`.
26
26
  */
27
27
  type ParamMetadata = {
28
- readonly index: number;
29
- readonly token: Token<unknown> | Constructor<unknown>;
30
- readonly optional: boolean;
31
- readonly name?: string;
28
+ /** Zero-based constructor parameter index. */readonly index: number; /** Token/constructor used to resolve this parameter. */
29
+ readonly token: Token<unknown> | Constructor<unknown>; /** Whether missing binding resolves to `undefined`. */
30
+ readonly optional: boolean; /** Optional named-binding discriminator. */
31
+ readonly name?: string; /** Optional tagged-binding discriminator. */
32
32
  readonly tag?: readonly [tag: string, value: unknown];
33
33
  /**
34
34
  * When true, the parameter receives every binding for `token` as an array (same semantics as
@@ -41,9 +41,9 @@ type ParamMetadata = {
41
41
  * Used both as a deps-array entry in `@injectable()` and as accessor-field injection metadata.
42
42
  */
43
43
  type InjectionDescriptor<Value = unknown> = {
44
- readonly token: Token<Value> | Constructor<Value>;
45
- readonly optional: boolean;
46
- readonly name?: string;
44
+ /** Token/constructor to resolve. */readonly token: Token<Value> | Constructor<Value>; /** Whether unbound token should resolve as `undefined`. */
45
+ readonly optional: boolean; /** Optional named-binding discriminator. */
46
+ readonly name?: string; /** Optional tagged-binding discriminator. */
47
47
  readonly tag?: readonly [tag: string, value: unknown]; /** When true, resolve every binding for {@link InjectionDescriptor.token} into an array. */
48
48
  readonly all?: boolean;
49
49
  };
@@ -51,7 +51,7 @@ type InjectionDescriptor<Value = unknown> = {
51
51
  * Constructor injection shape stored on the class `Symbol.metadata` object.
52
52
  */
53
53
  type ConstructorMetadata = {
54
- readonly params: readonly ParamMetadata[];
54
+ /** Ordered constructor dependency descriptors. */readonly params: readonly ParamMetadata[];
55
55
  };
56
56
  /**
57
57
  * Abstraction for reading DI metadata without tying callers to `Symbol.metadata` directly.
package/dist/module.d.mts CHANGED
@@ -17,7 +17,13 @@ import { BindingBuilder, Constructor } from "./binding.mjs";
17
17
  * binding in place and does not stack multiple registrations across lines.
18
18
  */
19
19
  type ModuleBuilder = {
20
+ /**
21
+ * Declares synchronous module dependencies to load before/alongside current setup.
22
+ */
20
23
  readonly import: (...modules: Module[]) => void;
24
+ /**
25
+ * Starts binding registration for a token/constructor within this module setup pass.
26
+ */
21
27
  readonly bind: <Value>(key: Token<Value> | Constructor<Value>) => BindingBuilder<Value>;
22
28
  };
23
29
  /**
@@ -26,7 +32,13 @@ type ModuleBuilder = {
26
32
  * Async sub-imports are collected and awaited **after** the setup callback returns.
27
33
  */
28
34
  type AsyncModuleBuilder = {
35
+ /**
36
+ * Declares sync/async module dependencies to be loaded by the async module loader.
37
+ */
29
38
  readonly import: (...modules: (Module | AsyncModule)[]) => void;
39
+ /**
40
+ * Starts binding registration for a token/constructor within this async module setup pass.
41
+ */
30
42
  readonly bind: <Value>(key: Token<Value> | Constructor<Value>) => BindingBuilder<Value>;
31
43
  };
32
44
  /**
package/dist/scope.d.mts CHANGED
@@ -37,6 +37,10 @@ declare class ScopeManager {
37
37
  * {@link singletonPendingPromises} but for scoped bindings).
38
38
  */
39
39
  private readonly scopedPendingPromises;
40
+ /**
41
+ * Internal constructor for root/child scope managers.
42
+ * Prefer {@link createRoot} and {@link createChildScope}.
43
+ */
40
44
  private constructor();
41
45
  /**
42
46
  * Creates a root scope manager that owns both the singleton cache and scoped cache.
package/dist/scope.mjs CHANGED
@@ -37,6 +37,10 @@ var ScopeManager = class ScopeManager {
37
37
  * {@link singletonPendingPromises} but for scoped bindings).
38
38
  */
39
39
  scopedPendingPromises;
40
+ /**
41
+ * Internal constructor for root/child scope managers.
42
+ * Prefer {@link createRoot} and {@link createChildScope}.
43
+ */
40
44
  constructor(singletonCache, scopedCache, ownsSingletonDisposal, singletonPendingPromises, scopedPendingPromises) {
41
45
  this.singletonCache = singletonCache;
42
46
  this.scopedCache = scopedCache;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codefast/di",
3
- "version": "0.3.14-canary.0",
3
+ "version": "0.3.14-canary.1",
4
4
  "description": "Lightweight dependency injection primitives for Codefast",
5
5
  "keywords": [
6
6
  "codefast",
@@ -81,6 +81,22 @@
81
81
  "types": "./dist/errors.d.mts",
82
82
  "import": "./dist/errors.mjs"
83
83
  },
84
+ "./graph-adapters/cytoscape": {
85
+ "types": "./dist/graph-adapters/cytoscape.d.mts",
86
+ "import": "./dist/graph-adapters/cytoscape.mjs"
87
+ },
88
+ "./graph-adapters/dot": {
89
+ "types": "./dist/graph-adapters/dot.d.mts",
90
+ "import": "./dist/graph-adapters/dot.mjs"
91
+ },
92
+ "./graph-adapters/reactflow": {
93
+ "types": "./dist/graph-adapters/reactflow.d.mts",
94
+ "import": "./dist/graph-adapters/reactflow.mjs"
95
+ },
96
+ "./graph-adapters/types": {
97
+ "types": "./dist/graph-adapters/types.d.mts",
98
+ "import": "./dist/graph-adapters/types.mjs"
99
+ },
84
100
  "./inspector": {
85
101
  "types": "./dist/inspector.d.mts",
86
102
  "import": "./dist/inspector.mjs"
@@ -141,7 +157,7 @@
141
157
  "typescript": "^6.0.2",
142
158
  "unplugin-swc": "^1.5.9",
143
159
  "vitest": "^4.1.4",
144
- "@codefast/typescript-config": "0.3.14-canary.0"
160
+ "@codefast/typescript-config": "0.3.14-canary.1"
145
161
  },
146
162
  "engines": {
147
163
  "node": ">=22.0.0"