@webpieces/nx-webpieces-rules 0.3.333 → 0.3.335

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.
Files changed (47) hide show
  1. package/executors.json +5 -0
  2. package/package.json +6 -6
  3. package/src/executors/generate/executor.js +7 -1
  4. package/src/executors/generate/executor.js.map +1 -1
  5. package/src/executors/validate-api-relations/executor.d.ts +18 -0
  6. package/src/executors/validate-api-relations/executor.js +51 -0
  7. package/src/executors/validate-api-relations/executor.js.map +1 -0
  8. package/src/executors/validate-api-relations/schema.json +8 -0
  9. package/src/executors/validate-architecture-unchanged/executor.js +22 -11
  10. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  11. package/src/executors/visualize/executor.js +3 -2
  12. package/src/executors/visualize/executor.js.map +1 -1
  13. package/src/executors/visualize-runtime/executor.js +1 -1
  14. package/src/executors/visualize-runtime/executor.js.map +1 -1
  15. package/src/lib/api-usage/api-relations-validator.d.ts +30 -0
  16. package/src/lib/api-usage/api-relations-validator.js +74 -0
  17. package/src/lib/api-usage/api-relations-validator.js.map +1 -0
  18. package/src/lib/api-usage/api-relations.d.ts +51 -0
  19. package/src/lib/api-usage/api-relations.js +34 -0
  20. package/src/lib/api-usage/api-relations.js.map +1 -0
  21. package/src/lib/api-usage/api-scanner.d.ts +60 -0
  22. package/src/lib/api-usage/api-scanner.js +239 -0
  23. package/src/lib/api-usage/api-scanner.js.map +1 -0
  24. package/src/lib/di-graph/analyzer-strategy.d.ts +1 -0
  25. package/src/lib/di-graph/analyzer-strategy.js +3 -0
  26. package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
  27. package/src/lib/graph-comparator.js +8 -0
  28. package/src/lib/graph-comparator.js.map +1 -1
  29. package/src/lib/graph-loader.js +18 -0
  30. package/src/lib/graph-loader.js.map +1 -1
  31. package/src/lib/graph-names.d.ts +17 -0
  32. package/src/lib/graph-names.js +24 -0
  33. package/src/lib/graph-names.js.map +1 -0
  34. package/src/lib/graph-responsibilities.d.ts +48 -0
  35. package/src/lib/graph-responsibilities.js +152 -0
  36. package/src/lib/graph-responsibilities.js.map +1 -0
  37. package/src/lib/graph-sorter.d.ts +5 -0
  38. package/src/lib/graph-sorter.js.map +1 -1
  39. package/src/lib/graph-visualizer.client.js +127 -0
  40. package/src/lib/graph-visualizer.d.ts +87 -27
  41. package/src/lib/graph-visualizer.js +316 -257
  42. package/src/lib/graph-visualizer.js.map +1 -1
  43. package/src/lib/role-resolver.d.ts +3 -0
  44. package/src/lib/role-resolver.js +4 -1
  45. package/src/lib/role-resolver.js.map +1 -1
  46. package/src/scripts/wp-design-visualize.js +1 -1
  47. package/src/scripts/wp-design-visualize.js.map +1 -1
@@ -6,17 +6,19 @@
6
6
  * - DOT format (for Graphviz)
7
7
  * - Interactive HTML (using viz.js)
8
8
  *
9
- * Output files go to tmp/webpieces/ for easy viewing without committing.
9
+ * All behavior lives on the injectable GraphVisualizer class so webpieces DI +
10
+ * @DocumentDesign can wire it — module-scope functions are a dead end the DI
11
+ * graph can't reach.
10
12
  */
11
13
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.generateDot = generateDot;
13
- exports.generateHTML = generateHTML;
14
- exports.writeVisualization = writeVisualization;
15
- exports.openVisualization = openVisualization;
14
+ exports.GraphVisualizer = exports.VisualizationPaths = void 0;
16
15
  const tslib_1 = require("tslib");
17
16
  const fs = tslib_1.__importStar(require("fs"));
18
17
  const path = tslib_1.__importStar(require("path"));
19
18
  const child_process_1 = require("child_process");
19
+ const graph_names_1 = require("./graph-names");
20
+ const graph_responsibilities_1 = require("./graph-responsibilities");
21
+ const toError_1 = require("../toError");
20
22
  /**
21
23
  * Framework (libType) colors for visualization — nodes are filled by the FIRST
22
24
  * env in their set that has a color, so it is obvious at a glance which side a
@@ -30,129 +32,161 @@ const FRAMEWORK_COLORS = {
30
32
  node: '#FFF9C4', // yellow - node (server base env)
31
33
  };
32
34
  const DEFAULT_FRAMEWORK_COLOR = '#F5F5F5'; // grey - unknown/empty env set
33
- /**
34
- * Fill color for an env set — the color of the first env in the set that has a
35
- * known color, else the default.
36
- */
37
- function frameworkColor(frameworks) {
38
- for (const env of frameworks) {
39
- const color = FRAMEWORK_COLORS[env];
40
- if (color !== undefined)
41
- return color;
42
- }
43
- return DEFAULT_FRAMEWORK_COLOR;
44
- }
45
- /**
46
- * Role border styling — fill stays keyed on framework; the border shows a
47
- * project's ROLE at a glance. Server and client are the top-level runnable
48
- * nodes, so they get bold, colored borders to stand out:
49
- * server → thick GREEN border (a runnable server app)
50
- * client → thick RED border (a client app, e.g. angular)
51
- * designed-lib → bold border (a library with a generated @DocumentDesign design)
52
- * lib / other → plain thin border
53
- */
54
- function roleBorderAttrs(role) {
55
- if (role === 'server')
56
- return ', color="green", penwidth=3';
57
- if (role === 'client')
58
- return ', color="red", penwidth=3';
59
- if (role === 'designed-lib')
60
- return ', penwidth=2';
61
- return '';
62
- }
63
- /**
64
- * Remove scope from name for display
65
- * '@scope/name' → 'name'
66
- * 'name' → 'name'
67
- */
68
- function getShortName(name) {
69
- return name.includes('/') ? name.split('/').pop() : name;
70
- }
71
35
  /**
72
36
  * Directory (repo-relative) that the committed architecture HTML lives in.
73
37
  * Node click-through links are computed relative to this so they resolve when
74
38
  * the file is opened straight from the checkout.
75
39
  */
76
40
  const ARCH_OUTPUT_DIR = 'architecture';
77
- /**
78
- * Click-through href for a node: the project's committed design.html, made
79
- * relative to architecture/dependencies.html. Returns null when the project has
80
- * no generated DI design (no design.json no clickable design page).
81
- *
82
- * designFile is repo-relative posix (e.g. 'packages/http/http-api/design.json');
83
- * we swap the extension and re-root it at architecture/ so the browser resolves
84
- * '../packages/http/http-api/design.html' from the checkout.
85
- */
86
- function designHtmlHref(designFile) {
87
- if (!designFile)
88
- return null;
89
- const designHtml = designFile.replace(/design\.json$/, 'design.html');
90
- return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);
41
+ class VisualizationPaths {
42
+ htmlPath;
43
+ constructor(htmlPath) {
44
+ this.htmlPath = htmlPath;
45
+ }
91
46
  }
92
- /**
93
- * Generate Graphviz DOT format from the graph
94
- */
95
- function generateDot(graph, title = 'Monorepo Dependency Architecture') {
96
- let dot = 'digraph Architecture {\n';
97
- dot += ' rankdir=TB;\n';
98
- dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
99
- dot += ' edge [fontname="Arial"];\n\n';
100
- // Group projects by level
101
- const levels = {};
102
- for (const [project, info] of Object.entries(graph)) {
103
- if (!levels[info.level])
104
- levels[info.level] = [];
105
- levels[info.level].push(project);
47
+ exports.VisualizationPaths = VisualizationPaths;
48
+ class GraphVisualizer {
49
+ names = new graph_names_1.GraphNames();
50
+ responsibilities = new graph_responsibilities_1.ResponsibilitiesRenderer();
51
+ /**
52
+ * Fill color for an env set — the color of the first env in the set that has
53
+ * a known color, else the default.
54
+ */
55
+ frameworkColor(frameworks) {
56
+ for (const env of frameworks) {
57
+ const color = FRAMEWORK_COLORS[env];
58
+ if (color !== undefined)
59
+ return color;
60
+ }
61
+ return DEFAULT_FRAMEWORK_COLOR;
106
62
  }
107
- // Nodes: fill colored by framework env set (libType), border shaped by role;
108
- // the label shows the env set + role (e.g. [browser, node] · server).
109
- // A node with a generated DI design also gets a URL so the rendered SVG box
110
- // is clickable it opens that project's committed design.html in a new tab.
111
- for (const [project, info] of Object.entries(graph)) {
112
- const shortName = getShortName(project);
113
- const frameworks = info.framework ?? [];
114
- const role = info.role ?? 'lib';
115
- const color = frameworkColor(frameworks);
116
- const border = roleBorderAttrs(role);
117
- const href = designHtmlHref(info.designFile);
118
- const link = href ? `, URL="${href}", target="_blank"` : '';
119
- const envSet = `[${frameworks.join(', ')}]`;
120
- const labelMeta = `L${info.level} · ${envSet} · ${role}`;
121
- dot += ` "${shortName}" [fillcolor="${color}"${border}${link}, label="${shortName}\\n(${labelMeta})"];\n`;
63
+ /**
64
+ * Role border styling fill stays keyed on framework; the border shows a
65
+ * project's ROLE at a glance. Server and client are the top-level runnable
66
+ * nodes, so they get bold, colored borders to stand out:
67
+ * server → thick GREEN border (a runnable server app)
68
+ * client → thick RED border (a client app, e.g. angular)
69
+ * designed-lib → bold border (a library with a generated @DocumentDesign design)
70
+ * lib / other → plain thin border
71
+ */
72
+ roleBorderAttrs(role) {
73
+ if (role === 'server')
74
+ return ', color="green", penwidth=3';
75
+ if (role === 'client')
76
+ return ', color="red", penwidth=3';
77
+ if (role === 'api-lib')
78
+ return ', color="#EF6C00", penwidth=2';
79
+ if (role === 'designed-lib')
80
+ return ', penwidth=2';
81
+ return '';
122
82
  }
123
- dot += '\n';
124
- // Create same-rank subgraphs for each level
125
- for (const [level, projects] of Object.entries(levels)) {
126
- dot += ` { rank=same; `;
127
- projects.forEach((p) => {
128
- const shortName = getShortName(p);
129
- dot += `"${shortName}"; `;
130
- });
83
+ /**
84
+ * Edge styling by API-relation kind (why the edge exists):
85
+ * implements → DASHED (a controller serves this api-lib's contract)
86
+ * uses → SOLID blue, thicker (a generated client calls it)
87
+ * uses-implements → DASHED purple, thicker (does both — implements some
88
+ * contracts of the api-lib, uses others)
89
+ * plain lib (none) → the default thin solid arrow, unchanged.
90
+ * `kind` is undefined for every non-api-lib dependency edge.
91
+ */
92
+ edgeAttrs(kind) {
93
+ if (kind === 'implements')
94
+ return ' [style=dashed]';
95
+ if (kind === 'uses')
96
+ return ' [color="#1976d2", penwidth=2]';
97
+ if (kind === 'uses-implements')
98
+ return ' [style=dashed, color="#8e24aa", penwidth=2]';
99
+ return '';
100
+ }
101
+ /**
102
+ * Click-through href for a node: the project's committed design.html, made
103
+ * relative to architecture/dependencies.html. Returns null when the project
104
+ * has no generated DI design (no design.json → no clickable design page).
105
+ */
106
+ designHtmlHref(designFile) {
107
+ if (!designFile)
108
+ return null;
109
+ const designHtml = designFile.replace(/design\.json$/, 'design.html');
110
+ return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);
111
+ }
112
+ /**
113
+ * Generate Graphviz DOT format from the graph
114
+ */
115
+ generateDot(graph, title = 'Monorepo Dependency Architecture') {
116
+ let dot = 'digraph Architecture {\n';
117
+ dot += ' rankdir=TB;\n';
118
+ dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
119
+ dot += ' edge [fontname="Arial"];\n\n';
120
+ // Group projects by level
121
+ const levels = {};
122
+ for (const project of Object.keys(graph)) {
123
+ const level = graph[project].level;
124
+ if (!levels[level])
125
+ levels[level] = [];
126
+ levels[level].push(project);
127
+ }
128
+ dot += this.dotNodes(graph);
129
+ dot += '\n';
130
+ // Create same-rank subgraphs for each level
131
+ for (const projects of Object.values(levels)) {
132
+ dot += ` { rank=same; `;
133
+ for (const p of projects) {
134
+ dot += `"${this.names.getShortName(p)}"; `;
135
+ }
136
+ dot += '}\n';
137
+ }
138
+ dot += '\n';
139
+ dot += this.dotEdges(graph);
140
+ dot += '\n labelloc="t";\n';
141
+ dot += ` label="${title}\\n(from architecture/dependencies.json)";\n`;
142
+ dot += ' fontsize=20;\n';
131
143
  dot += '}\n';
144
+ return dot;
132
145
  }
133
- dot += '\n';
134
- // Create edges (dependencies)
135
- for (const [project, info] of Object.entries(graph)) {
136
- const shortName = getShortName(project);
137
- for (const dep of info.dependsOn || []) {
138
- const depShortName = getShortName(dep);
139
- dot += ` "${shortName}" -> "${depShortName}";\n`;
146
+ // Node lines: fill colored by framework env set (libType), border shaped by
147
+ // role; the label shows the env set + role (e.g. [browser, node] · server). A
148
+ // node with a generated DI design also gets a URL so the rendered SVG box is
149
+ // clickable it opens that project's committed design.html in a new tab.
150
+ dotNodes(graph) {
151
+ let dot = '';
152
+ for (const project of Object.keys(graph)) {
153
+ const info = graph[project];
154
+ const shortName = this.names.getShortName(project);
155
+ const frameworks = info.framework ?? [];
156
+ const role = info.role ?? 'lib';
157
+ const color = this.frameworkColor(frameworks);
158
+ const border = this.roleBorderAttrs(role);
159
+ const href = this.designHtmlHref(info.designFile);
160
+ const link = href ? `, URL="${href}", target="_blank"` : '';
161
+ const envSet = `[${frameworks.join(', ')}]`;
162
+ const labelMeta = `L${info.level} · ${envSet} · ${role}`;
163
+ dot += ` "${shortName}" [fillcolor="${color}"${border}${link}, label="${shortName}\\n(${labelMeta})"];\n`;
140
164
  }
165
+ return dot;
141
166
  }
142
- dot += '\n labelloc="t";\n';
143
- dot += ` label="${title}\\n(from architecture/dependencies.json)";\n`;
144
- dot += ' fontsize=20;\n';
145
- dot += '}\n';
146
- return dot;
147
- }
148
- /**
149
- * Generate interactive HTML with embedded SVG using viz.js
150
- */
151
- function generateHTML(dot, title = 'Monorepo Dependency Architecture') {
152
- const styles = generateHTMLStyles();
153
- const legend = generateHTMLLegend();
154
- const script = generateHTMLScript(dot);
155
- return `<!DOCTYPE html>
167
+ // Edge lines (dependencies). An edge to an api-lib is styled by WHY it exists
168
+ // (implements/uses/uses-implements, from apiRelations); every other dependency
169
+ // keeps the default plain arrow.
170
+ dotEdges(graph) {
171
+ let dot = '';
172
+ for (const project of Object.keys(graph)) {
173
+ const shortName = this.names.getShortName(project);
174
+ const info = graph[project];
175
+ for (const dep of info.dependsOn || []) {
176
+ const attrs = this.edgeAttrs(info.apiRelations?.[dep]?.kind);
177
+ dot += ` "${shortName}" -> "${this.names.getShortName(dep)}"${attrs};\n`;
178
+ }
179
+ }
180
+ return dot;
181
+ }
182
+ /**
183
+ * Generate interactive HTML with embedded SVG using viz.js
184
+ */
185
+ generateHTML(dot, title = 'Monorepo Dependency Architecture', lockControl = '', responsibilitiesHtml = '') {
186
+ const styles = this.styles();
187
+ const legend = this.legend();
188
+ const script = this.script(dot);
189
+ return `<!DOCTYPE html>
156
190
  <html>
157
191
  <head>
158
192
  <meta charset="utf-8">
@@ -165,14 +199,44 @@ function generateHTML(dot, title = 'Monorepo Dependency Architecture') {
165
199
  <h1>${title}</h1>
166
200
  <p class="hint">💡 Click any box with a generated DI design to open its <strong>design.html</strong> (what the AI sees inside that project).</p>
167
201
  <p class="hint">🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it (all the way up) <em>and</em> every dependency below it (all the way down), with all the boxes and lines between — while the rest of the graph dims so you can follow one box at a glance.</p>
202
+ ${lockControl}
168
203
  ${legend}
169
204
  <div id="graph"></div>
205
+ ${responsibilitiesHtml}
170
206
  <script>${script}</script>
171
207
  </body>
172
208
  </html>`;
173
- }
174
- function generateHTMLStyles() {
175
- return `
209
+ }
210
+ /**
211
+ * The lock control (a single-select dropdown, rendered above the legend).
212
+ * Picking a module LOCKS the graph into that box's hover view — its full
213
+ * ancestor + descendant chain stays lit while everything else stays dimmed —
214
+ * and narrows the responsibilities list below the graph to just that chain.
215
+ * The first option, "All", is the default and clears the lock. Hover still
216
+ * works on top of a lock; leaving a box returns to the locked view.
217
+ *
218
+ * Options are ordered by level DESCENDING to match the responsibilities cards.
219
+ */
220
+ lockControl(graph) {
221
+ const projects = Object.keys(graph);
222
+ projects.sort((a, b) => {
223
+ const levelDiff = graph[b].level - graph[a].level;
224
+ if (levelDiff !== 0)
225
+ return levelDiff;
226
+ return a.localeCompare(b);
227
+ });
228
+ let options = '';
229
+ for (const project of projects) {
230
+ const shortName = this.names.getShortName(project);
231
+ options += `<option value="${shortName}">L${graph[project].level} · ${shortName}</option>`;
232
+ }
233
+ return `<div class="wp-lock-control">
234
+ <label for="wp-lock">🔒 Lock a box (dim the rest &amp; filter responsibilities):</label>
235
+ <select id="wp-lock"><option value="">All (no lock)</option>${options}</select>
236
+ </div>`;
237
+ }
238
+ styles() {
239
+ return `
176
240
  body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }
177
241
  h1 { text-align: center; color: #333; }
178
242
  .hint { text-align: center; color: #555; margin: 0 0 16px; }
@@ -191,13 +255,12 @@ function generateHTMLStyles() {
191
255
  stroke-width: 5;
192
256
  filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));
193
257
  }
194
- /* Hover-highlight (wired up in JS after viz.js renders see
195
- * wireHoverHighlight). Hovering a node adds .wp-dim to the <svg> and
196
- * .wp-focus/.wp-neighbor/.wp-hl to the connected box, its neighbors, and
197
- * its edges. We ONLY dim: the connected subgraph keeps its exact normal
198
- * look (full opacity), the rest recedes. The un-dim rules repeat the
199
- * "svg.wp-dim" ancestor so they out-specify the dim rule (which has an
200
- * extra type selector) — else the highlighted subgraph stays dimmed. */
258
+ /* Hover-highlight (wired up in JS after viz.js renders). Hovering a node
259
+ * adds .wp-dim to the <svg> and .wp-focus/.wp-neighbor/.wp-hl to the
260
+ * connected box, its neighbors, and its edges. We ONLY dim: the connected
261
+ * subgraph keeps its exact normal look (full opacity), the rest recedes.
262
+ * The un-dim rules repeat "svg.wp-dim" so they out-specify the dim rule
263
+ * (which has an extra type selector) — else the subgraph stays dimmed. */
201
264
  #graph .node, #graph .edge { transition: opacity 0.12s ease; }
202
265
  #graph svg.wp-dim .node,
203
266
  #graph svg.wp-dim .edge { opacity: 0.15; }
@@ -219,12 +282,8 @@ function generateHTMLStyles() {
219
282
  border-radius: 8px;
220
283
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
221
284
  }
222
- .legend h2 {
223
- margin-top: 0;
224
- }
225
- .legend-item {
226
- margin: 8px 0;
227
- }
285
+ .legend h2 { margin-top: 0; }
286
+ .legend-item { margin: 8px 0; }
228
287
  .legend-box {
229
288
  display: inline-block;
230
289
  width: 20px;
@@ -233,10 +292,56 @@ function generateHTMLStyles() {
233
292
  margin-right: 10px;
234
293
  vertical-align: middle;
235
294
  }
295
+ ${this.componentStyles()}
236
296
  `;
237
- }
238
- function generateHTMLLegend() {
239
- return `<div class="legend">
297
+ }
298
+ // Styles for the lock dropdown and the responsibilities card list below the
299
+ // graph. Split out of styles() to keep each method within the line limit.
300
+ componentStyles() {
301
+ return `
302
+ .wp-lock-control {
303
+ max-width: 600px;
304
+ margin: 0 auto 16px;
305
+ padding: 12px 15px;
306
+ background: white;
307
+ border-radius: 8px;
308
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
309
+ text-align: center;
310
+ }
311
+ .wp-lock-control label { font-weight: bold; color: #333; margin-right: 8px; }
312
+ .wp-lock-control select { font-size: 14px; padding: 4px 8px; }
313
+ #wp-responsibilities { max-width: 900px; margin: 24px auto 0; }
314
+ #wp-responsibilities h2 { color: #333; }
315
+ .wp-resp-card {
316
+ background: white;
317
+ border-radius: 8px;
318
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
319
+ margin: 10px 0;
320
+ padding: 10px 15px;
321
+ }
322
+ .wp-resp-card > summary { cursor: pointer; color: #333; }
323
+ .wp-resp-level {
324
+ display: inline-block;
325
+ min-width: 26px;
326
+ padding: 1px 6px;
327
+ margin-right: 6px;
328
+ border-radius: 4px;
329
+ background: #eef;
330
+ font-size: 12px;
331
+ font-weight: bold;
332
+ text-align: center;
333
+ }
334
+ .wp-resp-body { margin-top: 8px; color: #444; }
335
+ .wp-resp-body code {
336
+ background: #f2f2f2;
337
+ padding: 1px 4px;
338
+ border-radius: 3px;
339
+ font-family: monospace;
340
+ }
341
+ .wp-hidden { display: none; }`;
342
+ }
343
+ legend() {
344
+ return `<div class="legend">
240
345
  <h2>Legend — fill = framework (libType), border = role</h2>
241
346
  <div class="legend-item">
242
347
  <span class="legend-box" style="background: #FCE4EC;"></span>
@@ -274,137 +379,91 @@ function generateHTMLLegend() {
274
379
  <span class="legend-box" style="border: 1px solid #ccc;"></span>
275
380
  <strong>lib:</strong> plain library, no generated design (thin border)
276
381
  </div>
382
+ <div class="legend-item">
383
+ <span class="legend-box" style="border: 2px solid #EF6C00;"></span>
384
+ <strong>api-lib:</strong> API-contract library (defines <code>@ApiPath</code>/<code>@Rpc</code>/<code>@PubSub</code> <code>*Api</code> classes)
385
+ </div>
386
+ <h2 style="margin-top: 18px;">Edge lines — <em>why</em> a project depends on an api-lib</h2>
387
+ <div class="legend-item">
388
+ <svg width="42" height="12" style="vertical-align: middle; margin-right: 10px;"><line x1="0" y1="6" x2="42" y2="6" stroke="#333" stroke-width="2" stroke-dasharray="5,3"/></svg>
389
+ <strong>implements:</strong> serves the API (a controller is registered via <code>addRoutes</code>)
390
+ </div>
391
+ <div class="legend-item">
392
+ <svg width="42" height="12" style="vertical-align: middle; margin-right: 10px;"><line x1="0" y1="6" x2="42" y2="6" stroke="#1976d2" stroke-width="2"/></svg>
393
+ <strong>uses:</strong> calls the API (generates an rpc/pubsub client via <code>createRpcClient</code>/<code>createPubSubClient</code>)
394
+ </div>
395
+ <div class="legend-item">
396
+ <svg width="42" height="12" style="vertical-align: middle; margin-right: 10px;"><line x1="0" y1="6" x2="42" y2="6" stroke="#8e24aa" stroke-width="2" stroke-dasharray="5,3"/></svg>
397
+ <strong>uses-implements:</strong> both — implements some of the api-lib's contracts, uses others
398
+ </div>
399
+ <div class="legend-item">
400
+ <svg width="42" height="12" style="vertical-align: middle; margin-right: 10px;"><line x1="0" y1="6" x2="42" y2="6" stroke="#999" stroke-width="1.5"/></svg>
401
+ <strong>plain dependency:</strong> a normal library import (no API relationship)
402
+ </div>
277
403
  <div class="legend-item" style="margin-top: 15px;">
278
404
  <em>Each node label shows its dependency level (L#), its framework env set (e.g. [browser, node]), and its role. Rows are laid out by level (top = no dependencies), with the deepest libraries at the bottom. Transitive dependencies are allowed but not shown.</em>
279
405
  </div>
280
406
  </div>`;
281
- }
282
- /**
283
- * The page script (injected as a string). After viz.js renders the Graphviz
284
- * SVG, `wireHoverHighlight` indexes its nodes and edges so hovering a box
285
- * boldens every connection (incoming AND outgoing) and highlights the boxes on
286
- * the other end, dimming the rest. viz.js emits a predictable structure:
287
- * <g class="node"><title>NAME</title> ... </g>
288
- * <g class="edge"><title>FROM->TO</title> <path/> <polygon/> ... </g>
289
- * The edge <title> text (decoded by the DOM as "FROM->TO") gives both
290
- * endpoints, so adjacency is built without touching the DOT.
291
- */
292
- function generateHTMLScript(dot) {
293
- return `
294
- const dot = ${JSON.stringify(dot)};
295
- const viz = new Viz();
296
- viz.renderSVGElement(dot)
297
- .then(element => {
298
- document.getElementById('graph').appendChild(element);
299
- wireHoverHighlight(element);
300
- })
301
- .catch(err => {
302
- console.error(err);
303
- document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';
304
- });
305
- function wireHoverHighlight(svg) {
306
- const nodeByName = new Map();
307
- svg.querySelectorAll('g.node').forEach(g => {
308
- const t = g.querySelector('title');
309
- if (t) nodeByName.set(t.textContent.trim(), g);
310
- });
311
- // Directed adjacency: in* = entering (up/ancestors), out* = leaving (down/deps).
312
- const inEdges = new Map(), outEdges = new Map(), inNodes = new Map(), outNodes = new Map();
313
- const ensure = (map, key) => {
314
- let v = map.get(key);
315
- if (!v) { v = new Set(); map.set(key, v); }
316
- return v;
317
- };
318
- svg.querySelectorAll('g.edge').forEach(edge => {
319
- const t = edge.querySelector('title');
320
- if (!t) return;
321
- const idx = t.textContent.indexOf('->');
322
- if (idx < 0) return;
323
- const from = t.textContent.slice(0, idx).trim();
324
- const to = t.textContent.slice(idx + 2).trim();
325
- ensure(outEdges, from).add(edge);
326
- ensure(inEdges, to).add(edge);
327
- ensure(outNodes, from).add(to);
328
- ensure(inNodes, to).add(from);
329
- });
330
- const clear = () => {
331
- svg.classList.remove('wp-dim');
332
- svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach(el => {
333
- el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');
334
- });
335
- };
336
- const highlight = (name, focusEl) => {
337
- clear();
338
- svg.classList.add('wp-dim');
339
- focusEl.classList.add('wp-focus');
340
- // Transitively light ancestors (up) then descendants (down): edges
341
- // reached -> wp-hl, boxes -> wp-neighbor. visited guards cycles.
342
- [[inNodes, inEdges], [outNodes, outEdges]].forEach(dir => {
343
- const visited = new Set(); const stack = [name];
344
- while (stack.length) {
345
- const cur = stack.pop();
346
- (dir[1].get(cur) || []).forEach(e => e.classList.add('wp-hl'));
347
- (dir[0].get(cur) || []).forEach(next => {
348
- if (visited.has(next)) return;
349
- visited.add(next); stack.push(next);
350
- const g = nodeByName.get(next); if (g) g.classList.add('wp-neighbor');
351
- });
352
- }
353
- });
354
- };
355
- nodeByName.forEach((g, name) => {
356
- g.addEventListener('mouseenter', () => highlight(name, g));
357
- g.addEventListener('mouseleave', clear);
358
- });
359
- }
360
- `;
361
- }
362
- /**
363
- * Write the committed architecture visualization to architecture/dependencies.html,
364
- * next to dependencies.json.
365
- *
366
- * This is a checked-in artifact, regenerated deterministically by
367
- * architecture:generate so the boxes stay clickable into each project's
368
- * design.html. The DOT is embedded in the HTML (rendered client-side by
369
- * viz.js), so no separate .dot file is committed — same as design.html. Output
370
- * is deterministic (sorted graph in → same bytes out) so git only shows a diff
371
- * when the architecture actually changed.
372
- */
373
- function writeVisualization(graph, workspaceRoot, title = 'Monorepo Dependency Architecture') {
374
- const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);
375
- // Ensure directory exists
376
- if (!fs.existsSync(outputDir)) {
377
- fs.mkdirSync(outputDir, { recursive: true });
378
407
  }
379
- const html = generateHTML(generateDot(graph, title), title);
380
- const htmlPath = path.join(outputDir, 'dependencies.html');
381
- fs.writeFileSync(htmlPath, html, 'utf-8');
382
- return { htmlPath };
383
- }
384
- /**
385
- * Open the HTML visualization in the default browser
386
- */
387
- function openVisualization(htmlPath) {
388
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
389
- try {
390
- const platform = process.platform;
391
- let openCommand;
392
- if (platform === 'darwin') {
393
- openCommand = `open "${htmlPath}"`;
408
+ /**
409
+ * The page script. The browser code lives in graph-visualizer.client.js (a
410
+ * plain .js asset, NOT a TS template literal) so its dim/highlight/lock
411
+ * functions can be ordinary browser functions — the TS lint rules that scan
412
+ * .ts template strings would otherwise forbid them, and browser JS cannot
413
+ * carry TS return annotations. We inline it and substitute the DOT.
414
+ */
415
+ script(dot) {
416
+ const clientJs = fs.readFileSync(path.join(__dirname, 'graph-visualizer.client.js'), 'utf-8');
417
+ return clientJs.split('__DOT__').join(JSON.stringify(dot));
418
+ }
419
+ /**
420
+ * Write the committed architecture visualization to
421
+ * architecture/dependencies.html, next to dependencies.json.
422
+ *
423
+ * This is a checked-in artifact, regenerated deterministically by
424
+ * architecture:generate so the boxes stay clickable into each project's
425
+ * design.html. The DOT is embedded in the HTML (rendered client-side by
426
+ * viz.js). Output is deterministic (sorted graph in → same bytes out) so git
427
+ * only shows a diff when the architecture actually changed.
428
+ */
429
+ writeVisualization(graph, workspaceRoot, title = 'Monorepo Dependency Architecture') {
430
+ const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);
431
+ if (!fs.existsSync(outputDir)) {
432
+ fs.mkdirSync(outputDir, { recursive: true });
394
433
  }
395
- else if (platform === 'win32') {
396
- openCommand = `start "" "${htmlPath}"`;
434
+ const lockControl = this.lockControl(graph);
435
+ const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);
436
+ const html = this.generateHTML(this.generateDot(graph, title), title, lockControl, responsibilities);
437
+ const htmlPath = path.join(outputDir, 'dependencies.html');
438
+ fs.writeFileSync(htmlPath, html, 'utf-8');
439
+ return new VisualizationPaths(htmlPath);
440
+ }
441
+ /**
442
+ * Open the HTML visualization in the default browser
443
+ */
444
+ openVisualization(htmlPath) {
445
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
446
+ try {
447
+ const platform = process.platform;
448
+ let openCommand;
449
+ if (platform === 'darwin') {
450
+ openCommand = `open "${htmlPath}"`;
451
+ }
452
+ else if (platform === 'win32') {
453
+ openCommand = `start "" "${htmlPath}"`;
454
+ }
455
+ else {
456
+ openCommand = `xdg-open "${htmlPath}"`;
457
+ }
458
+ (0, child_process_1.execSync)(openCommand, { stdio: 'ignore' });
459
+ return true;
397
460
  }
398
- else {
399
- openCommand = `xdg-open "${htmlPath}"`;
461
+ catch (err) {
462
+ const error = (0, toError_1.toError)(err);
463
+ console.warn(`⚠️ Could not open browser: ${error.message}`);
464
+ return false;
400
465
  }
401
- (0, child_process_1.execSync)(openCommand, { stdio: 'ignore' });
402
- return true;
403
- }
404
- catch (err) {
405
- //const error = toError(err);
406
- void err;
407
- return false;
408
466
  }
409
467
  }
468
+ exports.GraphVisualizer = GraphVisualizer;
410
469
  //# sourceMappingURL=graph-visualizer.js.map