@webpieces/nx-webpieces-rules 0.3.322 → 0.3.323
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.323",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@webpieces/ai-hook-rules": "0.3.
|
|
25
|
-
"@webpieces/code-rules": "0.3.
|
|
26
|
-
"@webpieces/eslint-rules": "0.3.
|
|
27
|
-
"@webpieces/pr-gate": "0.3.
|
|
28
|
-
"@webpieces/rules-config": "0.3.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.3.323",
|
|
25
|
+
"@webpieces/code-rules": "0.3.323",
|
|
26
|
+
"@webpieces/eslint-rules": "0.3.323",
|
|
27
|
+
"@webpieces/pr-gate": "0.3.323",
|
|
28
|
+
"@webpieces/rules-config": "0.3.323",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -114,8 +114,11 @@ function stackScript() {
|
|
|
114
114
|
if (!node) continue;
|
|
115
115
|
const outline = node.querySelector('polygon, polyline, path');
|
|
116
116
|
if (!outline) continue;
|
|
117
|
-
//
|
|
118
|
-
|
|
117
|
+
// We insertBefore(firstChild), so the LAST inserted paints furthest back. Insert
|
|
118
|
+
// nearest (-5) first then farthest (-10): final paint order is -10, -5, original —
|
|
119
|
+
// a clean 3-card stack where all three cards are visible. (The reverse order hid
|
|
120
|
+
// the -5 card entirely behind the -10 one, so only 2 boxes showed.)
|
|
121
|
+
for (const offset of [-5, -10]) {
|
|
119
122
|
const ghost = outline.cloneNode(false);
|
|
120
123
|
ghost.setAttribute('transform', 'translate(' + offset + ',' + offset + ')');
|
|
121
124
|
ghost.setAttribute('fill', '#ffffff');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"design-visualizer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design HTML Visualizer\n *\n * Builds one HTML page per project showing EVERY controller/root design as\n * its own Graphviz graph (rendered client-side with viz.js, same pipeline as\n * the architecture visualization). Output goes to tmp/webpieces/ — a view,\n * never committed (the committed artifacts are design.json/design.md).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DiDesign, DiGraph, DiNode } from './model';\nimport { sortGraph } from './serializer';\nimport { generateDesignDot, isStackedNode } from './dot';\n\nexport class DesignVisualizationPaths {\n constructor(\n public readonly dotPath: string,\n public readonly htmlPath: string\n ) {}\n}\n\nclass DesignGraphEntry {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n /** Node ids to paint as a stack of instances (transient). Sorted, so the HTML is stable. */\n public readonly stackedIds: string[]\n ) {}\n}\n\n/** The transient nodes of one design, in the design's (already sorted) node order. */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackedIdsOf(design: DiDesign): string[] {\n return design.nodes.filter((node: DiNode) => isStackedNode(node)).map((node: DiNode) => node.id);\n}\n\nfunction htmlEscape(text: string): string {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nfunction pageStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .back { max-width: 95%; margin: 0 auto 8px; }\n .back a { color: #1565C0; text-decoration: none; }\n .back a:hover { text-decoration: underline; }\n h2 { color: #333; margin-bottom: 4px; }\n .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }\n .section { background: white; padding: 20px; border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }\n .graph { text-align: center; overflow-x: auto; }\n .legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;\n border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n .legend-item { margin: 8px 0; }\n .legend-box { display: inline-block; width: 20px; height: 20px;\n border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }\n `;\n}\n\nfunction pageLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Design root</strong> — level 0, the @DocumentDesign entry class of the tree\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>Class</strong> — injectable class (constructor injection)</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FCE4EC; border-style: dashed;\"></span>\n <strong>Unresolved</strong> — token the analyzer could not resolve</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #EDE7F6; border: 3px double #5e35b1;\"></span>\n <strong>External</strong> — class from a published package outside this workspace; shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E1F5FE; border: 3px double #0277bd;\"></span>\n <strong>API client</strong> — generated <code>createApiClient</code> proxy (service/network boundary); shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5; margin-left: 12px; box-shadow: -5px -5px 0 -1px #fff, -5px -5px 0 0 #ccc, -10px -10px 0 -1px #fff, -10px -10px 0 0 #ccc;\"></span>\n <strong>Stack of boxes</strong> — a TRANSIENT class: every arrow into it resolves its OWN\n instance. A single box is a singleton, whose arrows all share one instance.</div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>One graph per controller/root; a shared dependency appears in each root's tree.\n Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>\n </div>`;\n}\n\n/**\n * Paint the \"many instances\" glyph: for each transient node, clone its outline twice and offset\n * the copies up-and-left BEHIND the real box, so you see a stack of three whose back two show only\n * their top and left edges.\n *\n * Graphviz has no offset-stack primitive, so we do it on the SVG viz.js hands back before it is\n * attached. That costs nothing in review stability — the committed design.html holds only this\n * static script plus a sorted id list; the SVG itself is produced in the browser at view time.\n *\n * Nodes are matched by their <title>, which Graphviz always emits as the node id. (Its `class`\n * attribute arrived in Graphviz 2.40 and is not verified to survive viz.js@2.1.2.)\n */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackScript(): string {\n return `\n function paintStacks(element, stackedIds) {\n const nodes = element.querySelectorAll('g.node');\n const byTitle = new Map();\n nodes.forEach(n => {\n const title = n.querySelector('title');\n if (title) byTitle.set(title.textContent, n);\n });\n for (const id of stackedIds) {\n const node = byTitle.get(id);\n if (!node) continue;\n const outline = node.querySelector('polygon, polyline, path');\n if (!outline) continue;\n // Farthest copy first so the nearer one paints over it; both go behind the original.\n for (const offset of [-10, -5]) {\n const ghost = outline.cloneNode(false);\n ghost.setAttribute('transform', 'translate(' + offset + ',' + offset + ')');\n ghost.setAttribute('fill', '#ffffff');\n node.insertBefore(ghost, node.firstChild);\n }\n }\n }\n `;\n}\n\nfunction renderScript(entries: DesignGraphEntry[]): string {\n return `\n const graphs = ${JSON.stringify(entries)};\n ${stackScript()}\n const viz = new Viz();\n for (const g of graphs) {\n viz.renderSVGElement(g.dot)\n .then(element => {\n paintStacks(element, g.stackedIds);\n document.getElementById(g.id).appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';\n });\n }\n `;\n}\n\n/**\n * Build the full HTML page for a project's DI designs — one section (heading\n * + meta + rendered graph) per controller/root design.\n *\n * `backHref`, when given, renders a \"back to architecture\" link at the top —\n * used by the committed per-project design.html so a reader who clicked in from\n * dependencies.html can click back out. Omitted for the tmp view.\n */\nexport function generateDesignHTML(graph: DiGraph, backHref?: string): string {\n const title = `DI Designs — ${graph.project}`;\n const entries: DesignGraphEntry[] = [];\n const sections: string[] = [];\n const backLink = backHref\n ? `<p class=\"back\"><a href=\"${htmlEscape(backHref)}\">← Back to architecture graph</a></p>`\n : '';\n\n graph.designs.forEach((design: DiDesign, index: number) => {\n const id = `graph-${index}`;\n entries.push(new DesignGraphEntry(id, generateDesignDot(design), stackedIdsOf(design)));\n sections.push(`<div class=\"section\">\n <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>\n <div class=\"meta\">${htmlEscape(design.file)}</div>\n <div id=\"${id}\" class=\"graph\"></div>\n </div>`);\n });\n\n const body =\n sections.length > 0\n ? sections.join('\\n ')\n : '<div class=\"section\"><em>No DI-registered classes found in this project.</em></div>';\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${htmlEscape(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>${pageStyles()}</style>\n</head>\n<body>\n ${backLink}\n <h1>${htmlEscape(title)}</h1>\n ${pageLegend()}\n ${body}\n <script>${renderScript(entries)}</script>\n</body>\n</html>`;\n}\n\n/**\n * Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs\n * concatenated, for debugging) and return the paths.\n */\nexport function writeDesignVisualization(\n graph: DiGraph,\n workspaceRoot: string\n): DesignVisualizationPaths {\n sortGraph(graph);\n\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const allDot = graph.designs.map((design: DiDesign) => generateDesignDot(design)).join('\\n');\n const dotPath = path.join(outputDir, `design-${graph.project}.dot`);\n fs.writeFileSync(dotPath, allDot, 'utf-8');\n\n const htmlPath = path.join(outputDir, `design-${graph.project}.html`);\n fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');\n\n return new DesignVisualizationPaths(dotPath, htmlPath);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"design-visualizer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design HTML Visualizer\n *\n * Builds one HTML page per project showing EVERY controller/root design as\n * its own Graphviz graph (rendered client-side with viz.js, same pipeline as\n * the architecture visualization). Output goes to tmp/webpieces/ — a view,\n * never committed (the committed artifacts are design.json/design.md).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DiDesign, DiGraph, DiNode } from './model';\nimport { sortGraph } from './serializer';\nimport { generateDesignDot, isStackedNode } from './dot';\n\nexport class DesignVisualizationPaths {\n constructor(\n public readonly dotPath: string,\n public readonly htmlPath: string\n ) {}\n}\n\nclass DesignGraphEntry {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n /** Node ids to paint as a stack of instances (transient). Sorted, so the HTML is stable. */\n public readonly stackedIds: string[]\n ) {}\n}\n\n/** The transient nodes of one design, in the design's (already sorted) node order. */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackedIdsOf(design: DiDesign): string[] {\n return design.nodes.filter((node: DiNode) => isStackedNode(node)).map((node: DiNode) => node.id);\n}\n\nfunction htmlEscape(text: string): string {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nfunction pageStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .back { max-width: 95%; margin: 0 auto 8px; }\n .back a { color: #1565C0; text-decoration: none; }\n .back a:hover { text-decoration: underline; }\n h2 { color: #333; margin-bottom: 4px; }\n .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }\n .section { background: white; padding: 20px; border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }\n .graph { text-align: center; overflow-x: auto; }\n .legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;\n border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n .legend-item { margin: 8px 0; }\n .legend-box { display: inline-block; width: 20px; height: 20px;\n border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }\n `;\n}\n\nfunction pageLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Design root</strong> — level 0, the @DocumentDesign entry class of the tree\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>Class</strong> — injectable class (constructor injection)</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FCE4EC; border-style: dashed;\"></span>\n <strong>Unresolved</strong> — token the analyzer could not resolve</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #EDE7F6; border: 3px double #5e35b1;\"></span>\n <strong>External</strong> — class from a published package outside this workspace; shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E1F5FE; border: 3px double #0277bd;\"></span>\n <strong>API client</strong> — generated <code>createApiClient</code> proxy (service/network boundary); shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5; margin-left: 12px; box-shadow: -5px -5px 0 -1px #fff, -5px -5px 0 0 #ccc, -10px -10px 0 -1px #fff, -10px -10px 0 0 #ccc;\"></span>\n <strong>Stack of boxes</strong> — a TRANSIENT class: every arrow into it resolves its OWN\n instance. A single box is a singleton, whose arrows all share one instance.</div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>One graph per controller/root; a shared dependency appears in each root's tree.\n Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>\n </div>`;\n}\n\n/**\n * Paint the \"many instances\" glyph: for each transient node, clone its outline twice and offset\n * the copies up-and-left BEHIND the real box, so you see a stack of three whose back two show only\n * their top and left edges.\n *\n * Graphviz has no offset-stack primitive, so we do it on the SVG viz.js hands back before it is\n * attached. That costs nothing in review stability — the committed design.html holds only this\n * static script plus a sorted id list; the SVG itself is produced in the browser at view time.\n *\n * Nodes are matched by their <title>, which Graphviz always emits as the node id. (Its `class`\n * attribute arrived in Graphviz 2.40 and is not verified to survive viz.js@2.1.2.)\n */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackScript(): string {\n return `\n function paintStacks(element, stackedIds) {\n const nodes = element.querySelectorAll('g.node');\n const byTitle = new Map();\n nodes.forEach(n => {\n const title = n.querySelector('title');\n if (title) byTitle.set(title.textContent, n);\n });\n for (const id of stackedIds) {\n const node = byTitle.get(id);\n if (!node) continue;\n const outline = node.querySelector('polygon, polyline, path');\n if (!outline) continue;\n // We insertBefore(firstChild), so the LAST inserted paints furthest back. Insert\n // nearest (-5) first then farthest (-10): final paint order is -10, -5, original —\n // a clean 3-card stack where all three cards are visible. (The reverse order hid\n // the -5 card entirely behind the -10 one, so only 2 boxes showed.)\n for (const offset of [-5, -10]) {\n const ghost = outline.cloneNode(false);\n ghost.setAttribute('transform', 'translate(' + offset + ',' + offset + ')');\n ghost.setAttribute('fill', '#ffffff');\n node.insertBefore(ghost, node.firstChild);\n }\n }\n }\n `;\n}\n\nfunction renderScript(entries: DesignGraphEntry[]): string {\n return `\n const graphs = ${JSON.stringify(entries)};\n ${stackScript()}\n const viz = new Viz();\n for (const g of graphs) {\n viz.renderSVGElement(g.dot)\n .then(element => {\n paintStacks(element, g.stackedIds);\n document.getElementById(g.id).appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';\n });\n }\n `;\n}\n\n/**\n * Build the full HTML page for a project's DI designs — one section (heading\n * + meta + rendered graph) per controller/root design.\n *\n * `backHref`, when given, renders a \"back to architecture\" link at the top —\n * used by the committed per-project design.html so a reader who clicked in from\n * dependencies.html can click back out. Omitted for the tmp view.\n */\nexport function generateDesignHTML(graph: DiGraph, backHref?: string): string {\n const title = `DI Designs — ${graph.project}`;\n const entries: DesignGraphEntry[] = [];\n const sections: string[] = [];\n const backLink = backHref\n ? `<p class=\"back\"><a href=\"${htmlEscape(backHref)}\">← Back to architecture graph</a></p>`\n : '';\n\n graph.designs.forEach((design: DiDesign, index: number) => {\n const id = `graph-${index}`;\n entries.push(new DesignGraphEntry(id, generateDesignDot(design), stackedIdsOf(design)));\n sections.push(`<div class=\"section\">\n <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>\n <div class=\"meta\">${htmlEscape(design.file)}</div>\n <div id=\"${id}\" class=\"graph\"></div>\n </div>`);\n });\n\n const body =\n sections.length > 0\n ? sections.join('\\n ')\n : '<div class=\"section\"><em>No DI-registered classes found in this project.</em></div>';\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${htmlEscape(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>${pageStyles()}</style>\n</head>\n<body>\n ${backLink}\n <h1>${htmlEscape(title)}</h1>\n ${pageLegend()}\n ${body}\n <script>${renderScript(entries)}</script>\n</body>\n</html>`;\n}\n\n/**\n * Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs\n * concatenated, for debugging) and return the paths.\n */\nexport function writeDesignVisualization(\n graph: DiGraph,\n workspaceRoot: string\n): DesignVisualizationPaths {\n sortGraph(graph);\n\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const allDot = graph.designs.map((design: DiDesign) => generateDesignDot(design)).join('\\n');\n const dotPath = path.join(outputDir, `design-${graph.project}.dot`);\n fs.writeFileSync(dotPath, allDot, 'utf-8');\n\n const htmlPath = path.join(outputDir, `design-${graph.project}.html`);\n fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');\n\n return new DesignVisualizationPaths(dotPath, htmlPath);\n}\n"]}
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
import { DiDesign, DiNode } from './model';
|
|
10
10
|
/**
|
|
11
11
|
* A transient node is 1-to-many: EVERY arrow into it resolves its own instance, unlike a
|
|
12
|
-
* singleton whose arrows all share one.
|
|
13
|
-
*
|
|
14
|
-
* 3-box offset stack over it — see design-visualizer.ts.
|
|
12
|
+
* singleton whose arrows all share one. design.html paints these as a real 3-box offset stack
|
|
13
|
+
* (flat overlapping cards) — see paintStacks() in design-visualizer.ts.
|
|
15
14
|
*
|
|
16
15
|
* Leaves (constant/dynamic) and unresolved boxes are never instances, so they never stack.
|
|
17
16
|
*/
|
package/src/lib/di-graph/dot.js
CHANGED
|
@@ -30,9 +30,8 @@ function dotEscape(text) {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* A transient node is 1-to-many: EVERY arrow into it resolves its own instance, unlike a
|
|
33
|
-
* singleton whose arrows all share one.
|
|
34
|
-
*
|
|
35
|
-
* 3-box offset stack over it — see design-visualizer.ts.
|
|
33
|
+
* singleton whose arrows all share one. design.html paints these as a real 3-box offset stack
|
|
34
|
+
* (flat overlapping cards) — see paintStacks() in design-visualizer.ts.
|
|
36
35
|
*
|
|
37
36
|
* Leaves (constant/dynamic) and unresolved boxes are never instances, so they never stack.
|
|
38
37
|
*/
|
|
@@ -64,9 +63,10 @@ function nodeStatement(node) {
|
|
|
64
63
|
const isBoundary = node.kind === 'external' || node.kind === 'api';
|
|
65
64
|
const penwidth = isRootKind || isBoundary ? ', penwidth=2' : '';
|
|
66
65
|
const peripheries = isBoundary ? ', peripheries=2' : '';
|
|
67
|
-
// Transient => a fresh instance per injection.
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
// Transient => a fresh instance per injection. Kept as a plain box here; design.html paints
|
|
67
|
+
// a flat 3-card stack behind it (paintStacks). We deliberately avoid Graphviz's box3d glyph —
|
|
68
|
+
// its perspective slab clashed with the flat stack and read as an extra "3D box" on top.
|
|
69
|
+
return ` "${dotEscape(node.id)}" [fillcolor="${color}", style="${styles.join(',')}", label="${label}"${penwidth}${peripheries}];\n`;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Generate a Graphviz DOT digraph for one design (one controller/root tree).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dot.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/dot.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;
|
|
1
|
+
{"version":3,"file":"dot.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/dot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design DOT Emitter\n *\n * Renders one DiDesign (a single controller/root's dependency tree) as a\n * Graphviz DOT digraph, mirroring the look of the architecture graph\n * (lib/graph-visualizer.ts): rankdir=TB with { rank=same } layers per level,\n * so the controller (level 0) sits at the top and injections fan downward.\n */\n\nimport { DiDesign, DiNode } from './model';\n\n/**\n * Node fill colors by DI node kind.\n */\nconst KIND_COLORS: Record<string, string> = {\n controller: '#E3F2FD', // light blue — the root/entry class (server @DocumentDesign)\n apiImplementation: '#E0F2F1', // light teal — the root/entry class (designed-lib @DocumentDesign)\n component: '#E8F5E9', // light green — the root/entry Angular component\n class: '#F5F5F5', // neutral — plain injectable class\n constant: '#FFF3E0', // light orange — toConstantValue leaf\n dynamic: '#FFF3E0', // light orange — toDynamicValue leaf\n unresolved: '#FCE4EC', // light pink — token the analyzer could not resolve\n external: '#EDE7F6', // light violet — class from a published package; walk stops here\n api: '#E1F5FE', // light cyan — generated API-client proxy; service boundary, walk stops here\n};\n\n/** Escape a string for use inside a double-quoted DOT identifier/label. */\nfunction dotEscape(text: string): string {\n return text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\n/**\n * A transient node is 1-to-many: EVERY arrow into it resolves its own instance, unlike a\n * singleton whose arrows all share one. design.html paints these as a real 3-box offset stack\n * (flat overlapping cards) — see paintStacks() in design-visualizer.ts.\n *\n * Leaves (constant/dynamic) and unresolved boxes are never instances, so they never stack.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over a DiNode, matching every sibling in this file\nexport function isStackedNode(node: DiNode): boolean {\n if (node.kind === 'constant' || node.kind === 'dynamic' || node.kind === 'unresolved') return false;\n return node.scope === 'transient';\n}\n\nfunction nodeStatement(node: DiNode): string {\n const color = KIND_COLORS[node.kind] ?? '#F5F5F5';\n // Injected as an API → show the contract on top, the impl class in parens beneath.\n const name = node.api\n ? `${dotEscape(node.api)}\\\\n(${dotEscape(node.className)})`\n : dotEscape(node.className);\n const label = `${name}\\\\n(L${node.level}, ${node.scope})`;\n const styles: string[] = ['filled'];\n if (node.kind === 'constant' || node.kind === 'dynamic') styles.push('rounded');\n if (node.kind === 'unresolved') styles.push('dashed');\n if (node.kind === 'api') styles.push('rounded');\n // External/api boundary: bold double border so it reads as \"stops here, not\n // expanded\" and is not mistaken for the pink dashed `unresolved` boxes.\n if (node.kind === 'external' || node.kind === 'api') styles.push('bold');\n const isRootKind =\n node.kind === 'controller' || node.kind === 'apiImplementation' || node.kind === 'component';\n const isBoundary = node.kind === 'external' || node.kind === 'api';\n const penwidth = isRootKind || isBoundary ? ', penwidth=2' : '';\n const peripheries = isBoundary ? ', peripheries=2' : '';\n // Transient => a fresh instance per injection. Kept as a plain box here; design.html paints\n // a flat 3-card stack behind it (paintStacks). We deliberately avoid Graphviz's box3d glyph —\n // its perspective slab clashed with the flat stack and read as an extra \"3D box\" on top.\n return ` \"${dotEscape(node.id)}\" [fillcolor=\"${color}\", style=\"${styles.join(',')}\", label=\"${label}\"${penwidth}${peripheries}];\\n`;\n}\n\n/**\n * Generate a Graphviz DOT digraph for one design (one controller/root tree).\n * Deterministic for a serializer-sorted design.\n */\nexport function generateDesignDot(design: DiDesign): string {\n let dot = `digraph \"${dotEscape(design.root)}\" {\\n`;\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=11];\\n\\n';\n\n // Nodes, colored by kind, labeled with their level + scope\n for (const node of design.nodes) {\n dot += nodeStatement(node);\n }\n\n dot += '\\n';\n\n // One { rank=same } layer per level so the root stays on top\n const levels = new Map<number, string[]>();\n for (const node of design.nodes) {\n const layer = levels.get(node.level) ?? [];\n layer.push(node.id);\n levels.set(node.level, layer);\n }\n const sortedLevels = Array.from(levels.keys()).sort((a: number, b: number) => a - b);\n for (const level of sortedLevels) {\n const ids = levels.get(level) ?? [];\n dot += ` { rank=same; ${ids.map((id: string) => `\"${dotEscape(id)}\"`).join('; ')}; }\\n`;\n }\n\n dot += '\\n';\n\n // Constructor-injection edges, unlabeled — the arrow alone shows the\n // dependency. The param/field name, token and tokenKey stay in design.json\n // for tooling that wants them.\n for (const edge of design.edges) {\n dot += ` \"${dotEscape(edge.from)}\" -> \"${dotEscape(edge.to)}\";\\n`;\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotEscape(design.root)}\\\\n(${design.rootKind}, Level 0-${design.maxLevel})\";\\n`;\n dot += ' fontsize=16;\\n';\n dot += '}\\n';\n\n return dot;\n}\n"]}
|