@webpieces/nx-webpieces-rules 0.3.332 → 0.3.334

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * Output files go to tmp/webpieces/ for easy viewing without committing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { toError } from '../toError';\n\n/**\n * Framework (libType) colors for visualization — nodes are filled by the FIRST\n * env in their set that has a color, so it is obvious at a glance which side a\n * project targets. A project's full env set is shown in the label.\n */\nconst FRAMEWORK_COLORS: Record<string, string> = {\n angular: '#FCE4EC', // pink - Angular front-end\n react: '#E3F2FD', // blue - React front-end\n browser: '#EDE7F6', // purple - browser (front-end base env)\n express: '#E8F5E9', // green - Express / server side\n node: '#FFF9C4', // yellow - node (server base env)\n};\n\nconst DEFAULT_FRAMEWORK_COLOR = '#F5F5F5'; // grey - unknown/empty env set\n\n/**\n * Fill color for an env set — the color of the first env in the set that has a\n * known color, else the default.\n */\nfunction frameworkColor(frameworks: string[]): string {\n for (const env of frameworks) {\n const color = FRAMEWORK_COLORS[env];\n if (color !== undefined) return color;\n }\n return DEFAULT_FRAMEWORK_COLOR;\n}\n\n/**\n * Role border styling — fill stays keyed on framework; the border shows a\n * project's ROLE at a glance. Server and client are the top-level runnable\n * nodes, so they get bold, colored borders to stand out:\n * server → thick GREEN border (a runnable server app)\n * client → thick RED border (a client app, e.g. angular)\n * designed-lib → bold border (a library with a generated @DocumentDesign design)\n * lib / other → plain thin border\n */\nfunction roleBorderAttrs(role: string): string {\n if (role === 'server') return ', color=\"green\", penwidth=3';\n if (role === 'client') return ', color=\"red\", penwidth=3';\n if (role === 'designed-lib') return ', penwidth=2';\n return '';\n}\n\n/**\n * Remove scope from name for display\n * '@scope/name' → 'name'\n * 'name' → 'name'\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/**\n * Directory (repo-relative) that the committed architecture HTML lives in.\n * Node click-through links are computed relative to this so they resolve when\n * the file is opened straight from the checkout.\n */\nconst ARCH_OUTPUT_DIR = 'architecture';\n\n/**\n * Click-through href for a node: the project's committed design.html, made\n * relative to architecture/dependencies.html. Returns null when the project has\n * no generated DI design (no design.json → no clickable design page).\n *\n * designFile is repo-relative posix (e.g. 'packages/http/http-api/design.json');\n * we swap the extension and re-root it at architecture/ so the browser resolves\n * '../packages/http/http-api/design.html' from the checkout.\n */\nfunction designHtmlHref(designFile: string | undefined): string | null {\n if (!designFile) return null;\n const designHtml = designFile.replace(/design\\.json$/, 'design.html');\n return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);\n}\n\n/**\n * Generate Graphviz DOT format from the graph\n */\nexport function generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n // Group projects by level\n const levels: Record<number, string[]> = {};\n for (const [project, info] of Object.entries(graph)) {\n if (!levels[info.level]) levels[info.level] = [];\n levels[info.level].push(project);\n }\n\n // Nodes: fill colored by framework env set (libType), border shaped by role;\n // the label shows the env set + role (e.g. [browser, node] · server).\n // A node with a generated DI design also gets a URL so the rendered SVG box\n // is clickable — it opens that project's committed design.html in a new tab.\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n const frameworks = info.framework ?? [];\n const role = info.role ?? 'lib';\n const color = frameworkColor(frameworks);\n const border = roleBorderAttrs(role);\n const href = designHtmlHref(info.designFile);\n const link = href ? `, URL=\"${href}\", target=\"_blank\"` : '';\n const envSet = `[${frameworks.join(', ')}]`;\n const labelMeta = `L${info.level} · ${envSet} · ${role}`;\n dot += ` \"${shortName}\" [fillcolor=\"${color}\"${border}${link}, label=\"${shortName}\\\\n(${labelMeta})\"];\\n`;\n }\n\n dot += '\\n';\n\n // Create same-rank subgraphs for each level\n for (const [level, projects] of Object.entries(levels)) {\n dot += ` { rank=same; `;\n projects.forEach((p) => {\n const shortName = getShortName(p);\n dot += `\"${shortName}\"; `;\n });\n dot += '}\\n';\n }\n\n dot += '\\n';\n\n // Create edges (dependencies)\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n for (const dep of info.dependsOn || []) {\n const depShortName = getShortName(dep);\n dot += ` \"${shortName}\" -> \"${depShortName}\";\\n`;\n }\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n}\n\n/**\n * Generate interactive HTML with embedded SVG using viz.js\n */\nexport function generateHTML(dot: string, title: string = 'Monorepo Dependency Architecture'): string {\n const styles = generateHTMLStyles();\n const legend = generateHTMLLegend();\n const script = generateHTMLScript(dot);\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\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>${styles}</style>\n</head>\n<body>\n <h1>${title}</h1>\n <p class=\"hint\">💡 Click any box with a generated DI design to open its <strong>design.html</strong> (what the AI sees inside that project).</p>\n <p class=\"hint\">🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it (all the way up) <em>and</em> every dependency below it (all the way down), with all the boxes and lines between — while the rest of the graph dims so you can follow one box at a glance.</p>\n ${legend}\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nfunction generateHTMLStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n /* viz.js renders node URLs as <a> — only boxes with a design.html get\n * one, so a:hover is a clickable-only signal. Make it pop clearly: a\n * thicker border PLUS a blue glow lift the box off the page, so it is\n * obvious which boxes you can click into vs. which you cannot. (A plain\n * stroke bump is invisible on server/client boxes, whose resting border\n * is already thick.) */\n #graph a { cursor: pointer; }\n #graph a polygon,\n #graph a ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n #graph a:hover polygon,\n #graph a:hover ellipse {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }\n /* Hover-highlight (wired up in JS after viz.js renders — see\n * wireHoverHighlight). Hovering a node adds .wp-dim to the <svg> and\n * .wp-focus/.wp-neighbor/.wp-hl to the connected box, its neighbors, and\n * its edges. We ONLY dim: the connected subgraph keeps its exact normal\n * look (full opacity), the rest recedes. The un-dim rules repeat the\n * \"svg.wp-dim\" ancestor so they out-specify the dim rule (which has an\n * extra type selector) — else the highlighted subgraph stays dimmed. */\n #graph .node, #graph .edge { transition: opacity 0.12s ease; }\n #graph svg.wp-dim .node,\n #graph svg.wp-dim .edge { opacity: 0.15; }\n #graph svg.wp-dim .node.wp-focus,\n #graph svg.wp-dim .node.wp-neighbor,\n #graph svg.wp-dim .edge.wp-hl { opacity: 1; }\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 600px;\n padding: 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 {\n margin-top: 0;\n }\n .legend-item {\n margin: 8px 0;\n }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n `;\n}\n\nfunction generateHTMLLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend — fill = framework (libType), border = role</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FCE4EC;\"></span>\n <strong>angular:</strong> Angular front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>react:</strong> React front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #EDE7F6;\"></span>\n <strong>browser:</strong> browser front-end base env\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>express:</strong> Express / server side\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF9C4;\"></span>\n <strong>node:</strong> node server base env\n </div>\n <div class=\"legend-item\" style=\"margin-top: 12px;\">\n <span class=\"legend-box\" style=\"border: 3px solid green;\"></span>\n <strong>server:</strong> runnable server app (thick green border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 3px solid red;\"></span>\n <strong>client:</strong> client app, e.g. angular (thick red border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 2px solid #333;\"></span>\n <strong>designed-lib:</strong> library with a generated @DocumentDesign design (bold border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 1px solid #ccc;\"></span>\n <strong>lib:</strong> plain library, no generated design (thin border)\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Each node label shows its dependency level (L#), its framework env set (e.g. [browser, node]), and its role. Rows are laid out by level (top = no dependencies), with the deepest libraries at the bottom. Transitive dependencies are allowed but not shown.</em>\n </div>\n </div>`;\n}\n\n/**\n * The page script (injected as a string). After viz.js renders the Graphviz\n * SVG, `wireHoverHighlight` indexes its nodes and edges so hovering a box\n * boldens every connection (incoming AND outgoing) and highlights the boxes on\n * the other end, dimming the rest. viz.js emits a predictable structure:\n * <g class=\"node\"><title>NAME</title> ... </g>\n * <g class=\"edge\"><title>FROM->TO</title> <path/> <polygon/> ... </g>\n * The edge <title> text (decoded by the DOM as \"FROM->TO\") gives both\n * endpoints, so adjacency is built without touching the DOT.\n */\nfunction generateHTMLScript(dot: string): string {\n return `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(element => {\n document.getElementById('graph').appendChild(element);\n wireHoverHighlight(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';\n });\n function wireHoverHighlight(svg) {\n const nodeByName = new Map();\n svg.querySelectorAll('g.node').forEach(g => {\n const t = g.querySelector('title');\n if (t) nodeByName.set(t.textContent.trim(), g);\n });\n // Directed adjacency: in* = entering (up/ancestors), out* = leaving (down/deps).\n const inEdges = new Map(), outEdges = new Map(), inNodes = new Map(), outNodes = new Map();\n const ensure = (map, key) => {\n let v = map.get(key);\n if (!v) { v = new Set(); map.set(key, v); }\n return v;\n };\n svg.querySelectorAll('g.edge').forEach(edge => {\n const t = edge.querySelector('title');\n if (!t) return;\n const idx = t.textContent.indexOf('->');\n if (idx < 0) return;\n const from = t.textContent.slice(0, idx).trim();\n const to = t.textContent.slice(idx + 2).trim();\n ensure(outEdges, from).add(edge);\n ensure(inEdges, to).add(edge);\n ensure(outNodes, from).add(to);\n ensure(inNodes, to).add(from);\n });\n const clear = () => {\n svg.classList.remove('wp-dim');\n svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach(el => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');\n });\n };\n const highlight = (name, focusEl) => {\n clear();\n svg.classList.add('wp-dim');\n focusEl.classList.add('wp-focus');\n // Transitively light ancestors (up) then descendants (down): edges\n // reached -> wp-hl, boxes -> wp-neighbor. visited guards cycles.\n [[inNodes, inEdges], [outNodes, outEdges]].forEach(dir => {\n const visited = new Set(); const stack = [name];\n while (stack.length) {\n const cur = stack.pop();\n (dir[1].get(cur) || []).forEach(e => e.classList.add('wp-hl'));\n (dir[0].get(cur) || []).forEach(next => {\n if (visited.has(next)) return;\n visited.add(next); stack.push(next);\n const g = nodeByName.get(next); if (g) g.classList.add('wp-neighbor');\n });\n }\n });\n };\n nodeByName.forEach((g, name) => {\n g.addEventListener('mouseenter', () => highlight(name, g));\n g.addEventListener('mouseleave', clear);\n });\n }\n `;\n}\n\ninterface VisualizationPaths {\n htmlPath: string;\n}\n\n/**\n * Write the committed architecture visualization to architecture/dependencies.html,\n * next to dependencies.json.\n *\n * This is a checked-in artifact, regenerated deterministically by\n * architecture:generate so the boxes stay clickable into each project's\n * design.html. The DOT is embedded in the HTML (rendered client-side by\n * viz.js), so no separate .dot file is committed — same as design.html. Output\n * is deterministic (sorted graph in → same bytes out) so git only shows a diff\n * when the architecture actually changed.\n */\nexport function writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'Monorepo Dependency Architecture'\n): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);\n\n // Ensure directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const html = generateHTML(generateDot(graph, title), title);\n const htmlPath = path.join(outputDir, 'dependencies.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return { htmlPath };\n}\n\n/**\n * Open the HTML visualization in the default browser\n */\nexport function openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;;AAEH,+CAAyB;AACzB,mDAA6B;AAC7B,iDAAyC;AAEzC,+CAA2C;AAC3C,qEAAoE;AACpE,wCAAqC;AAErC;;;;GAIG;AACH,MAAM,gBAAgB,GAA2B;IAC7C,OAAO,EAAE,SAAS,EAAE,6BAA6B;IACjD,KAAK,EAAE,SAAS,EAAE,2BAA2B;IAC7C,OAAO,EAAE,SAAS,EAAE,wCAAwC;IAC5D,OAAO,EAAE,SAAS,EAAE,iCAAiC;IACrD,IAAI,EAAE,SAAS,EAAE,kCAAkC;CACtD,CAAC;AAEF,MAAM,uBAAuB,GAAG,SAAS,CAAC,CAAC,+BAA+B;AAE1E;;;;GAIG;AACH,MAAM,eAAe,GAAG,cAAc,CAAC;AAEvC,MAAa,kBAAkB;IAC3B,QAAQ,CAAS;IAEjB,YAAY,QAAgB;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC7B,CAAC;CACJ;AAND,gDAMC;AAED,MAAa,eAAe;IACP,KAAK,GAAG,IAAI,wBAAU,EAAE,CAAC;IACzB,gBAAgB,GAAG,IAAI,iDAAwB,EAAE,CAAC;IAEnE;;;OAGG;IACK,cAAc,CAAC,UAAoB;QACvC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;YACpC,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;QAC1C,CAAC;QACD,OAAO,uBAAuB,CAAC;IACnC,CAAC;IAED;;;;;;;;OAQG;IACK,eAAe,CAAC,IAAY;QAChC,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,6BAA6B,CAAC;QAC5D,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,2BAA2B,CAAC;QAC1D,IAAI,IAAI,KAAK,cAAc;YAAE,OAAO,cAAc,CAAC;QACnD,OAAO,EAAE,CAAC;IACd,CAAC;IAED;;;;OAIG;IACK,cAAc,CAAC,UAA8B;QACjD,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC;QAC7B,MAAM,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,eAAe,EAAE,aAAa,CAAC,CAAC;QACtE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IAC5D,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,KAAoB,EAAE,QAAgB,kCAAkC;QAChF,IAAI,GAAG,GAAG,0BAA0B,CAAC;QACrC,GAAG,IAAI,iBAAiB,CAAC;QACzB,GAAG,IAAI,uDAAuD,CAAC;QAC/D,GAAG,IAAI,gCAAgC,CAAC;QAExC,0BAA0B;QAC1B,MAAM,MAAM,GAA6B,EAAE,CAAC;QAC5C,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC;YACnC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;YACvC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QAED,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAC5B,GAAG,IAAI,IAAI,CAAC;QAEZ,4CAA4C;QAC5C,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3C,GAAG,IAAI,iBAAiB,CAAC;YACzB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;gBACvB,GAAG,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;YAC/C,CAAC;YACD,GAAG,IAAI,KAAK,CAAC;QACjB,CAAC;QAED,GAAG,IAAI,IAAI,CAAC;QACZ,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAE5B,GAAG,IAAI,qBAAqB,CAAC;QAC7B,GAAG,IAAI,YAAY,KAAK,8CAA8C,CAAC;QACvE,GAAG,IAAI,kBAAkB,CAAC;QAC1B,GAAG,IAAI,KAAK,CAAC;QAEb,OAAO,GAAG,CAAC;IACf,CAAC;IAED,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,0EAA0E;IAClE,QAAQ,CAAC,KAAoB;QACjC,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YACnD,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC;YAChC,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;YAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAClD,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;YAC5C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,KAAK,MAAM,MAAM,MAAM,IAAI,EAAE,CAAC;YACzD,GAAG,IAAI,MAAM,SAAS,iBAAiB,KAAK,IAAI,MAAM,GAAG,IAAI,YAAY,SAAS,OAAO,SAAS,QAAQ,CAAC;QAC/G,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED,6BAA6B;IACrB,QAAQ,CAAC,KAAoB;QACjC,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACvC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YACnD,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,IAAI,EAAE,EAAE,CAAC;gBAC/C,GAAG,IAAI,MAAM,SAAS,SAAS,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;YACtE,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;OAEG;IACH,YAAY,CACR,GAAW,EACX,QAAgB,kCAAkC,EAClD,cAAsB,EAAE,EACxB,uBAA+B,EAAE;QAEjC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAEhC,OAAO;;;;aAIF,KAAK;;;aAGL,MAAM;;;UAGT,KAAK;;;MAGT,WAAW;MACX,MAAM;;MAEN,oBAAoB;cACZ,MAAM;;QAEZ,CAAC;IACL,CAAC;IAED;;;;;;;;;OASG;IACH,WAAW,CAAC,KAAoB;QAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE;YAC3C,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAClD,IAAI,SAAS,KAAK,CAAC;gBAAE,OAAO,SAAS,CAAC;YACtC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QACH,IAAI,OAAO,GAAG,EAAE,CAAC;QACjB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;YACnD,OAAO,IAAI,kBAAkB,SAAS,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,MAAM,SAAS,WAAW,CAAC;QAC/F,CAAC;QACD,OAAO;;sEAEuD,OAAO;WAClE,CAAC;IACR,CAAC;IAEO,MAAM;QACV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAwDL,IAAI,CAAC,eAAe,EAAE;KAC3B,CAAC;IACF,CAAC;IAED,4EAA4E;IAC5E,0EAA0E;IAClE,eAAe;QACnB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sCAwCuB,CAAC;IACnC,CAAC;IAEO,MAAM;QACV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAyCJ,CAAC;IACR,CAAC;IAED;;;;;;OAMG;IACK,MAAM,CAAC,GAAW;QACtB,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,4BAA4B,CAAC,EAAE,OAAO,CAAC,CAAC;QAC9F,OAAO,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,CAAC;IAED;;;;;;;;;OASG;IACH,kBAAkB,CACd,KAAoB,EACpB,aAAqB,EACrB,QAAgB,kCAAkC;QAElD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;QAE5D,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;QAED,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACrF,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC;QACrG,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;QAC3D,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAE1C,OAAO,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACH,iBAAiB,CAAC,QAAgB;QAC9B,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;YAClC,IAAI,WAAmB,CAAC;YAExB,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACxB,WAAW,GAAG,SAAS,QAAQ,GAAG,CAAC;YACvC,CAAC;iBAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;gBAC9B,WAAW,GAAG,aAAa,QAAQ,GAAG,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACJ,WAAW,GAAG,aAAa,QAAQ,GAAG,CAAC;YAC3C,CAAC;YAED,IAAA,wBAAQ,EAAC,WAAW,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC3C,OAAO,IAAI,CAAC;QAChB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,CAAC,IAAI,CAAC,+BAA+B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAC7D,OAAO,KAAK,CAAC;QACjB,CAAC;IACL,CAAC;CACJ;AA/YD,0CA+YC","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * All behavior lives on the injectable GraphVisualizer class so webpieces DI +\n * @DocumentDesign can wire it — module-scope functions are a dead end the DI\n * graph can't reach.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { GraphNames } from './graph-names';\nimport { ResponsibilitiesRenderer } from './graph-responsibilities';\nimport { toError } from '../toError';\n\n/**\n * Framework (libType) colors for visualization — nodes are filled by the FIRST\n * env in their set that has a color, so it is obvious at a glance which side a\n * project targets. A project's full env set is shown in the label.\n */\nconst FRAMEWORK_COLORS: Record<string, string> = {\n angular: '#FCE4EC', // pink - Angular front-end\n react: '#E3F2FD', // blue - React front-end\n browser: '#EDE7F6', // purple - browser (front-end base env)\n express: '#E8F5E9', // green - Express / server side\n node: '#FFF9C4', // yellow - node (server base env)\n};\n\nconst DEFAULT_FRAMEWORK_COLOR = '#F5F5F5'; // grey - unknown/empty env set\n\n/**\n * Directory (repo-relative) that the committed architecture HTML lives in.\n * Node click-through links are computed relative to this so they resolve when\n * the file is opened straight from the checkout.\n */\nconst ARCH_OUTPUT_DIR = 'architecture';\n\nexport class VisualizationPaths {\n htmlPath: string;\n\n constructor(htmlPath: string) {\n this.htmlPath = htmlPath;\n }\n}\n\nexport class GraphVisualizer {\n private readonly names = new GraphNames();\n private readonly responsibilities = new ResponsibilitiesRenderer();\n\n /**\n * Fill color for an env set — the color of the first env in the set that has\n * a known color, else the default.\n */\n private frameworkColor(frameworks: string[]): string {\n for (const env of frameworks) {\n const color = FRAMEWORK_COLORS[env];\n if (color !== undefined) return color;\n }\n return DEFAULT_FRAMEWORK_COLOR;\n }\n\n /**\n * Role border styling — fill stays keyed on framework; the border shows a\n * project's ROLE at a glance. Server and client are the top-level runnable\n * nodes, so they get bold, colored borders to stand out:\n * server → thick GREEN border (a runnable server app)\n * client → thick RED border (a client app, e.g. angular)\n * designed-lib → bold border (a library with a generated @DocumentDesign design)\n * lib / other → plain thin border\n */\n private roleBorderAttrs(role: string): string {\n if (role === 'server') return ', color=\"green\", penwidth=3';\n if (role === 'client') return ', color=\"red\", penwidth=3';\n if (role === 'designed-lib') return ', penwidth=2';\n return '';\n }\n\n /**\n * Click-through href for a node: the project's committed design.html, made\n * relative to architecture/dependencies.html. Returns null when the project\n * has no generated DI design (no design.json → no clickable design page).\n */\n private designHtmlHref(designFile: string | undefined): string | null {\n if (!designFile) return null;\n const designHtml = designFile.replace(/design\\.json$/, 'design.html');\n return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);\n }\n\n /**\n * Generate Graphviz DOT format from the graph\n */\n generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n // Group projects by level\n const levels: Record<number, string[]> = {};\n for (const project of Object.keys(graph)) {\n const level = graph[project].level;\n if (!levels[level]) levels[level] = [];\n levels[level].push(project);\n }\n\n dot += this.dotNodes(graph);\n dot += '\\n';\n\n // Create same-rank subgraphs for each level\n for (const projects of Object.values(levels)) {\n dot += ` { rank=same; `;\n for (const p of projects) {\n dot += `\"${this.names.getShortName(p)}\"; `;\n }\n dot += '}\\n';\n }\n\n dot += '\\n';\n dot += this.dotEdges(graph);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n }\n\n // Node lines: fill colored by framework env set (libType), border shaped by\n // role; the label shows the env set + role (e.g. [browser, node] · server). A\n // node with a generated DI design also gets a URL so the rendered SVG box is\n // clickable — it opens that project's committed design.html in a new tab.\n private dotNodes(graph: EnhancedGraph): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n const shortName = this.names.getShortName(project);\n const frameworks = info.framework ?? [];\n const role = info.role ?? 'lib';\n const color = this.frameworkColor(frameworks);\n const border = this.roleBorderAttrs(role);\n const href = this.designHtmlHref(info.designFile);\n const link = href ? `, URL=\"${href}\", target=\"_blank\"` : '';\n const envSet = `[${frameworks.join(', ')}]`;\n const labelMeta = `L${info.level} · ${envSet} · ${role}`;\n dot += ` \"${shortName}\" [fillcolor=\"${color}\"${border}${link}, label=\"${shortName}\\\\n(${labelMeta})\"];\\n`;\n }\n return dot;\n }\n\n // Edge lines (dependencies).\n private dotEdges(graph: EnhancedGraph): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const shortName = this.names.getShortName(project);\n for (const dep of graph[project].dependsOn || []) {\n dot += ` \"${shortName}\" -> \"${this.names.getShortName(dep)}\";\\n`;\n }\n }\n return dot;\n }\n\n /**\n * Generate interactive HTML with embedded SVG using viz.js\n */\n generateHTML(\n dot: string,\n title: string = 'Monorepo Dependency Architecture',\n lockControl: string = '',\n responsibilitiesHtml: string = ''\n ): string {\n const styles = this.styles();\n const legend = this.legend();\n const script = this.script(dot);\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\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>${styles}</style>\n</head>\n<body>\n <h1>${title}</h1>\n <p class=\"hint\">💡 Click any box with a generated DI design to open its <strong>design.html</strong> (what the AI sees inside that project).</p>\n <p class=\"hint\">🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it (all the way up) <em>and</em> every dependency below it (all the way down), with all the boxes and lines between — while the rest of the graph dims so you can follow one box at a glance.</p>\n ${lockControl}\n ${legend}\n <div id=\"graph\"></div>\n ${responsibilitiesHtml}\n <script>${script}</script>\n</body>\n</html>`;\n }\n\n /**\n * The lock control (a single-select dropdown, rendered above the legend).\n * Picking a module LOCKS the graph into that box's hover view — its full\n * ancestor + descendant chain stays lit while everything else stays dimmed —\n * and narrows the responsibilities list below the graph to just that chain.\n * The first option, \"All\", is the default and clears the lock. Hover still\n * works on top of a lock; leaving a box returns to the locked view.\n *\n * Options are ordered by level DESCENDING to match the responsibilities cards.\n */\n lockControl(graph: EnhancedGraph): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n let options = '';\n for (const project of projects) {\n const shortName = this.names.getShortName(project);\n options += `<option value=\"${shortName}\">L${graph[project].level} · ${shortName}</option>`;\n }\n return `<div class=\"wp-lock-control\">\n <label for=\"wp-lock\">🔒 Lock a box (dim the rest &amp; filter responsibilities):</label>\n <select id=\"wp-lock\"><option value=\"\">All (no lock)</option>${options}</select>\n </div>`;\n }\n\n private styles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n /* viz.js renders node URLs as <a> — only boxes with a design.html get\n * one, so a:hover is a clickable-only signal. Make it pop clearly: a\n * thicker border PLUS a blue glow lift the box off the page, so it is\n * obvious which boxes you can click into vs. which you cannot. (A plain\n * stroke bump is invisible on server/client boxes, whose resting border\n * is already thick.) */\n #graph a { cursor: pointer; }\n #graph a polygon,\n #graph a ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n #graph a:hover polygon,\n #graph a:hover ellipse {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }\n /* Hover-highlight (wired up in JS after viz.js renders). Hovering a node\n * adds .wp-dim to the <svg> and .wp-focus/.wp-neighbor/.wp-hl to the\n * connected box, its neighbors, and its edges. We ONLY dim: the connected\n * subgraph keeps its exact normal look (full opacity), the rest recedes.\n * The un-dim rules repeat \"svg.wp-dim\" so they out-specify the dim rule\n * (which has an extra type selector) — else the subgraph stays dimmed. */\n #graph .node, #graph .edge { transition: opacity 0.12s ease; }\n #graph svg.wp-dim .node,\n #graph svg.wp-dim .edge { opacity: 0.15; }\n #graph svg.wp-dim .node.wp-focus,\n #graph svg.wp-dim .node.wp-neighbor,\n #graph svg.wp-dim .edge.wp-hl { opacity: 1; }\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 600px;\n padding: 15px;\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; }\n .legend-item { margin: 8px 0; }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n ${this.componentStyles()}\n `;\n }\n\n // Styles for the lock dropdown and the responsibilities card list below the\n // graph. Split out of styles() to keep each method within the line limit.\n private componentStyles(): string {\n return `\n .wp-lock-control {\n max-width: 600px;\n margin: 0 auto 16px;\n padding: 12px 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n text-align: center;\n }\n .wp-lock-control label { font-weight: bold; color: #333; margin-right: 8px; }\n .wp-lock-control select { font-size: 14px; padding: 4px 8px; }\n #wp-responsibilities { max-width: 900px; margin: 24px auto 0; }\n #wp-responsibilities h2 { color: #333; }\n .wp-resp-card {\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n margin: 10px 0;\n padding: 10px 15px;\n }\n .wp-resp-card > summary { cursor: pointer; color: #333; }\n .wp-resp-level {\n display: inline-block;\n min-width: 26px;\n padding: 1px 6px;\n margin-right: 6px;\n border-radius: 4px;\n background: #eef;\n font-size: 12px;\n font-weight: bold;\n text-align: center;\n }\n .wp-resp-body { margin-top: 8px; color: #444; }\n .wp-resp-body code {\n background: #f2f2f2;\n padding: 1px 4px;\n border-radius: 3px;\n font-family: monospace;\n }\n .wp-hidden { display: none; }`;\n }\n\n private legend(): string {\n return `<div class=\"legend\">\n <h2>Legend — fill = framework (libType), border = role</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FCE4EC;\"></span>\n <strong>angular:</strong> Angular front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>react:</strong> React front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #EDE7F6;\"></span>\n <strong>browser:</strong> browser front-end base env\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>express:</strong> Express / server side\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF9C4;\"></span>\n <strong>node:</strong> node server base env\n </div>\n <div class=\"legend-item\" style=\"margin-top: 12px;\">\n <span class=\"legend-box\" style=\"border: 3px solid green;\"></span>\n <strong>server:</strong> runnable server app (thick green border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 3px solid red;\"></span>\n <strong>client:</strong> client app, e.g. angular (thick red border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 2px solid #333;\"></span>\n <strong>designed-lib:</strong> library with a generated @DocumentDesign design (bold border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 1px solid #ccc;\"></span>\n <strong>lib:</strong> plain library, no generated design (thin border)\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Each node label shows its dependency level (L#), its framework env set (e.g. [browser, node]), and its role. Rows are laid out by level (top = no dependencies), with the deepest libraries at the bottom. Transitive dependencies are allowed but not shown.</em>\n </div>\n </div>`;\n }\n\n /**\n * The page script. The browser code lives in graph-visualizer.client.js (a\n * plain .js asset, NOT a TS template literal) so its dim/highlight/lock\n * functions can be ordinary browser functions — the TS lint rules that scan\n * .ts template strings would otherwise forbid them, and browser JS cannot\n * carry TS return annotations. We inline it and substitute the DOT.\n */\n private script(dot: string): string {\n const clientJs = fs.readFileSync(path.join(__dirname, 'graph-visualizer.client.js'), 'utf-8');\n return clientJs.split('__DOT__').join(JSON.stringify(dot));\n }\n\n /**\n * Write the committed architecture visualization to\n * architecture/dependencies.html, next to dependencies.json.\n *\n * This is a checked-in artifact, regenerated deterministically by\n * architecture:generate so the boxes stay clickable into each project's\n * design.html. The DOT is embedded in the HTML (rendered client-side by\n * viz.js). Output is deterministic (sorted graph in → same bytes out) so git\n * only shows a diff when the architecture actually changed.\n */\n writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'Monorepo Dependency Architecture'\n ): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const lockControl = this.lockControl(graph);\n const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);\n const html = this.generateHTML(this.generateDot(graph, title), title, lockControl, responsibilities);\n const htmlPath = path.join(outputDir, 'dependencies.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return new VisualizationPaths(htmlPath);\n }\n\n /**\n * Open the HTML visualization in the default browser\n */\n openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not open browser: ${error.message}`);\n return false;\n }\n }\n}\n"]}
@@ -66,7 +66,7 @@ function renderOne(file, workspaceRoot) {
66
66
  const paths = (0, design_visualizer_1.writeDesignVisualization)(graph, workspaceRoot);
67
67
  const designCount = graph.designs.length;
68
68
  console.log(`✅ ${file.project}: ${designCount} design(s) → ${paths.htmlPath}`);
69
- if (!(0, graph_visualizer_1.openVisualization)(paths.htmlPath)) {
69
+ if (!new graph_visualizer_1.GraphVisualizer().openVisualization(paths.htmlPath)) {
70
70
  console.log(`⚠️ Could not auto-open. Open manually: ${paths.htmlPath}`);
71
71
  }
72
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wp-design-visualize.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/scripts/wp-design-visualize.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * wp-design-visualize — open per-project DI design graphs as Graphviz HTML.\n *\n * Usage:\n * wp-design-visualize interactive picker (numbered list)\n * wp-design-visualize <name|substring>… render matching project(s)\n * wp-design-visualize all render every project\n *\n * Reads each project's committed design.json (schemaVersion 2 — one design\n * per controller/root) and writes tmp/webpieces/design-<project>.html with\n * one graph PER CONTROLLER, the controller (level 0) at the top. Replaces the\n * old mermaid-based scripts/design-visualize.sh (which only showed the first\n * controller).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as readline from 'readline';\nimport { CliExitError, runMain } from '@webpieces/rules-config';\nimport { DesignFileRef, findDesignFiles, resolveSelections } from '../lib/di-graph/design-finder';\nimport { writeDesignVisualization } from '../lib/di-graph/design-visualizer';\nimport { openVisualization } from '../lib/graph-visualizer';\nimport { DiGraph } from '../lib/di-graph/model';\n\n/** Walk up from cwd to the workspace root (dir containing nx.json). */\nfunction findWorkspaceRoot(): string {\n let dir = process.cwd();\n for (let i = 0; i < 20; i++) {\n if (fs.existsSync(path.join(dir, 'nx.json'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n}\n\nfunction printList(files: DesignFileRef[]): void {\n console.log('\\nDI design graphs found:\\n');\n files.forEach((file: DesignFileRef, index: number) => {\n console.log(` ${String(index + 1).padStart(3)}. ${file.project.padEnd(24)} ${file.relPath}`);\n });\n console.log('');\n}\n\n/** Prompt for selections: numbers, names/substrings, or \"all\" (blank = cancel). */\nfunction promptSelections(): Promise<string[]> {\n return new Promise((resolve: (selections: string[]) => void) => {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n rl.question('Which designs? (numbers / names / all, blank to cancel): ', (answer: string) => {\n rl.close();\n resolve(answer.split(/[\\s,]+/).filter((part: string) => part.length > 0));\n });\n });\n}\n\nfunction loadDiGraph(file: DesignFileRef): DiGraph {\n const parsed = JSON.parse(fs.readFileSync(file.absPath, 'utf-8'));\n if (!Array.isArray(parsed.designs)) {\n throw new Error(\n `${file.relPath} is an old-format design.json (schemaVersion ${parsed.schemaVersion ?? 1}). ` +\n `Regenerate it with: pnpm nx run ${file.project}:di-graph-generate`\n );\n }\n return parsed as DiGraph;\n}\n\nfunction renderOne(file: DesignFileRef, workspaceRoot: string): void {\n const graph = loadDiGraph(file);\n const paths = writeDesignVisualization(graph, workspaceRoot);\n const designCount = graph.designs.length;\n console.log(`✅ ${file.project}: ${designCount} design(s) → ${paths.htmlPath}`);\n if (!openVisualization(paths.htmlPath)) {\n console.log(`⚠️ Could not auto-open. Open manually: ${paths.htmlPath}`);\n }\n}\n\nasync function main(): Promise<void> {\n const workspaceRoot = findWorkspaceRoot();\n const files = findDesignFiles(workspaceRoot);\n\n if (files.length === 0) {\n throw new CliExitError(1, '❌ No design.json files found. Run a build or: pnpm nx run-many --target=di-graph-generate');\n }\n\n let selections = process.argv.slice(2);\n if (selections.length === 0) {\n printList(files);\n selections = await promptSelections();\n if (selections.length === 0) {\n console.log('Cancelled.');\n return;\n }\n }\n\n const picked = resolveSelections(selections, files);\n for (const file of picked) {\n renderOne(file, workspaceRoot);\n }\n}\n\nif (require.main === module) runMain(main);\n"]}
1
+ {"version":3,"file":"wp-design-visualize.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/scripts/wp-design-visualize.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * wp-design-visualize — open per-project DI design graphs as Graphviz HTML.\n *\n * Usage:\n * wp-design-visualize interactive picker (numbered list)\n * wp-design-visualize <name|substring>… render matching project(s)\n * wp-design-visualize all render every project\n *\n * Reads each project's committed design.json (schemaVersion 2 — one design\n * per controller/root) and writes tmp/webpieces/design-<project>.html with\n * one graph PER CONTROLLER, the controller (level 0) at the top. Replaces the\n * old mermaid-based scripts/design-visualize.sh (which only showed the first\n * controller).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as readline from 'readline';\nimport { CliExitError, runMain } from '@webpieces/rules-config';\nimport { DesignFileRef, findDesignFiles, resolveSelections } from '../lib/di-graph/design-finder';\nimport { writeDesignVisualization } from '../lib/di-graph/design-visualizer';\nimport { GraphVisualizer } from '../lib/graph-visualizer';\nimport { DiGraph } from '../lib/di-graph/model';\n\n/** Walk up from cwd to the workspace root (dir containing nx.json). */\nfunction findWorkspaceRoot(): string {\n let dir = process.cwd();\n for (let i = 0; i < 20; i++) {\n if (fs.existsSync(path.join(dir, 'nx.json'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n}\n\nfunction printList(files: DesignFileRef[]): void {\n console.log('\\nDI design graphs found:\\n');\n files.forEach((file: DesignFileRef, index: number) => {\n console.log(` ${String(index + 1).padStart(3)}. ${file.project.padEnd(24)} ${file.relPath}`);\n });\n console.log('');\n}\n\n/** Prompt for selections: numbers, names/substrings, or \"all\" (blank = cancel). */\nfunction promptSelections(): Promise<string[]> {\n return new Promise((resolve: (selections: string[]) => void) => {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n rl.question('Which designs? (numbers / names / all, blank to cancel): ', (answer: string) => {\n rl.close();\n resolve(answer.split(/[\\s,]+/).filter((part: string) => part.length > 0));\n });\n });\n}\n\nfunction loadDiGraph(file: DesignFileRef): DiGraph {\n const parsed = JSON.parse(fs.readFileSync(file.absPath, 'utf-8'));\n if (!Array.isArray(parsed.designs)) {\n throw new Error(\n `${file.relPath} is an old-format design.json (schemaVersion ${parsed.schemaVersion ?? 1}). ` +\n `Regenerate it with: pnpm nx run ${file.project}:di-graph-generate`\n );\n }\n return parsed as DiGraph;\n}\n\nfunction renderOne(file: DesignFileRef, workspaceRoot: string): void {\n const graph = loadDiGraph(file);\n const paths = writeDesignVisualization(graph, workspaceRoot);\n const designCount = graph.designs.length;\n console.log(`✅ ${file.project}: ${designCount} design(s) → ${paths.htmlPath}`);\n if (!new GraphVisualizer().openVisualization(paths.htmlPath)) {\n console.log(`⚠️ Could not auto-open. Open manually: ${paths.htmlPath}`);\n }\n}\n\nasync function main(): Promise<void> {\n const workspaceRoot = findWorkspaceRoot();\n const files = findDesignFiles(workspaceRoot);\n\n if (files.length === 0) {\n throw new CliExitError(1, '❌ No design.json files found. Run a build or: pnpm nx run-many --target=di-graph-generate');\n }\n\n let selections = process.argv.slice(2);\n if (selections.length === 0) {\n printList(files);\n selections = await promptSelections();\n if (selections.length === 0) {\n console.log('Cancelled.');\n return;\n }\n }\n\n const picked = resolveSelections(selections, files);\n for (const file of picked) {\n renderOne(file, workspaceRoot);\n }\n}\n\nif (require.main === module) runMain(main);\n"]}