@webpieces/nx-webpieces-rules 0.4.519 → 0.4.521
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 +54 -34
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.js +46 -10
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +1 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/lib/api-usage/api-ast.d.ts +17 -1
- package/src/lib/api-usage/api-ast.js +39 -1
- package/src/lib/api-usage/api-ast.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +41 -0
- package/src/lib/api-usage/api-relations.js +15 -1
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/api-usage/external-systems.d.ts +61 -0
- package/src/lib/api-usage/external-systems.js +158 -0
- package/src/lib/api-usage/external-systems.js.map +1 -0
- package/src/lib/dot-syntax.d.ts +13 -0
- package/src/lib/dot-syntax.js +17 -0
- package/src/lib/dot-syntax.js.map +1 -1
- package/src/lib/graph-loader.d.ts +15 -3
- package/src/lib/graph-loader.js +36 -3
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-visualizer.client.js +7 -4
- package/src/lib/graph-visualizer.js +1 -2
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-graph-model.d.ts +38 -1
- package/src/lib/runtime-graph-model.js.map +1 -1
- package/src/lib/runtime-graph.d.ts +3 -3
- package/src/lib/runtime-graph.js +12 -3
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +14 -6
- package/src/lib/runtime-visualizer.js +238 -31
- package/src/lib/runtime-visualizer.js.map +1 -1
|
@@ -6,16 +6,24 @@
|
|
|
6
6
|
* each labeled with the api(s) they flow over) to DOT + interactive HTML in
|
|
7
7
|
* tmp/webpieces/runtime-architecture.{dot,html}.
|
|
8
8
|
*
|
|
9
|
-
* Each service node names the contracts it IMPLEMENTS
|
|
10
|
-
* single most important fact in a microservice architecture, and it used to be
|
|
9
|
+
* Each service node names the contracts it IMPLEMENTS. That list used to be
|
|
11
10
|
* collapsed into a server/client boolean and thrown away — leaving an api that a
|
|
12
11
|
* server serves but nothing in-repo calls completely invisible, and making a
|
|
13
|
-
* correct api design look like a detection failure.
|
|
12
|
+
* correct api design look like a detection failure. What a service USES is NOT
|
|
13
|
+
* listed: every use already draws an outgoing arrow labeled with the same
|
|
14
|
+
* contract, so repeating it in the box only made every box wider.
|
|
15
|
+
*
|
|
16
|
+
* Shape says what a node IS and line style says what a call IS. Solid = rpc: the
|
|
17
|
+
* request follows the arrow and the response flows back. Dashed = event: it flows
|
|
18
|
+
* in the arrow's direction and returns once it is queued. A queue is a sideways
|
|
19
|
+
* cylinder, a datastore an upright one.
|
|
14
20
|
*
|
|
15
21
|
* Calls that leave the repo (a contract NOTHING in-repo implements — firestore,
|
|
16
|
-
* gmail, ...) are drawn as
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* gmail, ...) are drawn as terminal nodes, so the vendor systems that actually
|
|
23
|
+
* page you at 3am stop being missing from the picture. One that DECLARES what it
|
|
24
|
+
* is, via an `@externalSystem` JSDoc tag or an `external:` nx tag, gets the shape
|
|
25
|
+
* of the thing it is; the rest stay generic dashed boxes. They are RENDER-ONLY:
|
|
26
|
+
* derivation, levels and cycle detection never see them.
|
|
19
27
|
*
|
|
20
28
|
* The same is true in the other direction for endpoints nothing in-repo CALLS: a
|
|
21
29
|
* `cron` method hangs off a clock and an `external` method off a dashed inbound
|
|
@@ -37,6 +45,41 @@ const LEVEL_COLORS = {
|
|
|
37
45
|
3: '#FCE4EC',
|
|
38
46
|
};
|
|
39
47
|
const QUEUE_FILL = '#FFF3E0';
|
|
48
|
+
/**
|
|
49
|
+
* The queue node is an `Mrecord` whose FIRST field is empty, which draws a rounded outline with a
|
|
50
|
+
* vertical cap line near one end — a cylinder lying on its side, distinguishing a queue from the
|
|
51
|
+
* upright cylinder that now means a database.
|
|
52
|
+
*
|
|
53
|
+
* Two things here are load-bearing and easy to break:
|
|
54
|
+
*
|
|
55
|
+
* 1. NO surrounding `{}`. Record fields lay out along the rank direction, and this graph is
|
|
56
|
+
* `rankdir=TB` (see {@link generateRuntimeDot}), where the default is horizontal — which is what
|
|
57
|
+
* we want. Adding braces TOGGLES that, turning the cap line into a band across the top.
|
|
58
|
+
* 2. The leading space is the empty field. The record parser trims it to nothing, which is the
|
|
59
|
+
* point; it must survive as its own field, so the `|` cannot be dropped.
|
|
60
|
+
*
|
|
61
|
+
* Graphviz has no sideways cylinder and never has: `orientation=` is documented as rotating POLYGON
|
|
62
|
+
* shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute (graphviz issue
|
|
63
|
+
* #2244, open since 2022 and still reproducible on 13.0.0). This is the closest native shape.
|
|
64
|
+
*/
|
|
65
|
+
const QUEUE_SHAPE = 'Mrecord';
|
|
66
|
+
const QUEUE_LABEL_PREFIX = ' |';
|
|
67
|
+
/** Fill for the upright cylinder standing for an external DATASTORE (firestore, postgres, ...). */
|
|
68
|
+
const DATABASE_FILL = '#E1F5FE';
|
|
69
|
+
/** Shape per external-system kind. Anything unrecognised falls back to the generic dashed box. */
|
|
70
|
+
const EXTERNAL_SHAPES = {
|
|
71
|
+
database: 'cylinder',
|
|
72
|
+
cache: 'cylinder',
|
|
73
|
+
queue: 'Mrecord',
|
|
74
|
+
storage: 'folder',
|
|
75
|
+
};
|
|
76
|
+
/** Fill per external-system kind, paired with {@link EXTERNAL_SHAPES}. */
|
|
77
|
+
const EXTERNAL_FILLS = {
|
|
78
|
+
database: DATABASE_FILL,
|
|
79
|
+
cache: DATABASE_FILL,
|
|
80
|
+
queue: QUEUE_FILL,
|
|
81
|
+
storage: '#F3E5F5',
|
|
82
|
+
};
|
|
40
83
|
/** Fill + border for the dashed terminal node standing for a system outside this repo. */
|
|
41
84
|
const EXTERNAL_FILL = '#FAFAFA';
|
|
42
85
|
const EXTERNAL_BORDER = '#9E9E9E';
|
|
@@ -87,8 +130,16 @@ function implementsEntries(svc) {
|
|
|
87
130
|
});
|
|
88
131
|
}
|
|
89
132
|
/**
|
|
90
|
-
* The full node label: name, role/level/declared service name, then the contracts it
|
|
91
|
-
*
|
|
133
|
+
* The full node label: name, role/level/declared service name, then the contracts it SERVES.
|
|
134
|
+
*
|
|
135
|
+
* What a service USES is deliberately absent. Every `uses` entry already draws an outgoing arrow —
|
|
136
|
+
* to the implementing service, to a queue, or to an external node — and the arrow carries the same
|
|
137
|
+
* contract name as its label, so listing them in the box restated the picture and made every box
|
|
138
|
+
* wider than it needed to be. `implements` stays because it has no such arrow: an api a service
|
|
139
|
+
* serves but nothing in-repo calls is invisible otherwise.
|
|
140
|
+
*
|
|
141
|
+
* The one case this loses information is `showExternalNodes:false`, where an outbound call draws no
|
|
142
|
+
* node and therefore no arrow. That is precisely what that opt-out asks for, and the legend says so.
|
|
92
143
|
*/
|
|
93
144
|
// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
|
|
94
145
|
function nodeLabel(name, svc) {
|
|
@@ -99,32 +150,38 @@ function nodeLabel(name, svc) {
|
|
|
99
150
|
let label = `${(0, dot_syntax_1.dotValue)(getShortName(name))}\\n(${role}, L${svc.level}${declared})`;
|
|
100
151
|
if (svc.implements.length > 0)
|
|
101
152
|
label += `\\nimplements: ${labelList(implementsEntries(svc))}`;
|
|
102
|
-
if (svc.uses.length > 0)
|
|
103
|
-
label += `\\nuses: ${labelList(svc.uses)}`;
|
|
104
153
|
return label;
|
|
105
154
|
}
|
|
106
155
|
/**
|
|
107
|
-
* DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer
|
|
108
|
-
* producer enqueues and the consumer is delivered later, so we draw
|
|
109
|
-
*
|
|
156
|
+
* DOT for ONE runtime edge. rpc → a direct labeled SOLID arrow (producer calls consumer, response
|
|
157
|
+
* comes back). pubsub → the producer enqueues and the consumer is delivered later, so we draw
|
|
158
|
+
* producer → QUEUE → consumer through a sideways-cylinder queue node with DASHED arrows.
|
|
159
|
+
*
|
|
160
|
+
* Solid vs dashed is the graph's one line-level distinction: solid is a call that returns a
|
|
161
|
+
* response, dashed is an event that returns as soon as it is queued.
|
|
110
162
|
*/
|
|
111
163
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
112
164
|
function edgeDot(edge, queues) {
|
|
113
165
|
const from = (0, dot_syntax_1.dotValue)(getShortName(edge.from));
|
|
114
166
|
const to = (0, dot_syntax_1.dotValue)(getShortName(edge.to));
|
|
115
|
-
|
|
167
|
+
// Kept RAW: an ordinary edge label needs dotValue, the record-mode queue label needs
|
|
168
|
+
// recordValue, and recordValue already applies dotValue — escaping here would double it.
|
|
169
|
+
const viaRaw = edge.via.map((v) => getShortName(v)).join(', ');
|
|
116
170
|
if (edge.type !== 'pubsub') {
|
|
117
|
-
return ` "${from}" -> "${to}" [label="${
|
|
171
|
+
return ` "${from}" -> "${to}" [label="${(0, dot_syntax_1.dotValue)(viaRaw)}"];\n`;
|
|
118
172
|
}
|
|
119
173
|
// The queue node is identified by the METHOD, not by the (from,to) pair, so every producer and
|
|
120
174
|
// consumer of one queue converges on ONE box — including a service that enqueues to itself,
|
|
121
175
|
// which then renders as a visible loop through its queue instead of vanishing.
|
|
122
176
|
const queueId = edge.queue === undefined ? `queue__${from}__${to}` : `queue__${dotId(edge.queue)}`;
|
|
123
177
|
const queueName = edge.queue === undefined ? undefined : queues[edge.queue]?.queueName;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
178
|
+
// Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the
|
|
179
|
+
// empty leading field that draws the cylinder's end cap.
|
|
180
|
+
const body = edge.queue === undefined
|
|
181
|
+
? `${(0, dot_syntax_1.recordValue)(viaRaw)}\\nqueue`
|
|
182
|
+
: `${(0, dot_syntax_1.recordValue)(edge.queue)}\\nqueue: ${(0, dot_syntax_1.recordValue)(queueName ?? edge.queue)}`;
|
|
183
|
+
return (` "${queueId}" [shape=${QUEUE_SHAPE}, style="filled", fillcolor="${QUEUE_FILL}", ` +
|
|
184
|
+
`label="${QUEUE_LABEL_PREFIX}${body}"];\n` +
|
|
128
185
|
` "${from}" -> "${queueId}" [label="enqueue", style=dashed];\n` +
|
|
129
186
|
` "${queueId}" -> "${to}" [label="deliver", style=dashed];\n`);
|
|
130
187
|
}
|
|
@@ -168,6 +225,46 @@ function triggerDot(graph, hidden) {
|
|
|
168
225
|
}
|
|
169
226
|
return dot;
|
|
170
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The DECLARED external systems — the ones that said what they are, so they get a shape that says
|
|
230
|
+
* it: a cylinder for a database, a folder for a bucket. Everything undeclared falls through to
|
|
231
|
+
* {@link externalDot}'s generic grey box, which is why adding this broke nothing existing.
|
|
232
|
+
*
|
|
233
|
+
* The arrows are SOLID. A call to firestore or postgres is synchronous — it returns a value — and
|
|
234
|
+
* the graph's rule is that solid means "response comes back". Being outside the repo is carried by
|
|
235
|
+
* the node's shape, never by the line style; conflating the two is what made a blocking database
|
|
236
|
+
* read look like an event.
|
|
237
|
+
*/
|
|
238
|
+
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
239
|
+
function externalSystemsDot(graph, hidden) {
|
|
240
|
+
const systems = graph.externalSystems ?? {};
|
|
241
|
+
const ids = Object.keys(systems).sort();
|
|
242
|
+
if (ids.length === 0)
|
|
243
|
+
return '';
|
|
244
|
+
let dot = '\n // Declared external systems — drawn with the shape of what they actually are.\n';
|
|
245
|
+
for (const id of ids) {
|
|
246
|
+
const system = systems[id];
|
|
247
|
+
const shape = EXTERNAL_SHAPES[system.kind] ?? 'box';
|
|
248
|
+
const fill = EXTERNAL_FILLS[system.kind] ?? EXTERNAL_FILL;
|
|
249
|
+
// An Mrecord-shaped system needs the same empty leading field as a queue node, or it
|
|
250
|
+
// renders as a plain box and silently loses the sideways-cylinder read.
|
|
251
|
+
const prefix = shape === QUEUE_SHAPE ? QUEUE_LABEL_PREFIX : '';
|
|
252
|
+
const text = shape === QUEUE_SHAPE ? (0, dot_syntax_1.recordValue)(system.label) : (0, dot_syntax_1.dotValue)(system.label);
|
|
253
|
+
dot +=
|
|
254
|
+
` "system__${dotId(id)}" [shape=${shape}, style="filled", fillcolor="${fill}", ` +
|
|
255
|
+
`label="${prefix}${text}\\n(external ${(0, dot_syntax_1.dotValue)(system.kind)})"];\n`;
|
|
256
|
+
}
|
|
257
|
+
for (const id of ids) {
|
|
258
|
+
const system = systems[id];
|
|
259
|
+
const via = system.apis.length === 0 ? '' : ` [label="${labelList([...system.apis].sort())}"]`;
|
|
260
|
+
for (const service of [...system.usedBy].sort()) {
|
|
261
|
+
if (hidden.has(service))
|
|
262
|
+
continue;
|
|
263
|
+
dot += ` "${(0, dot_syntax_1.dotValue)(getShortName(service))}" -> "system__${dotId(id)}"${via};\n`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return dot;
|
|
267
|
+
}
|
|
171
268
|
/**
|
|
172
269
|
* The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —
|
|
173
270
|
* a contract used by a node and implemented by nobody in-repo — which the derivation already
|
|
@@ -176,6 +273,9 @@ function triggerDot(graph, hidden) {
|
|
|
176
273
|
* Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts
|
|
177
274
|
* draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they
|
|
178
275
|
* are absent from levels, cycle detection and the transitive implements attribution.
|
|
276
|
+
*
|
|
277
|
+
* A contract carrying an `@externalSystem` declaration is skipped here — {@link externalSystemsDot}
|
|
278
|
+
* has already drawn it with a real shape, and rendering it in both places would double the node.
|
|
179
279
|
*/
|
|
180
280
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
181
281
|
function externalDot(graph, hidden) {
|
|
@@ -184,6 +284,8 @@ function externalDot(graph, hidden) {
|
|
|
184
284
|
for (const use of graph.unresolvedUses) {
|
|
185
285
|
if (hidden.has(use.service))
|
|
186
286
|
continue;
|
|
287
|
+
if (graph.apis[use.api]?.externalSystem !== undefined)
|
|
288
|
+
continue;
|
|
187
289
|
const external = (0, dot_syntax_1.dotValue)(getShortName(graph.apis[use.api]?.owner ?? use.api));
|
|
188
290
|
const key = `${use.service}${PAIR_SEP}${external}`;
|
|
189
291
|
if (!apisByPair.has(key))
|
|
@@ -206,9 +308,11 @@ function externalDot(graph, hidden) {
|
|
|
206
308
|
const service = parts[0];
|
|
207
309
|
const external = parts[1];
|
|
208
310
|
const via = labelList(apisByPair.get(key).sort());
|
|
311
|
+
// SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and
|
|
312
|
+
// "outside the repo" is already said by the node's dashed border.
|
|
209
313
|
dot +=
|
|
210
314
|
` "${(0, dot_syntax_1.dotValue)(getShortName(service))}" -> "external__${external}" ` +
|
|
211
|
-
`[label="${via}",
|
|
315
|
+
`[label="${via}", color="${EXTERNAL_BORDER}"];\n`;
|
|
212
316
|
}
|
|
213
317
|
return dot;
|
|
214
318
|
}
|
|
@@ -236,8 +340,10 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
236
340
|
dot += edgeDot(edge, graph.queues);
|
|
237
341
|
}
|
|
238
342
|
dot += triggerDot(graph, hidden);
|
|
239
|
-
if (options.showExternalNodes)
|
|
343
|
+
if (options.showExternalNodes) {
|
|
344
|
+
dot += externalSystemsDot(graph, hidden);
|
|
240
345
|
dot += externalDot(graph, hidden);
|
|
346
|
+
}
|
|
241
347
|
dot += '\n labelloc="t";\n';
|
|
242
348
|
dot += ` label="${(0, dot_syntax_1.dotValue)(title)}\\n(from architecture/runtime-dependencies.json)";\n`;
|
|
243
349
|
dot += ' fontsize=20;\n';
|
|
@@ -247,33 +353,134 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
247
353
|
(0, dot_syntax_1.assertValidDot)(dot, 'runtime-architecture.dot');
|
|
248
354
|
return dot;
|
|
249
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Inline SVG swatches for the legend, hand-drawn to match what Graphviz emits for each shape.
|
|
358
|
+
*
|
|
359
|
+
* Hand-drawn on purpose: the alternative is shelling out to Graphviz at generate time, which would
|
|
360
|
+
* make writing the HTML depend on a `dot` binary being installed — a dependency this tool does not
|
|
361
|
+
* otherwise have, since rendering happens in the browser.
|
|
362
|
+
*/
|
|
363
|
+
class LegendSwatches {
|
|
364
|
+
service = '<svg width="46" height="26"><rect x="1" y="3" width="44" height="20" rx="7" fill="#E8F5E9" stroke="#333"/></svg>';
|
|
365
|
+
/** Rounded outline + a cap line near the left end — the sideways cylinder the queue node draws. */
|
|
366
|
+
queue = `<svg width="46" height="26"><rect x="1" y="4" width="44" height="18" rx="9" fill="${QUEUE_FILL}" stroke="#333"/>` +
|
|
367
|
+
'<line x1="12" y1="4" x2="12" y2="22" stroke="#333"/></svg>';
|
|
368
|
+
database = `<svg width="46" height="26"><path d="M8,7 a15,4 0 0 1 30,0 v12 a15,4 0 0 1 -30,0 z" fill="${DATABASE_FILL}" stroke="#333"/>` +
|
|
369
|
+
'<path d="M8,7 a15,4 0 0 0 30,0" fill="none" stroke="#333"/></svg>';
|
|
370
|
+
storage = '<svg width="46" height="26"><path d="M2,22 V6 H16 l3,3 H44 V22 Z" fill="#F3E5F5" stroke="#333"/></svg>';
|
|
371
|
+
external = `<svg width="46" height="26"><rect x="1" y="3" width="44" height="20" fill="${EXTERNAL_FILL}" ` +
|
|
372
|
+
`stroke="${EXTERNAL_BORDER}" stroke-dasharray="4,3"/></svg>`;
|
|
373
|
+
cron = `<svg width="46" height="26"><circle cx="23" cy="13" r="11" fill="${CRON_FILL}" stroke="${CRON_BORDER}"/>` +
|
|
374
|
+
'<text x="23" y="18" font-size="12" text-anchor="middle">⏰</text></svg>';
|
|
375
|
+
solid = '<svg width="60" height="20"><line x1="2" y1="10" x2="48" y2="10" stroke="#333" stroke-width="1.5"/>' +
|
|
376
|
+
'<path d="M48,6 L57,10 L48,14 Z" fill="#333"/></svg>';
|
|
377
|
+
dashed = '<svg width="60" height="20"><line x1="2" y1="10" x2="48" y2="10" stroke="#333" stroke-width="1.5" ' +
|
|
378
|
+
'stroke-dasharray="5,4"/><path d="M48,6 L57,10 L48,14 Z" fill="#333"/></svg>';
|
|
379
|
+
scheduled = `<svg width="60" height="20"><line x1="2" y1="10" x2="48" y2="10" stroke="${CRON_BORDER}" stroke-width="1.5"/>` +
|
|
380
|
+
`<path d="M48,6 L57,10 L48,14 Z" fill="${CRON_BORDER}"/></svg>`;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The legend. Three columns — what a box IS, what a line MEANS, how to read a box — replacing the
|
|
384
|
+
* three paragraphs of prose that used to restate the picture in words. Styled after
|
|
385
|
+
* {@link GraphVisualizer}'s legend so the two graphs in this repo look like one tool.
|
|
386
|
+
*/
|
|
387
|
+
// webpieces-disable no-function-outside-class -- HTML builder, matching the sibling builders in this file
|
|
388
|
+
function legendHtml() {
|
|
389
|
+
const sw = new LegendSwatches();
|
|
390
|
+
const item = (swatch, text) => `<div class="legend-item"><span class="sw">${swatch}</span><span>${text}</span></div>`;
|
|
391
|
+
return `<div class="legend">
|
|
392
|
+
<h2>Legend</h2>
|
|
393
|
+
<div class="legend-columns">
|
|
394
|
+
<div class="legend-col">
|
|
395
|
+
<h3>Node shapes — <em>what a box is</em></h3>
|
|
396
|
+
${item(sw.service, '<strong>service</strong> — a deployable in this repo; fill is its dependency level')}
|
|
397
|
+
${item(sw.queue, '<strong>queue</strong> — one box <em>per method</em>, the unit Cloud Tasks and Terraform actually create')}
|
|
398
|
+
${item(sw.database, '<strong>database</strong> — a datastore outside this repo')}
|
|
399
|
+
${item(sw.storage, '<strong>object storage</strong> — a bucket outside this repo')}
|
|
400
|
+
${item(sw.external, '<strong>external system</strong> — outside this repo; nothing here implements it. Pointing <strong>OUT</strong> = a system this repo calls (firestore, gmail). Pointing <strong>IN</strong> = an endpoint driven from outside (a Pub/Sub push, a Gmail or Twilio webhook).')}
|
|
401
|
+
${item(sw.cron, '<strong>cron</strong> — a scheduler fires this endpoint')}
|
|
402
|
+
</div>
|
|
403
|
+
<div class="legend-col">
|
|
404
|
+
<h3>Lines — <em>what a call is</em></h3>
|
|
405
|
+
${item(sw.solid, '<strong>solid = rpc</strong> — the request follows the arrow, the response flows back')}
|
|
406
|
+
${item(sw.dashed, '<strong>dashed = event</strong> — asynchronous: the event flows in the direction of the arrow and returns once it is in the queue')}
|
|
407
|
+
${item(sw.scheduled, '<strong>scheduled</strong> — a cron invocation')}
|
|
408
|
+
<div class="legend-note"><em>Every line is labeled with the contract the call flows over. A service that enqueues to itself loops through its own queue — a queue decouples the two sides, so it is not a dependency cycle.</em></div>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="legend-col">
|
|
411
|
+
<h3>Reading a box</h3>
|
|
412
|
+
<pre class="legend-box-anatomy">name
|
|
413
|
+
(server|client, L#)
|
|
414
|
+
implements: <contracts it serves>
|
|
415
|
+
</pre>
|
|
416
|
+
<div class="legend-note">A box lists only what it <strong>serves</strong>. What it <em>calls</em> is on its outgoing arrows.</div>
|
|
417
|
+
<div class="legend-note"><code>(via <lib>)</code> = served through an embedded library, not its own source.</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>`;
|
|
421
|
+
}
|
|
250
422
|
function generateRuntimeHtml(dot, title) {
|
|
423
|
+
// @viz-js/viz v3 (`dist/viz-global.js`, a UMD bundle exposing a global `Viz` with the WASM
|
|
424
|
+
// inlined — one file, no second request). Replaces viz.js 2.1.2, last published in 2018 and
|
|
425
|
+
// carrying Graphviz ~2.40; this build carries Graphviz 15. Note the API differs twice over:
|
|
426
|
+
// `instance()` returns a promise where v2 used `new Viz()`, and `renderSVGElement` is then
|
|
427
|
+
// SYNCHRONOUS where v2's returned a promise.
|
|
251
428
|
const script = `
|
|
252
429
|
const dot = ${JSON.stringify(dot)};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.then(el => document.getElementById('graph').appendChild(el))
|
|
430
|
+
Viz.instance()
|
|
431
|
+
.then(viz => document.getElementById('graph').appendChild(viz.renderSVGElement(dot)))
|
|
256
432
|
.catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });
|
|
257
433
|
`;
|
|
258
434
|
return `<!DOCTYPE html>
|
|
259
435
|
<html>
|
|
260
436
|
<head>
|
|
437
|
+
<!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With
|
|
438
|
+
no declared charset the browser falls back to a locale guess and renders it as mojibake
|
|
439
|
+
("â °") whenever the page is served without a charset header. -->
|
|
440
|
+
<meta charset="utf-8">
|
|
261
441
|
<title>${title}</title>
|
|
262
|
-
<script src="https://cdn.jsdelivr.net/npm/viz
|
|
263
|
-
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js"></script>
|
|
442
|
+
<script src="https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js"></script>
|
|
264
443
|
<style>
|
|
265
444
|
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }
|
|
266
445
|
h1 { text-align: center; color: #333; }
|
|
267
|
-
#graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }
|
|
268
|
-
|
|
446
|
+
#graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }
|
|
447
|
+
#graph svg { max-width: 100%; height: auto; }
|
|
448
|
+
.legend {
|
|
449
|
+
margin: 20px auto;
|
|
450
|
+
max-width: 1100px;
|
|
451
|
+
padding: 15px 20px;
|
|
452
|
+
background: white;
|
|
453
|
+
border-radius: 8px;
|
|
454
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
455
|
+
}
|
|
456
|
+
.legend h2 { margin-top: 0; color: #333; }
|
|
457
|
+
.legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }
|
|
458
|
+
.legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
|
459
|
+
.legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }
|
|
460
|
+
/* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every
|
|
461
|
+
* inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */
|
|
462
|
+
.legend-note { margin: 9px 0; line-height: 1.5; color: #444; }
|
|
463
|
+
.legend-box-anatomy {
|
|
464
|
+
margin: 0 0 12px;
|
|
465
|
+
padding: 8px 10px;
|
|
466
|
+
background: #f7f7f7;
|
|
467
|
+
border-radius: 4px;
|
|
468
|
+
font-family: monospace;
|
|
469
|
+
font-size: 12px;
|
|
470
|
+
line-height: 1.5;
|
|
471
|
+
color: #333;
|
|
472
|
+
white-space: pre;
|
|
473
|
+
overflow-x: auto;
|
|
474
|
+
}
|
|
475
|
+
.sw { flex: 0 0 auto; display: inline-flex; }
|
|
476
|
+
code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }
|
|
477
|
+
@media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }
|
|
269
478
|
</style>
|
|
270
479
|
</head>
|
|
271
480
|
<body>
|
|
272
481
|
<h1>${title}</h1>
|
|
273
|
-
<div class="note">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>cloudtasks</strong> = producer → <em>queue</em> (cylinder) → consumer: the producer enqueues a Cloud Task and the consumer is delivered it later. There is one queue box <em>per method</em> (the unit Cloud Tasks and Terraform actually create), so a service that enqueues to <em>itself</em> correctly shows a loop through its own queue — a queue decouples the two sides, so it is not a dependency cycle.</div>
|
|
274
|
-
<div class="note">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via <lib>)</em> means the service serves that contract through an embedded library rather than its own source.</div>
|
|
275
|
-
<div class="note">Entry points nothing in this repo calls: a <strong>⏰ clock</strong> is a <em>cron</em> endpoint fired by a scheduler, and a <strong>dashed box pointing IN</strong> is an <em>external</em> endpoint driven from outside (a Pub/Sub push subscription, a Gmail or Twilio webhook). A <strong>dashed box pointing OUT</strong> is the reverse: a system OUTSIDE this repo (firestore, gmail, ...) that this repo calls and nothing here implements.</div>
|
|
276
482
|
<div id="graph"></div>
|
|
483
|
+
${legendHtml()}
|
|
277
484
|
<script>${script}</script>
|
|
278
485
|
</body>
|
|
279
486
|
</html>`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;AA0MH,gDA0CC;AAwCD,8DAiBC;;AA3SD,+CAAyB;AACzB,mDAA6B;AAE7B,6CAAwD;AAExD,MAAM,YAAY,GAA2B;IACzC,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;CACf,CAAC;AAEF,MAAM,UAAU,GAAG,SAAS,CAAC;AAE7B,0FAA0F;AAC1F,MAAM,aAAa,GAAG,SAAS,CAAC;AAChC,MAAM,eAAe,GAAG,SAAS,CAAC;AAElC,iFAAiF;AACjF,MAAM,SAAS,GAAG,SAAS,CAAC;AAC5B,MAAM,WAAW,GAAG,SAAS,CAAC;AAE9B,+FAA+F;AAC/F,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B,iGAAiG;AACjG,MAAM,QAAQ,GAAG,GAAG,CAAC;AAErB,4CAA4C;AAC5C,MAAa,iBAAiB;IAON;IANpB;IACI;;;;OAIG;IACa,oBAA6B,IAAI;QAAjC,sBAAiB,GAAjB,iBAAiB,CAAgB;IAClD,CAAC;CACP;AATD,8CASC;AAED,SAAS,YAAY,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC;AAED,2FAA2F;AAC3F,uGAAuG;AACvG,SAAS,SAAS,CAAC,OAAiB;IAChC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAa,EAAE,EAAE,CAAC,IAAA,qBAAQ,EAAC,KAAK,CAAC,CAAC,CAAC;IAC7D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,mBAAmB,EAAE,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,mBAAmB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,uGAAuG;AACvG,SAAS,iBAAiB,CAAC,GAAmB;IAC1C,OAAO,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE;QACtC,MAAM,GAAG,GAAG,GAAG,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC;QACrC,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,SAAS,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;IACzE,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;GAGG;AACH,uGAAuG;AACvG,SAAS,SAAS,CAAC,IAAY,EAAE,GAAmB;IAChD,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC7D,6FAA6F;IAC7F,sDAAsD;IACtD,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAA,qBAAQ,EAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC;IAC7F,IAAI,KAAK,GAAG,GAAG,IAAA,qBAAQ,EAAC,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,MAAM,GAAG,CAAC,KAAK,GAAG,QAAQ,GAAG,CAAC;IACpF,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,IAAI,kBAAkB,SAAS,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IAC9F,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,IAAI,YAAY,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;IACpE,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,wGAAwG;AACxG,SAAS,OAAO,CAAC,IAAiB,EAAE,MAAoC;IACpE,MAAM,IAAI,GAAG,IAAA,qBAAQ,EAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,GAAG,IAAA,qBAAQ,EAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAA,qBAAQ,EAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9E,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,MAAM,IAAI,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC;IACxD,CAAC;IACD,+FAA+F;IAC/F,4FAA4F;IAC5F,+EAA+E;IAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,IAAI,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;IACnG,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IACvF,MAAM,KAAK,GACP,IAAI,CAAC,KAAK,KAAK,SAAS;QACpB,CAAC,CAAC,GAAG,GAAG,UAAU;QAClB,CAAC,CAAC,GAAG,IAAA,qBAAQ,EAAC,IAAI,CAAC,KAAK,CAAC,aAAa,IAAA,qBAAQ,EAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;IAClF,OAAO,CACH,MAAM,OAAO,iDAAiD,UAAU,aAAa,KAAK,OAAO;QACjG,MAAM,IAAI,SAAS,OAAO,sCAAsC;QAChE,MAAM,OAAO,SAAS,EAAE,sCAAsC,CACjE,CAAC;AACN,CAAC;AAED,qFAAqF;AACrF,oGAAoG;AACpG,SAAS,KAAK,CAAC,GAAW;IACtB,OAAO,GAAG,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;GAQG;AACH,wGAAwG;AACxG,SAAS,UAAU,CAAC,KAAmB,EAAE,MAAmB;IACxD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAiB,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACtF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErC,IAAI,GAAG,GAAG,0FAA0F,CAAC;IACrG,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,IAAA,qBAAQ,EAAC,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,IAAA,qBAAQ,EAAC,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC3D,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,MAAM,EAAE,GAAG,SAAS,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YAChE,MAAM,QAAQ,GAAG,IAAA,qBAAQ,EAAC,OAAO,CAAC,SAAS,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YACnF,GAAG;gBACC,MAAM,EAAE,+CAA+C,SAAS,KAAK;oBACrE,UAAU,WAAW,yBAAyB;oBAC9C,MAAM,EAAE,SAAS,OAAO,aAAa,KAAK,MAAM,QAAQ,aAAa,WAAW,OAAO,CAAC;YAC5F,SAAS;QACb,CAAC;QACD,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,GAAG;YACC,MAAM,EAAE,mDAAmD,aAAa,KAAK;gBAC7E,UAAU,eAAe,aAAa,IAAA,qBAAQ,EAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B;gBACtF,MAAM,EAAE,SAAS,OAAO,aAAa,KAAK,2BAA2B,eAAe,OAAO,CAAC;IACpG,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,wGAAwG;AACxG,SAAS,WAAW,CAAC,KAAmB,EAAE,MAAmB;IACzD,sDAAsD;IACtD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC/C,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QACrC,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,SAAS;QACtC,MAAM,QAAQ,GAAG,IAAA,qBAAQ,EAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/E,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,OAAO,GAAG,QAAQ,GAAG,QAAQ,EAAE,CAAC;QACnD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAClD,UAAU,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,UAAU,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAErC,IAAI,GAAG,GAAG,qFAAqF,CAAC;IAChG,8FAA8F;IAC9F,oDAAoD;IACpD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/F,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC3C,GAAG;YACC,gBAAgB,QAAQ,mDAAmD,aAAa,KAAK;gBAC7F,UAAU,eAAe,aAAa,QAAQ,oBAAoB,CAAC;IAC3E,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAC1B,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QACnD,GAAG;YACC,MAAM,IAAA,qBAAQ,EAAC,YAAY,CAAC,OAAO,CAAC,CAAC,mBAAmB,QAAQ,IAAI;gBACpE,WAAW,GAAG,2BAA2B,eAAe,OAAO,CAAC;IACxE,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,4DAA4D;AAC5D,wGAAwG;AACxG,SAAgB,kBAAkB,CAC9B,KAAmB,EACnB,QAAgB,gCAAgC,EAChD,UAA6B,IAAI,iBAAiB,EAAE;IAEpD,IAAI,GAAG,GAAG,iCAAiC,CAAC;IAC5C,GAAG,IAAI,iBAAiB,CAAC;IACzB,GAAG,IAAI,iEAAiE,CAAC;IACzE,GAAG,IAAI,6CAA6C,CAAC;IAErD,4EAA4E;IAC5E,0EAA0E;IAC1E,MAAM,MAAM,GAAG,IAAI,GAAG,CAClB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,KAAK,KAAK,CAAC,CACnG,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC;QACnD,GAAG,IAAI,MAAM,IAAA,qBAAQ,EAAC,YAAY,CAAC,IAAI,CAAC,CAAC,iBAAiB,KAAK,aAAa,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC;IAC5G,CAAC;IAED,GAAG,IAAI,IAAI,CAAC;IAEZ,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QACpC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAAE,SAAS;QAC3D,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;IAED,GAAG,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAEjC,IAAI,OAAO,CAAC,iBAAiB;QAAE,GAAG,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAEjE,GAAG,IAAI,qBAAqB,CAAC;IAC7B,GAAG,IAAI,YAAY,IAAA,qBAAQ,EAAC,KAAK,CAAC,sDAAsD,CAAC;IACzF,GAAG,IAAI,kBAAkB,CAAC;IAC1B,GAAG,IAAI,KAAK,CAAC;IACb,6FAA6F;IAC7F,iGAAiG;IACjG,IAAA,2BAAc,EAAC,GAAG,EAAE,0BAA0B,CAAC,CAAC;IAChD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,SAAS,mBAAmB,CAAC,GAAW,EAAE,KAAa;IACnD,MAAM,MAAM,GAAG;sBACG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;;;;;KAKpC,CAAC;IACF,OAAO;;;aAGE,KAAK;;;;;;;;;;;UAWR,KAAK;;;;;cAKD,MAAM;;QAEZ,CAAC;AACT,CAAC;AAOD,yDAAyD;AACzD,SAAgB,yBAAyB,CACrC,KAAmB,EACnB,aAAqB,EACrB,QAAgB,gCAAgC,EAChD,UAA6B,IAAI,iBAAiB,EAAE;IAEpD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;IAC/D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5E,MAAM,GAAG,GAAG,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,0BAA0B,CAAC,CAAC;IACjE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;IAExC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,2BAA2B,CAAC,CAAC;IACnE,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;IAErE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACjC,CAAC","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS and USES. That list is the\n * single most important fact in a microservice architecture, and it used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that\n * actually page you at 3am stop being missing from the picture. They are\n * RENDER-ONLY: derivation, levels and cycle detection never see them.\n *\n * The same is true in the other direction for endpoints nothing in-repo CALLS: a\n * `cron` method hangs off a clock and an `external` method off a dashed inbound\n * box. Those are the entry points that wake a service up at 3am, and a graph\n * built only from in-repo callers cannot show them at all.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeQueue, RuntimeService, RuntimeTrigger } from './runtime-graph';\nimport { dotValue, assertValidDot } from './dot-syntax';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Fill + border for the clock node standing for a scheduler-driven endpoint. */\nconst CRON_FILL = '#FFF9C4';\nconst CRON_BORDER = '#F9A825';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it serves and\n * the contracts it calls. A node with neither reads exactly as before.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n if (svc.uses.length > 0) label += `\\\\nuses: ${labelList(svc.uses)}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the\n * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer\n * with a cylinder queue node and dashed enqueue/deliver arrows.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge, queues: Record<string, RuntimeQueue>): string {\n const from = dotValue(getShortName(edge.from));\n const to = dotValue(getShortName(edge.to));\n const via = edge.via.map((v: string) => dotValue(getShortName(v))).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${via}\"];\\n`;\n }\n // The queue node is identified by the METHOD, not by the (from,to) pair, so every producer and\n // consumer of one queue converges on ONE box — including a service that enqueues to itself,\n // which then renders as a visible loop through its queue instead of vanishing.\n const queueId = edge.queue === undefined ? `queue__${from}__${to}` : `queue__${dotId(edge.queue)}`;\n const queueName = edge.queue === undefined ? undefined : queues[edge.queue]?.queueName;\n const label =\n edge.queue === undefined\n ? `${via}\\\\nqueue`\n : `${dotValue(edge.queue)}\\\\nqueue: ${dotValue(queueName ?? edge.queue)}`;\n return (\n ` \"${queueId}\" [shape=cylinder, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", label=\"${label}\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/** A DOT-safe node-id fragment: anything but letters, digits and `_` becomes `_`. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction dotId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/**\n * The clock and outside-system nodes for endpoints NOTHING in-repo calls.\n *\n * A cron sweep and a GCP push subscription are real runtime entry points with real Terraform behind\n * them, but they produce no runtime EDGE (there is no in-repo caller), so until now they were simply\n * absent — a server's most operationally interesting endpoint could be invisible on its own graph.\n * Both are drawn pointing INTO the service that serves them, the opposite direction from\n * {@link externalDot}'s outbound vendor calls.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n let dot = '\\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\\n';\n for (const trigger of triggers) {\n const service = dotValue(getShortName(trigger.service));\n const label = dotValue(`${trigger.api}.${trigger.method}`);\n if (trigger.kind === 'cron') {\n const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;\n const schedule = dotValue(trigger.queueName ?? `${trigger.api}-${trigger.method}`);\n dot +=\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`;\n continue;\n }\n const id = `inbound__${dotId(trigger.api)}`;\n dot +=\n ` \"${id}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n(external caller)\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n const external = dotValue(getShortName(graph.apis[use.api]?.owner ?? use.api));\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n dot +=\n ` \"${dotValue(getShortName(service))}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${dotValue(getShortName(name))}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge, graph.queues);\n }\n\n dot += triggerDot(graph, hidden);\n\n if (options.showExternalNodes) dot += externalDot(graph, hidden);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n const script = `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(el => document.getElementById('graph').appendChild(el))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }\n .note { max-width: 700px; margin: 12px auto; color: #555; text-align: center; }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div class=\"note\">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>cloudtasks</strong> = producer → <em>queue</em> (cylinder) → consumer: the producer enqueues a Cloud Task and the consumer is delivered it later. There is one queue box <em>per method</em> (the unit Cloud Tasks and Terraform actually create), so a service that enqueues to <em>itself</em> correctly shows a loop through its own queue — a queue decouples the two sides, so it is not a dependency cycle.</div>\n <div class=\"note\">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via <lib>)</em> means the service serves that contract through an embedded library rather than its own source.</div>\n <div class=\"note\">Entry points nothing in this repo calls: a <strong>⏰ clock</strong> is a <em>cron</em> endpoint fired by a scheduler, and a <strong>dashed box pointing IN</strong> is an <em>external</em> endpoint driven from outside (a Pub/Sub push subscription, a Gmail or Twilio webhook). A <strong>dashed box pointing OUT</strong> is the reverse: a system OUTSIDE this repo (firestore, gmail, ...) that this repo calls and nothing here implements.</div>\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS. That list used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure. What a service USES is NOT\n * listed: every use already draws an outgoing arrow labeled with the same\n * contract, so repeating it in the box only made every box wider.\n *\n * Shape says what a node IS and line style says what a call IS. Solid = rpc: the\n * request follows the arrow and the response flows back. Dashed = event: it flows\n * in the arrow's direction and returns once it is queued. A queue is a sideways\n * cylinder, a datastore an upright one.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as terminal nodes, so the vendor systems that actually\n * page you at 3am stop being missing from the picture. One that DECLARES what it\n * is, via an `@externalSystem` JSDoc tag or an `external:` nx tag, gets the shape\n * of the thing it is; the rest stay generic dashed boxes. They are RENDER-ONLY:\n * derivation, levels and cycle detection never see them.\n *\n * The same is true in the other direction for endpoints nothing in-repo CALLS: a\n * `cron` method hangs off a clock and an `external` method off a dashed inbound\n * box. Those are the entry points that wake a service up at 3am, and a graph\n * built only from in-repo callers cannot show them at all.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeQueue, RuntimeService, RuntimeTrigger } from './runtime-graph';\nimport { dotValue, recordValue, assertValidDot } from './dot-syntax';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/**\n * The queue node is an `Mrecord` whose FIRST field is empty, which draws a rounded outline with a\n * vertical cap line near one end — a cylinder lying on its side, distinguishing a queue from the\n * upright cylinder that now means a database.\n *\n * Two things here are load-bearing and easy to break:\n *\n * 1. NO surrounding `{}`. Record fields lay out along the rank direction, and this graph is\n * `rankdir=TB` (see {@link generateRuntimeDot}), where the default is horizontal — which is what\n * we want. Adding braces TOGGLES that, turning the cap line into a band across the top.\n * 2. The leading space is the empty field. The record parser trims it to nothing, which is the\n * point; it must survive as its own field, so the `|` cannot be dropped.\n *\n * Graphviz has no sideways cylinder and never has: `orientation=` is documented as rotating POLYGON\n * shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute (graphviz issue\n * #2244, open since 2022 and still reproducible on 13.0.0). This is the closest native shape.\n */\nconst QUEUE_SHAPE = 'Mrecord';\nconst QUEUE_LABEL_PREFIX = ' |';\n\n/** Fill for the upright cylinder standing for an external DATASTORE (firestore, postgres, ...). */\nconst DATABASE_FILL = '#E1F5FE';\n\n/** Shape per external-system kind. Anything unrecognised falls back to the generic dashed box. */\nconst EXTERNAL_SHAPES: Record<string, string> = {\n database: 'cylinder',\n cache: 'cylinder',\n queue: 'Mrecord',\n storage: 'folder',\n};\n\n/** Fill per external-system kind, paired with {@link EXTERNAL_SHAPES}. */\nconst EXTERNAL_FILLS: Record<string, string> = {\n database: DATABASE_FILL,\n cache: DATABASE_FILL,\n queue: QUEUE_FILL,\n storage: '#F3E5F5',\n};\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Fill + border for the clock node standing for a scheduler-driven endpoint. */\nconst CRON_FILL = '#FFF9C4';\nconst CRON_BORDER = '#F9A825';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it SERVES.\n *\n * What a service USES is deliberately absent. Every `uses` entry already draws an outgoing arrow —\n * to the implementing service, to a queue, or to an external node — and the arrow carries the same\n * contract name as its label, so listing them in the box restated the picture and made every box\n * wider than it needed to be. `implements` stays because it has no such arrow: an api a service\n * serves but nothing in-repo calls is invisible otherwise.\n *\n * The one case this loses information is `showExternalNodes:false`, where an outbound call draws no\n * node and therefore no arrow. That is precisely what that opt-out asks for, and the legend says so.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled SOLID arrow (producer calls consumer, response\n * comes back). pubsub → the producer enqueues and the consumer is delivered later, so we draw\n * producer → QUEUE → consumer through a sideways-cylinder queue node with DASHED arrows.\n *\n * Solid vs dashed is the graph's one line-level distinction: solid is a call that returns a\n * response, dashed is an event that returns as soon as it is queued.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge, queues: Record<string, RuntimeQueue>): string {\n const from = dotValue(getShortName(edge.from));\n const to = dotValue(getShortName(edge.to));\n // Kept RAW: an ordinary edge label needs dotValue, the record-mode queue label needs\n // recordValue, and recordValue already applies dotValue — escaping here would double it.\n const viaRaw = edge.via.map((v: string) => getShortName(v)).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${dotValue(viaRaw)}\"];\\n`;\n }\n // The queue node is identified by the METHOD, not by the (from,to) pair, so every producer and\n // consumer of one queue converges on ONE box — including a service that enqueues to itself,\n // which then renders as a visible loop through its queue instead of vanishing.\n const queueId = edge.queue === undefined ? `queue__${from}__${to}` : `queue__${dotId(edge.queue)}`;\n const queueName = edge.queue === undefined ? undefined : queues[edge.queue]?.queueName;\n // Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the\n // empty leading field that draws the cylinder's end cap.\n const body =\n edge.queue === undefined\n ? `${recordValue(viaRaw)}\\\\nqueue`\n : `${recordValue(edge.queue)}\\\\nqueue: ${recordValue(queueName ?? edge.queue)}`;\n return (\n ` \"${queueId}\" [shape=${QUEUE_SHAPE}, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", ` +\n `label=\"${QUEUE_LABEL_PREFIX}${body}\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/** A DOT-safe node-id fragment: anything but letters, digits and `_` becomes `_`. */\n// webpieces-disable no-function-outside-class -- DOT id builder, matching getShortName in this file\nfunction dotId(raw: string): string {\n return raw.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/**\n * The clock and outside-system nodes for endpoints NOTHING in-repo calls.\n *\n * A cron sweep and a GCP push subscription are real runtime entry points with real Terraform behind\n * them, but they produce no runtime EDGE (there is no in-repo caller), so until now they were simply\n * absent — a server's most operationally interesting endpoint could be invisible on its own graph.\n * Both are drawn pointing INTO the service that serves them, the opposite direction from\n * {@link externalDot}'s outbound vendor calls.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction triggerDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const triggers = graph.triggers.filter((t: RuntimeTrigger) => !hidden.has(t.service));\n if (triggers.length === 0) return '';\n\n let dot = '\\n // Entry points nothing in this repo calls: a clock, or a system outside the repo.\\n';\n for (const trigger of triggers) {\n const service = dotValue(getShortName(trigger.service));\n const label = dotValue(`${trigger.api}.${trigger.method}`);\n if (trigger.kind === 'cron') {\n const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;\n const schedule = dotValue(trigger.queueName ?? `${trigger.api}-${trigger.method}`);\n dot +=\n ` \"${id}\" [shape=circle, style=\"filled\", fillcolor=\"${CRON_FILL}\", ` +\n `color=\"${CRON_BORDER}\", label=\"⏰\\\\ncron\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\\\\n${schedule}\", color=\"${CRON_BORDER}\"];\\n`;\n continue;\n }\n const id = `inbound__${dotId(trigger.api)}`;\n dot +=\n ` \"${id}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${dotValue(trigger.api)}\\\\n(external caller)\"];\\n` +\n ` \"${id}\" -> \"${service}\" [label=\"${label}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/**\n * The DECLARED external systems — the ones that said what they are, so they get a shape that says\n * it: a cylinder for a database, a folder for a bucket. Everything undeclared falls through to\n * {@link externalDot}'s generic grey box, which is why adding this broke nothing existing.\n *\n * The arrows are SOLID. A call to firestore or postgres is synchronous — it returns a value — and\n * the graph's rule is that solid means \"response comes back\". Being outside the repo is carried by\n * the node's shape, never by the line style; conflating the two is what made a blocking database\n * read look like an event.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalSystemsDot(graph: RuntimeGraph, hidden: Set<string>): string {\n const systems = graph.externalSystems ?? {};\n const ids = Object.keys(systems).sort();\n if (ids.length === 0) return '';\n\n let dot = '\\n // Declared external systems — drawn with the shape of what they actually are.\\n';\n for (const id of ids) {\n const system = systems[id];\n const shape = EXTERNAL_SHAPES[system.kind] ?? 'box';\n const fill = EXTERNAL_FILLS[system.kind] ?? EXTERNAL_FILL;\n // An Mrecord-shaped system needs the same empty leading field as a queue node, or it\n // renders as a plain box and silently loses the sideways-cylinder read.\n const prefix = shape === QUEUE_SHAPE ? QUEUE_LABEL_PREFIX : '';\n const text = shape === QUEUE_SHAPE ? recordValue(system.label) : dotValue(system.label);\n dot +=\n ` \"system__${dotId(id)}\" [shape=${shape}, style=\"filled\", fillcolor=\"${fill}\", ` +\n `label=\"${prefix}${text}\\\\n(external ${dotValue(system.kind)})\"];\\n`;\n }\n for (const id of ids) {\n const system = systems[id];\n const via = system.apis.length === 0 ? '' : ` [label=\"${labelList([...system.apis].sort())}\"]`;\n for (const service of [...system.usedBy].sort()) {\n if (hidden.has(service)) continue;\n dot += ` \"${dotValue(getShortName(service))}\" -> \"system__${dotId(id)}\"${via};\\n`;\n }\n }\n return dot;\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n *\n * A contract carrying an `@externalSystem` declaration is skipped here — {@link externalSystemsDot}\n * has already drawn it with a real shape, and rendering it in both places would double the node.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n if (graph.apis[use.api]?.externalSystem !== undefined) continue;\n const external = dotValue(getShortName(graph.apis[use.api]?.owner ?? use.api));\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n // SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and\n // \"outside the repo\" is already said by the node's dashed border.\n dot +=\n ` \"${dotValue(getShortName(service))}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${dotValue(getShortName(name))}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge, graph.queues);\n }\n\n dot += triggerDot(graph, hidden);\n\n if (options.showExternalNodes) {\n dot += externalSystemsDot(graph, hidden);\n dot += externalDot(graph, hidden);\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\n/**\n * Inline SVG swatches for the legend, hand-drawn to match what Graphviz emits for each shape.\n *\n * Hand-drawn on purpose: the alternative is shelling out to Graphviz at generate time, which would\n * make writing the HTML depend on a `dot` binary being installed — a dependency this tool does not\n * otherwise have, since rendering happens in the browser.\n */\nclass LegendSwatches {\n readonly service =\n '<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"3\" width=\"44\" height=\"20\" rx=\"7\" fill=\"#E8F5E9\" stroke=\"#333\"/></svg>';\n /** Rounded outline + a cap line near the left end — the sideways cylinder the queue node draws. */\n readonly queue =\n `<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"4\" width=\"44\" height=\"18\" rx=\"9\" fill=\"${QUEUE_FILL}\" stroke=\"#333\"/>` +\n '<line x1=\"12\" y1=\"4\" x2=\"12\" y2=\"22\" stroke=\"#333\"/></svg>';\n readonly database =\n `<svg width=\"46\" height=\"26\"><path d=\"M8,7 a15,4 0 0 1 30,0 v12 a15,4 0 0 1 -30,0 z\" fill=\"${DATABASE_FILL}\" stroke=\"#333\"/>` +\n '<path d=\"M8,7 a15,4 0 0 0 30,0\" fill=\"none\" stroke=\"#333\"/></svg>';\n readonly storage =\n '<svg width=\"46\" height=\"26\"><path d=\"M2,22 V6 H16 l3,3 H44 V22 Z\" fill=\"#F3E5F5\" stroke=\"#333\"/></svg>';\n readonly external =\n `<svg width=\"46\" height=\"26\"><rect x=\"1\" y=\"3\" width=\"44\" height=\"20\" fill=\"${EXTERNAL_FILL}\" ` +\n `stroke=\"${EXTERNAL_BORDER}\" stroke-dasharray=\"4,3\"/></svg>`;\n readonly cron =\n `<svg width=\"46\" height=\"26\"><circle cx=\"23\" cy=\"13\" r=\"11\" fill=\"${CRON_FILL}\" stroke=\"${CRON_BORDER}\"/>` +\n '<text x=\"23\" y=\"18\" font-size=\"12\" text-anchor=\"middle\">⏰</text></svg>';\n readonly solid =\n '<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"#333\" stroke-width=\"1.5\"/>' +\n '<path d=\"M48,6 L57,10 L48,14 Z\" fill=\"#333\"/></svg>';\n readonly dashed =\n '<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"#333\" stroke-width=\"1.5\" ' +\n 'stroke-dasharray=\"5,4\"/><path d=\"M48,6 L57,10 L48,14 Z\" fill=\"#333\"/></svg>';\n readonly scheduled =\n `<svg width=\"60\" height=\"20\"><line x1=\"2\" y1=\"10\" x2=\"48\" y2=\"10\" stroke=\"${CRON_BORDER}\" stroke-width=\"1.5\"/>` +\n `<path d=\"M48,6 L57,10 L48,14 Z\" fill=\"${CRON_BORDER}\"/></svg>`;\n}\n\n/**\n * The legend. Three columns — what a box IS, what a line MEANS, how to read a box — replacing the\n * three paragraphs of prose that used to restate the picture in words. Styled after\n * {@link GraphVisualizer}'s legend so the two graphs in this repo look like one tool.\n */\n// webpieces-disable no-function-outside-class -- HTML builder, matching the sibling builders in this file\nfunction legendHtml(): string {\n const sw = new LegendSwatches();\n const item = (swatch: string, text: string): string =>\n `<div class=\"legend-item\"><span class=\"sw\">${swatch}</span><span>${text}</span></div>`;\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-columns\">\n <div class=\"legend-col\">\n <h3>Node shapes — <em>what a box is</em></h3>\n ${item(sw.service, '<strong>service</strong> — a deployable in this repo; fill is its dependency level')}\n ${item(sw.queue, '<strong>queue</strong> — one box <em>per method</em>, the unit Cloud Tasks and Terraform actually create')}\n ${item(sw.database, '<strong>database</strong> — a datastore outside this repo')}\n ${item(sw.storage, '<strong>object storage</strong> — a bucket outside this repo')}\n ${item(sw.external, '<strong>external system</strong> — outside this repo; nothing here implements it. Pointing <strong>OUT</strong> = a system this repo calls (firestore, gmail). Pointing <strong>IN</strong> = an endpoint driven from outside (a Pub/Sub push, a Gmail or Twilio webhook).')}\n ${item(sw.cron, '<strong>cron</strong> — a scheduler fires this endpoint')}\n </div>\n <div class=\"legend-col\">\n <h3>Lines — <em>what a call is</em></h3>\n ${item(sw.solid, '<strong>solid = rpc</strong> — the request follows the arrow, the response flows back')}\n ${item(sw.dashed, '<strong>dashed = event</strong> — asynchronous: the event flows in the direction of the arrow and returns once it is in the queue')}\n ${item(sw.scheduled, '<strong>scheduled</strong> — a cron invocation')}\n <div class=\"legend-note\"><em>Every line is labeled with the contract the call flows over. A service that enqueues to itself loops through its own queue — a queue decouples the two sides, so it is not a dependency cycle.</em></div>\n </div>\n <div class=\"legend-col\">\n <h3>Reading a box</h3>\n <pre class=\"legend-box-anatomy\">name\n(server|client, L#)\nimplements: <contracts it serves>\n</pre>\n <div class=\"legend-note\">A box lists only what it <strong>serves</strong>. What it <em>calls</em> is on its outgoing arrows.</div>\n <div class=\"legend-note\"><code>(via <lib>)</code> = served through an embedded library, not its own source.</div>\n </div>\n </div>\n </div>`;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n // @viz-js/viz v3 (`dist/viz-global.js`, a UMD bundle exposing a global `Viz` with the WASM\n // inlined — one file, no second request). Replaces viz.js 2.1.2, last published in 2018 and\n // carrying Graphviz ~2.40; this build carries Graphviz 15. Note the API differs twice over:\n // `instance()` returns a promise where v2 used `new Viz()`, and `renderSVGElement` is then\n // SYNCHRONOUS where v2's returned a promise.\n const script = `\n const dot = ${JSON.stringify(dot)};\n Viz.instance()\n .then(viz => document.getElementById('graph').appendChild(viz.renderSVGElement(dot)))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <!-- REQUIRED: the cron node's label is a literal ⏰, and the DOT is embedded in this file. With\n no declared charset the browser falls back to a locale guess and renders it as mojibake\n (\"â °\") whenever the page is served without a charset header. -->\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; overflow-x: auto; }\n #graph svg { max-width: 100%; height: auto; }\n .legend {\n margin: 20px auto;\n max-width: 1100px;\n padding: 15px 20px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 { margin-top: 0; color: #333; }\n .legend-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; align-items: start; }\n .legend-col h3 { margin: 0 0 10px; color: #333; font-size: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px; }\n .legend-item { margin: 9px 0; display: flex; align-items: center; gap: 10px; line-height: 1.4; color: #444; }\n /* Prose rows carry no swatch, so they must NOT be flex containers: flex would promote every\n * inline <strong>/<em>/<code> to a flex item and shred the sentence into columns. */\n .legend-note { margin: 9px 0; line-height: 1.5; color: #444; }\n .legend-box-anatomy {\n margin: 0 0 12px;\n padding: 8px 10px;\n background: #f7f7f7;\n border-radius: 4px;\n font-family: monospace;\n font-size: 12px;\n line-height: 1.5;\n color: #333;\n white-space: pre;\n overflow-x: auto;\n }\n .sw { flex: 0 0 auto; display: inline-flex; }\n code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n @media (max-width: 900px) { .legend-columns { grid-template-columns: 1fr; } }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div id=\"graph\"></div>\n ${legendHtml()}\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
|