@webpieces/nx-webpieces-rules 0.3.332 → 0.3.334

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.
@@ -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,137 @@ 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 === 'designed-lib')
78
+ return ', penwidth=2';
79
+ return '';
122
80
  }
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
- });
81
+ /**
82
+ * Click-through href for a node: the project's committed design.html, made
83
+ * relative to architecture/dependencies.html. Returns null when the project
84
+ * has no generated DI design (no design.json → no clickable design page).
85
+ */
86
+ 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);
91
+ }
92
+ /**
93
+ * Generate Graphviz DOT format from the graph
94
+ */
95
+ 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 of Object.keys(graph)) {
103
+ const level = graph[project].level;
104
+ if (!levels[level])
105
+ levels[level] = [];
106
+ levels[level].push(project);
107
+ }
108
+ dot += this.dotNodes(graph);
109
+ dot += '\n';
110
+ // Create same-rank subgraphs for each level
111
+ for (const projects of Object.values(levels)) {
112
+ dot += ` { rank=same; `;
113
+ for (const p of projects) {
114
+ dot += `"${this.names.getShortName(p)}"; `;
115
+ }
116
+ dot += '}\n';
117
+ }
118
+ dot += '\n';
119
+ dot += this.dotEdges(graph);
120
+ dot += '\n labelloc="t";\n';
121
+ dot += ` label="${title}\\n(from architecture/dependencies.json)";\n`;
122
+ dot += ' fontsize=20;\n';
131
123
  dot += '}\n';
124
+ return dot;
132
125
  }
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`;
126
+ // Node lines: fill colored by framework env set (libType), border shaped by
127
+ // role; the label shows the env set + role (e.g. [browser, node] · server). A
128
+ // node with a generated DI design also gets a URL so the rendered SVG box is
129
+ // clickable it opens that project's committed design.html in a new tab.
130
+ dotNodes(graph) {
131
+ let dot = '';
132
+ for (const project of Object.keys(graph)) {
133
+ const info = graph[project];
134
+ const shortName = this.names.getShortName(project);
135
+ const frameworks = info.framework ?? [];
136
+ const role = info.role ?? 'lib';
137
+ const color = this.frameworkColor(frameworks);
138
+ const border = this.roleBorderAttrs(role);
139
+ const href = this.designHtmlHref(info.designFile);
140
+ const link = href ? `, URL="${href}", target="_blank"` : '';
141
+ const envSet = `[${frameworks.join(', ')}]`;
142
+ const labelMeta = `L${info.level} · ${envSet} · ${role}`;
143
+ dot += ` "${shortName}" [fillcolor="${color}"${border}${link}, label="${shortName}\\n(${labelMeta})"];\n`;
140
144
  }
145
+ return dot;
141
146
  }
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>
147
+ // Edge lines (dependencies).
148
+ dotEdges(graph) {
149
+ let dot = '';
150
+ for (const project of Object.keys(graph)) {
151
+ const shortName = this.names.getShortName(project);
152
+ for (const dep of graph[project].dependsOn || []) {
153
+ dot += ` "${shortName}" -> "${this.names.getShortName(dep)}";\n`;
154
+ }
155
+ }
156
+ return dot;
157
+ }
158
+ /**
159
+ * Generate interactive HTML with embedded SVG using viz.js
160
+ */
161
+ generateHTML(dot, title = 'Monorepo Dependency Architecture', lockControl = '', responsibilitiesHtml = '') {
162
+ const styles = this.styles();
163
+ const legend = this.legend();
164
+ const script = this.script(dot);
165
+ return `<!DOCTYPE html>
156
166
  <html>
157
167
  <head>
158
168
  <meta charset="utf-8">
@@ -165,14 +175,44 @@ function generateHTML(dot, title = 'Monorepo Dependency Architecture') {
165
175
  <h1>${title}</h1>
166
176
  <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
177
  <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>
178
+ ${lockControl}
168
179
  ${legend}
169
180
  <div id="graph"></div>
181
+ ${responsibilitiesHtml}
170
182
  <script>${script}</script>
171
183
  </body>
172
184
  </html>`;
173
- }
174
- function generateHTMLStyles() {
175
- return `
185
+ }
186
+ /**
187
+ * The lock control (a single-select dropdown, rendered above the legend).
188
+ * Picking a module LOCKS the graph into that box's hover view — its full
189
+ * ancestor + descendant chain stays lit while everything else stays dimmed —
190
+ * and narrows the responsibilities list below the graph to just that chain.
191
+ * The first option, "All", is the default and clears the lock. Hover still
192
+ * works on top of a lock; leaving a box returns to the locked view.
193
+ *
194
+ * Options are ordered by level DESCENDING to match the responsibilities cards.
195
+ */
196
+ lockControl(graph) {
197
+ const projects = Object.keys(graph);
198
+ projects.sort((a, b) => {
199
+ const levelDiff = graph[b].level - graph[a].level;
200
+ if (levelDiff !== 0)
201
+ return levelDiff;
202
+ return a.localeCompare(b);
203
+ });
204
+ let options = '';
205
+ for (const project of projects) {
206
+ const shortName = this.names.getShortName(project);
207
+ options += `<option value="${shortName}">L${graph[project].level} · ${shortName}</option>`;
208
+ }
209
+ return `<div class="wp-lock-control">
210
+ <label for="wp-lock">🔒 Lock a box (dim the rest &amp; filter responsibilities):</label>
211
+ <select id="wp-lock"><option value="">All (no lock)</option>${options}</select>
212
+ </div>`;
213
+ }
214
+ styles() {
215
+ return `
176
216
  body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }
177
217
  h1 { text-align: center; color: #333; }
178
218
  .hint { text-align: center; color: #555; margin: 0 0 16px; }
@@ -191,13 +231,12 @@ function generateHTMLStyles() {
191
231
  stroke-width: 5;
192
232
  filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));
193
233
  }
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. */
234
+ /* Hover-highlight (wired up in JS after viz.js renders). Hovering a node
235
+ * adds .wp-dim to the <svg> and .wp-focus/.wp-neighbor/.wp-hl to the
236
+ * connected box, its neighbors, and its edges. We ONLY dim: the connected
237
+ * subgraph keeps its exact normal look (full opacity), the rest recedes.
238
+ * The un-dim rules repeat "svg.wp-dim" so they out-specify the dim rule
239
+ * (which has an extra type selector) — else the subgraph stays dimmed. */
201
240
  #graph .node, #graph .edge { transition: opacity 0.12s ease; }
202
241
  #graph svg.wp-dim .node,
203
242
  #graph svg.wp-dim .edge { opacity: 0.15; }
@@ -219,12 +258,8 @@ function generateHTMLStyles() {
219
258
  border-radius: 8px;
220
259
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
221
260
  }
222
- .legend h2 {
223
- margin-top: 0;
224
- }
225
- .legend-item {
226
- margin: 8px 0;
227
- }
261
+ .legend h2 { margin-top: 0; }
262
+ .legend-item { margin: 8px 0; }
228
263
  .legend-box {
229
264
  display: inline-block;
230
265
  width: 20px;
@@ -233,10 +268,56 @@ function generateHTMLStyles() {
233
268
  margin-right: 10px;
234
269
  vertical-align: middle;
235
270
  }
271
+ ${this.componentStyles()}
236
272
  `;
237
- }
238
- function generateHTMLLegend() {
239
- return `<div class="legend">
273
+ }
274
+ // Styles for the lock dropdown and the responsibilities card list below the
275
+ // graph. Split out of styles() to keep each method within the line limit.
276
+ componentStyles() {
277
+ return `
278
+ .wp-lock-control {
279
+ max-width: 600px;
280
+ margin: 0 auto 16px;
281
+ padding: 12px 15px;
282
+ background: white;
283
+ border-radius: 8px;
284
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
285
+ text-align: center;
286
+ }
287
+ .wp-lock-control label { font-weight: bold; color: #333; margin-right: 8px; }
288
+ .wp-lock-control select { font-size: 14px; padding: 4px 8px; }
289
+ #wp-responsibilities { max-width: 900px; margin: 24px auto 0; }
290
+ #wp-responsibilities h2 { color: #333; }
291
+ .wp-resp-card {
292
+ background: white;
293
+ border-radius: 8px;
294
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
295
+ margin: 10px 0;
296
+ padding: 10px 15px;
297
+ }
298
+ .wp-resp-card > summary { cursor: pointer; color: #333; }
299
+ .wp-resp-level {
300
+ display: inline-block;
301
+ min-width: 26px;
302
+ padding: 1px 6px;
303
+ margin-right: 6px;
304
+ border-radius: 4px;
305
+ background: #eef;
306
+ font-size: 12px;
307
+ font-weight: bold;
308
+ text-align: center;
309
+ }
310
+ .wp-resp-body { margin-top: 8px; color: #444; }
311
+ .wp-resp-body code {
312
+ background: #f2f2f2;
313
+ padding: 1px 4px;
314
+ border-radius: 3px;
315
+ font-family: monospace;
316
+ }
317
+ .wp-hidden { display: none; }`;
318
+ }
319
+ legend() {
320
+ return `<div class="legend">
240
321
  <h2>Legend — fill = framework (libType), border = role</h2>
241
322
  <div class="legend-item">
242
323
  <span class="legend-box" style="background: #FCE4EC;"></span>
@@ -278,133 +359,66 @@ function generateHTMLLegend() {
278
359
  <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
360
  </div>
280
361
  </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
362
  }
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}"`;
363
+ /**
364
+ * The page script. The browser code lives in graph-visualizer.client.js (a
365
+ * plain .js asset, NOT a TS template literal) so its dim/highlight/lock
366
+ * functions can be ordinary browser functions — the TS lint rules that scan
367
+ * .ts template strings would otherwise forbid them, and browser JS cannot
368
+ * carry TS return annotations. We inline it and substitute the DOT.
369
+ */
370
+ script(dot) {
371
+ const clientJs = fs.readFileSync(path.join(__dirname, 'graph-visualizer.client.js'), 'utf-8');
372
+ return clientJs.split('__DOT__').join(JSON.stringify(dot));
373
+ }
374
+ /**
375
+ * Write the committed architecture visualization to
376
+ * architecture/dependencies.html, next to dependencies.json.
377
+ *
378
+ * This is a checked-in artifact, regenerated deterministically by
379
+ * architecture:generate so the boxes stay clickable into each project's
380
+ * design.html. The DOT is embedded in the HTML (rendered client-side by
381
+ * viz.js). Output is deterministic (sorted graph in → same bytes out) so git
382
+ * only shows a diff when the architecture actually changed.
383
+ */
384
+ writeVisualization(graph, workspaceRoot, title = 'Monorepo Dependency Architecture') {
385
+ const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);
386
+ if (!fs.existsSync(outputDir)) {
387
+ fs.mkdirSync(outputDir, { recursive: true });
394
388
  }
395
- else if (platform === 'win32') {
396
- openCommand = `start "" "${htmlPath}"`;
389
+ const lockControl = this.lockControl(graph);
390
+ const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);
391
+ const html = this.generateHTML(this.generateDot(graph, title), title, lockControl, responsibilities);
392
+ const htmlPath = path.join(outputDir, 'dependencies.html');
393
+ fs.writeFileSync(htmlPath, html, 'utf-8');
394
+ return new VisualizationPaths(htmlPath);
395
+ }
396
+ /**
397
+ * Open the HTML visualization in the default browser
398
+ */
399
+ openVisualization(htmlPath) {
400
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
401
+ try {
402
+ const platform = process.platform;
403
+ let openCommand;
404
+ if (platform === 'darwin') {
405
+ openCommand = `open "${htmlPath}"`;
406
+ }
407
+ else if (platform === 'win32') {
408
+ openCommand = `start "" "${htmlPath}"`;
409
+ }
410
+ else {
411
+ openCommand = `xdg-open "${htmlPath}"`;
412
+ }
413
+ (0, child_process_1.execSync)(openCommand, { stdio: 'ignore' });
414
+ return true;
397
415
  }
398
- else {
399
- openCommand = `xdg-open "${htmlPath}"`;
416
+ catch (err) {
417
+ const error = (0, toError_1.toError)(err);
418
+ console.warn(`⚠️ Could not open browser: ${error.message}`);
419
+ return false;
400
420
  }
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
421
  }
409
422
  }
423
+ exports.GraphVisualizer = GraphVisualizer;
410
424
  //# sourceMappingURL=graph-visualizer.js.map