@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.
- package/executors.json +5 -0
- package/package.json +6 -6
- package/src/executors/generate/executor.js +7 -1
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-api-relations/executor.d.ts +18 -0
- package/src/executors/validate-api-relations/executor.js +51 -0
- package/src/executors/validate-api-relations/executor.js.map +1 -0
- package/src/executors/validate-api-relations/schema.json +8 -0
- package/src/executors/validate-architecture-unchanged/executor.js +22 -11
- package/src/executors/validate-architecture-unchanged/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/api-usage/api-relations-validator.d.ts +30 -0
- package/src/lib/api-usage/api-relations-validator.js +74 -0
- package/src/lib/api-usage/api-relations-validator.js.map +1 -0
- package/src/lib/api-usage/api-relations.d.ts +51 -0
- package/src/lib/api-usage/api-relations.js +34 -0
- package/src/lib/api-usage/api-relations.js.map +1 -0
- package/src/lib/api-usage/api-scanner.d.ts +60 -0
- package/src/lib/api-usage/api-scanner.js +239 -0
- package/src/lib/api-usage/api-scanner.js.map +1 -0
- package/src/lib/di-graph/analyzer-strategy.d.ts +1 -0
- package/src/lib/di-graph/analyzer-strategy.js +3 -0
- package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
- package/src/lib/graph-comparator.js +8 -0
- package/src/lib/graph-comparator.js.map +1 -1
- package/src/lib/graph-loader.js +18 -0
- package/src/lib/graph-loader.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-sorter.d.ts +5 -0
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.client.js +127 -0
- package/src/lib/graph-visualizer.d.ts +87 -27
- package/src/lib/graph-visualizer.js +316 -257
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/role-resolver.d.ts +3 -0
- package/src/lib/role-resolver.js +4 -1
- package/src/lib/role-resolver.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,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
|
-
|
|
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 === 'api-lib')
|
|
78
|
+
return ', color="#EF6C00", penwidth=2';
|
|
79
|
+
if (role === 'designed-lib')
|
|
80
|
+
return ', penwidth=2';
|
|
81
|
+
return '';
|
|
122
82
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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 & 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
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
399
|
-
|
|
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
|