@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.
- package/package.json +6 -6
- package/src/executors/generate/executor.js +1 -1
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/visualize/executor.js +3 -2
- package/src/executors/visualize/executor.js.map +1 -1
- package/src/executors/visualize-runtime/executor.js +1 -1
- package/src/executors/visualize-runtime/executor.js.map +1 -1
- package/src/lib/graph-names.d.ts +17 -0
- package/src/lib/graph-names.js +24 -0
- package/src/lib/graph-names.js.map +1 -0
- package/src/lib/graph-responsibilities.d.ts +48 -0
- package/src/lib/graph-responsibilities.js +152 -0
- package/src/lib/graph-responsibilities.js.map +1 -0
- package/src/lib/graph-visualizer.client.js +127 -0
- package/src/lib/graph-visualizer.d.ts +77 -27
- package/src/lib/graph-visualizer.js +271 -257
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/scripts/wp-design-visualize.js +1 -1
- 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
|
-
*
|
|
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.
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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 & 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
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
399
|
-
|
|
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
|