@webpieces/nx-webpieces-rules 0.3.314 → 0.3.320

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.
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.BindingTable = void 0;
18
18
  exports.isExternalClass = isExternalClass;
19
19
  exports.resolveClassDeclaration = resolveClassDeclaration;
20
+ exports.isApiClientBoundary = isApiClientBoundary;
20
21
  exports.decoratorCall = decoratorCall;
21
22
  exports.decoratorName = decoratorName;
22
23
  exports.classDecorators = classDecorators;
@@ -28,6 +29,8 @@ const token_resolver_1 = require("./token-resolver");
28
29
  const BIND_METHOD_NAMES = new Set(['to', 'toSelf', 'toConstantValue', 'toDynamicValue']);
29
30
  class BindingTable {
30
31
  byToken = new Map();
32
+ /** providerTokenKey -> the class its get() resolves. See bindFrameworkProvider. */
33
+ providerTargets = new Map();
31
34
  add(binding) {
32
35
  const list = this.byToken.get(binding.tokenKey);
33
36
  if (list) {
@@ -40,6 +43,18 @@ class BindingTable {
40
43
  lookup(tokenKey) {
41
44
  return this.byToken.get(tokenKey) ?? [];
42
45
  }
46
+ /** Record `bindFrameworkProvider(ProviderClass, TargetClass)`. */
47
+ addProviderTarget(providerTokenKey, target) {
48
+ this.providerTargets.set(providerTokenKey, target);
49
+ }
50
+ /**
51
+ * The class a Provider hands out, if this token is a registered Provider subclass.
52
+ * The walker follows this so `Factory -> XProvider -> X` is visible in the design,
53
+ * instead of the provider dead-ending as an opaque toDynamicValue leaf.
54
+ */
55
+ providerTarget(providerTokenKey) {
56
+ return this.providerTargets.get(providerTokenKey);
57
+ }
43
58
  }
44
59
  exports.BindingTable = BindingTable;
45
60
  function isAnalyzableFile(sourceFile) {
@@ -62,7 +77,12 @@ function isExternalClass(cls) {
62
77
  const sourceFile = cls.getSourceFile();
63
78
  return sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/');
64
79
  }
65
- /** Walk `.inSingletonScope()` / `.inTransientScope()` suffixes above a binding call. */
80
+ /**
81
+ * Walk `.inSingletonScope()` / `.inTransientScope()` suffixes above a binding call.
82
+ *
83
+ * No scope call at all means TRANSIENT, not "unknown": that is inversify's
84
+ * `DEFAULT_DEFAULT_SCOPE`, and no `new Container(...)` in this workspace overrides `defaultScope`.
85
+ */
66
86
  function scopeFromChain(bindingCall) {
67
87
  let node = bindingCall;
68
88
  while (node.parent &&
@@ -76,7 +96,7 @@ function scopeFromChain(bindingCall) {
76
96
  return 'transient';
77
97
  node = node.parent.parent;
78
98
  }
79
- return 'unknown';
99
+ return 'transient';
80
100
  }
81
101
  /**
82
102
  * If `expr` is (or resolves through the checker to) a class declaration, return it.
@@ -141,7 +161,50 @@ function collectBindCall(call, checker, workspaceRoot, table) {
141
161
  const kind = methodName === 'toConstantValue' ? 'toConstantValue' : 'toDynamicValue';
142
162
  const valueExpr = call.arguments[0];
143
163
  const valueText = valueExpr ? firstLine(valueExpr.getText()) : '';
144
- table.add(new model_1.Binding(token.key, token.display, kind, scope, null, valueText, file));
164
+ const isApiBoundary = kind === 'toDynamicValue' && valueExpr ? isApiClientBoundary(valueExpr, checker) : false;
165
+ table.add(new model_1.Binding(token.key, token.display, kind, scope, null, valueText, file, [], isApiBoundary));
166
+ }
167
+ /**
168
+ * True when `expr` (the argument to `.toDynamicValue(...)` / an Angular
169
+ * `useFactory`) builds an API-client proxy: it contains a `createApiClient(Api, ...)`
170
+ * call whose first argument resolves to an @ApiPath-decorated contract class.
171
+ *
172
+ * This is the DI-graph boundary marker for generated (and external) API clients:
173
+ * the walk renders such a binding as an `api` leaf and stops, rather than
174
+ * descending into the client's own transport config (ClientConfig → ...).
175
+ */
176
+ // webpieces-disable no-function-outside-class -- pure AST predicate, matching every sibling in this file
177
+ function isApiClientBoundary(expr, checker) {
178
+ const call = findCreateApiClientCall(expr);
179
+ if (!call || call.arguments.length === 0)
180
+ return false;
181
+ const apiClass = resolveClassDeclaration(call.arguments[0], checker);
182
+ return apiClass ? hasApiPathDecorator(apiClass) : false;
183
+ }
184
+ /** Find the first `createApiClient(...)` call anywhere inside `node`, else null. */
185
+ // webpieces-disable no-function-outside-class -- pure AST walker, matching every sibling in this file
186
+ function findCreateApiClientCall(node) {
187
+ if (ts.isCallExpression(node)) {
188
+ const callee = node.expression;
189
+ const name = ts.isIdentifier(callee)
190
+ ? callee.text
191
+ : ts.isPropertyAccessExpression(callee)
192
+ ? callee.name.text
193
+ : null;
194
+ if (name === 'createApiClient')
195
+ return node;
196
+ }
197
+ let found = null;
198
+ ts.forEachChild(node, (child) => {
199
+ if (!found)
200
+ found = findCreateApiClientCall(child);
201
+ });
202
+ return found;
203
+ }
204
+ /** True when the class carries the `@ApiPath(...)` contract decorator. */
205
+ // webpieces-disable no-function-outside-class -- pure AST predicate, matching every sibling in this file
206
+ function hasApiPathDecorator(cls) {
207
+ return classDecorators(cls).some((d) => decoratorName(d) === 'ApiPath');
145
208
  }
146
209
  function firstLine(text) {
147
210
  const line = text.split('\n')[0].trim();
@@ -169,11 +232,16 @@ function collectDecoratorBindings(cls, checker, workspaceRoot, table) {
169
232
  const file = (0, token_resolver_1.relativeFile)(workspaceRoot, cls.getSourceFile());
170
233
  for (const decorator of classDecorators(cls)) {
171
234
  const name = decoratorName(decorator);
172
- // provideFrameworkSingleton(As) are the framework-registry twins of provideSingleton(As)
173
- // (see @webpieces/core-context frameworkProvide.ts) — same self/token binding, singleton scope.
174
- if (name === 'provideSingleton' || name === 'provideTransient' || name === 'provideFrameworkSingleton') {
235
+ // provideFrameworkSingleton(As)/Transient are the framework-registry twins of
236
+ // provideSingleton(As)/Transient (see @webpieces/core-context frameworkProvide.ts) —
237
+ // same self/token binding, same scopes.
238
+ if (name === 'provideSingleton' ||
239
+ name === 'provideTransient' ||
240
+ name === 'provideFrameworkSingleton' ||
241
+ name === 'provideFrameworkTransient') {
175
242
  const token = (0, token_resolver_1.classTokenKey)(cls, workspaceRoot);
176
- const scope = name === 'provideTransient' ? 'transient' : 'singleton';
243
+ const transient = name === 'provideTransient' || name === 'provideFrameworkTransient';
244
+ const scope = transient ? 'transient' : 'singleton';
177
245
  table.add(new model_1.Binding(token.key, token.display, 'decorator', scope, cls, '', file));
178
246
  }
179
247
  else if (name === 'provideSingletonAs' || name === 'provideFrameworkSingletonAs') {
@@ -186,6 +254,28 @@ function collectDecoratorBindings(cls, checker, workspaceRoot, table) {
186
254
  }
187
255
  }
188
256
  }
257
+ /**
258
+ * `bindFrameworkProvider(TOKEN, X)` — the Guice-style Provider registration in
259
+ * @webpieces/core-context. Records that a `Provider<X>` injected under TOKEN yields X.
260
+ *
261
+ * The TOKEN is whatever names the provider (a Symbol, since `Provider<T>` is erased at runtime and
262
+ * cannot be its own token). We never draw a node for it: a Provider is DI plumbing, not wiring.
263
+ * The walker renders `Consumer -> X` directly, and X's OWN binding decides X's scope — and hence
264
+ * whether the design draws one box (a lazy singleton) or a stack (a fresh instance per get()).
265
+ */
266
+ // webpieces-disable no-function-outside-class -- ts AST visitor, matching every sibling collector in this file
267
+ function collectProviderBinding(call, checker, workspaceRoot, table) {
268
+ if (!ts.isIdentifier(call.expression) || call.expression.text !== 'bindFrameworkProvider')
269
+ return;
270
+ if (call.arguments.length < 2)
271
+ return;
272
+ // The token may be a Symbol, a class, anything — resolveTokenKey canonicalizes all of them.
273
+ const token = (0, token_resolver_1.resolveTokenKey)(call.arguments[0], checker, workspaceRoot);
274
+ const targetClass = resolveClassDeclaration(call.arguments[1], checker);
275
+ if (!targetClass)
276
+ return;
277
+ table.addProviderTarget(token.key, targetClass);
278
+ }
189
279
  /**
190
280
  * Pass 1: collect every binding in the program into a token-keyed table.
191
281
  */
@@ -197,6 +287,7 @@ function collectBindings(program, checker, workspaceRoot) {
197
287
  const visit = (node) => {
198
288
  if (ts.isCallExpression(node)) {
199
289
  collectBindCall(node, checker, workspaceRoot, table);
290
+ collectProviderBinding(node, checker, workspaceRoot, table);
200
291
  }
201
292
  else if (ts.isClassDeclaration(node)) {
202
293
  collectDecoratorBindings(node, checker, workspaceRoot, table);
@@ -1 +1 @@
1
- {"version":3,"file":"bindings.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/bindings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Binding Table (pass 1 of the DI graph analyzer)\n *\n * Scans every source file in the project's TypeScript program (skipping .d.ts and\n * node_modules) and collects all Inversify bindings into a Map<tokenKey, Binding[]>:\n *\n * - ContainerModule bodies: bind(TOKEN).to(Impl) / .toSelf() / .toConstantValue(x)\n * / .toDynamicValue(fn), with .inSingletonScope() etc.\n * - Decorators: @provideSingleton() / @provideTransient() (self-binding)\n * and @provideSingletonAs(TOKEN)\n *\n * Arrays because multiInject tokens (e.g. HEADER_TYPES.PlatformHeadersExtension) are\n * bound once per ContainerModule across several packages.\n */\n\nimport * as ts from 'typescript';\nimport { Binding, BindingKind, DiScope } from './model';\nimport { classTokenKey, relativeFile, resolveTokenKey } from './token-resolver';\n\nconst BIND_METHOD_NAMES = new Set(['to', 'toSelf', 'toConstantValue', 'toDynamicValue']);\n\nexport class BindingTable {\n private readonly byToken = new Map<string, Binding[]>();\n\n add(binding: Binding): void {\n const list = this.byToken.get(binding.tokenKey);\n if (list) {\n list.push(binding);\n } else {\n this.byToken.set(binding.tokenKey, [binding]);\n }\n }\n\n lookup(tokenKey: string): Binding[] {\n return this.byToken.get(tokenKey) ?? [];\n }\n}\n\nfunction isAnalyzableFile(sourceFile: ts.SourceFile): boolean {\n if (sourceFile.isDeclarationFile) return false;\n if (sourceFile.fileName.includes('/node_modules/')) return false;\n return true;\n}\n\n/**\n * A class the walker treats as an EXTERNAL boundary: its declaration lives in a\n * `.d.ts` file or under `node_modules` — i.e. a published package outside this\n * nx workspace. The exact inverse of {@link isAnalyzableFile}'s file test. In an\n * nx monorepo internal libs resolve through tsconfig path mappings to real `.ts`\n * source, so they are NOT external and keep expanding; only third-party packages\n * (resolved to `.d.ts` in `node_modules`) trip this. Pass-2 renders such a class\n * as a leaf `external` node and stops — it does not descend into its ctor deps.\n */\nexport function isExternalClass(cls: ts.ClassDeclaration): boolean {\n const sourceFile = cls.getSourceFile();\n return sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/');\n}\n\n/** Walk `.inSingletonScope()` / `.inTransientScope()` suffixes above a binding call. */\nfunction scopeFromChain(bindingCall: ts.CallExpression): DiScope {\n let node: ts.Node = bindingCall;\n while (\n node.parent &&\n ts.isPropertyAccessExpression(node.parent) &&\n node.parent.parent &&\n ts.isCallExpression(node.parent.parent)\n ) {\n const methodName = node.parent.name.text;\n if (methodName === 'inSingletonScope') return 'singleton';\n if (methodName === 'inTransientScope') return 'transient';\n node = node.parent.parent;\n }\n return 'unknown';\n}\n\n/**\n * If `expr` is (or resolves through the checker to) a class declaration, return it.\n */\nexport function resolveClassDeclaration(\n expr: ts.Expression,\n checker: ts.TypeChecker,\n): ts.ClassDeclaration | null {\n let symbol = checker.getSymbolAtLocation(expr);\n if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {\n symbol = checker.getAliasedSymbol(symbol);\n }\n for (const decl of symbol?.declarations ?? []) {\n if (ts.isClassDeclaration(decl)) return decl;\n }\n return null;\n}\n\n/**\n * Recognize `bind(TOKEN)` at the bottom of a fluent chain. Accepts a bare `bind(...)`\n * identifier call or `options.bind(...)` property call.\n */\nfunction asBindCall(expr: ts.Expression): ts.CallExpression | null {\n if (!ts.isCallExpression(expr)) return null;\n const callee = expr.expression;\n if (ts.isIdentifier(callee) && callee.text === 'bind') return expr;\n if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'bind') return expr;\n return null;\n}\n\n/**\n * Handle one `<receiver>.to*(...)` call: if the receiver bottoms out at bind(TOKEN),\n * record the binding.\n */\nfunction collectBindCall(\n call: ts.CallExpression,\n checker: ts.TypeChecker,\n workspaceRoot: string,\n table: BindingTable,\n): void {\n if (!ts.isPropertyAccessExpression(call.expression)) return;\n const methodName = call.expression.name.text;\n if (!BIND_METHOD_NAMES.has(methodName)) return;\n\n const bindCall = asBindCall(call.expression.expression);\n if (!bindCall || bindCall.arguments.length === 0) return;\n\n const tokenExpr = bindCall.arguments[0];\n const file = relativeFile(workspaceRoot, call.getSourceFile());\n const scope = scopeFromChain(call);\n\n if (methodName === 'toSelf') {\n const cls = resolveClassDeclaration(tokenExpr, checker);\n const token = cls\n ? classTokenKey(cls, workspaceRoot)\n : resolveTokenKey(tokenExpr, checker, workspaceRoot);\n table.add(new Binding(token.key, tokenExpr.getText(), 'toSelf', scope, cls, '', file));\n return;\n }\n\n const token = resolveTokenKey(tokenExpr, checker, workspaceRoot);\n\n if (methodName === 'to') {\n const implExpr = call.arguments[0];\n const cls = implExpr ? resolveClassDeclaration(implExpr, checker) : null;\n const valueText = implExpr ? implExpr.getText() : '';\n table.add(new Binding(token.key, token.display, 'to', scope, cls, valueText, file));\n return;\n }\n\n const kind: BindingKind = methodName === 'toConstantValue' ? 'toConstantValue' : 'toDynamicValue';\n const valueExpr = call.arguments[0];\n const valueText = valueExpr ? firstLine(valueExpr.getText()) : '';\n table.add(new Binding(token.key, token.display, kind, scope, null, valueText, file));\n}\n\nfunction firstLine(text: string): string {\n const line = text.split('\\n')[0].trim();\n return line.length > 60 ? line.slice(0, 57) + '...' : line;\n}\n\n/** Return the decorator call expression when `decorator` is `@name(...)`, else null. */\nexport function decoratorCall(decorator: ts.Decorator): ts.CallExpression | null {\n return ts.isCallExpression(decorator.expression) ? decorator.expression : null;\n}\n\n/** The identifier name of a decorator like `@provideSingleton()` or `@inject(X)`. */\nexport function decoratorName(decorator: ts.Decorator): string | null {\n const call = decoratorCall(decorator);\n const callee = call ? call.expression : decorator.expression;\n if (ts.isIdentifier(callee)) return callee.text;\n if (ts.isPropertyAccessExpression(callee)) return callee.name.text;\n return null;\n}\n\nexport function classDecorators(cls: ts.ClassDeclaration): ts.Decorator[] {\n const decorators = ts.getDecorators(cls);\n return decorators ? [...decorators] : [];\n}\n\nfunction collectDecoratorBindings(\n cls: ts.ClassDeclaration,\n checker: ts.TypeChecker,\n workspaceRoot: string,\n table: BindingTable,\n): void {\n const file = relativeFile(workspaceRoot, cls.getSourceFile());\n for (const decorator of classDecorators(cls)) {\n const name = decoratorName(decorator);\n // provideFrameworkSingleton(As) are the framework-registry twins of provideSingleton(As)\n // (see @webpieces/core-context frameworkProvide.ts) — same self/token binding, singleton scope.\n if (name === 'provideSingleton' || name === 'provideTransient' || name === 'provideFrameworkSingleton') {\n const token = classTokenKey(cls, workspaceRoot);\n const scope: DiScope = name === 'provideTransient' ? 'transient' : 'singleton';\n table.add(new Binding(token.key, token.display, 'decorator', scope, cls, '', file));\n } else if (name === 'provideSingletonAs' || name === 'provideFrameworkSingletonAs') {\n const call = decoratorCall(decorator);\n const tokenExpr = call?.arguments[0];\n if (!tokenExpr) continue;\n const token = resolveTokenKey(tokenExpr, checker, workspaceRoot);\n table.add(new Binding(token.key, token.display, 'decorator', 'singleton', cls, '', file));\n }\n }\n}\n\n/**\n * Pass 1: collect every binding in the program into a token-keyed table.\n */\nexport function collectBindings(\n program: ts.Program,\n checker: ts.TypeChecker,\n workspaceRoot: string,\n): BindingTable {\n const table = new BindingTable();\n\n for (const sourceFile of program.getSourceFiles()) {\n if (!isAnalyzableFile(sourceFile)) continue;\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node)) {\n collectBindCall(node, checker, workspaceRoot, table);\n } else if (ts.isClassDeclaration(node)) {\n collectDecoratorBindings(node, checker, workspaceRoot, table);\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n }\n\n return table;\n}\n"]}
1
+ {"version":3,"file":"bindings.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/bindings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Binding Table (pass 1 of the DI graph analyzer)\n *\n * Scans every source file in the project's TypeScript program (skipping .d.ts and\n * node_modules) and collects all Inversify bindings into a Map<tokenKey, Binding[]>:\n *\n * - ContainerModule bodies: bind(TOKEN).to(Impl) / .toSelf() / .toConstantValue(x)\n * / .toDynamicValue(fn), with .inSingletonScope() etc.\n * - Decorators: @provideSingleton() / @provideTransient() (self-binding)\n * and @provideSingletonAs(TOKEN)\n *\n * Arrays because multiInject tokens (e.g. HEADER_TYPES.PlatformHeadersExtension) are\n * bound once per ContainerModule across several packages.\n */\n\nimport * as ts from 'typescript';\nimport { Binding, BindingKind, DiScope } from './model';\nimport { classTokenKey, relativeFile, resolveTokenKey } from './token-resolver';\n\nconst BIND_METHOD_NAMES = new Set(['to', 'toSelf', 'toConstantValue', 'toDynamicValue']);\n\nexport class BindingTable {\n private readonly byToken = new Map<string, Binding[]>();\n /** providerTokenKey -> the class its get() resolves. See bindFrameworkProvider. */\n private readonly providerTargets = new Map<string, ts.ClassDeclaration>();\n\n add(binding: Binding): void {\n const list = this.byToken.get(binding.tokenKey);\n if (list) {\n list.push(binding);\n } else {\n this.byToken.set(binding.tokenKey, [binding]);\n }\n }\n\n lookup(tokenKey: string): Binding[] {\n return this.byToken.get(tokenKey) ?? [];\n }\n\n /** Record `bindFrameworkProvider(ProviderClass, TargetClass)`. */\n addProviderTarget(providerTokenKey: string, target: ts.ClassDeclaration): void {\n this.providerTargets.set(providerTokenKey, target);\n }\n\n /**\n * The class a Provider hands out, if this token is a registered Provider subclass.\n * The walker follows this so `Factory -> XProvider -> X` is visible in the design,\n * instead of the provider dead-ending as an opaque toDynamicValue leaf.\n */\n providerTarget(providerTokenKey: string): ts.ClassDeclaration | undefined {\n return this.providerTargets.get(providerTokenKey);\n }\n}\n\nfunction isAnalyzableFile(sourceFile: ts.SourceFile): boolean {\n if (sourceFile.isDeclarationFile) return false;\n if (sourceFile.fileName.includes('/node_modules/')) return false;\n return true;\n}\n\n/**\n * A class the walker treats as an EXTERNAL boundary: its declaration lives in a\n * `.d.ts` file or under `node_modules` — i.e. a published package outside this\n * nx workspace. The exact inverse of {@link isAnalyzableFile}'s file test. In an\n * nx monorepo internal libs resolve through tsconfig path mappings to real `.ts`\n * source, so they are NOT external and keep expanding; only third-party packages\n * (resolved to `.d.ts` in `node_modules`) trip this. Pass-2 renders such a class\n * as a leaf `external` node and stops — it does not descend into its ctor deps.\n */\nexport function isExternalClass(cls: ts.ClassDeclaration): boolean {\n const sourceFile = cls.getSourceFile();\n return sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/');\n}\n\n/**\n * Walk `.inSingletonScope()` / `.inTransientScope()` suffixes above a binding call.\n *\n * No scope call at all means TRANSIENT, not \"unknown\": that is inversify's\n * `DEFAULT_DEFAULT_SCOPE`, and no `new Container(...)` in this workspace overrides `defaultScope`.\n */\nfunction scopeFromChain(bindingCall: ts.CallExpression): DiScope {\n let node: ts.Node = bindingCall;\n while (\n node.parent &&\n ts.isPropertyAccessExpression(node.parent) &&\n node.parent.parent &&\n ts.isCallExpression(node.parent.parent)\n ) {\n const methodName = node.parent.name.text;\n if (methodName === 'inSingletonScope') return 'singleton';\n if (methodName === 'inTransientScope') return 'transient';\n node = node.parent.parent;\n }\n return 'transient';\n}\n\n/**\n * If `expr` is (or resolves through the checker to) a class declaration, return it.\n */\nexport function resolveClassDeclaration(\n expr: ts.Expression,\n checker: ts.TypeChecker,\n): ts.ClassDeclaration | null {\n let symbol = checker.getSymbolAtLocation(expr);\n if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {\n symbol = checker.getAliasedSymbol(symbol);\n }\n for (const decl of symbol?.declarations ?? []) {\n if (ts.isClassDeclaration(decl)) return decl;\n }\n return null;\n}\n\n/**\n * Recognize `bind(TOKEN)` at the bottom of a fluent chain. Accepts a bare `bind(...)`\n * identifier call or `options.bind(...)` property call.\n */\nfunction asBindCall(expr: ts.Expression): ts.CallExpression | null {\n if (!ts.isCallExpression(expr)) return null;\n const callee = expr.expression;\n if (ts.isIdentifier(callee) && callee.text === 'bind') return expr;\n if (ts.isPropertyAccessExpression(callee) && callee.name.text === 'bind') return expr;\n return null;\n}\n\n/**\n * Handle one `<receiver>.to*(...)` call: if the receiver bottoms out at bind(TOKEN),\n * record the binding.\n */\nfunction collectBindCall(\n call: ts.CallExpression,\n checker: ts.TypeChecker,\n workspaceRoot: string,\n table: BindingTable,\n): void {\n if (!ts.isPropertyAccessExpression(call.expression)) return;\n const methodName = call.expression.name.text;\n if (!BIND_METHOD_NAMES.has(methodName)) return;\n\n const bindCall = asBindCall(call.expression.expression);\n if (!bindCall || bindCall.arguments.length === 0) return;\n\n const tokenExpr = bindCall.arguments[0];\n const file = relativeFile(workspaceRoot, call.getSourceFile());\n const scope = scopeFromChain(call);\n\n if (methodName === 'toSelf') {\n const cls = resolveClassDeclaration(tokenExpr, checker);\n const token = cls\n ? classTokenKey(cls, workspaceRoot)\n : resolveTokenKey(tokenExpr, checker, workspaceRoot);\n table.add(new Binding(token.key, tokenExpr.getText(), 'toSelf', scope, cls, '', file));\n return;\n }\n\n const token = resolveTokenKey(tokenExpr, checker, workspaceRoot);\n\n if (methodName === 'to') {\n const implExpr = call.arguments[0];\n const cls = implExpr ? resolveClassDeclaration(implExpr, checker) : null;\n const valueText = implExpr ? implExpr.getText() : '';\n table.add(new Binding(token.key, token.display, 'to', scope, cls, valueText, file));\n return;\n }\n\n const kind: BindingKind = methodName === 'toConstantValue' ? 'toConstantValue' : 'toDynamicValue';\n const valueExpr = call.arguments[0];\n const valueText = valueExpr ? firstLine(valueExpr.getText()) : '';\n const isApiBoundary = kind === 'toDynamicValue' && valueExpr ? isApiClientBoundary(valueExpr, checker) : false;\n table.add(new Binding(token.key, token.display, kind, scope, null, valueText, file, [], isApiBoundary));\n}\n\n/**\n * True when `expr` (the argument to `.toDynamicValue(...)` / an Angular\n * `useFactory`) builds an API-client proxy: it contains a `createApiClient(Api, ...)`\n * call whose first argument resolves to an @ApiPath-decorated contract class.\n *\n * This is the DI-graph boundary marker for generated (and external) API clients:\n * the walk renders such a binding as an `api` leaf and stops, rather than\n * descending into the client's own transport config (ClientConfig → ...).\n */\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching every sibling in this file\nexport function isApiClientBoundary(expr: ts.Expression, checker: ts.TypeChecker): boolean {\n const call = findCreateApiClientCall(expr);\n if (!call || call.arguments.length === 0) return false;\n const apiClass = resolveClassDeclaration(call.arguments[0], checker);\n return apiClass ? hasApiPathDecorator(apiClass) : false;\n}\n\n/** Find the first `createApiClient(...)` call anywhere inside `node`, else null. */\n// webpieces-disable no-function-outside-class -- pure AST walker, matching every sibling in this file\nfunction findCreateApiClientCall(node: ts.Node): ts.CallExpression | null {\n if (ts.isCallExpression(node)) {\n const callee = node.expression;\n const name = ts.isIdentifier(callee)\n ? callee.text\n : ts.isPropertyAccessExpression(callee)\n ? callee.name.text\n : null;\n if (name === 'createApiClient') return node;\n }\n let found: ts.CallExpression | null = null;\n ts.forEachChild(node, (child: ts.Node) => {\n if (!found) found = findCreateApiClientCall(child);\n });\n return found;\n}\n\n/** True when the class carries the `@ApiPath(...)` contract decorator. */\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching every sibling in this file\nfunction hasApiPathDecorator(cls: ts.ClassDeclaration): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === 'ApiPath');\n}\n\nfunction firstLine(text: string): string {\n const line = text.split('\\n')[0].trim();\n return line.length > 60 ? line.slice(0, 57) + '...' : line;\n}\n\n/** Return the decorator call expression when `decorator` is `@name(...)`, else null. */\nexport function decoratorCall(decorator: ts.Decorator): ts.CallExpression | null {\n return ts.isCallExpression(decorator.expression) ? decorator.expression : null;\n}\n\n/** The identifier name of a decorator like `@provideSingleton()` or `@inject(X)`. */\nexport function decoratorName(decorator: ts.Decorator): string | null {\n const call = decoratorCall(decorator);\n const callee = call ? call.expression : decorator.expression;\n if (ts.isIdentifier(callee)) return callee.text;\n if (ts.isPropertyAccessExpression(callee)) return callee.name.text;\n return null;\n}\n\nexport function classDecorators(cls: ts.ClassDeclaration): ts.Decorator[] {\n const decorators = ts.getDecorators(cls);\n return decorators ? [...decorators] : [];\n}\n\nfunction collectDecoratorBindings(\n cls: ts.ClassDeclaration,\n checker: ts.TypeChecker,\n workspaceRoot: string,\n table: BindingTable,\n): void {\n const file = relativeFile(workspaceRoot, cls.getSourceFile());\n for (const decorator of classDecorators(cls)) {\n const name = decoratorName(decorator);\n // provideFrameworkSingleton(As)/Transient are the framework-registry twins of\n // provideSingleton(As)/Transient (see @webpieces/core-context frameworkProvide.ts) —\n // same self/token binding, same scopes.\n if (\n name === 'provideSingleton' ||\n name === 'provideTransient' ||\n name === 'provideFrameworkSingleton' ||\n name === 'provideFrameworkTransient'\n ) {\n const token = classTokenKey(cls, workspaceRoot);\n const transient = name === 'provideTransient' || name === 'provideFrameworkTransient';\n const scope: DiScope = transient ? 'transient' : 'singleton';\n table.add(new Binding(token.key, token.display, 'decorator', scope, cls, '', file));\n } else if (name === 'provideSingletonAs' || name === 'provideFrameworkSingletonAs') {\n const call = decoratorCall(decorator);\n const tokenExpr = call?.arguments[0];\n if (!tokenExpr) continue;\n const token = resolveTokenKey(tokenExpr, checker, workspaceRoot);\n table.add(new Binding(token.key, token.display, 'decorator', 'singleton', cls, '', file));\n }\n }\n}\n\n/**\n * `bindFrameworkProvider(TOKEN, X)` — the Guice-style Provider registration in\n * @webpieces/core-context. Records that a `Provider<X>` injected under TOKEN yields X.\n *\n * The TOKEN is whatever names the provider (a Symbol, since `Provider<T>` is erased at runtime and\n * cannot be its own token). We never draw a node for it: a Provider is DI plumbing, not wiring.\n * The walker renders `Consumer -> X` directly, and X's OWN binding decides X's scope — and hence\n * whether the design draws one box (a lazy singleton) or a stack (a fresh instance per get()).\n */\n// webpieces-disable no-function-outside-class -- ts AST visitor, matching every sibling collector in this file\nfunction collectProviderBinding(\n call: ts.CallExpression,\n checker: ts.TypeChecker,\n workspaceRoot: string,\n table: BindingTable,\n): void {\n if (!ts.isIdentifier(call.expression) || call.expression.text !== 'bindFrameworkProvider') return;\n if (call.arguments.length < 2) return;\n\n // The token may be a Symbol, a class, anything — resolveTokenKey canonicalizes all of them.\n const token = resolveTokenKey(call.arguments[0], checker, workspaceRoot);\n const targetClass = resolveClassDeclaration(call.arguments[1], checker);\n if (!targetClass) return;\n\n table.addProviderTarget(token.key, targetClass);\n}\n\n/**\n * Pass 1: collect every binding in the program into a token-keyed table.\n */\nexport function collectBindings(\n program: ts.Program,\n checker: ts.TypeChecker,\n workspaceRoot: string,\n): BindingTable {\n const table = new BindingTable();\n\n for (const sourceFile of program.getSourceFiles()) {\n if (!isAnalyzableFile(sourceFile)) continue;\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node)) {\n collectBindCall(node, checker, workspaceRoot, table);\n collectProviderBinding(node, checker, workspaceRoot, table);\n } else if (ts.isClassDeclaration(node)) {\n collectDecoratorBindings(node, checker, workspaceRoot, table);\n }\n ts.forEachChild(node, visit);\n };\n visit(sourceFile);\n }\n\n return table;\n}\n"]}
@@ -28,11 +28,20 @@ exports.DesignVisualizationPaths = DesignVisualizationPaths;
28
28
  class DesignGraphEntry {
29
29
  id;
30
30
  dot;
31
- constructor(id, dot) {
31
+ stackedIds;
32
+ constructor(id, dot,
33
+ /** Node ids to paint as a stack of instances (transient). Sorted, so the HTML is stable. */
34
+ stackedIds) {
32
35
  this.id = id;
33
36
  this.dot = dot;
37
+ this.stackedIds = stackedIds;
34
38
  }
35
39
  }
40
+ /** The transient nodes of one design, in the design's (already sorted) node order. */
41
+ // webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file
42
+ function stackedIdsOf(design) {
43
+ return design.nodes.filter((node) => (0, dot_1.isStackedNode)(node)).map((node) => node.id);
44
+ }
36
45
  function htmlEscape(text) {
37
46
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
38
47
  }
@@ -68,18 +77,65 @@ function pageLegend() {
68
77
  <strong>Unresolved</strong> — token the analyzer could not resolve</div>
69
78
  <div class="legend-item"><span class="legend-box" style="background: #EDE7F6; border: 3px double #5e35b1;"></span>
70
79
  <strong>External</strong> — class from a published package outside this workspace; shown as a boundary, not expanded</div>
80
+ <div class="legend-item"><span class="legend-box" style="background: #E1F5FE; border: 3px double #0277bd;"></span>
81
+ <strong>API client</strong> — generated <code>createApiClient</code> proxy (service/network boundary); shown as a boundary, not expanded</div>
82
+ <div class="legend-item"><span class="legend-box" style="background: #F5F5F5; margin-left: 12px; box-shadow: -5px -5px 0 -1px #fff, -5px -5px 0 0 #ccc, -10px -10px 0 -1px #fff, -10px -10px 0 0 #ccc;"></span>
83
+ <strong>Stack of boxes</strong> — a TRANSIENT class: every arrow into it resolves its OWN
84
+ instance. A single box is a singleton, whose arrows all share one instance.</div>
71
85
  <div class="legend-item" style="margin-top: 15px;">
72
86
  <em>One graph per controller/root; a shared dependency appears in each root's tree.
73
87
  Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>
74
88
  </div>`;
75
89
  }
90
+ /**
91
+ * Paint the "many instances" glyph: for each transient node, clone its outline twice and offset
92
+ * the copies up-and-left BEHIND the real box, so you see a stack of three whose back two show only
93
+ * their top and left edges.
94
+ *
95
+ * Graphviz has no offset-stack primitive, so we do it on the SVG viz.js hands back before it is
96
+ * attached. That costs nothing in review stability — the committed design.html holds only this
97
+ * static script plus a sorted id list; the SVG itself is produced in the browser at view time.
98
+ *
99
+ * Nodes are matched by their <title>, which Graphviz always emits as the node id. (Its `class`
100
+ * attribute arrived in Graphviz 2.40 and is not verified to survive viz.js@2.1.2.)
101
+ */
102
+ // webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file
103
+ function stackScript() {
104
+ return `
105
+ function paintStacks(element, stackedIds) {
106
+ const nodes = element.querySelectorAll('g.node');
107
+ const byTitle = new Map();
108
+ nodes.forEach(n => {
109
+ const title = n.querySelector('title');
110
+ if (title) byTitle.set(title.textContent, n);
111
+ });
112
+ for (const id of stackedIds) {
113
+ const node = byTitle.get(id);
114
+ if (!node) continue;
115
+ const outline = node.querySelector('polygon, polyline, path');
116
+ if (!outline) continue;
117
+ // Farthest copy first so the nearer one paints over it; both go behind the original.
118
+ for (const offset of [-10, -5]) {
119
+ const ghost = outline.cloneNode(false);
120
+ ghost.setAttribute('transform', 'translate(' + offset + ',' + offset + ')');
121
+ ghost.setAttribute('fill', '#ffffff');
122
+ node.insertBefore(ghost, node.firstChild);
123
+ }
124
+ }
125
+ }
126
+ `;
127
+ }
76
128
  function renderScript(entries) {
77
129
  return `
78
130
  const graphs = ${JSON.stringify(entries)};
131
+ ${stackScript()}
79
132
  const viz = new Viz();
80
133
  for (const g of graphs) {
81
134
  viz.renderSVGElement(g.dot)
82
- .then(element => { document.getElementById(g.id).appendChild(element); })
135
+ .then(element => {
136
+ paintStacks(element, g.stackedIds);
137
+ document.getElementById(g.id).appendChild(element);
138
+ })
83
139
  .catch(err => {
84
140
  console.error(err);
85
141
  document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';
@@ -104,7 +160,7 @@ function generateDesignHTML(graph, backHref) {
104
160
  : '';
105
161
  graph.designs.forEach((design, index) => {
106
162
  const id = `graph-${index}`;
107
- entries.push(new DesignGraphEntry(id, (0, dot_1.generateDesignDot)(design)));
163
+ entries.push(new DesignGraphEntry(id, (0, dot_1.generateDesignDot)(design), stackedIdsOf(design)));
108
164
  sections.push(`<div class="section">
109
165
  <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>
110
166
  <div class="meta">${htmlEscape(design.file)}</div>
@@ -1 +1 @@
1
- {"version":3,"file":"design-visualizer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design HTML Visualizer\n *\n * Builds one HTML page per project showing EVERY controller/root design as\n * its own Graphviz graph (rendered client-side with viz.js, same pipeline as\n * the architecture visualization). Output goes to tmp/webpieces/ — a view,\n * never committed (the committed artifacts are design.json/design.md).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DiDesign, DiGraph } from './model';\nimport { sortGraph } from './serializer';\nimport { generateDesignDot } from './dot';\n\nexport class DesignVisualizationPaths {\n constructor(\n public readonly dotPath: string,\n public readonly htmlPath: string\n ) {}\n}\n\nclass DesignGraphEntry {\n constructor(\n public readonly id: string,\n public readonly dot: string\n ) {}\n}\n\nfunction htmlEscape(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nfunction pageStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .back { max-width: 95%; margin: 0 auto 8px; }\n .back a { color: #1565C0; text-decoration: none; }\n .back a:hover { text-decoration: underline; }\n h2 { color: #333; margin-bottom: 4px; }\n .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }\n .section { background: white; padding: 20px; border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }\n .graph { text-align: center; overflow-x: auto; }\n .legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;\n border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n .legend-item { margin: 8px 0; }\n .legend-box { display: inline-block; width: 20px; height: 20px;\n border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }\n `;\n}\n\nfunction pageLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Design root</strong> — level 0, the @DocumentDesign entry class of the tree\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>Class</strong> — injectable class (constructor injection)</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FCE4EC; border-style: dashed;\"></span>\n <strong>Unresolved</strong> — token the analyzer could not resolve</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #EDE7F6; border: 3px double #5e35b1;\"></span>\n <strong>External</strong> — class from a published package outside this workspace; shown as a boundary, not expanded</div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>One graph per controller/root; a shared dependency appears in each root's tree.\n Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>\n </div>`;\n}\n\nfunction renderScript(entries: DesignGraphEntry[]): string {\n return `\n const graphs = ${JSON.stringify(entries)};\n const viz = new Viz();\n for (const g of graphs) {\n viz.renderSVGElement(g.dot)\n .then(element => { document.getElementById(g.id).appendChild(element); })\n .catch(err => {\n console.error(err);\n document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';\n });\n }\n `;\n}\n\n/**\n * Build the full HTML page for a project's DI designs — one section (heading\n * + meta + rendered graph) per controller/root design.\n *\n * `backHref`, when given, renders a \"back to architecture\" link at the top —\n * used by the committed per-project design.html so a reader who clicked in from\n * dependencies.html can click back out. Omitted for the tmp view.\n */\nexport function generateDesignHTML(graph: DiGraph, backHref?: string): string {\n const title = `DI Designs — ${graph.project}`;\n const entries: DesignGraphEntry[] = [];\n const sections: string[] = [];\n const backLink = backHref\n ? `<p class=\"back\"><a href=\"${htmlEscape(backHref)}\">← Back to architecture graph</a></p>`\n : '';\n\n graph.designs.forEach((design: DiDesign, index: number) => {\n const id = `graph-${index}`;\n entries.push(new DesignGraphEntry(id, generateDesignDot(design)));\n sections.push(`<div class=\"section\">\n <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>\n <div class=\"meta\">${htmlEscape(design.file)}</div>\n <div id=\"${id}\" class=\"graph\"></div>\n </div>`);\n });\n\n const body =\n sections.length > 0\n ? sections.join('\\n ')\n : '<div class=\"section\"><em>No DI-registered classes found in this project.</em></div>';\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${htmlEscape(title)}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>${pageStyles()}</style>\n</head>\n<body>\n ${backLink}\n <h1>${htmlEscape(title)}</h1>\n ${pageLegend()}\n ${body}\n <script>${renderScript(entries)}</script>\n</body>\n</html>`;\n}\n\n/**\n * Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs\n * concatenated, for debugging) and return the paths.\n */\nexport function writeDesignVisualization(\n graph: DiGraph,\n workspaceRoot: string\n): DesignVisualizationPaths {\n sortGraph(graph);\n\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const allDot = graph.designs.map((design: DiDesign) => generateDesignDot(design)).join('\\n');\n const dotPath = path.join(outputDir, `design-${graph.project}.dot`);\n fs.writeFileSync(dotPath, allDot, 'utf-8');\n\n const htmlPath = path.join(outputDir, `design-${graph.project}.html`);\n fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');\n\n return new DesignVisualizationPaths(dotPath, htmlPath);\n}\n"]}
1
+ {"version":3,"file":"design-visualizer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design HTML Visualizer\n *\n * Builds one HTML page per project showing EVERY controller/root design as\n * its own Graphviz graph (rendered client-side with viz.js, same pipeline as\n * the architecture visualization). Output goes to tmp/webpieces/ — a view,\n * never committed (the committed artifacts are design.json/design.md).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DiDesign, DiGraph, DiNode } from './model';\nimport { sortGraph } from './serializer';\nimport { generateDesignDot, isStackedNode } from './dot';\n\nexport class DesignVisualizationPaths {\n constructor(\n public readonly dotPath: string,\n public readonly htmlPath: string\n ) {}\n}\n\nclass DesignGraphEntry {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n /** Node ids to paint as a stack of instances (transient). Sorted, so the HTML is stable. */\n public readonly stackedIds: string[]\n ) {}\n}\n\n/** The transient nodes of one design, in the design's (already sorted) node order. */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackedIdsOf(design: DiDesign): string[] {\n return design.nodes.filter((node: DiNode) => isStackedNode(node)).map((node: DiNode) => node.id);\n}\n\nfunction htmlEscape(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nfunction pageStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .back { max-width: 95%; margin: 0 auto 8px; }\n .back a { color: #1565C0; text-decoration: none; }\n .back a:hover { text-decoration: underline; }\n h2 { color: #333; margin-bottom: 4px; }\n .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }\n .section { background: white; padding: 20px; border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }\n .graph { text-align: center; overflow-x: auto; }\n .legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;\n border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n .legend-item { margin: 8px 0; }\n .legend-box { display: inline-block; width: 20px; height: 20px;\n border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }\n `;\n}\n\nfunction pageLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Design root</strong> — level 0, the @DocumentDesign entry class of the tree\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>Class</strong> — injectable class (constructor injection)</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FCE4EC; border-style: dashed;\"></span>\n <strong>Unresolved</strong> — token the analyzer could not resolve</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #EDE7F6; border: 3px double #5e35b1;\"></span>\n <strong>External</strong> — class from a published package outside this workspace; shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E1F5FE; border: 3px double #0277bd;\"></span>\n <strong>API client</strong> — generated <code>createApiClient</code> proxy (service/network boundary); shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5; margin-left: 12px; box-shadow: -5px -5px 0 -1px #fff, -5px -5px 0 0 #ccc, -10px -10px 0 -1px #fff, -10px -10px 0 0 #ccc;\"></span>\n <strong>Stack of boxes</strong> — a TRANSIENT class: every arrow into it resolves its OWN\n instance. A single box is a singleton, whose arrows all share one instance.</div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>One graph per controller/root; a shared dependency appears in each root's tree.\n Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>\n </div>`;\n}\n\n/**\n * Paint the \"many instances\" glyph: for each transient node, clone its outline twice and offset\n * the copies up-and-left BEHIND the real box, so you see a stack of three whose back two show only\n * their top and left edges.\n *\n * Graphviz has no offset-stack primitive, so we do it on the SVG viz.js hands back before it is\n * attached. That costs nothing in review stability — the committed design.html holds only this\n * static script plus a sorted id list; the SVG itself is produced in the browser at view time.\n *\n * Nodes are matched by their <title>, which Graphviz always emits as the node id. (Its `class`\n * attribute arrived in Graphviz 2.40 and is not verified to survive viz.js@2.1.2.)\n */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackScript(): string {\n return `\n function paintStacks(element, stackedIds) {\n const nodes = element.querySelectorAll('g.node');\n const byTitle = new Map();\n nodes.forEach(n => {\n const title = n.querySelector('title');\n if (title) byTitle.set(title.textContent, n);\n });\n for (const id of stackedIds) {\n const node = byTitle.get(id);\n if (!node) continue;\n const outline = node.querySelector('polygon, polyline, path');\n if (!outline) continue;\n // Farthest copy first so the nearer one paints over it; both go behind the original.\n for (const offset of [-10, -5]) {\n const ghost = outline.cloneNode(false);\n ghost.setAttribute('transform', 'translate(' + offset + ',' + offset + ')');\n ghost.setAttribute('fill', '#ffffff');\n node.insertBefore(ghost, node.firstChild);\n }\n }\n }\n `;\n}\n\nfunction renderScript(entries: DesignGraphEntry[]): string {\n return `\n const graphs = ${JSON.stringify(entries)};\n ${stackScript()}\n const viz = new Viz();\n for (const g of graphs) {\n viz.renderSVGElement(g.dot)\n .then(element => {\n paintStacks(element, g.stackedIds);\n document.getElementById(g.id).appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';\n });\n }\n `;\n}\n\n/**\n * Build the full HTML page for a project's DI designs — one section (heading\n * + meta + rendered graph) per controller/root design.\n *\n * `backHref`, when given, renders a \"back to architecture\" link at the top —\n * used by the committed per-project design.html so a reader who clicked in from\n * dependencies.html can click back out. Omitted for the tmp view.\n */\nexport function generateDesignHTML(graph: DiGraph, backHref?: string): string {\n const title = `DI Designs — ${graph.project}`;\n const entries: DesignGraphEntry[] = [];\n const sections: string[] = [];\n const backLink = backHref\n ? `<p class=\"back\"><a href=\"${htmlEscape(backHref)}\">← Back to architecture graph</a></p>`\n : '';\n\n graph.designs.forEach((design: DiDesign, index: number) => {\n const id = `graph-${index}`;\n entries.push(new DesignGraphEntry(id, generateDesignDot(design), stackedIdsOf(design)));\n sections.push(`<div class=\"section\">\n <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>\n <div class=\"meta\">${htmlEscape(design.file)}</div>\n <div id=\"${id}\" class=\"graph\"></div>\n </div>`);\n });\n\n const body =\n sections.length > 0\n ? sections.join('\\n ')\n : '<div class=\"section\"><em>No DI-registered classes found in this project.</em></div>';\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${htmlEscape(title)}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>${pageStyles()}</style>\n</head>\n<body>\n ${backLink}\n <h1>${htmlEscape(title)}</h1>\n ${pageLegend()}\n ${body}\n <script>${renderScript(entries)}</script>\n</body>\n</html>`;\n}\n\n/**\n * Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs\n * concatenated, for debugging) and return the paths.\n */\nexport function writeDesignVisualization(\n graph: DiGraph,\n workspaceRoot: string\n): DesignVisualizationPaths {\n sortGraph(graph);\n\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const allDot = graph.designs.map((design: DiDesign) => generateDesignDot(design)).join('\\n');\n const dotPath = path.join(outputDir, `design-${graph.project}.dot`);\n fs.writeFileSync(dotPath, allDot, 'utf-8');\n\n const htmlPath = path.join(outputDir, `design-${graph.project}.html`);\n fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');\n\n return new DesignVisualizationPaths(dotPath, htmlPath);\n}\n"]}
@@ -6,7 +6,16 @@
6
6
  * (lib/graph-visualizer.ts): rankdir=TB with { rank=same } layers per level,
7
7
  * so the controller (level 0) sits at the top and injections fan downward.
8
8
  */
9
- import { DiDesign } from './model';
9
+ import { DiDesign, DiNode } from './model';
10
+ /**
11
+ * A transient node is 1-to-many: EVERY arrow into it resolves its own instance, unlike a
12
+ * singleton whose arrows all share one. `box3d` is Graphviz's stacked-slab glyph, so the raw
13
+ * .dot (and any external DOT viewer) reads "many" too. design.html additionally paints a real
14
+ * 3-box offset stack over it — see design-visualizer.ts.
15
+ *
16
+ * Leaves (constant/dynamic) and unresolved boxes are never instances, so they never stack.
17
+ */
18
+ export declare function isStackedNode(node: DiNode): boolean;
10
19
  /**
11
20
  * Generate a Graphviz DOT digraph for one design (one controller/root tree).
12
21
  * Deterministic for a serializer-sorted design.
@@ -8,6 +8,7 @@
8
8
  * so the controller (level 0) sits at the top and injections fan downward.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.isStackedNode = isStackedNode;
11
12
  exports.generateDesignDot = generateDesignDot;
12
13
  /**
13
14
  * Node fill colors by DI node kind.
@@ -21,11 +22,26 @@ const KIND_COLORS = {
21
22
  dynamic: '#FFF3E0', // light orange — toDynamicValue leaf
22
23
  unresolved: '#FCE4EC', // light pink — token the analyzer could not resolve
23
24
  external: '#EDE7F6', // light violet — class from a published package; walk stops here
25
+ api: '#E1F5FE', // light cyan — generated API-client proxy; service boundary, walk stops here
24
26
  };
25
27
  /** Escape a string for use inside a double-quoted DOT identifier/label. */
26
28
  function dotEscape(text) {
27
29
  return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
28
30
  }
31
+ /**
32
+ * A transient node is 1-to-many: EVERY arrow into it resolves its own instance, unlike a
33
+ * singleton whose arrows all share one. `box3d` is Graphviz's stacked-slab glyph, so the raw
34
+ * .dot (and any external DOT viewer) reads "many" too. design.html additionally paints a real
35
+ * 3-box offset stack over it — see design-visualizer.ts.
36
+ *
37
+ * Leaves (constant/dynamic) and unresolved boxes are never instances, so they never stack.
38
+ */
39
+ // webpieces-disable no-function-outside-class -- pure predicate over a DiNode, matching every sibling in this file
40
+ function isStackedNode(node) {
41
+ if (node.kind === 'constant' || node.kind === 'dynamic' || node.kind === 'unresolved')
42
+ return false;
43
+ return node.scope === 'transient';
44
+ }
29
45
  function nodeStatement(node) {
30
46
  const color = KIND_COLORS[node.kind] ?? '#F5F5F5';
31
47
  // Injected as an API → show the contract on top, the impl class in parens beneath.
@@ -38,14 +54,19 @@ function nodeStatement(node) {
38
54
  styles.push('rounded');
39
55
  if (node.kind === 'unresolved')
40
56
  styles.push('dashed');
41
- // External boundary: bold double border so it reads as "stops here, not expanded"
42
- // and is not mistaken for the pink dashed `unresolved` boxes.
43
- if (node.kind === 'external')
57
+ if (node.kind === 'api')
58
+ styles.push('rounded');
59
+ // External/api boundary: bold double border so it reads as "stops here, not
60
+ // expanded" and is not mistaken for the pink dashed `unresolved` boxes.
61
+ if (node.kind === 'external' || node.kind === 'api')
44
62
  styles.push('bold');
45
63
  const isRootKind = node.kind === 'controller' || node.kind === 'apiImplementation' || node.kind === 'component';
46
- const penwidth = isRootKind || node.kind === 'external' ? ', penwidth=2' : '';
47
- const peripheries = node.kind === 'external' ? ', peripheries=2' : '';
48
- return ` "${dotEscape(node.id)}" [fillcolor="${color}", style="${styles.join(',')}", label="${label}"${penwidth}${peripheries}];\n`;
64
+ const isBoundary = node.kind === 'external' || node.kind === 'api';
65
+ const penwidth = isRootKind || isBoundary ? ', penwidth=2' : '';
66
+ const peripheries = isBoundary ? ', peripheries=2' : '';
67
+ // Transient => a fresh instance per injection. box3d reads as a stack of instances.
68
+ const shape = isStackedNode(node) ? ', shape=box3d' : '';
69
+ return ` "${dotEscape(node.id)}" [fillcolor="${color}", style="${styles.join(',')}", label="${label}"${penwidth}${peripheries}${shape}];\n`;
49
70
  }
50
71
  /**
51
72
  * Generate a Graphviz DOT digraph for one design (one controller/root tree).
@@ -1 +1 @@
1
- {"version":3,"file":"dot.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/dot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design DOT Emitter\n *\n * Renders one DiDesign (a single controller/root's dependency tree) as a\n * Graphviz DOT digraph, mirroring the look of the architecture graph\n * (lib/graph-visualizer.ts): rankdir=TB with { rank=same } layers per level,\n * so the controller (level 0) sits at the top and injections fan downward.\n */\n\nimport { DiDesign, DiNode } from './model';\n\n/**\n * Node fill colors by DI node kind.\n */\nconst KIND_COLORS: Record<string, string> = {\n controller: '#E3F2FD', // light blue — the root/entry class (server @DocumentDesign)\n apiImplementation: '#E0F2F1', // light teal — the root/entry class (designed-lib @DocumentDesign)\n component: '#E8F5E9', // light green — the root/entry Angular component\n class: '#F5F5F5', // neutral — plain injectable class\n constant: '#FFF3E0', // light orange — toConstantValue leaf\n dynamic: '#FFF3E0', // light orange — toDynamicValue leaf\n unresolved: '#FCE4EC', // light pink — token the analyzer could not resolve\n external: '#EDE7F6', // light violet — class from a published package; walk stops here\n};\n\n/** Escape a string for use inside a double-quoted DOT identifier/label. */\nfunction dotEscape(text: string): string {\n return text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nfunction nodeStatement(node: DiNode): string {\n const color = KIND_COLORS[node.kind] ?? '#F5F5F5';\n // Injected as an API → show the contract on top, the impl class in parens beneath.\n const name = node.api\n ? `${dotEscape(node.api)}\\\\n(${dotEscape(node.className)})`\n : dotEscape(node.className);\n const label = `${name}\\\\n(L${node.level}, ${node.scope})`;\n const styles: string[] = ['filled'];\n if (node.kind === 'constant' || node.kind === 'dynamic') styles.push('rounded');\n if (node.kind === 'unresolved') styles.push('dashed');\n // External boundary: bold double border so it reads as \"stops here, not expanded\"\n // and is not mistaken for the pink dashed `unresolved` boxes.\n if (node.kind === 'external') styles.push('bold');\n const isRootKind =\n node.kind === 'controller' || node.kind === 'apiImplementation' || node.kind === 'component';\n const penwidth = isRootKind || node.kind === 'external' ? ', penwidth=2' : '';\n const peripheries = node.kind === 'external' ? ', peripheries=2' : '';\n return ` \"${dotEscape(node.id)}\" [fillcolor=\"${color}\", style=\"${styles.join(',')}\", label=\"${label}\"${penwidth}${peripheries}];\\n`;\n}\n\n/**\n * Generate a Graphviz DOT digraph for one design (one controller/root tree).\n * Deterministic for a serializer-sorted design.\n */\nexport function generateDesignDot(design: DiDesign): string {\n let dot = `digraph \"${dotEscape(design.root)}\" {\\n`;\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=11];\\n\\n';\n\n // Nodes, colored by kind, labeled with their level + scope\n for (const node of design.nodes) {\n dot += nodeStatement(node);\n }\n\n dot += '\\n';\n\n // One { rank=same } layer per level so the root stays on top\n const levels = new Map<number, string[]>();\n for (const node of design.nodes) {\n const layer = levels.get(node.level) ?? [];\n layer.push(node.id);\n levels.set(node.level, layer);\n }\n const sortedLevels = Array.from(levels.keys()).sort((a: number, b: number) => a - b);\n for (const level of sortedLevels) {\n const ids = levels.get(level) ?? [];\n dot += ` { rank=same; ${ids.map((id: string) => `\"${dotEscape(id)}\"`).join('; ')}; }\\n`;\n }\n\n dot += '\\n';\n\n // Constructor-injection edges, unlabeled — the arrow alone shows the\n // dependency. The param/field name, token and tokenKey stay in design.json\n // for tooling that wants them.\n for (const edge of design.edges) {\n dot += ` \"${dotEscape(edge.from)}\" -> \"${dotEscape(edge.to)}\";\\n`;\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotEscape(design.root)}\\\\n(${design.rootKind}, Level 0-${design.maxLevel})\";\\n`;\n dot += ' fontsize=16;\\n';\n dot += '}\\n';\n\n return dot;\n}\n"]}
1
+ {"version":3,"file":"dot.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/dot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design DOT Emitter\n *\n * Renders one DiDesign (a single controller/root's dependency tree) as a\n * Graphviz DOT digraph, mirroring the look of the architecture graph\n * (lib/graph-visualizer.ts): rankdir=TB with { rank=same } layers per level,\n * so the controller (level 0) sits at the top and injections fan downward.\n */\n\nimport { DiDesign, DiNode } from './model';\n\n/**\n * Node fill colors by DI node kind.\n */\nconst KIND_COLORS: Record<string, string> = {\n controller: '#E3F2FD', // light blue — the root/entry class (server @DocumentDesign)\n apiImplementation: '#E0F2F1', // light teal — the root/entry class (designed-lib @DocumentDesign)\n component: '#E8F5E9', // light green — the root/entry Angular component\n class: '#F5F5F5', // neutral — plain injectable class\n constant: '#FFF3E0', // light orange — toConstantValue leaf\n dynamic: '#FFF3E0', // light orange — toDynamicValue leaf\n unresolved: '#FCE4EC', // light pink — token the analyzer could not resolve\n external: '#EDE7F6', // light violet — class from a published package; walk stops here\n api: '#E1F5FE', // light cyan — generated API-client proxy; service boundary, walk stops here\n};\n\n/** Escape a string for use inside a double-quoted DOT identifier/label. */\nfunction dotEscape(text: string): string {\n return text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\n/**\n * A transient node is 1-to-many: EVERY arrow into it resolves its own instance, unlike a\n * singleton whose arrows all share one. `box3d` is Graphviz's stacked-slab glyph, so the raw\n * .dot (and any external DOT viewer) reads \"many\" too. design.html additionally paints a real\n * 3-box offset stack over it — see design-visualizer.ts.\n *\n * Leaves (constant/dynamic) and unresolved boxes are never instances, so they never stack.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over a DiNode, matching every sibling in this file\nexport function isStackedNode(node: DiNode): boolean {\n if (node.kind === 'constant' || node.kind === 'dynamic' || node.kind === 'unresolved') return false;\n return node.scope === 'transient';\n}\n\nfunction nodeStatement(node: DiNode): string {\n const color = KIND_COLORS[node.kind] ?? '#F5F5F5';\n // Injected as an API → show the contract on top, the impl class in parens beneath.\n const name = node.api\n ? `${dotEscape(node.api)}\\\\n(${dotEscape(node.className)})`\n : dotEscape(node.className);\n const label = `${name}\\\\n(L${node.level}, ${node.scope})`;\n const styles: string[] = ['filled'];\n if (node.kind === 'constant' || node.kind === 'dynamic') styles.push('rounded');\n if (node.kind === 'unresolved') styles.push('dashed');\n if (node.kind === 'api') styles.push('rounded');\n // External/api boundary: bold double border so it reads as \"stops here, not\n // expanded\" and is not mistaken for the pink dashed `unresolved` boxes.\n if (node.kind === 'external' || node.kind === 'api') styles.push('bold');\n const isRootKind =\n node.kind === 'controller' || node.kind === 'apiImplementation' || node.kind === 'component';\n const isBoundary = node.kind === 'external' || node.kind === 'api';\n const penwidth = isRootKind || isBoundary ? ', penwidth=2' : '';\n const peripheries = isBoundary ? ', peripheries=2' : '';\n // Transient => a fresh instance per injection. box3d reads as a stack of instances.\n const shape = isStackedNode(node) ? ', shape=box3d' : '';\n return ` \"${dotEscape(node.id)}\" [fillcolor=\"${color}\", style=\"${styles.join(',')}\", label=\"${label}\"${penwidth}${peripheries}${shape}];\\n`;\n}\n\n/**\n * Generate a Graphviz DOT digraph for one design (one controller/root tree).\n * Deterministic for a serializer-sorted design.\n */\nexport function generateDesignDot(design: DiDesign): string {\n let dot = `digraph \"${dotEscape(design.root)}\" {\\n`;\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=11];\\n\\n';\n\n // Nodes, colored by kind, labeled with their level + scope\n for (const node of design.nodes) {\n dot += nodeStatement(node);\n }\n\n dot += '\\n';\n\n // One { rank=same } layer per level so the root stays on top\n const levels = new Map<number, string[]>();\n for (const node of design.nodes) {\n const layer = levels.get(node.level) ?? [];\n layer.push(node.id);\n levels.set(node.level, layer);\n }\n const sortedLevels = Array.from(levels.keys()).sort((a: number, b: number) => a - b);\n for (const level of sortedLevels) {\n const ids = levels.get(level) ?? [];\n dot += ` { rank=same; ${ids.map((id: string) => `\"${dotEscape(id)}\"`).join('; ')}; }\\n`;\n }\n\n dot += '\\n';\n\n // Constructor-injection edges, unlabeled — the arrow alone shows the\n // dependency. The param/field name, token and tokenKey stay in design.json\n // for tooling that wants them.\n for (const edge of design.edges) {\n dot += ` \"${dotEscape(edge.from)}\" -> \"${dotEscape(edge.to)}\";\\n`;\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotEscape(design.root)}\\\\n(${design.rootKind}, Level 0-${design.maxLevel})\";\\n`;\n dot += ' fontsize=16;\\n';\n dot += '}\\n';\n\n return dot;\n}\n"]}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.toDesignMarkdown = toDesignMarkdown;
11
+ const dot_1 = require("./dot");
11
12
  const serializer_1 = require("./serializer");
12
13
  /** Mermaid node ids must be simple identifiers — map graph ids to safe ids. */
13
14
  function mermaidId(id) {
@@ -34,6 +35,14 @@ function nodeStatement(node) {
34
35
  // External boundary — subroutine box (double vertical bars) + distinct class.
35
36
  if (node.kind === 'external')
36
37
  return ` ${id}[["${text}"]]:::external`;
38
+ // API-client proxy boundary — stadium subroutine (rounded) + distinct class.
39
+ if (node.kind === 'api')
40
+ return ` ${id}[["${text}"]]:::api`;
41
+ // Transient (1-to-many). A plain box with a dashed accent, NOT mermaid's `@{ shape: procs }`
42
+ // stack: that needs mermaid >= 11.3 and design.md is rendered by whatever version the viewer
43
+ // ships (GitHub, an IDE preview) — this repo pins none. design.html draws the real stack.
44
+ if ((0, dot_1.isStackedNode)(node))
45
+ return ` ${id}["${text}"]:::many`;
37
46
  return ` ${id}["${text}"]`;
38
47
  }
39
48
  function graphBody(design) {
@@ -53,6 +62,9 @@ function graphBody(design) {
53
62
  lines.push(' classDef component fill:#2da44e,color:#ffffff,stroke:#1a7f37');
54
63
  lines.push(' classDef unresolved fill:#f0ad4e,color:#000000,stroke:#b8860b,stroke-dasharray: 5 5');
55
64
  lines.push(' classDef external fill:#b39ddb,color:#000000,stroke:#5e35b1,stroke-width:3px');
65
+ lines.push(' classDef api fill:#4fc3f7,color:#000000,stroke:#0277bd,stroke-width:3px');
66
+ // TRANSIENT: every arrow in resolves its own instance (a singleton's arrows share one).
67
+ lines.push(' classDef many fill:#eceff1,color:#000000,stroke:#546e7a,stroke-width:2px,stroke-dasharray: 4 2');
56
68
  return lines;
57
69
  }
58
70
  /** One `## Root` section with the design's Mermaid diagram. */
@@ -99,8 +111,9 @@ function toDesignMarkdown(graph) {
99
111
  'name and token are in `design.json`). Rounded nodes are',
100
112
  '`toConstantValue`/`useValue` and `toDynamicValue`/`useFactory` leaves; dashed',
101
113
  'nodes are tokens the analyzer could not resolve; double-bordered nodes are',
102
- 'classes from a published package outside this workspace shown as a boundary',
103
- 'but not expanded into their internals.',
114
+ 'boundaries shown but not expanded: violet = a class from a published package',
115
+ 'outside this workspace; cyan = a generated API-client proxy (`createApiClient`),',
116
+ 'i.e. a service/network boundary whose remote impl lives in another process.',
104
117
  '',
105
118
  ];
106
119
  return header.concat(sections).concat(legend).join('\n');