@webpieces/nx-webpieces-rules 0.4.853 → 0.4.854
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/README.md +14 -0
- package/package.json +7 -7
- package/src/index.d.ts +1 -0
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
- package/src/lib/graph-filter.client.d.ts +40 -0
- package/src/lib/graph-filter.client.js +190 -0
- package/src/lib/graph-filter.client.js.map +1 -0
- package/src/lib/graph-level-bands.d.ts +0 -18
- package/src/lib/graph-level-bands.js +4 -3
- package/src/lib/graph-level-bands.js.map +1 -1
- package/src/lib/graph-node-menu.d.ts +1 -0
- package/src/lib/graph-node-menu.js +29 -0
- package/src/lib/graph-node-menu.js.map +1 -1
- package/src/lib/graph-render-model.d.ts +32 -0
- package/src/lib/graph-render-model.js +76 -0
- package/src/lib/graph-render-model.js.map +1 -0
- package/src/lib/graph-visualizer.client.d.ts +14 -73
- package/src/lib/graph-visualizer.client.js +71 -243
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +8 -4
- package/src/lib/graph-visualizer.js +38 -20
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-html-page.d.ts +6 -3
- package/src/lib/runtime-html-page.js +14 -6
- package/src/lib/runtime-html-page.js.map +1 -1
- package/src/lib/runtime-visualizer.client.d.ts +8 -19
- package/src/lib/runtime-visualizer.client.js +28 -41
- package/src/lib/runtime-visualizer.client.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +6 -2
- package/src/lib/runtime-visualizer.js +55 -43
- package/src/lib/runtime-visualizer.js.map +1 -1
|
@@ -45,28 +45,17 @@ declare class QueueCylinders {
|
|
|
45
45
|
private boundsOf;
|
|
46
46
|
private pathEl;
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Every node of the runtime graph opens the shared floating menu.
|
|
50
|
-
*
|
|
51
|
-
* There is exactly ONE item, and it is the lock. NO "View Design": a node here is a running SERVICE,
|
|
52
|
-
* a queue, a datastore or a third-party system, not an nx project, so there is no design.html to point
|
|
53
|
-
* at and the item is ABSENT rather than present-and-dead.
|
|
54
|
-
*
|
|
55
|
-
* LOCK means the literal thing the box shows: dim every other node and every edge, light the locked
|
|
56
|
-
* box alone. That is `WpNodeLock`, the same lock a design page uses — this page has no lock dropdown
|
|
57
|
-
* and no responsibilities list, so there is no second control for it to fall out of step with, and
|
|
58
|
-
* the menu label is derived from the lock's own state on every open.
|
|
59
|
-
*/
|
|
48
|
+
/** Runtime keeps the single-box Lock while Filter follows the full directed chain. */
|
|
60
49
|
declare class RuntimeNodeMenu {
|
|
61
50
|
private readonly svg;
|
|
62
51
|
private readonly lock;
|
|
63
|
-
|
|
52
|
+
private readonly page;
|
|
53
|
+
constructor(svg: SVGSVGElement, lock: WpNodeLock, page: RuntimePage);
|
|
64
54
|
wire(): void;
|
|
65
55
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
render(): void;
|
|
56
|
+
declare class RuntimePage extends WpFilterPage {
|
|
57
|
+
private lock;
|
|
58
|
+
protected captureBinding(svg: SVGSVGElement | null): () => void;
|
|
59
|
+
protected prepareSvg(svg: SVGSVGElement): void;
|
|
60
|
+
protected wireSvg(svg: SVGSVGElement): void;
|
|
72
61
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* renderSVGElement is then SYNCHRONOUS — v2's returned a promise);
|
|
14
14
|
* 2. upgrade every queue node into a TRUE horizontal cylinder;
|
|
15
15
|
* 3. make every node clickable, opening the SHARED floating menu (graph-node-menu.ts — the same one
|
|
16
|
-
* architecture/dependencies.html and every design.html use)
|
|
16
|
+
* architecture/dependencies.html and every design.html use) with independent Lock/Unlock and chain filtering.
|
|
17
17
|
*
|
|
18
18
|
* Why (2) is post-processing rather than a shape:
|
|
19
19
|
*
|
|
@@ -160,58 +160,45 @@ class QueueCylinders {
|
|
|
160
160
|
return el;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Every node of the runtime graph opens the shared floating menu.
|
|
165
|
-
*
|
|
166
|
-
* There is exactly ONE item, and it is the lock. NO "View Design": a node here is a running SERVICE,
|
|
167
|
-
* a queue, a datastore or a third-party system, not an nx project, so there is no design.html to point
|
|
168
|
-
* at and the item is ABSENT rather than present-and-dead.
|
|
169
|
-
*
|
|
170
|
-
* LOCK means the literal thing the box shows: dim every other node and every edge, light the locked
|
|
171
|
-
* box alone. That is `WpNodeLock`, the same lock a design page uses — this page has no lock dropdown
|
|
172
|
-
* and no responsibilities list, so there is no second control for it to fall out of step with, and
|
|
173
|
-
* the menu label is derived from the lock's own state on every open.
|
|
174
|
-
*/
|
|
163
|
+
/** Runtime keeps the single-box Lock while Filter follows the full directed chain. */
|
|
175
164
|
class RuntimeNodeMenu {
|
|
176
165
|
svg;
|
|
177
166
|
lock;
|
|
178
|
-
|
|
167
|
+
page;
|
|
168
|
+
constructor(svg, lock, page) {
|
|
179
169
|
this.svg = svg;
|
|
180
|
-
this.lock =
|
|
170
|
+
this.lock = lock;
|
|
171
|
+
this.page = page;
|
|
181
172
|
}
|
|
182
173
|
wire() {
|
|
183
174
|
WpNodeMenu.wire(this.svg, (name, node) => {
|
|
184
175
|
const label = this.lock.isLocked(name) ? 'Unlock' : 'Lock';
|
|
185
|
-
return [new WpNodeMenuItem(label, () => { this.lock.toggle(name, node); })];
|
|
176
|
+
return [new WpNodeMenuItem(label, () => { this.lock.toggle(name, node); }), this.page.filterItem(name)];
|
|
186
177
|
});
|
|
187
178
|
}
|
|
188
179
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
console.error(err);
|
|
210
|
-
const host = document.getElementById('graph');
|
|
211
|
-
if (host !== null)
|
|
212
|
-
host.innerHTML = '<pre>' + String(err) + '</pre>';
|
|
180
|
+
class RuntimePage extends WpFilterPage {
|
|
181
|
+
lock = null;
|
|
182
|
+
captureBinding(svg) {
|
|
183
|
+
const previous = this.lock;
|
|
184
|
+
return () => {
|
|
185
|
+
this.lock = previous;
|
|
186
|
+
if (svg !== null)
|
|
187
|
+
this.lock?.rebind(svg);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
prepareSvg(svg) { new QueueCylinders().applyTo(svg); }
|
|
191
|
+
wireSvg(svg) {
|
|
192
|
+
if (this.lock === null)
|
|
193
|
+
this.lock = new WpNodeLock(svg);
|
|
194
|
+
else
|
|
195
|
+
this.lock.rebind(svg);
|
|
196
|
+
new RuntimeNodeMenu(svg, this.lock, this).wire();
|
|
197
|
+
svg.querySelectorAll('g.node').forEach(node => {
|
|
198
|
+
if (node.querySelector('title')?.textContent === this.anchor)
|
|
199
|
+
node.classList.add('wp-filter-anchor');
|
|
213
200
|
});
|
|
214
201
|
}
|
|
215
202
|
}
|
|
216
|
-
new RuntimePage().render();
|
|
203
|
+
new RuntimePage(__RENDER_MODEL__).render();
|
|
217
204
|
//# sourceMappingURL=runtime-visualizer.client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for tmp/webpieces/runtime-architecture.html.\n *\n * runtime-visualizer.ts reads this file's COMPILED output with readFileSync, substitutes the DOT\n * placeholder (see viz-client-globals.d.ts) with the JSON-encoded DOT, and inlines the result into a <script> tag. It runs in a\n * browser, never in node — but it is ordinary TypeScript, compiled in place by tsc like the package's\n * bin entry points, and linted like every other file here. (It used to be a committed .js asset\n * exempted from `no-js-files`; see graph-visualizer.client.ts for why that exemption is gone.)\n *\n * Three jobs:\n * 1. render the DOT with @viz-js/viz v3 (instance() resolves the WASM renderer, and\n * renderSVGElement is then SYNCHRONOUS — v2's returned a promise);\n * 2. upgrade every queue node into a TRUE horizontal cylinder;\n * 3. make every node clickable, opening the SHARED floating menu (graph-node-menu.ts — the same one\n * architecture/dependencies.html and every design.html use) whose only item here is Lock/Unlock.\n *\n * Why (2) is post-processing rather than a shape:\n *\n * Graphviz has exactly one `cylinder` and it is upright-only. `orientation=` is documented as rotating\n * POLYGON shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute — graphviz\n * issue #2244, open since 2022 and still reproducible on Graphviz 15, the version this page's renderer\n * carries. All 54 native shapes were compared at the real label size; none is a horizontal cylinder. So\n * the DOT emits `Mrecord` (which renders sensibly on its own for anyone running `dot` over the\n * committed .dot file) and this script redraws it in the browser, where the geometry is computed from\n * each node's actual bounding box and therefore fits any label width — which a fixed image asset never\n * can.\n */\n\n// The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both\n// scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token\n// in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.\n\n/** Cap width as a fraction of the node's half-height. Below ~0.6 it reads as a rounded box. */\nconst CAP_RATIO = 0.75;\n/** A node narrower than this is not a queue box worth reshaping. */\nconst MIN_WIDTH = 40;\n\n/** The union bounding box of a node's shape elements, in user space. Data-only → a class. */\nclass Bounds {\n constructor(\n readonly x0: number,\n readonly x1: number,\n readonly y0: number,\n readonly y1: number,\n ) {}\n\n width(): number {\n return this.x1 - this.x0;\n }\n\n halfHeight(): number {\n return (this.y1 - this.y0) / 2;\n }\n}\n\n/**\n * Redraws the nodes the DOT marked `wp_queue` as cylinders lying on their side.\n *\n * Selected by CLASS, not by id prefix: a queue-kind external system shares the `system__` id space with\n * databases, and a database is an UPRIGHT cylinder that must be left alone.\n */\nclass QueueCylinders {\n private static readonly SVG_NS = 'http://www.w3.org/2000/svg';\n\n applyTo(svg: SVGSVGElement): void {\n svg.querySelectorAll('g.wp_queue').forEach((node: Element): void => { this.reshape(node); });\n }\n\n /**\n * Replace one node's shape elements with a horizontal cylinder sized to its own bounding box.\n *\n * The bbox is measured from the shape elements BEFORE they are removed, never from getBBox() on the\n * whole group — the group includes the text, which is inset, so using it would shrink the cylinder\n * inside the very label it is meant to contain.\n */\n private reshape(node: Element): void {\n const shapes = node.querySelectorAll('polygon, path, polyline');\n if (shapes.length === 0) return;\n const box = this.boundsOf(shapes);\n if (box === null || box.width() < MIN_WIDTH) return;\n\n const first = shapes[0];\n const fill = first.getAttribute('fill') ?? 'none';\n const stroke = first.getAttribute('stroke') ?? 'black';\n // Measured BEFORE the shapes go: the record separator is where the label text starts.\n const separator = this.separatorX(shapes);\n shapes.forEach((shape: Element): void => { shape.remove(); });\n\n const ry = box.halfHeight();\n const rx = this.capRadius(box, ry, separator);\n const body =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' L' + (box.x1 - rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x1 - rx) + ',' + box.y1 +\n ' L' + (box.x0 + rx) + ',' + box.y1 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y0 + ' Z';\n // Only the NEAR end cap is drawn: that single arc is what reads as \"tube\" rather than\n // \"stadium\", and a real cylinder hides the far one behind the body.\n const cap =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y1;\n\n const anchor = node.firstChild;\n if (anchor === null || anchor.nextSibling === null) return;\n node.insertBefore(this.pathEl(fill, stroke, body), anchor.nextSibling);\n node.insertBefore(this.pathEl('none', stroke, cap), anchor.nextSibling.nextSibling);\n\n // The label is deliberately NOT nudged. QUEUE_LABEL_PREFIX already gives the node an empty\n // leading record field, so Graphviz has centred the text in the space to the RIGHT of where the\n // cap lands. Shifting it again double-counts that offset and pushes the longest line out\n // through the far end of the cylinder.\n }\n\n /**\n * A box listing several queues is TALL, and a cap scaled off half-height alone would then reach\n * past the empty leading field and sit on top of the first characters of the label. Graphviz sizes\n * that field from the font, not from the label's height, so the separator is the only honest bound\n * on how far right the cap may go.\n */\n private capRadius(box: Bounds, ry: number, separator: number | null): number {\n const fromHeight = Math.max(6, ry * CAP_RATIO);\n if (separator === null) return fromHeight;\n return Math.max(6, Math.min(fromHeight, separator - box.x0));\n }\n\n /**\n * The x of the record's field separator — the VERTICAL polyline Graphviz draws between the empty\n * leading field and the label — or null if this node has none.\n */\n private separatorX(shapes: NodeListOf<Element>): number | null {\n for (const shape of Array.from(shapes)) {\n if (shape.tagName !== 'polyline') continue;\n const nums = (shape.getAttribute('points') ?? '').match(/-?[\\d.]+/g);\n if (nums === null || nums.length < 4) continue;\n const x = parseFloat(nums[0]);\n if (Math.abs(x - parseFloat(nums[2])) < 0.5) return x;\n }\n return null;\n }\n\n /** The union bounding box of some SVG shape elements, from their raw geometry attributes. */\n private boundsOf(shapes: NodeListOf<Element>): Bounds | null {\n const xs: number[] = [];\n const ys: number[] = [];\n for (const shape of Array.from(shapes)) {\n const pts = shape.getAttribute('points');\n const source = pts ?? shape.getAttribute('d') ?? '';\n const nums = source.match(/-?[\\d.]+/g);\n if (nums === null) continue;\n for (let n = 0; n + 1 < nums.length; n += 2) {\n xs.push(parseFloat(nums[n]));\n ys.push(parseFloat(nums[n + 1]));\n }\n }\n if (xs.length === 0) return null;\n return new Bounds(Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys));\n }\n\n private pathEl(fill: string, stroke: string, d: string): SVGPathElement {\n const el = document.createElementNS(QueueCylinders.SVG_NS, 'path');\n el.setAttribute('fill', fill);\n el.setAttribute('stroke', stroke);\n el.setAttribute('d', d);\n return el;\n }\n}\n\n/**\n * Every node of the runtime graph opens the shared floating menu.\n *\n * There is exactly ONE item, and it is the lock. NO \"View Design\": a node here is a running SERVICE,\n * a queue, a datastore or a third-party system, not an nx project, so there is no design.html to point\n * at and the item is ABSENT rather than present-and-dead.\n *\n * LOCK means the literal thing the box shows: dim every other node and every edge, light the locked\n * box alone. That is `WpNodeLock`, the same lock a design page uses — this page has no lock dropdown\n * and no responsibilities list, so there is no second control for it to fall out of step with, and\n * the menu label is derived from the lock's own state on every open.\n */\nclass RuntimeNodeMenu {\n private readonly lock: WpNodeLock;\n\n constructor(private readonly svg: SVGSVGElement) {\n this.lock = new WpNodeLock(svg);\n }\n\n wire(): void {\n WpNodeMenu.wire(this.svg, (name: string, node: SVGGElement): WpNodeMenuItem[] => {\n const label = this.lock.isLocked(name) ? 'Unlock' : 'Lock';\n return [new WpNodeMenuItem(label, (): void => { this.lock.toggle(name, node); })];\n });\n }\n}\n\n/**\n * Renders the DOT, reshapes the queues, wires the node menu, and reports a failure into the page\n * rather than only the console.\n */\nclass RuntimePage {\n render(): void {\n Viz.instance()\n .then((viz: VizInstance): void => {\n const element = viz.renderSVGElement(__DOT__);\n new QueueCylinders().applyTo(element);\n const host = document.getElementById('graph');\n if (host === null) return;\n host.appendChild(element);\n // AFTER the cylinders: wiring reads each node's <title>, which the redraw leaves\n // alone, but the clickable class belongs on the shape that is finally there.\n new RuntimeNodeMenu(element).wire();\n })\n // webpieces-disable no-any-unknown -- a promise rejection reason is untyped BY THE LANGUAGE (any value can be thrown), and this browser script cannot import the repo's toError helper; it is stringified, never dereferenced\n .catch((err: unknown): void => {\n console.error(err);\n const host = document.getElementById('graph');\n if (host !== null) host.innerHTML = '<pre>' + String(err) + '</pre>';\n });\n }\n}\n\nnew RuntimePage().render();\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for tmp/webpieces/runtime-architecture.html.\n *\n * runtime-visualizer.ts reads this file's COMPILED output with readFileSync, substitutes the DOT\n * placeholder (see viz-client-globals.d.ts) with the JSON-encoded DOT, and inlines the result into a <script> tag. It runs in a\n * browser, never in node — but it is ordinary TypeScript, compiled in place by tsc like the package's\n * bin entry points, and linted like every other file here. (It used to be a committed .js asset\n * exempted from `no-js-files`; see graph-visualizer.client.ts for why that exemption is gone.)\n *\n * Three jobs:\n * 1. render the DOT with @viz-js/viz v3 (instance() resolves the WASM renderer, and\n * renderSVGElement is then SYNCHRONOUS — v2's returned a promise);\n * 2. upgrade every queue node into a TRUE horizontal cylinder;\n * 3. make every node clickable, opening the SHARED floating menu (graph-node-menu.ts — the same one\n * architecture/dependencies.html and every design.html use) with independent Lock/Unlock and chain filtering.\n *\n * Why (2) is post-processing rather than a shape:\n *\n * Graphviz has exactly one `cylinder` and it is upright-only. `orientation=` is documented as rotating\n * POLYGON shapes, and `cylinder` is drawn with beziers, so it silently ignores the attribute — graphviz\n * issue #2244, open since 2022 and still reproducible on Graphviz 15, the version this page's renderer\n * carries. All 54 native shapes were compared at the real label size; none is a horizontal cylinder. So\n * the DOT emits `Mrecord` (which renders sensibly on its own for anyone running `dot` over the\n * committed .dot file) and this script redraws it in the browser, where the geometry is computed from\n * each node's actual bounding box and therefore fits any label width — which a fixed image asset never\n * can.\n */\n\n// The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both\n// scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token\n// in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.\n\n/** Cap width as a fraction of the node's half-height. Below ~0.6 it reads as a rounded box. */\nconst CAP_RATIO = 0.75;\n/** A node narrower than this is not a queue box worth reshaping. */\nconst MIN_WIDTH = 40;\n\n/** The union bounding box of a node's shape elements, in user space. Data-only → a class. */\nclass Bounds {\n constructor(\n readonly x0: number,\n readonly x1: number,\n readonly y0: number,\n readonly y1: number,\n ) {}\n\n width(): number {\n return this.x1 - this.x0;\n }\n\n halfHeight(): number {\n return (this.y1 - this.y0) / 2;\n }\n}\n\n/**\n * Redraws the nodes the DOT marked `wp_queue` as cylinders lying on their side.\n *\n * Selected by CLASS, not by id prefix: a queue-kind external system shares the `system__` id space with\n * databases, and a database is an UPRIGHT cylinder that must be left alone.\n */\nclass QueueCylinders {\n private static readonly SVG_NS = 'http://www.w3.org/2000/svg';\n\n applyTo(svg: SVGSVGElement): void {\n svg.querySelectorAll('g.wp_queue').forEach((node: Element): void => { this.reshape(node); });\n }\n\n /**\n * Replace one node's shape elements with a horizontal cylinder sized to its own bounding box.\n *\n * The bbox is measured from the shape elements BEFORE they are removed, never from getBBox() on the\n * whole group — the group includes the text, which is inset, so using it would shrink the cylinder\n * inside the very label it is meant to contain.\n */\n private reshape(node: Element): void {\n const shapes = node.querySelectorAll('polygon, path, polyline');\n if (shapes.length === 0) return;\n const box = this.boundsOf(shapes);\n if (box === null || box.width() < MIN_WIDTH) return;\n\n const first = shapes[0];\n const fill = first.getAttribute('fill') ?? 'none';\n const stroke = first.getAttribute('stroke') ?? 'black';\n // Measured BEFORE the shapes go: the record separator is where the label text starts.\n const separator = this.separatorX(shapes);\n shapes.forEach((shape: Element): void => { shape.remove(); });\n\n const ry = box.halfHeight();\n const rx = this.capRadius(box, ry, separator);\n const body =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' L' + (box.x1 - rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x1 - rx) + ',' + box.y1 +\n ' L' + (box.x0 + rx) + ',' + box.y1 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y0 + ' Z';\n // Only the NEAR end cap is drawn: that single arc is what reads as \"tube\" rather than\n // \"stadium\", and a real cylinder hides the far one behind the body.\n const cap =\n 'M' + (box.x0 + rx) + ',' + box.y0 +\n ' A' + rx + ',' + ry + ' 0 0 1 ' + (box.x0 + rx) + ',' + box.y1;\n\n const anchor = node.firstChild;\n if (anchor === null || anchor.nextSibling === null) return;\n node.insertBefore(this.pathEl(fill, stroke, body), anchor.nextSibling);\n node.insertBefore(this.pathEl('none', stroke, cap), anchor.nextSibling.nextSibling);\n\n // The label is deliberately NOT nudged. QUEUE_LABEL_PREFIX already gives the node an empty\n // leading record field, so Graphviz has centred the text in the space to the RIGHT of where the\n // cap lands. Shifting it again double-counts that offset and pushes the longest line out\n // through the far end of the cylinder.\n }\n\n /**\n * A box listing several queues is TALL, and a cap scaled off half-height alone would then reach\n * past the empty leading field and sit on top of the first characters of the label. Graphviz sizes\n * that field from the font, not from the label's height, so the separator is the only honest bound\n * on how far right the cap may go.\n */\n private capRadius(box: Bounds, ry: number, separator: number | null): number {\n const fromHeight = Math.max(6, ry * CAP_RATIO);\n if (separator === null) return fromHeight;\n return Math.max(6, Math.min(fromHeight, separator - box.x0));\n }\n\n /**\n * The x of the record's field separator — the VERTICAL polyline Graphviz draws between the empty\n * leading field and the label — or null if this node has none.\n */\n private separatorX(shapes: NodeListOf<Element>): number | null {\n for (const shape of Array.from(shapes)) {\n if (shape.tagName !== 'polyline') continue;\n const nums = (shape.getAttribute('points') ?? '').match(/-?[\\d.]+/g);\n if (nums === null || nums.length < 4) continue;\n const x = parseFloat(nums[0]);\n if (Math.abs(x - parseFloat(nums[2])) < 0.5) return x;\n }\n return null;\n }\n\n /** The union bounding box of some SVG shape elements, from their raw geometry attributes. */\n private boundsOf(shapes: NodeListOf<Element>): Bounds | null {\n const xs: number[] = [];\n const ys: number[] = [];\n for (const shape of Array.from(shapes)) {\n const pts = shape.getAttribute('points');\n const source = pts ?? shape.getAttribute('d') ?? '';\n const nums = source.match(/-?[\\d.]+/g);\n if (nums === null) continue;\n for (let n = 0; n + 1 < nums.length; n += 2) {\n xs.push(parseFloat(nums[n]));\n ys.push(parseFloat(nums[n + 1]));\n }\n }\n if (xs.length === 0) return null;\n return new Bounds(Math.min(...xs), Math.max(...xs), Math.min(...ys), Math.max(...ys));\n }\n\n private pathEl(fill: string, stroke: string, d: string): SVGPathElement {\n const el = document.createElementNS(QueueCylinders.SVG_NS, 'path');\n el.setAttribute('fill', fill);\n el.setAttribute('stroke', stroke);\n el.setAttribute('d', d);\n return el;\n }\n}\n\n/** Runtime keeps the single-box Lock while Filter follows the full directed chain. */\nclass RuntimeNodeMenu {\n constructor(private readonly svg: SVGSVGElement, private readonly lock: WpNodeLock, private readonly page: RuntimePage) {}\n\n wire(): void {\n WpNodeMenu.wire(this.svg, (name: string, node: SVGGElement): WpNodeMenuItem[] => {\n const label = this.lock.isLocked(name) ? 'Unlock' : 'Lock';\n return [new WpNodeMenuItem(label, (): void => { this.lock.toggle(name, node); }), this.page.filterItem(name)];\n });\n }\n}\n\nclass RuntimePage extends WpFilterPage {\n private lock: WpNodeLock | null = null;\n\n protected override captureBinding(svg: SVGSVGElement | null): () => void {\n const previous = this.lock;\n return (): void => {\n this.lock = previous;\n if (svg !== null) this.lock?.rebind(svg);\n };\n }\n\n protected override prepareSvg(svg: SVGSVGElement): void { new QueueCylinders().applyTo(svg); }\n\n protected wireSvg(svg: SVGSVGElement): void {\n if (this.lock === null) this.lock = new WpNodeLock(svg);\n else this.lock.rebind(svg);\n new RuntimeNodeMenu(svg, this.lock, this).wire();\n svg.querySelectorAll<SVGGElement>('g.node').forEach(node => {\n if (node.querySelector('title')?.textContent === this.anchor) node.classList.add('wp-filter-anchor');\n });\n }\n}\n\nnew RuntimePage(__RENDER_MODEL__).render();\n"]}
|
|
@@ -37,13 +37,15 @@
|
|
|
37
37
|
*
|
|
38
38
|
* EVERY node on the rendered page is clickable and opens the SHARED floating menu
|
|
39
39
|
* (graph-node-menu.ts — one implementation, also used by architecture/dependencies.html
|
|
40
|
-
* and every project's design.html).
|
|
40
|
+
* and every project's design.html). Lock/Unlock dims every
|
|
41
41
|
* other node and every edge and lights the locked box alone. There is no "View Design"
|
|
42
42
|
* item here — a node is a running service, queue, datastore or third-party system, not
|
|
43
43
|
* an nx project, so no design.html exists to point at and the item is absent rather than
|
|
44
|
-
* dead.
|
|
44
|
+
* dead. Filter Unconnected follows separate incoming/outgoing chains and compacts the rendered
|
|
45
|
+
* topology; Turn off Filter restores the full picture. Lock remains independent of Filter.
|
|
45
46
|
*/
|
|
46
47
|
import type { RuntimeGraph } from './runtime-graph';
|
|
48
|
+
import { GraphRenderModel } from './graph-render-model';
|
|
47
49
|
/** Render options for the runtime graph. */
|
|
48
50
|
export declare class RuntimeVizOptions {
|
|
49
51
|
/**
|
|
@@ -62,6 +64,8 @@ export declare class RuntimeVizOptions {
|
|
|
62
64
|
}
|
|
63
65
|
/** Build the Graphviz DOT for the runtime service graph. */
|
|
64
66
|
export declare function generateRuntimeDot(graph: RuntimeGraph, title?: string, options?: RuntimeVizOptions): string;
|
|
67
|
+
/** Capture synthetic drawable nodes and edges at their original emission sites. */
|
|
68
|
+
export declare function generateRuntimeRenderModel(graph: RuntimeGraph, title?: string, options?: RuntimeVizOptions): GraphRenderModel;
|
|
65
69
|
export interface RuntimeVisualizationPaths {
|
|
66
70
|
dotPath: string;
|
|
67
71
|
htmlPath: string;
|
|
@@ -38,20 +38,23 @@
|
|
|
38
38
|
*
|
|
39
39
|
* EVERY node on the rendered page is clickable and opens the SHARED floating menu
|
|
40
40
|
* (graph-node-menu.ts — one implementation, also used by architecture/dependencies.html
|
|
41
|
-
* and every project's design.html).
|
|
41
|
+
* and every project's design.html). Lock/Unlock dims every
|
|
42
42
|
* other node and every edge and lights the locked box alone. There is no "View Design"
|
|
43
43
|
* item here — a node is a running service, queue, datastore or third-party system, not
|
|
44
44
|
* an nx project, so no design.html exists to point at and the item is absent rather than
|
|
45
|
-
* dead.
|
|
45
|
+
* dead. Filter Unconnected follows separate incoming/outgoing chains and compacts the rendered
|
|
46
|
+
* topology; Turn off Filter restores the full picture. Lock remains independent of Filter.
|
|
46
47
|
*/
|
|
47
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
49
|
exports.RuntimeVizOptions = void 0;
|
|
49
50
|
exports.generateRuntimeDot = generateRuntimeDot;
|
|
51
|
+
exports.generateRuntimeRenderModel = generateRuntimeRenderModel;
|
|
50
52
|
exports.writeRuntimeVisualization = writeRuntimeVisualization;
|
|
51
53
|
const tslib_1 = require("tslib");
|
|
52
54
|
const fs = tslib_1.__importStar(require("fs"));
|
|
53
55
|
const path = tslib_1.__importStar(require("path"));
|
|
54
56
|
const dot_syntax_1 = require("./dot-syntax");
|
|
57
|
+
const graph_render_model_1 = require("./graph-render-model");
|
|
55
58
|
const runtime_html_page_1 = require("./runtime-html-page");
|
|
56
59
|
const runtime_viz_theme_1 = require("./runtime-viz-theme");
|
|
57
60
|
/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */
|
|
@@ -192,18 +195,18 @@ function queueLine(key, queue) {
|
|
|
192
195
|
return line;
|
|
193
196
|
return `${line}\\nqueue: ${(0, dot_syntax_1.recordValue)(queue.queueName)}`;
|
|
194
197
|
}
|
|
195
|
-
/** The node statement + enqueue/deliver arrows for one queue box.
|
|
198
|
+
/** The node statement + enqueue/deliver arrows for one queue box. Service identities remain raw; only emitted DOT is escaped. */
|
|
196
199
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
197
|
-
function queueBoxDot(id, body, producers, consumers) {
|
|
200
|
+
function queueBoxDot(id, body, producers, consumers, model) {
|
|
198
201
|
// Record-mode label: the text must clear recordValue(), and QUEUE_LABEL_PREFIX supplies the
|
|
199
202
|
// empty leading field that draws the cylinder's end cap. Drop it and the node silently
|
|
200
203
|
// degrades to a plain box.
|
|
201
|
-
let dot = ` "${id}" [shape=${runtime_viz_theme_1.QUEUE_SHAPE}, style="filled", fillcolor="${runtime_viz_theme_1.QUEUE_FILL}", ` +
|
|
202
|
-
`class="${runtime_viz_theme_1.QUEUE_CLASS}", label="${runtime_viz_theme_1.QUEUE_LABEL_PREFIX}${body}"];\n
|
|
204
|
+
let dot = model.node(id, ` "${id}" [shape=${runtime_viz_theme_1.QUEUE_SHAPE}, style="filled", fillcolor="${runtime_viz_theme_1.QUEUE_FILL}", ` +
|
|
205
|
+
`class="${runtime_viz_theme_1.QUEUE_CLASS}", label="${runtime_viz_theme_1.QUEUE_LABEL_PREFIX}${body}"];\n`);
|
|
203
206
|
for (const producer of producers)
|
|
204
|
-
dot += ` "${producer}" -> "${id}" [label="enqueue", style=dashed];\n
|
|
207
|
+
dot += model.edge(producer, id, ` "${serviceNodeId(producer)}" -> "${id}" [label="enqueue", style=dashed];\n`);
|
|
205
208
|
for (const consumer of consumers)
|
|
206
|
-
dot += ` "${id}" -> "${consumer}" [label="deliver", style=dashed];\n
|
|
209
|
+
dot += model.edge(id, consumer, ` "${id}" -> "${serviceNodeId(consumer)}" [label="deliver", style=dashed];\n`);
|
|
207
210
|
return dot;
|
|
208
211
|
}
|
|
209
212
|
/**
|
|
@@ -245,7 +248,7 @@ function groupQueues(byQueue, graph) {
|
|
|
245
248
|
* one is stuck) at a fraction of the node count.
|
|
246
249
|
*/
|
|
247
250
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
248
|
-
function queuesDot(graph, hidden) {
|
|
251
|
+
function queuesDot(graph, hidden, model) {
|
|
249
252
|
const queued = graph.runtimeEdges.filter((e) => e.type === 'pubsub' && !hidden.has(e.from) && !hidden.has(e.to));
|
|
250
253
|
if (queued.length === 0)
|
|
251
254
|
return '';
|
|
@@ -255,12 +258,12 @@ function queuesDot(graph, hidden) {
|
|
|
255
258
|
// per-method queue at all, so it keeps the historical unnamed per-pair box.
|
|
256
259
|
const byQueue = new Map();
|
|
257
260
|
for (const edge of queued) {
|
|
258
|
-
const from =
|
|
259
|
-
const to =
|
|
261
|
+
const from = edge.from;
|
|
262
|
+
const to = edge.to;
|
|
260
263
|
if (edge.queue === undefined) {
|
|
261
264
|
// Kept RAW: recordValue already applies dotValue, so escaping here would double it.
|
|
262
265
|
const viaRaw = edge.via.map((v) => getShortName(v)).join(', ');
|
|
263
|
-
dot += queueBoxDot(`queue__${dotId(edge.from)}__${dotId(edge.to)}`, `${(0, dot_syntax_1.recordValue)(viaRaw)}\\nqueue`, [from], [to]);
|
|
266
|
+
dot += queueBoxDot(`queue__${dotId(edge.from)}__${dotId(edge.to)}`, `${(0, dot_syntax_1.recordValue)(viaRaw)}\\nqueue`, [from], [to], model);
|
|
264
267
|
continue;
|
|
265
268
|
}
|
|
266
269
|
if (!byQueue.has(edge.queue))
|
|
@@ -272,7 +275,7 @@ function queuesDot(graph, hidden) {
|
|
|
272
275
|
const body = group.members
|
|
273
276
|
.map((key) => queueLine(key, graph.queues[key]))
|
|
274
277
|
.join('\\n');
|
|
275
|
-
dot += queueBoxDot(`queue__${dotId(group.members[0])}`, body, group.producers, group.consumers);
|
|
278
|
+
dot += queueBoxDot(`queue__${dotId(group.members[0])}`, body, group.producers, group.consumers, model);
|
|
276
279
|
}
|
|
277
280
|
return dot;
|
|
278
281
|
}
|
|
@@ -291,7 +294,7 @@ function dotId(raw) {
|
|
|
291
294
|
* copy-pasted attribute string would have let the two drift until they stopped being one box.
|
|
292
295
|
*/
|
|
293
296
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
294
|
-
function externalSystemNodeDot(identity, kind, label, subtitle) {
|
|
297
|
+
function externalSystemNodeDot(identity, kind, label, subtitle, model) {
|
|
295
298
|
const shape = runtime_viz_theme_1.EXTERNAL_SHAPES[kind] ?? 'box';
|
|
296
299
|
const fill = runtime_viz_theme_1.EXTERNAL_FILLS[kind] ?? runtime_viz_theme_1.EXTERNAL_FILL;
|
|
297
300
|
// An Mrecord-shaped system needs the same empty leading field as a queue node, or it renders as
|
|
@@ -302,7 +305,7 @@ function externalSystemNodeDot(identity, kind, label, subtitle) {
|
|
|
302
305
|
// Only a queue-kind system is marked: a database here is an UPRIGHT cylinder and must not be
|
|
303
306
|
// caught by the browser-side reshaping that lays queues on their side.
|
|
304
307
|
const marker = isQueue ? `class="${runtime_viz_theme_1.QUEUE_CLASS}", ` : '';
|
|
305
|
-
return (` "system__${dotId(identity)}" [shape=${shape}, style="filled", fillcolor="${fill}", ` +
|
|
308
|
+
return model.node(`system__${dotId(identity)}`, ` "system__${dotId(identity)}" [shape=${shape}, style="filled", fillcolor="${fill}", ` +
|
|
306
309
|
`${marker}label="${prefix}${text}\\n(${subtitle})"];\n`);
|
|
307
310
|
}
|
|
308
311
|
/**
|
|
@@ -326,7 +329,7 @@ function externalSystemNodeDot(identity, kind, label, subtitle) {
|
|
|
326
329
|
* must not restate the node statement.
|
|
327
330
|
*/
|
|
328
331
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
329
|
-
function triggerDot(graph, hidden, options) {
|
|
332
|
+
function triggerDot(graph, hidden, options, model) {
|
|
330
333
|
const triggers = graph.triggers.filter((t) => !hidden.has(t.service));
|
|
331
334
|
if (triggers.length === 0)
|
|
332
335
|
return '';
|
|
@@ -340,10 +343,9 @@ function triggerDot(graph, hidden, options) {
|
|
|
340
343
|
if (trigger.kind === 'cron') {
|
|
341
344
|
const id = `cron__${dotId(`${trigger.api}_${trigger.method}`)}`;
|
|
342
345
|
const schedule = (0, dot_syntax_1.dotValue)(trigger.queueName ?? `${trigger.api}-${trigger.method}`);
|
|
343
|
-
dot +=
|
|
344
|
-
`
|
|
345
|
-
|
|
346
|
-
` "${id}" -> "${service}" [label="${label}\\n${schedule}", color="${runtime_viz_theme_1.CRON_BORDER}"];\n`;
|
|
346
|
+
dot += model.node(id, ` "${id}" [shape=circle, style="filled", fillcolor="${runtime_viz_theme_1.CRON_FILL}", ` +
|
|
347
|
+
`color="${runtime_viz_theme_1.CRON_BORDER}", label="⏰\\ncron"];\n`) +
|
|
348
|
+
model.edge(id, trigger.service, ` "${id}" -> "${service}" [label="${label}\\n${schedule}", color="${runtime_viz_theme_1.CRON_BORDER}"];\n`);
|
|
347
349
|
continue;
|
|
348
350
|
}
|
|
349
351
|
const caller = trigger.caller;
|
|
@@ -357,11 +359,11 @@ function triggerDot(graph, hidden, options) {
|
|
|
357
359
|
emitted.add(id);
|
|
358
360
|
dot +=
|
|
359
361
|
caller === undefined
|
|
360
|
-
? ` "${id}" [shape=box, style="dotted,filled", fillcolor="${runtime_viz_theme_1.EXTERNAL_FILL}", ` +
|
|
361
|
-
`color="${runtime_viz_theme_1.EXTERNAL_BORDER}", label="${(0, dot_syntax_1.dotValue)(trigger.api)}\\n? unknown caller"];\n`
|
|
362
|
-
: externalSystemNodeDot(caller.label, caller.kind, caller.label, 'external caller');
|
|
362
|
+
? model.node(id, ` "${id}" [shape=box, style="dotted,filled", fillcolor="${runtime_viz_theme_1.EXTERNAL_FILL}", ` +
|
|
363
|
+
`color="${runtime_viz_theme_1.EXTERNAL_BORDER}", label="${(0, dot_syntax_1.dotValue)(trigger.api)}\\n? unknown caller"];\n`)
|
|
364
|
+
: externalSystemNodeDot(caller.label, caller.kind, caller.label, 'external caller', model);
|
|
363
365
|
}
|
|
364
|
-
dot += ` "${id}" -> "${service}" [label="${label}", style=dashed, color="${runtime_viz_theme_1.EXTERNAL_BORDER}"];\n
|
|
366
|
+
dot += model.edge(id, trigger.service, ` "${id}" -> "${service}" [label="${label}", style=dashed, color="${runtime_viz_theme_1.EXTERNAL_BORDER}"];\n`);
|
|
365
367
|
}
|
|
366
368
|
return dot;
|
|
367
369
|
}
|
|
@@ -376,7 +378,7 @@ function triggerDot(graph, hidden, options) {
|
|
|
376
378
|
* read look like an event.
|
|
377
379
|
*/
|
|
378
380
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
379
|
-
function externalSystemsDot(graph, hidden) {
|
|
381
|
+
function externalSystemsDot(graph, hidden, model) {
|
|
380
382
|
const systems = graph.externalSystems ?? {};
|
|
381
383
|
const ids = Object.keys(systems).sort();
|
|
382
384
|
if (ids.length === 0)
|
|
@@ -384,7 +386,7 @@ function externalSystemsDot(graph, hidden) {
|
|
|
384
386
|
let dot = '\n // Declared external systems — drawn with the shape of what they actually are.\n';
|
|
385
387
|
for (const id of ids) {
|
|
386
388
|
const system = systems[id];
|
|
387
|
-
dot += externalSystemNodeDot(id, system.kind, system.label, `external ${(0, dot_syntax_1.dotValue)(system.kind)}
|
|
389
|
+
dot += externalSystemNodeDot(id, system.kind, system.label, `external ${(0, dot_syntax_1.dotValue)(system.kind)}`, model);
|
|
388
390
|
}
|
|
389
391
|
for (const id of ids) {
|
|
390
392
|
const system = systems[id];
|
|
@@ -392,7 +394,7 @@ function externalSystemsDot(graph, hidden) {
|
|
|
392
394
|
for (const service of [...system.usedBy].sort()) {
|
|
393
395
|
if (hidden.has(service))
|
|
394
396
|
continue;
|
|
395
|
-
dot += ` "${serviceNodeId(service)}" -> "system__${dotId(id)}"${via};\n
|
|
397
|
+
dot += model.edge(service, `system__${dotId(id)}`, ` "${serviceNodeId(service)}" -> "system__${dotId(id)}"${via};\n`);
|
|
396
398
|
}
|
|
397
399
|
}
|
|
398
400
|
return dot;
|
|
@@ -410,7 +412,7 @@ function externalSystemsDot(graph, hidden) {
|
|
|
410
412
|
* has already drawn it with a real shape, and rendering it in both places would double the node.
|
|
411
413
|
*/
|
|
412
414
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
413
|
-
function externalDot(graph, hidden) {
|
|
415
|
+
function externalDot(graph, hidden, model) {
|
|
414
416
|
// "service|externalName" -> the apis flowing over it.
|
|
415
417
|
const apisByPair = new Map();
|
|
416
418
|
for (const use of graph.unresolvedUses) {
|
|
@@ -434,9 +436,8 @@ function externalDot(graph, hidden) {
|
|
|
434
436
|
// carries the bare name.
|
|
435
437
|
const externals = new Set([...apisByPair.keys()].map((key) => key.split(PAIR_SEP)[1]));
|
|
436
438
|
for (const external of [...externals].sort()) {
|
|
437
|
-
dot +=
|
|
438
|
-
`
|
|
439
|
-
`color="${runtime_viz_theme_1.EXTERNAL_BORDER}", label="${(0, dot_syntax_1.dotValue)(getShortName(external))}\\n(external)"];\n`;
|
|
439
|
+
dot += model.node(`external__${external}`, ` "external__${(0, dot_syntax_1.dotValue)(external)}" [shape=box, style="dashed,filled", fillcolor="${runtime_viz_theme_1.EXTERNAL_FILL}", ` +
|
|
440
|
+
`color="${runtime_viz_theme_1.EXTERNAL_BORDER}", label="${(0, dot_syntax_1.dotValue)(getShortName(external))}\\n(external)"];\n`);
|
|
440
441
|
}
|
|
441
442
|
for (const key of [...apisByPair.keys()].sort()) {
|
|
442
443
|
const parts = key.split(PAIR_SEP);
|
|
@@ -445,19 +446,25 @@ function externalDot(graph, hidden) {
|
|
|
445
446
|
const via = labelList(apisByPair.get(key).sort());
|
|
446
447
|
// SOLID: this is a synchronous call that returns a value. Dashed is reserved for events, and
|
|
447
448
|
// "outside the repo" is already said by the node's dashed border.
|
|
448
|
-
dot +=
|
|
449
|
-
`
|
|
450
|
-
`[label="${via}", color="${runtime_viz_theme_1.EXTERNAL_BORDER}"];\n`;
|
|
449
|
+
dot += model.edge(service, `external__${external}`, ` "${serviceNodeId(service)}" -> "external__${(0, dot_syntax_1.dotValue)(external)}" ` +
|
|
450
|
+
`[label="${via}", color="${runtime_viz_theme_1.EXTERNAL_BORDER}"];\n`);
|
|
451
451
|
}
|
|
452
452
|
return dot;
|
|
453
453
|
}
|
|
454
454
|
/** Build the Graphviz DOT for the runtime service graph. */
|
|
455
455
|
// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here
|
|
456
456
|
function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', options = new RuntimeVizOptions()) {
|
|
457
|
+
return generateRuntimeRenderModel(graph, title, options).fullDot;
|
|
458
|
+
}
|
|
459
|
+
/** Capture synthetic drawable nodes and edges at their original emission sites. */
|
|
460
|
+
// webpieces-disable no-function-outside-class -- renderer entry point matching generateRuntimeDot
|
|
461
|
+
function generateRuntimeRenderModel(graph, title = 'WebPieces Runtime Architecture', options = new RuntimeVizOptions()) {
|
|
462
|
+
const model = new graph_render_model_1.GraphRenderModel();
|
|
457
463
|
let dot = 'digraph RuntimeArchitecture {\n';
|
|
458
464
|
dot += ' rankdir=TB;\n';
|
|
459
465
|
dot += ' node [shape=box, style="filled,rounded", fontname="Arial"];\n';
|
|
460
466
|
dot += ' edge [fontname="Arial", fontsize=10];\n\n';
|
|
467
|
+
model.header = dot;
|
|
461
468
|
// Services tagged drawOnGraph:false stay in the JSON but are omitted here —
|
|
462
469
|
// both their node and any edge touching them are dropped from the render.
|
|
463
470
|
const hidden = new Set(Object.keys(graph.services).filter((name) => graph.services[name].drawOnGraph === false));
|
|
@@ -466,7 +473,7 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
466
473
|
continue;
|
|
467
474
|
const svc = graph.services[name];
|
|
468
475
|
const color = runtime_viz_theme_1.LEVEL_COLORS[svc.level] || '#F5F5F5';
|
|
469
|
-
dot += ` "${serviceNodeId(name)}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n
|
|
476
|
+
dot += model.node(name, ` "${serviceNodeId(name)}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n`);
|
|
470
477
|
}
|
|
471
478
|
dot += '\n';
|
|
472
479
|
for (const edge of graph.runtimeEdges) {
|
|
@@ -475,14 +482,15 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
475
482
|
// Queued hops are merged across methods, so they cannot be emitted one edge at a time.
|
|
476
483
|
if (edge.type === 'pubsub')
|
|
477
484
|
continue;
|
|
478
|
-
dot += edgeDot(edge);
|
|
485
|
+
dot += model.edge(edge.from, edge.to, edgeDot(edge));
|
|
479
486
|
}
|
|
480
|
-
dot += queuesDot(graph, hidden);
|
|
481
|
-
dot += triggerDot(graph, hidden, options);
|
|
487
|
+
dot += queuesDot(graph, hidden, model);
|
|
488
|
+
dot += triggerDot(graph, hidden, options, model);
|
|
482
489
|
if (options.showExternalNodes) {
|
|
483
|
-
dot += externalSystemsDot(graph, hidden);
|
|
484
|
-
dot += externalDot(graph, hidden);
|
|
490
|
+
dot += externalSystemsDot(graph, hidden, model);
|
|
491
|
+
dot += externalDot(graph, hidden, model);
|
|
485
492
|
}
|
|
493
|
+
const footerStart = dot.length;
|
|
486
494
|
dot += '\n labelloc="t";\n';
|
|
487
495
|
dot += ` label="${(0, dot_syntax_1.dotValue)(title)}\\n(from architecture/runtime-dependencies.json)";\n`;
|
|
488
496
|
dot += ' fontsize=20;\n';
|
|
@@ -490,18 +498,22 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
490
498
|
// Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked
|
|
491
499
|
// HERE — a graph that cannot render is a generation failure, not a blank page to discover later.
|
|
492
500
|
(0, dot_syntax_1.assertValidDot)(dot, 'runtime-architecture.dot');
|
|
493
|
-
|
|
501
|
+
model.footer = dot.slice(footerStart);
|
|
502
|
+
model.completeEndpoints();
|
|
503
|
+
model.fullDot = dot;
|
|
504
|
+
return model;
|
|
494
505
|
}
|
|
495
506
|
/** Write the DOT + HTML renderings to tmp/webpieces/. */
|
|
496
507
|
function writeRuntimeVisualization(graph, workspaceRoot, title = 'WebPieces Runtime Architecture', options = new RuntimeVizOptions()) {
|
|
497
508
|
const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');
|
|
498
509
|
if (!fs.existsSync(outputDir))
|
|
499
510
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
500
|
-
const
|
|
511
|
+
const model = generateRuntimeRenderModel(graph, title, options);
|
|
512
|
+
const dot = model.fullDot;
|
|
501
513
|
const dotPath = path.join(outputDir, 'runtime-architecture.dot');
|
|
502
514
|
fs.writeFileSync(dotPath, dot, 'utf-8');
|
|
503
515
|
const htmlPath = path.join(outputDir, 'runtime-architecture.html');
|
|
504
|
-
fs.writeFileSync(htmlPath, new runtime_html_page_1.RuntimeHtmlPage().render(
|
|
516
|
+
fs.writeFileSync(htmlPath, new runtime_html_page_1.RuntimeHtmlPage().render(model, title), 'utf-8');
|
|
505
517
|
return { dotPath, htmlPath };
|
|
506
518
|
}
|
|
507
519
|
//# sourceMappingURL=runtime-visualizer.js.map
|