@webpieces/nx-webpieces-rules 0.4.873 → 0.4.875

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.
Files changed (35) hide show
  1. package/README.md +31 -18
  2. package/package.json +7 -7
  3. package/src/executors/visualize/executor.d.ts +17 -3
  4. package/src/executors/visualize/executor.js +53 -33
  5. package/src/executors/visualize/executor.js.map +1 -1
  6. package/src/lib/graph-color-modes.d.ts +56 -34
  7. package/src/lib/graph-color-modes.js +111 -100
  8. package/src/lib/graph-color-modes.js.map +1 -1
  9. package/src/lib/graph-filter.client.d.ts +13 -2
  10. package/src/lib/graph-filter.client.js +27 -7
  11. package/src/lib/graph-filter.client.js.map +1 -1
  12. package/src/lib/graph-impact.d.ts +37 -3
  13. package/src/lib/graph-impact.js +42 -11
  14. package/src/lib/graph-impact.js.map +1 -1
  15. package/src/lib/graph-legend.js +1 -1
  16. package/src/lib/graph-legend.js.map +1 -1
  17. package/src/lib/graph-level-bands.d.ts +8 -0
  18. package/src/lib/graph-level-bands.js +13 -1
  19. package/src/lib/graph-level-bands.js.map +1 -1
  20. package/src/lib/graph-node-menu.js +10 -108
  21. package/src/lib/graph-node-menu.js.map +1 -1
  22. package/src/lib/graph-page-shell.d.ts +31 -1
  23. package/src/lib/graph-page-shell.js +75 -7
  24. package/src/lib/graph-page-shell.js.map +1 -1
  25. package/src/lib/graph-page-styles.d.ts +14 -3
  26. package/src/lib/graph-page-styles.js +89 -18
  27. package/src/lib/graph-page-styles.js.map +1 -1
  28. package/src/lib/graph-render-model.d.ts +14 -6
  29. package/src/lib/graph-render-model.js +22 -8
  30. package/src/lib/graph-render-model.js.map +1 -1
  31. package/src/lib/graph-visualizer.client.d.ts +82 -5
  32. package/src/lib/graph-visualizer.client.js +351 -23
  33. package/src/lib/graph-visualizer.client.js.map +1 -1
  34. package/src/lib/graph-visualizer.js +1 -1
  35. package/src/lib/graph-visualizer.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n /** `nodeDot` picks each box's statement — the architecture page passes its current color mode's. */\n render(\n retained: Set<string>,\n nodeDot: (node: RenderNodeJson) => string = (node: RenderNodeJson): string => node.dot,\n ): string {\n const nodes = this.model.nodes.filter((node: RenderNodeJson): boolean => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge) => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band) =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id) => retained.has(id)),\n ),\n )\n .filter((band) => band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node: RenderNodeJson): string => nodeDot(node)).join('') +\n this.bandDot(bands) +\n edges.map((edge) => edge.dot).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n dot += ` \"${anchor}\" ${nodeAttrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void {\n return (): void => {};\n }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n /** The statement drawn for one box. The architecture page answers with its color mode's. */\n protected nodeDot(node: RenderNodeJson): string {\n return node.dot;\n }\n /** Whether an unfiltered draw may use the model's precomputed `fullDot` verbatim. */\n protected usesFullDot(): boolean {\n return true;\n }\n\n /**\n * Draw again with the current filter — after a color-mode switch. A control outside the graph\n * (a drawer button) keeps keyboard focus; the redraw would otherwise move it into the graph.\n */\n redraw(): void {\n const active = document.activeElement;\n const host = document.getElementById('graph');\n const outside = active instanceof HTMLElement && host !== null && !host.contains(active);\n this.filter(this.anchor);\n if (outside && active.isConnected) active.focus({ preventScroll: true });\n }\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained =\n anchor === null\n ? new Set(this.model.nodes.map((node) => node.id))\n : this.chain.nodes(anchor);\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const focusName = WpNodeMenu.focusedName(previousSvg);\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null && this.usesFullDot()\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(retained, (node: RenderNodeJson): string =>\n this.nodeDot(node),\n );\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n WpNodeMenu.restoreFocus(svg, focusName);\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n if (previousSvg !== null) WpNodeMenu.restoreFocus(previousSvg, focusName);\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n /**\n * `nodeDot` picks each box's statement — the architecture page passes its current color mode's.\n * `keepEmptyLevels` keeps a level whose boxes were all filtered out as a thin labeled band, so\n * every remaining box stays in its own L-row (the architecture page); otherwise it is dropped.\n */\n render(\n retained: Set<string>,\n nodeDot: (node: RenderNodeJson) => string = (node: RenderNodeJson): string => node.dot,\n keepEmptyLevels = false,\n ): string {\n const nodes = this.model.nodes.filter((node: RenderNodeJson): boolean => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge: RenderEdgeJson): boolean => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band: RenderBandJson): WpRenderBand =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id: string): boolean => retained.has(id)),\n ),\n )\n .filter((band: WpRenderBand): boolean => keepEmptyLevels || band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node: RenderNodeJson): string => nodeDot(node)).join('') +\n this.bandDot(bands) +\n edges.map((edge) => edge.dot).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n // The same bytes as LevelBandLayout.emptyBandAttrs (graph-level-bands.ts).\n const attrs =\n band.nodeNames.length > 0\n ? nodeAttrs\n : '[shape=plaintext, style=\"\", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor=\"#9a9eab\", ' +\n `label=\"L${band.level} · no matching projects\", class=\"wp-layout wp-empty-level\"]`;\n dot += ` \"${anchor}\" ${attrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void {\n return (): void => {};\n }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n /** The statement drawn for one box. The architecture page answers with its color mode's. */\n protected nodeDot(node: RenderNodeJson): string {\n return node.dot;\n }\n /** Whether an unfiltered draw may use the model's precomputed `fullDot` verbatim. */\n protected usesFullDot(): boolean {\n return true;\n }\n /**\n * The page's own filter on top of \"Hide unconnected\": the architecture page intersects the set\n * with its Filter popover (changes, runtime, role). Everything, by default.\n */\n protected narrow(retained: Set<string>): Set<string> {\n return retained;\n }\n /** Whether a level emptied by filtering stays as a thin labeled band (see WpFilteredDot.render). */\n protected keepsEmptyLevels(): boolean {\n return false;\n }\n\n /**\n * Draw again with the current filter — after a color-mode switch. A control outside the graph\n * (a drawer button) keeps keyboard focus; the redraw would otherwise move it into the graph.\n */\n redraw(): void {\n const active = document.activeElement;\n const host = document.getElementById('graph');\n const outside = active instanceof HTMLElement && host !== null && !host.contains(active);\n this.filter(this.anchor);\n if (outside && active.isConnected) active.focus({ preventScroll: true });\n }\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained = this.narrow(\n anchor === null\n ? new Set(this.model.nodes.map((node: RenderNodeJson): string => node.id))\n : this.chain.nodes(anchor),\n );\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const focusName = WpNodeMenu.focusedName(previousSvg);\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null && this.usesFullDot()\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(\n retained,\n (node: RenderNodeJson): string => this.nodeDot(node),\n this.keepsEmptyLevels(),\n );\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n WpNodeMenu.restoreFocus(svg, focusName);\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n if (previousSvg !== null) WpNodeMenu.restoreFocus(previousSvg, focusName);\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
@@ -29,6 +29,10 @@
29
29
  * directory and everything in it, so a consumer repo needs no gitignore edit and the PR gate's
30
30
  * "build left nothing uncommitted" check never sees it.
31
31
  *
32
+ * Both `architecture:generate` and `architecture:visualize` refresh it: the answer changes with the
33
+ * branch, not with the architecture, so viewing the SAVED graph re-scans Impact against it without
34
+ * regenerating anything else (#1158).
35
+ *
32
36
  * Impact is a convenience. When nx or git cannot answer, NO sidecar is written (a stale one is
33
37
  * removed) and the page disables Impact with a one-line reason. It never fails generation.
34
38
  */
@@ -55,6 +59,18 @@ export declare class ImpactReport {
55
59
  */
56
60
  readonly buildInputs: string[];
57
61
  readonly changedFiles: number;
62
+ /**
63
+ * Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the
64
+ * Filter popover's "Changed + what they use". ("Changed + what uses them" is touched +
65
+ * affected, which nx already computed as the dependents whose tests re-run.)
66
+ */
67
+ readonly dependencies: string[];
68
+ /**
69
+ * Changed files no project owns (repo-relative, sorted): workspace-global inputs such as
70
+ * pnpm-lock.yaml. When nothing is touched they are WHY everything is affected, and the
71
+ * legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
72
+ */
73
+ readonly globalFiles: string[];
58
74
  constructor(available: boolean, reason: string, base: string,
59
75
  /** Affected AND owning a changed file (node ids). */
60
76
  touched: string[],
@@ -66,7 +82,19 @@ export declare class ImpactReport {
66
82
  * restores them from cache when it has them and compiles them otherwise. Which of the two
67
83
  * happens is not knowable here, so the page claims neither.
68
84
  */
69
- buildInputs: string[], changedFiles: number);
85
+ buildInputs: string[], changedFiles: number,
86
+ /**
87
+ * Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the
88
+ * Filter popover's "Changed + what they use". ("Changed + what uses them" is touched +
89
+ * affected, which nx already computed as the dependents whose tests re-run.)
90
+ */
91
+ dependencies: string[],
92
+ /**
93
+ * Changed files no project owns (repo-relative, sorted): workspace-global inputs such as
94
+ * pnpm-lock.yaml. When nothing is touched they are WHY everything is affected, and the
95
+ * legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
96
+ */
97
+ globalFiles: string[]);
70
98
  }
71
99
  /** A scan that could not produce an answer: no sidecar is written, only this reason printed. */
72
100
  export declare class ImpactUnavailable {
@@ -91,8 +119,14 @@ export declare class NxImpactScanner {
91
119
  private readonly names;
92
120
  constructor(diffScope?: DiffScope, nx?: NxCli, ownership?: ProjectFileOwners);
93
121
  scan(workspaceRoot: string, graph: EnhancedGraph): Promise<ImpactReport | ImpactUnavailable>;
94
- /** A pure downward walk over the graph's own dependsOn edges: no nx call. */
95
- buildInputs(affected: string[], graph: EnhancedGraph): string[];
122
+ /** Changed files no project owns; none are claimed when ownership itself was unavailable. */
123
+ globalFiles(files: string[], owners: Map<string, string>): string[];
124
+ /**
125
+ * Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over
126
+ * the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of
127
+ * the touched set, "what they use".
128
+ */
129
+ buildInputs(roots: string[], graph: EnhancedGraph): string[];
96
130
  /** Affected projects that own one of the changed files, by nx's own ownership. */
97
131
  touched(affected: string[], files: string[], owners: Map<string, string>): string[];
98
132
  /** The JSON list, narrowed to projects the graph draws; null when it is not a list of names. */
@@ -30,6 +30,10 @@
30
30
  * directory and everything in it, so a consumer repo needs no gitignore edit and the PR gate's
31
31
  * "build left nothing uncommitted" check never sees it.
32
32
  *
33
+ * Both `architecture:generate` and `architecture:visualize` refresh it: the answer changes with the
34
+ * branch, not with the architecture, so viewing the SAVED graph re-scans Impact against it without
35
+ * regenerating anything else (#1158).
36
+ *
33
37
  * Impact is a convenience. When nx or git cannot answer, NO sidecar is written (a stale one is
34
38
  * removed) and the page disables Impact with a one-line reason. It never fails generation.
35
39
  */
@@ -58,6 +62,8 @@ class ImpactReport {
58
62
  affected;
59
63
  buildInputs;
60
64
  changedFiles;
65
+ dependencies;
66
+ globalFiles;
61
67
  constructor(available, reason, base,
62
68
  /** Affected AND owning a changed file (node ids). */
63
69
  touched,
@@ -69,7 +75,19 @@ class ImpactReport {
69
75
  * restores them from cache when it has them and compiles them otherwise. Which of the two
70
76
  * happens is not knowable here, so the page claims neither.
71
77
  */
72
- buildInputs, changedFiles) {
78
+ buildInputs, changedFiles,
79
+ /**
80
+ * Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the
81
+ * Filter popover's "Changed + what they use". ("Changed + what uses them" is touched +
82
+ * affected, which nx already computed as the dependents whose tests re-run.)
83
+ */
84
+ dependencies,
85
+ /**
86
+ * Changed files no project owns (repo-relative, sorted): workspace-global inputs such as
87
+ * pnpm-lock.yaml. When nothing is touched they are WHY everything is affected, and the
88
+ * legend names them. Empty when nx's file ownership was unavailable — then nothing is known.
89
+ */
90
+ globalFiles) {
73
91
  this.available = available;
74
92
  this.reason = reason;
75
93
  this.base = base;
@@ -77,6 +95,8 @@ class ImpactReport {
77
95
  this.affected = affected;
78
96
  this.buildInputs = buildInputs;
79
97
  this.changedFiles = changedFiles;
98
+ this.dependencies = dependencies;
99
+ this.globalFiles = globalFiles;
80
100
  }
81
101
  }
82
102
  exports.ImpactReport = ImpactReport;
@@ -159,17 +179,28 @@ class NxImpactScanner {
159
179
  const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);
160
180
  const short = base.slice(0, 7);
161
181
  if (affectedSet.length === 0)
162
- return new ImpactReport(false, `nothing affected since the fork point ${short}`, short, [], [], [], files.length);
163
- const touched = this.touched(affectedSet, files, await this.owners());
182
+ return new ImpactReport(false, `nothing affected since the fork point ${short}`, short, [], [], [], files.length, [], []);
183
+ const owners = await this.owners();
184
+ const touched = this.touched(affectedSet, files, owners);
164
185
  const rest = affectedSet.filter((project) => !touched.includes(project));
165
186
  const inputs = this.buildInputs(affectedSet, graph);
166
- return new ImpactReport(true, '', short, this.ids(touched), this.ids(rest), this.ids(inputs), files.length);
187
+ return new ImpactReport(true, '', short, this.ids(touched), this.ids(rest), this.ids(inputs), files.length, this.ids(this.buildInputs(touched, graph)), this.globalFiles(files, owners));
188
+ }
189
+ /** Changed files no project owns; none are claimed when ownership itself was unavailable. */
190
+ globalFiles(files, owners) {
191
+ if (owners.size === 0)
192
+ return [];
193
+ return files.filter((file) => !owners.has(file)).sort();
167
194
  }
168
- /** A pure downward walk over the graph's own dependsOn edges: no nx call. */
169
- buildInputs(affected, graph) {
170
- const inSet = new Set(affected);
171
- const seen = new Set(affected);
172
- const stack = [...affected];
195
+ /**
196
+ * Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over
197
+ * the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of
198
+ * the touched set, "what they use".
199
+ */
200
+ buildInputs(roots, graph) {
201
+ const inSet = new Set(roots);
202
+ const seen = new Set(roots);
203
+ const stack = [...roots];
173
204
  const inputs = [];
174
205
  while (stack.length > 0) {
175
206
  const project = stack.pop();
@@ -237,7 +268,7 @@ class ImpactSidecar {
237
268
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; a write failure must never fail generation
238
269
  try {
239
270
  fs.mkdirSync(dir, { recursive: true });
240
- fs.writeFileSync(path.join(dir, '.gitignore'), '# Written by architecture:generate. Per-branch data: never commit it.\n*\n', 'utf-8');
271
+ fs.writeFileSync(path.join(dir, '.gitignore'), '# Written by architecture:generate and architecture:visualize. Per-branch data: never commit it.\n*\n', 'utf-8');
241
272
  const file = path.join(dir, exports.IMPACT_SIDECAR_FILE);
242
273
  fs.writeFileSync(file, this.script(report), 'utf-8');
243
274
  return file;
@@ -254,7 +285,7 @@ class ImpactSidecar {
254
285
  }
255
286
  script(report) {
256
287
  const json = JSON.stringify(report).replace(/</g, '\\u003c');
257
- return `// Generated by architecture:generate for one branch. Gitignored; do not commit.\nwindow.__WP_IMPACT__ = ${json};\n`;
288
+ return `// Generated by architecture:generate or architecture:visualize for one branch. Gitignored; do not commit.\nwindow.__WP_IMPACT__ = ${json};\n`;
258
289
  }
259
290
  }
260
291
  exports.ImpactSidecar = ImpactSidecar;
@@ -1 +1 @@
1
- {"version":3,"file":"graph-impact.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-impact.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Impact mode's data: what THIS BRANCH's build looks like, in three shades (plus untouched grey).\n *\n * - BUILD INPUT is every transitive dependency of the affected set that is not itself affected:\n * `ci` dependsOn `^build`, so they must exist as build output though their inputs did not change.\n * nx restores them from cache when it has them and compiles them otherwise — which one happens is\n * not knowable here, so nothing claims it. A pure downward walk over the graph's own edges.\n *\n * - AFFECTED is `nx show projects --affected --base=<fork point> --json`, with NO `--head`, so\n * uncommitted and untracked work counts. nx already gets right everything a hand-rolled diff gets\n * wrong: lockfile and package.json changes resolved per project, root/global inputs, namedInputs,\n * implicitDependencies, nested roots and deleted files. This uses its public CLI surface only, never\n * nx internals.\n * - TOUCHED is the subset of that set which OWNS a changed file, by nx's own file-to-project\n * ownership (the project file map from `createProjectFileMapUsingProjectGraph`, public devkit API)\n * — never a path-prefix guess. A project whose only change is a DELETED file is not in that map\n * (the file is gone), so it shows as affected rather than touched.\n *\n * The base is the repo's existing merge-base computation (`DiffScope.detectBase` in rules-config:\n * `git merge-base HEAD origin/main`, falling back to `main`) — never NX_BASE/NX_HEAD, whose value\n * depends on whoever launched the process. The changed-file list is `DiffScope.getChangedFiles`, the\n * same base→working-tree diff (with untracked files) every code rule uses.\n *\n * The answer changes per branch, so it never goes into the committed dependencies.html or\n * dependencies.json (both stay byte-stable across branches). It goes into a SIDECAR,\n * `architecture/.impact/dependencies.impact.js`: a plain script setting one window global, loaded\n * with `<script src>` — the one way a page opened straight from disk (file://) can read a\n * neighbouring file. The directory carries its own `.gitignore` containing `*`, which ignores the\n * directory and everything in it, so a consumer repo needs no gitignore edit and the PR gate's\n * \"build left nothing uncommitted\" check never sees it.\n *\n * Impact is a convenience. When nx or git cannot answer, NO sidecar is written (a stale one is\n * removed) and the page disables Impact with a one-line reason. It never fails generation.\n */\n\nimport { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectFileMapUsingProjectGraph, createProjectGraphAsync } from '@nx/devkit';\nimport { DiffScope, ChangedFilesOptions } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { GraphNames } from './graph-names';\nimport { toError } from '../toError';\n\n/** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */\nexport const IMPACT_SIDECAR_DIR = '.impact';\nexport const IMPACT_SIDECAR_FILE = 'dependencies.impact.js';\nexport const IMPACT_SIDECAR_SRC = `${IMPACT_SIDECAR_DIR}/${IMPACT_SIDECAR_FILE}`;\n/** The nx call is capped; a slow or wedged nx disables Impact instead of stalling generation. */\nconst NX_TIMEOUT_MS = 15000;\n\n/** What the sidecar carries; the page reads it as `window.__WP_IMPACT__`. */\nexport class ImpactReport {\n constructor(\n public readonly available: boolean,\n public readonly reason: string,\n public readonly base: string,\n /** Affected AND owning a changed file (node ids). */\n public readonly touched: string[],\n /** Affected but owning no changed file (node ids): their ci re-runs. */\n public readonly affected: string[],\n /**\n * Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn\n * `^build`, so they must exist as build output although their inputs did not change: nx\n * restores them from cache when it has them and compiles them otherwise. Which of the two\n * happens is not knowable here, so the page claims neither.\n */\n public readonly buildInputs: string[],\n public readonly changedFiles: number,\n ) {}\n}\n\n/** A scan that could not produce an answer: no sidecar is written, only this reason printed. */\nexport class ImpactUnavailable {\n constructor(public readonly reason: string) {}\n}\n\n/** Runs the workspace's own nx CLI. Injected so specs never spawn nx. */\nexport class NxCli {\n /** stdout, or null when nx cannot be found, exits non-zero or exceeds the timeout. */\n run(workspaceRoot: string, args: string[], timeoutMs: number): string | null {\n const bin = this.binary(workspaceRoot);\n if (bin === null) return null;\n const result = spawnSync(process.execPath, [bin, ...args], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n timeout: timeoutMs,\n stdio: 'pipe',\n maxBuffer: 16 * 1024 * 1024,\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n\n /** Resolved the way node would from the workspace, so a linked worktree finds its main tree's nx. */\n private binary(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a workspace without nx resolves nothing; that is an answer, not an error\n try {\n return require.resolve('nx/bin/nx.js', { paths: [workspaceRoot] });\n } catch (err: unknown) {\n const error = toError(err);\n void error; // no nx installed here — Impact is simply unavailable\n return null;\n }\n }\n}\n\n/** nx's own file → project ownership, from the public devkit file map. Injected for specs. */\nexport class ProjectFileOwners {\n async owners(): Promise<Map<string, string>> {\n const fileMap = await createProjectFileMapUsingProjectGraph(await createProjectGraphAsync());\n const owners = new Map<string, string>();\n for (const project of Object.keys(fileMap)) {\n for (const data of fileMap[project]) owners.set(data.file, project);\n }\n return owners;\n }\n}\n\nexport class NxImpactScanner {\n private readonly names = new GraphNames();\n\n constructor(\n private readonly diffScope: DiffScope = new DiffScope(),\n private readonly nx: NxCli = new NxCli(),\n private readonly ownership: ProjectFileOwners = new ProjectFileOwners(),\n ) {}\n\n async scan(workspaceRoot: string, graph: EnhancedGraph): Promise<ImpactReport | ImpactUnavailable> {\n const base = this.diffScope.detectBase(workspaceRoot);\n if (base === null) return new ImpactUnavailable('no fork point: neither origin/main nor main resolves (or git is unavailable)');\n const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);\n if (json === null) return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`);\n const affectedSet = this.parse(json, graph);\n if (affectedSet === null) return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list');\n const options = new ChangedFilesOptions();\n options.tsOnly = false;\n options.includeDeletions = true;\n const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);\n const short = base.slice(0, 7);\n if (affectedSet.length === 0)\n return new ImpactReport(false, `nothing affected since the fork point ${short}`, short, [], [], [], files.length);\n const touched = this.touched(affectedSet, files, await this.owners());\n const rest = affectedSet.filter((project: string): boolean => !touched.includes(project));\n const inputs = this.buildInputs(affectedSet, graph);\n return new ImpactReport(true, '', short, this.ids(touched), this.ids(rest), this.ids(inputs), files.length);\n }\n\n /** A pure downward walk over the graph's own dependsOn edges: no nx call. */\n buildInputs(affected: string[], graph: EnhancedGraph): string[] {\n const inSet = new Set<string>(affected);\n const seen = new Set<string>(affected);\n const stack = [...affected];\n const inputs: string[] = [];\n while (stack.length > 0) {\n const project = stack.pop() as string;\n for (const dep of graph[project]?.dependsOn ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n stack.push(dep);\n if (!inSet.has(dep) && graph[dep] !== undefined) inputs.push(dep);\n }\n }\n return inputs.sort();\n }\n\n /** Affected projects that own one of the changed files, by nx's own ownership. */\n touched(affected: string[], files: string[], owners: Map<string, string>): string[] {\n const owning = new Set<string>();\n for (const file of files) {\n const owner = owners.get(file);\n if (owner !== undefined) owning.add(owner);\n }\n return affected.filter((project: string): boolean => owning.has(project));\n }\n\n /** The JSON list, narrowed to projects the graph draws; null when it is not a list of names. */\n parse(json: string, graph: EnhancedGraph): string[] | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- untrusted CLI output; anything unparseable is reported as a reason\n try {\n // webpieces-disable no-any-unknown -- CLI JSON is untrusted until narrowed below\n const parsed: unknown = JSON.parse(json);\n if (!Array.isArray(parsed)) return null;\n return parsed\n .filter((name: unknown): name is string => typeof name === 'string' && graph[name] !== undefined)\n .sort();\n } catch (err: unknown) {\n const error = toError(err);\n void error; // not JSON — the caller reports it\n return null;\n }\n }\n\n /** Ownership failing is not fatal: the set then shows in one shade, all \"affected\". */\n private async owners(): Promise<Map<string, string>> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the second shade is optional\n try {\n return await this.ownership.owners();\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Impact: nx file ownership unavailable (${error.message}); showing one shade`);\n return new Map<string, string>();\n }\n }\n\n private ids(projects: string[]): string[] {\n return projects.map((project: string): string => this.names.getNodeId(project));\n }\n}\n\n/** Writes (or removes) the sidecar, self-ignored, beside architecture/dependencies.html. */\nexport class ImpactSidecar {\n /** Never throws: a sidecar that cannot be written just leaves Impact disabled on the page. */\n write(architectureDir: string, report: ImpactReport): string | null {\n const dir = path.join(architectureDir, IMPACT_SIDECAR_DIR);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; a write failure must never fail generation\n try {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(\n path.join(dir, '.gitignore'),\n '# Written by architecture:generate. Per-branch data: never commit it.\\n*\\n',\n 'utf-8',\n );\n const file = path.join(dir, IMPACT_SIDECAR_FILE);\n fs.writeFileSync(file, this.script(report), 'utf-8');\n return file;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not write the impact sidecar: ${error.message}`);\n return null;\n }\n }\n\n /** A stale sidecar from an earlier branch must never be shown as this branch's impact. */\n remove(architectureDir: string): void {\n fs.rmSync(path.join(architectureDir, IMPACT_SIDECAR_DIR, IMPACT_SIDECAR_FILE), { force: true });\n }\n\n script(report: ImpactReport): string {\n const json = JSON.stringify(report).replace(/</g, '\\\\u003c');\n return `// Generated by architecture:generate for one branch. Gitignored; do not commit.\\nwindow.__WP_IMPACT__ = ${json};\\n`;\n }\n}\n\n/** The generator's one call: scan, write, and report the result and its cost in one line. */\nexport class ImpactRefresh {\n constructor(\n private readonly scanner: NxImpactScanner = new NxImpactScanner(),\n private readonly sidecar: ImpactSidecar = new ImpactSidecar(),\n ) {}\n\n /** Never throws — Impact is optional, generation is not. */\n async run(architectureDir: string, workspaceRoot: string, graph: EnhancedGraph): Promise<string> {\n const started = Date.now();\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; any failure becomes a reason, never a failed generation\n try {\n const report = await this.scanner.scan(workspaceRoot, graph);\n const cost = `${Date.now() - started} ms`;\n if (report instanceof ImpactUnavailable) {\n this.sidecar.remove(architectureDir);\n return `ℹ️ Impact disabled, no sidecar written: ${report.reason} (${cost})`;\n }\n const file = this.sidecar.write(architectureDir, report);\n if (file === null) return `⚠️ Impact sidecar not written (${cost})`;\n if (!report.available) return `ℹ️ Impact disabled on the page: ${report.reason} (${cost})`;\n return (\n `✅ Impact vs ${report.base}: ${report.touched.length} touched · ${report.affected.length} affected · ` +\n `${report.buildInputs.length} build inputs, from ${report.changedFiles} changed file(s) (${cost}) → ${file}`\n );\n } catch (err: unknown) {\n const error = toError(err);\n return `⚠️ Impact skipped: ${error.message}`;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"graph-impact.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-impact.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Impact mode's data: what THIS BRANCH's build looks like, in three shades (plus untouched grey).\n *\n * - BUILD INPUT is every transitive dependency of the affected set that is not itself affected:\n * `ci` dependsOn `^build`, so they must exist as build output though their inputs did not change.\n * nx restores them from cache when it has them and compiles them otherwise — which one happens is\n * not knowable here, so nothing claims it. A pure downward walk over the graph's own edges.\n *\n * - AFFECTED is `nx show projects --affected --base=<fork point> --json`, with NO `--head`, so\n * uncommitted and untracked work counts. nx already gets right everything a hand-rolled diff gets\n * wrong: lockfile and package.json changes resolved per project, root/global inputs, namedInputs,\n * implicitDependencies, nested roots and deleted files. This uses its public CLI surface only, never\n * nx internals.\n * - TOUCHED is the subset of that set which OWNS a changed file, by nx's own file-to-project\n * ownership (the project file map from `createProjectFileMapUsingProjectGraph`, public devkit API)\n * — never a path-prefix guess. A project whose only change is a DELETED file is not in that map\n * (the file is gone), so it shows as affected rather than touched.\n *\n * The base is the repo's existing merge-base computation (`DiffScope.detectBase` in rules-config:\n * `git merge-base HEAD origin/main`, falling back to `main`) — never NX_BASE/NX_HEAD, whose value\n * depends on whoever launched the process. The changed-file list is `DiffScope.getChangedFiles`, the\n * same base→working-tree diff (with untracked files) every code rule uses.\n *\n * The answer changes per branch, so it never goes into the committed dependencies.html or\n * dependencies.json (both stay byte-stable across branches). It goes into a SIDECAR,\n * `architecture/.impact/dependencies.impact.js`: a plain script setting one window global, loaded\n * with `<script src>` — the one way a page opened straight from disk (file://) can read a\n * neighbouring file. The directory carries its own `.gitignore` containing `*`, which ignores the\n * directory and everything in it, so a consumer repo needs no gitignore edit and the PR gate's\n * \"build left nothing uncommitted\" check never sees it.\n *\n * Both `architecture:generate` and `architecture:visualize` refresh it: the answer changes with the\n * branch, not with the architecture, so viewing the SAVED graph re-scans Impact against it without\n * regenerating anything else (#1158).\n *\n * Impact is a convenience. When nx or git cannot answer, NO sidecar is written (a stale one is\n * removed) and the page disables Impact with a one-line reason. It never fails generation.\n */\n\nimport { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectFileMapUsingProjectGraph, createProjectGraphAsync } from '@nx/devkit';\nimport { DiffScope, ChangedFilesOptions } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { GraphNames } from './graph-names';\nimport { toError } from '../toError';\n\n/** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */\nexport const IMPACT_SIDECAR_DIR = '.impact';\nexport const IMPACT_SIDECAR_FILE = 'dependencies.impact.js';\nexport const IMPACT_SIDECAR_SRC = `${IMPACT_SIDECAR_DIR}/${IMPACT_SIDECAR_FILE}`;\n/** The nx call is capped; a slow or wedged nx disables Impact instead of stalling generation. */\nconst NX_TIMEOUT_MS = 15000;\n\n/** What the sidecar carries; the page reads it as `window.__WP_IMPACT__`. */\nexport class ImpactReport {\n constructor(\n public readonly available: boolean,\n public readonly reason: string,\n public readonly base: string,\n /** Affected AND owning a changed file (node ids). */\n public readonly touched: string[],\n /** Affected but owning no changed file (node ids): their ci re-runs. */\n public readonly affected: string[],\n /**\n * Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn\n * `^build`, so they must exist as build output although their inputs did not change: nx\n * restores them from cache when it has them and compiles them otherwise. Which of the two\n * happens is not knowable here, so the page claims neither.\n */\n public readonly buildInputs: string[],\n public readonly changedFiles: number,\n /**\n * Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the\n * Filter popover's \"Changed + what they use\". (\"Changed + what uses them\" is touched +\n * affected, which nx already computed as the dependents whose tests re-run.)\n */\n public readonly dependencies: string[],\n /**\n * Changed files no project owns (repo-relative, sorted): workspace-global inputs such as\n * pnpm-lock.yaml. When nothing is touched they are WHY everything is affected, and the\n * legend names them. Empty when nx's file ownership was unavailable — then nothing is known.\n */\n public readonly globalFiles: string[],\n ) {}\n}\n\n/** A scan that could not produce an answer: no sidecar is written, only this reason printed. */\nexport class ImpactUnavailable {\n constructor(public readonly reason: string) {}\n}\n\n/** Runs the workspace's own nx CLI. Injected so specs never spawn nx. */\nexport class NxCli {\n /** stdout, or null when nx cannot be found, exits non-zero or exceeds the timeout. */\n run(workspaceRoot: string, args: string[], timeoutMs: number): string | null {\n const bin = this.binary(workspaceRoot);\n if (bin === null) return null;\n const result = spawnSync(process.execPath, [bin, ...args], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n timeout: timeoutMs,\n stdio: 'pipe',\n maxBuffer: 16 * 1024 * 1024,\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n\n /** Resolved the way node would from the workspace, so a linked worktree finds its main tree's nx. */\n private binary(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a workspace without nx resolves nothing; that is an answer, not an error\n try {\n return require.resolve('nx/bin/nx.js', { paths: [workspaceRoot] });\n } catch (err: unknown) {\n const error = toError(err);\n void error; // no nx installed here — Impact is simply unavailable\n return null;\n }\n }\n}\n\n/** nx's own file → project ownership, from the public devkit file map. Injected for specs. */\nexport class ProjectFileOwners {\n async owners(): Promise<Map<string, string>> {\n const fileMap = await createProjectFileMapUsingProjectGraph(await createProjectGraphAsync());\n const owners = new Map<string, string>();\n for (const project of Object.keys(fileMap)) {\n for (const data of fileMap[project]) owners.set(data.file, project);\n }\n return owners;\n }\n}\n\nexport class NxImpactScanner {\n private readonly names = new GraphNames();\n\n constructor(\n private readonly diffScope: DiffScope = new DiffScope(),\n private readonly nx: NxCli = new NxCli(),\n private readonly ownership: ProjectFileOwners = new ProjectFileOwners(),\n ) {}\n\n async scan(workspaceRoot: string, graph: EnhancedGraph): Promise<ImpactReport | ImpactUnavailable> {\n const base = this.diffScope.detectBase(workspaceRoot);\n if (base === null) return new ImpactUnavailable('no fork point: neither origin/main nor main resolves (or git is unavailable)');\n const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);\n if (json === null) return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`);\n const affectedSet = this.parse(json, graph);\n if (affectedSet === null) return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list');\n const options = new ChangedFilesOptions();\n options.tsOnly = false;\n options.includeDeletions = true;\n const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);\n const short = base.slice(0, 7);\n if (affectedSet.length === 0)\n return new ImpactReport(false, `nothing affected since the fork point ${short}`, short, [], [], [], files.length, [], []);\n const owners = await this.owners();\n const touched = this.touched(affectedSet, files, owners);\n const rest = affectedSet.filter((project: string): boolean => !touched.includes(project));\n const inputs = this.buildInputs(affectedSet, graph);\n return new ImpactReport(\n true,\n '',\n short,\n this.ids(touched),\n this.ids(rest),\n this.ids(inputs),\n files.length,\n this.ids(this.buildInputs(touched, graph)),\n this.globalFiles(files, owners),\n );\n }\n\n /** Changed files no project owns; none are claimed when ownership itself was unavailable. */\n globalFiles(files: string[], owners: Map<string, string>): string[] {\n if (owners.size === 0) return [];\n return files.filter((file: string): boolean => !owners.has(file)).sort();\n }\n\n /**\n * Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over\n * the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of\n * the touched set, \"what they use\".\n */\n buildInputs(roots: string[], graph: EnhancedGraph): string[] {\n const inSet = new Set<string>(roots);\n const seen = new Set<string>(roots);\n const stack = [...roots];\n const inputs: string[] = [];\n while (stack.length > 0) {\n const project = stack.pop() as string;\n for (const dep of graph[project]?.dependsOn ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n stack.push(dep);\n if (!inSet.has(dep) && graph[dep] !== undefined) inputs.push(dep);\n }\n }\n return inputs.sort();\n }\n\n /** Affected projects that own one of the changed files, by nx's own ownership. */\n touched(affected: string[], files: string[], owners: Map<string, string>): string[] {\n const owning = new Set<string>();\n for (const file of files) {\n const owner = owners.get(file);\n if (owner !== undefined) owning.add(owner);\n }\n return affected.filter((project: string): boolean => owning.has(project));\n }\n\n /** The JSON list, narrowed to projects the graph draws; null when it is not a list of names. */\n parse(json: string, graph: EnhancedGraph): string[] | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- untrusted CLI output; anything unparseable is reported as a reason\n try {\n // webpieces-disable no-any-unknown -- CLI JSON is untrusted until narrowed below\n const parsed: unknown = JSON.parse(json);\n if (!Array.isArray(parsed)) return null;\n return parsed\n .filter((name: unknown): name is string => typeof name === 'string' && graph[name] !== undefined)\n .sort();\n } catch (err: unknown) {\n const error = toError(err);\n void error; // not JSON — the caller reports it\n return null;\n }\n }\n\n /** Ownership failing is not fatal: the set then shows in one shade, all \"affected\". */\n private async owners(): Promise<Map<string, string>> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the second shade is optional\n try {\n return await this.ownership.owners();\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Impact: nx file ownership unavailable (${error.message}); showing one shade`);\n return new Map<string, string>();\n }\n }\n\n private ids(projects: string[]): string[] {\n return projects.map((project: string): string => this.names.getNodeId(project));\n }\n}\n\n/** Writes (or removes) the sidecar, self-ignored, beside architecture/dependencies.html. */\nexport class ImpactSidecar {\n /** Never throws: a sidecar that cannot be written just leaves Impact disabled on the page. */\n write(architectureDir: string, report: ImpactReport): string | null {\n const dir = path.join(architectureDir, IMPACT_SIDECAR_DIR);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; a write failure must never fail generation\n try {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(\n path.join(dir, '.gitignore'),\n '# Written by architecture:generate and architecture:visualize. Per-branch data: never commit it.\\n*\\n',\n 'utf-8',\n );\n const file = path.join(dir, IMPACT_SIDECAR_FILE);\n fs.writeFileSync(file, this.script(report), 'utf-8');\n return file;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not write the impact sidecar: ${error.message}`);\n return null;\n }\n }\n\n /** A stale sidecar from an earlier branch must never be shown as this branch's impact. */\n remove(architectureDir: string): void {\n fs.rmSync(path.join(architectureDir, IMPACT_SIDECAR_DIR, IMPACT_SIDECAR_FILE), { force: true });\n }\n\n script(report: ImpactReport): string {\n const json = JSON.stringify(report).replace(/</g, '\\\\u003c');\n return `// Generated by architecture:generate or architecture:visualize for one branch. Gitignored; do not commit.\\nwindow.__WP_IMPACT__ = ${json};\\n`;\n }\n}\n\n/** The generator's one call: scan, write, and report the result and its cost in one line. */\nexport class ImpactRefresh {\n constructor(\n private readonly scanner: NxImpactScanner = new NxImpactScanner(),\n private readonly sidecar: ImpactSidecar = new ImpactSidecar(),\n ) {}\n\n /** Never throws — Impact is optional, generation is not. */\n async run(architectureDir: string, workspaceRoot: string, graph: EnhancedGraph): Promise<string> {\n const started = Date.now();\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; any failure becomes a reason, never a failed generation\n try {\n const report = await this.scanner.scan(workspaceRoot, graph);\n const cost = `${Date.now() - started} ms`;\n if (report instanceof ImpactUnavailable) {\n this.sidecar.remove(architectureDir);\n return `ℹ️ Impact disabled, no sidecar written: ${report.reason} (${cost})`;\n }\n const file = this.sidecar.write(architectureDir, report);\n if (file === null) return `⚠️ Impact sidecar not written (${cost})`;\n if (!report.available) return `ℹ️ Impact disabled on the page: ${report.reason} (${cost})`;\n return (\n `✅ Impact vs ${report.base}: ${report.touched.length} touched · ${report.affected.length} affected · ` +\n `${report.buildInputs.length} build inputs, from ${report.changedFiles} changed file(s) (${cost}) → ${file}`\n );\n } catch (err: unknown) {\n const error = toError(err);\n return `⚠️ Impact skipped: ${error.message}`;\n }\n }\n}\n"]}
@@ -16,7 +16,7 @@ const rules_sdk_1 = require("@webpieces/rules-sdk");
16
16
  const graph_color_modes_1 = require("./graph-color-modes");
17
17
  const SWATCH_W = 26;
18
18
  const SWATCH_H = 16;
19
- const FRAME = '#2b2f3a';
19
+ const FRAME = '#3a3c46';
20
20
  class GraphLegend {
21
21
  /** One section per mode; the page shows the section of the current mode and hides the rest. */
22
22
  sections(facts) {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-legend.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-legend.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture graph's legend, generated per color mode FROM THE PROJECTS ACTUALLY DRAWN.\n *\n * The hand-written legend this replaces listed five frameworks when six existed and never mentioned\n * the `app` and `bundle` borders the renderer drew (#1155). Generating the rows from the same tables\n * the renderer colors with (graph-color-modes.ts) means a color in use always has a row, and a color\n * nothing uses never does.\n *\n * Swatches are inline SVG drawn with presentation attributes, never `style=`, so the page carries\n * no inline CSS.\n */\n\nimport { KNOWN_FRAMEWORKS, KnownFramework } from '@webpieces/rules-sdk';\nimport {\n BASE_ORDER,\n FRAMEWORK_STYLES,\n FrameworkStyle,\n GraphMode,\n IMPACT_AFFECTED,\n IMPACT_BUILD_INPUT,\n IMPACT_BUILD_INPUT_BORDER,\n IMPACT_TOUCHED,\n IMPACT_UNTOUCHED,\n LegendFacts,\n NO_RUNTIME_COLOR,\n ROLE_STYLES,\n RoleStyle,\n} from './graph-color-modes';\n\nconst SWATCH_W = 26;\nconst SWATCH_H = 16;\nconst FRAME = '#2b2f3a';\n\nexport class GraphLegend {\n /** One section per mode; the page shows the section of the current mode and hides the rest. */\n sections(facts: LegendFacts): string {\n return (\n this.section(GraphMode.RUNTIME, this.runtimeRows(facts), false) +\n this.section(GraphMode.ARCHITECTURE, this.architectureRows(facts), true) +\n this.section(GraphMode.IMPACT, this.impactRows(), true)\n );\n }\n\n /** The edge key: why a line exists. Static — every graph can carry every edge kind. */\n edgeKey(): string {\n return (\n '<div class=\"wp-legend-list\">' +\n this.edgeRow('#e9eaf0', '', 'uses', 'calls the API, or a plain library import') +\n this.edgeRow('#e9eaf0', '4 3', 'implements', 'serves the API (labeled with its contracts)') +\n this.edgeRow('#7aa2ff', '4 3', 'uses + implements', 'implements some contracts, uses others') +\n '</div>'\n );\n }\n\n private section(mode: GraphMode, rows: string, hidden: boolean): string {\n const hide = hidden ? ' hidden' : '';\n return `<div class=\"wp-legend-list\" data-wp-legend-mode=\"${mode}\"${hide}>${rows}</div>`;\n }\n\n /** Base runtimes first (stripe order), then specializations, in the shared vocabulary's order. */\n runtimeRows(facts: LegendFacts): string {\n const present = (framework: KnownFramework): boolean => facts.frameworks.includes(framework);\n const ordered: FrameworkStyle[] = [\n ...BASE_ORDER.map((base: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[base]),\n ...KNOWN_FRAMEWORKS.map((f: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[f]).filter(\n (style: FrameworkStyle): boolean => !style.isBase(),\n ),\n ];\n let rows = '';\n for (const style of ordered) {\n if (!present(style.framework)) continue;\n rows += this.row(style.framework, this.envSwatch(style), style.framework, style.description);\n }\n if (facts.multiRuntime)\n rows += this.row('multi', this.stripesSwatch(), 'several runtimes', 'one equal stripe per runtime');\n if (facts.noRuntime)\n rows += this.row('none', this.solid(NO_RUNTIME_COLOR, '', ''), 'no framework tag', 'runs nowhere declared');\n return rows;\n }\n\n architectureRows(facts: LegendFacts): string {\n let rows = '';\n for (const style of ROLE_STYLES) {\n if (!facts.roles.includes(style.role)) continue;\n rows += this.row(style.role, this.roleSwatch(style), style.role, style.description);\n }\n return rows;\n }\n\n /** Which box is which is only known from the branch's sidecar, so all four rows always show. */\n private impactRows(): string {\n return (\n this.row('touched', this.solid(IMPACT_TOUCHED, '', ''), 'touched', 'files changed on this branch') +\n this.row('affected', this.solid(IMPACT_AFFECTED, IMPACT_TOUCHED, ''), 'affected', 'tests and build re-run') +\n this.row(\n 'build-input',\n this.solid(IMPACT_BUILD_INPUT, IMPACT_BUILD_INPUT_BORDER, '3 2'),\n 'build input',\n 'compiled or restored from cache, unchanged',\n ) +\n this.row('untouched', this.solid(IMPACT_UNTOUCHED, '', ''), 'untouched', 'not part of this build') +\n '<p class=\"wp-impact-note\" data-wp-impact-note></p>'\n );\n }\n\n private row(key: string, swatch: string, title: string, detail: string): string {\n return (\n `<div class=\"wp-legend-row\" data-wp-legend=\"${key}\">${swatch}` +\n `<span>${title}<small>${detail}</small></span></div>`\n );\n }\n\n private open(): string {\n return `<svg width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" viewBox=\"0 0 ${SWATCH_W} ${SWATCH_H}\" aria-hidden=\"true\">`;\n }\n\n private frame(): string {\n return `<rect x=\"0.5\" y=\"0.5\" width=\"${SWATCH_W - 1}\" height=\"${SWATCH_H - 1}\" fill=\"none\" stroke=\"${FRAME}\"/></svg>`;\n }\n\n /** A base runtime is solid; a specialization is its inset on the base color. */\n private envSwatch(style: FrameworkStyle): string {\n const base = `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[style.base].color}\"/>`;\n if (style.isBase()) return this.open() + base + this.frame();\n const inset = `<rect x=\"4\" y=\"4\" width=\"${SWATCH_W - 8}\" height=\"${SWATCH_H - 8}\" rx=\"2\" fill=\"${style.color}\"/>`;\n return this.open() + base + inset + this.frame();\n }\n\n private stripesSwatch(): string {\n const width = SWATCH_W / BASE_ORDER.length;\n const stripes = BASE_ORDER.map(\n (base: KnownFramework, index: number): string =>\n `<rect x=\"${(index * width).toFixed(2)}\" width=\"${width.toFixed(2)}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[base].color}\"/>`,\n ).join('');\n return this.open() + stripes + this.frame();\n }\n\n private roleSwatch(style: RoleStyle): string {\n return this.solid(style.fill, '', '');\n }\n\n /** A filled swatch, optionally with an inner ring (dashed when `dash` is set). */\n private solid(fill: string, outline: string, dash: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const ring =\n outline === ''\n ? ''\n : `<rect x=\"2\" y=\"2\" width=\"${SWATCH_W - 4}\" height=\"${SWATCH_H - 4}\" fill=\"none\" stroke=\"${outline}\" stroke-width=\"2\"${dashed}/>`;\n return this.open() + `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${fill}\"/>` + ring + this.frame();\n }\n\n private edgeRow(color: string, dash: string, title: string, detail: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const line = `<svg width=\"28\" height=\"10\" aria-hidden=\"true\"><line x1=\"0\" y1=\"5\" x2=\"28\" y2=\"5\" stroke=\"${color}\" stroke-width=\"1.6\"${dashed}/></svg>`;\n return this.row(title, line, title, detail);\n }\n}\n"]}
1
+ {"version":3,"file":"graph-legend.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-legend.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture graph's legend, generated per color mode FROM THE PROJECTS ACTUALLY DRAWN.\n *\n * The hand-written legend this replaces listed five frameworks when six existed and never mentioned\n * the `app` and `bundle` borders the renderer drew (#1155). Generating the rows from the same tables\n * the renderer colors with (graph-color-modes.ts) means a color in use always has a row, and a color\n * nothing uses never does.\n *\n * Swatches are inline SVG drawn with presentation attributes, never `style=`, so the page carries\n * no inline CSS.\n */\n\nimport { KNOWN_FRAMEWORKS, KnownFramework } from '@webpieces/rules-sdk';\nimport {\n BASE_ORDER,\n FRAMEWORK_STYLES,\n FrameworkStyle,\n GraphMode,\n IMPACT_AFFECTED,\n IMPACT_BUILD_INPUT,\n IMPACT_BUILD_INPUT_BORDER,\n IMPACT_TOUCHED,\n IMPACT_UNTOUCHED,\n LegendFacts,\n NO_RUNTIME_COLOR,\n ROLE_STYLES,\n RoleStyle,\n} from './graph-color-modes';\n\nconst SWATCH_W = 26;\nconst SWATCH_H = 16;\nconst FRAME = '#3a3c46';\n\nexport class GraphLegend {\n /** One section per mode; the page shows the section of the current mode and hides the rest. */\n sections(facts: LegendFacts): string {\n return (\n this.section(GraphMode.RUNTIME, this.runtimeRows(facts), false) +\n this.section(GraphMode.ARCHITECTURE, this.architectureRows(facts), true) +\n this.section(GraphMode.IMPACT, this.impactRows(), true)\n );\n }\n\n /** The edge key: why a line exists. Static — every graph can carry every edge kind. */\n edgeKey(): string {\n return (\n '<div class=\"wp-legend-list\">' +\n this.edgeRow('#e9eaf0', '', 'uses', 'calls the API, or a plain library import') +\n this.edgeRow('#e9eaf0', '4 3', 'implements', 'serves the API (labeled with its contracts)') +\n this.edgeRow('#7aa2ff', '4 3', 'uses + implements', 'implements some contracts, uses others') +\n '</div>'\n );\n }\n\n private section(mode: GraphMode, rows: string, hidden: boolean): string {\n const hide = hidden ? ' hidden' : '';\n return `<div class=\"wp-legend-list\" data-wp-legend-mode=\"${mode}\"${hide}>${rows}</div>`;\n }\n\n /** Base runtimes first (stripe order), then specializations, in the shared vocabulary's order. */\n runtimeRows(facts: LegendFacts): string {\n const present = (framework: KnownFramework): boolean => facts.frameworks.includes(framework);\n const ordered: FrameworkStyle[] = [\n ...BASE_ORDER.map((base: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[base]),\n ...KNOWN_FRAMEWORKS.map((f: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[f]).filter(\n (style: FrameworkStyle): boolean => !style.isBase(),\n ),\n ];\n let rows = '';\n for (const style of ordered) {\n if (!present(style.framework)) continue;\n rows += this.row(style.framework, this.envSwatch(style), style.framework, style.description);\n }\n if (facts.multiRuntime)\n rows += this.row('multi', this.stripesSwatch(), 'several runtimes', 'one equal stripe per runtime');\n if (facts.noRuntime)\n rows += this.row('none', this.solid(NO_RUNTIME_COLOR, '', ''), 'no framework tag', 'runs nowhere declared');\n return rows;\n }\n\n architectureRows(facts: LegendFacts): string {\n let rows = '';\n for (const style of ROLE_STYLES) {\n if (!facts.roles.includes(style.role)) continue;\n rows += this.row(style.role, this.roleSwatch(style), style.role, style.description);\n }\n return rows;\n }\n\n /** Which box is which is only known from the branch's sidecar, so all four rows always show. */\n private impactRows(): string {\n return (\n this.row('touched', this.solid(IMPACT_TOUCHED, '', ''), 'touched', 'files changed on this branch') +\n this.row('affected', this.solid(IMPACT_AFFECTED, IMPACT_TOUCHED, ''), 'affected', 'tests and build re-run') +\n this.row(\n 'build-input',\n this.solid(IMPACT_BUILD_INPUT, IMPACT_BUILD_INPUT_BORDER, '3 2'),\n 'build input',\n 'compiled or restored from cache, unchanged',\n ) +\n this.row('untouched', this.solid(IMPACT_UNTOUCHED, '', ''), 'untouched', 'not part of this build') +\n '<p class=\"wp-impact-note\" data-wp-impact-note></p>'\n );\n }\n\n private row(key: string, swatch: string, title: string, detail: string): string {\n return (\n `<div class=\"wp-legend-row\" data-wp-legend=\"${key}\">${swatch}` +\n `<span>${title}<small>${detail}</small></span></div>`\n );\n }\n\n private open(): string {\n return `<svg width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" viewBox=\"0 0 ${SWATCH_W} ${SWATCH_H}\" aria-hidden=\"true\">`;\n }\n\n private frame(): string {\n return `<rect x=\"0.5\" y=\"0.5\" width=\"${SWATCH_W - 1}\" height=\"${SWATCH_H - 1}\" fill=\"none\" stroke=\"${FRAME}\"/></svg>`;\n }\n\n /** A base runtime is solid; a specialization is its inset on the base color. */\n private envSwatch(style: FrameworkStyle): string {\n const base = `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[style.base].color}\"/>`;\n if (style.isBase()) return this.open() + base + this.frame();\n const inset = `<rect x=\"4\" y=\"4\" width=\"${SWATCH_W - 8}\" height=\"${SWATCH_H - 8}\" rx=\"2\" fill=\"${style.color}\"/>`;\n return this.open() + base + inset + this.frame();\n }\n\n private stripesSwatch(): string {\n const width = SWATCH_W / BASE_ORDER.length;\n const stripes = BASE_ORDER.map(\n (base: KnownFramework, index: number): string =>\n `<rect x=\"${(index * width).toFixed(2)}\" width=\"${width.toFixed(2)}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[base].color}\"/>`,\n ).join('');\n return this.open() + stripes + this.frame();\n }\n\n private roleSwatch(style: RoleStyle): string {\n return this.solid(style.fill, '', '');\n }\n\n /** A filled swatch, optionally with an inner ring (dashed when `dash` is set). */\n private solid(fill: string, outline: string, dash: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const ring =\n outline === ''\n ? ''\n : `<rect x=\"2\" y=\"2\" width=\"${SWATCH_W - 4}\" height=\"${SWATCH_H - 4}\" fill=\"none\" stroke=\"${outline}\" stroke-width=\"2\"${dashed}/>`;\n return this.open() + `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${fill}\"/>` + ring + this.frame();\n }\n\n private edgeRow(color: string, dash: string, title: string, detail: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const line = `<svg width=\"28\" height=\"10\" aria-hidden=\"true\"><line x1=\"0\" y1=\"5\" x2=\"28\" y2=\"5\" stroke=\"${color}\" stroke-width=\"1.6\"${dashed}/></svg>`;\n return this.row(title, line, title, detail);\n }\n}\n"]}
@@ -17,6 +17,14 @@ export declare class LevelBandLayout {
17
17
  * whichever ones exist.
18
18
  */
19
19
  dot(bands: LevelBand[]): string;
20
+ /**
21
+ * A level with NO box left in it (the page's Filter removed them all) is never dropped: the next
22
+ * level would slide up into its row, so L0 would stop being the bottom row. Its anchor becomes a
23
+ * thin plain-text band instead — visible, labeled, still in the chain — so every remaining box
24
+ * stays in its own L-row. Still `wp-layout`: never clickable, never walked as a dependency.
25
+ * graph-filter.client.ts mirrors these bytes (a global browser script cannot import).
26
+ */
27
+ emptyBandAttrs(level: number): string;
20
28
  /**
21
29
  * The ordering chain between two adjacent bands. When either band is crowded
22
30
  * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a
@@ -64,7 +64,8 @@ class LevelBandLayout {
64
64
  dot(bands) {
65
65
  let dot = '';
66
66
  for (const band of bands) {
67
- dot += ` "${band.anchorName()}" ${LAYOUT_NODE_ATTRS};\n`;
67
+ const attrs = band.nodeNames.length === 0 ? this.emptyBandAttrs(band.level) : LAYOUT_NODE_ATTRS;
68
+ dot += ` "${band.anchorName()}" ${attrs};\n`;
68
69
  dot += ` { rank=same; "${band.anchorName()}"; `;
69
70
  for (const name of band.nodeNames)
70
71
  dot += `"${(0, dot_syntax_1.dotValue)(name)}"; `;
@@ -75,6 +76,17 @@ class LevelBandLayout {
75
76
  }
76
77
  return dot;
77
78
  }
79
+ /**
80
+ * A level with NO box left in it (the page's Filter removed them all) is never dropped: the next
81
+ * level would slide up into its row, so L0 would stop being the bottom row. Its anchor becomes a
82
+ * thin plain-text band instead — visible, labeled, still in the chain — so every remaining box
83
+ * stays in its own L-row. Still `wp-layout`: never clickable, never walked as a dependency.
84
+ * graph-filter.client.ts mirrors these bytes (a global browser script cannot import).
85
+ */
86
+ emptyBandAttrs(level) {
87
+ return ('[shape=plaintext, style="", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor="#9a9eab", ' +
88
+ `label="L${level} · no matching projects", class="${LAYOUT_CLASS} wp-empty-level"]`);
89
+ }
78
90
  /**
79
91
  * The ordering chain between two adjacent bands. When either band is crowded
80
92
  * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a
@@ -1 +1 @@
1
- {"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":";;;AAAA,6CAAwC;AAExC;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,wFAAwF;AACxF,MAAM,iBAAiB,GACnB,wEAAwE,YAAY,IAAI,CAAC;AAE7F,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,wBAAwB,YAAY,IAAI,CAAC;AAEnE;;;GAGG;AACH,MAAa,SAAS;IAClB,KAAK,CAAS;IACd,0FAA0F;IAC1F,SAAS,CAAW;IAEpB,YAAY,KAAa,EAAE,SAAmB;QAC1C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,UAAU;QACN,OAAO,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC;CACJ;AAbD,8BAaC;AAED,oFAAoF;AACpF,MAAa,eAAe;IACxB;;;;OAIG;IACH,GAAG,CAAC,KAAkB;QAClB,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,GAAG,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;YAC1D,GAAG,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;YACjD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS;gBAAE,GAAG,IAAI,IAAI,IAAA,qBAAQ,EAAC,IAAI,CAAC,KAAK,CAAC;YAClE,GAAG,IAAI,KAAK,CAAC;QACjB,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACK,OAAO,CAAC,KAAgB,EAAE,KAAgB;QAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB;eACtD,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB,CAAC;QACrD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;QAC1F,CAAC;QACD,MAAM,MAAM,GAAG,uBAAuB,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO,MAAM,MAAM,KAAK,iBAAiB,KAAK;cACxC,mBAAmB,MAAM,QAAQ;cACjC,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,MAAM,KAAK,iBAAiB,KAAK;cAClE,MAAM,MAAM,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;IAC7E,CAAC;CACJ;AAtCD,0CAsCC","sourcesContent":["import { dotValue } from './dot-syntax';\n\n/**\n * Level bands for the architecture graph.\n *\n * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,\n * descending as you read down, L0 — the foundation everything is built on — always last.\n *\n * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a\n * level's boxes to one row but says nothing about where that row goes, so graphviz derived each\n * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an\n * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and\n * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in\n * the top row, and what parked an L0 lib in the same row as the L6 servers).\n *\n * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in\n * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of\n * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake\n * dependency is ever drawn between two projects.\n */\n\n/**\n * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a\n * crowded row have vertical room to fan out instead of cutting straight through the row below.\n * Eleven-plus boxes in a row is where that became unreadable in practice.\n */\nconst SPACER_MIN_BAND_SIZE = 10;\n\n/**\n * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops\n * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page\n * today; the class is how the DOT tells the browser client which elements are scaffolding, and the\n * client skips them when indexing so an anchor can never be walked as a dependency.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */\nconst LAYOUT_NODE_ATTRS =\n `[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"${LAYOUT_CLASS}\"]`;\n\n/** Attributes for the invisible edges that force one band strictly above the next. */\nconst LAYOUT_EDGE_ATTRS = `[style=invis, class=\"${LAYOUT_CLASS}\"]`;\n\n/**\n * One horizontal band of the graph: every visible project sharing a dependency level, plus the\n * invisible anchor that pins the band's vertical position.\n */\nexport class LevelBand {\n level: number;\n /** Short names of the visible projects on this level, sorted for deterministic output. */\n nodeNames: string[];\n\n constructor(level: number, nodeNames: string[]) {\n this.level = level;\n this.nodeNames = nodeNames;\n }\n\n anchorName(): string {\n return `__wp_layout_L${this.level}`;\n }\n}\n\n/** Emits the DOT that pins each band to its own rank, in descending level order. */\nexport class LevelBandLayout {\n /**\n * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be\n * contiguous — only the levels actually present get a band, and the chain links\n * whichever ones exist.\n */\n dot(bands: LevelBand[]): string {\n let dot = '';\n for (const band of bands) {\n dot += ` \"${band.anchorName()}\" ${LAYOUT_NODE_ATTRS};\\n`;\n dot += ` { rank=same; \"${band.anchorName()}\"; `;\n for (const name of band.nodeNames) dot += `\"${dotValue(name)}\"; `;\n dot += '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n dot += this.bandGap(bands[i], bands[i + 1]);\n }\n return dot;\n }\n\n /**\n * The ordering chain between two adjacent bands. When either band is crowded\n * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a\n * wide row then have a whole rank of vertical room to fan out in, instead of being drawn\n * straight through the row below.\n */\n private bandGap(upper: LevelBand, lower: LevelBand): string {\n const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE\n || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;\n if (!crowded) {\n return ` \"${upper.anchorName()}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n return ` \"${spacer}\" ${LAYOUT_NODE_ATTRS};\\n`\n + ` { rank=same; \"${spacer}\"; }\\n`\n + ` \"${upper.anchorName()}\" -> \"${spacer}\" ${LAYOUT_EDGE_ATTRS};\\n`\n + ` \"${spacer}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-level-bands.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-level-bands.ts"],"names":[],"mappings":";;;AAAA,6CAAwC;AAExC;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,YAAY,GAAG,WAAW,CAAC;AAEjC,wFAAwF;AACxF,MAAM,iBAAiB,GACnB,wEAAwE,YAAY,IAAI,CAAC;AAE7F,sFAAsF;AACtF,MAAM,iBAAiB,GAAG,wBAAwB,YAAY,IAAI,CAAC;AAEnE;;;GAGG;AACH,MAAa,SAAS;IAClB,KAAK,CAAS;IACd,0FAA0F;IAC1F,SAAS,CAAW;IAEpB,YAAY,KAAa,EAAE,SAAmB;QAC1C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED,UAAU;QACN,OAAO,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC;IACxC,CAAC;CACJ;AAbD,8BAaC;AAED,oFAAoF;AACpF,MAAa,eAAe;IACxB;;;;OAIG;IACH,GAAG,CAAC,KAAkB;QAClB,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;YAChG,GAAG,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,KAAK,KAAK,KAAK,CAAC;YAC9C,GAAG,IAAI,mBAAmB,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;YACjD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS;gBAAE,GAAG,IAAI,IAAI,IAAA,qBAAQ,EAAC,IAAI,CAAC,KAAK,CAAC;YAClE,GAAG,IAAI,KAAK,CAAC;QACjB,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;;;;;OAMG;IACH,cAAc,CAAC,KAAa;QACxB,OAAO,CACH,kGAAkG;YAClG,WAAW,KAAK,oCAAoC,YAAY,mBAAmB,CACtF,CAAC;IACN,CAAC;IAED;;;;;OAKG;IACK,OAAO,CAAC,KAAgB,EAAE,KAAgB;QAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB;eACtD,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,oBAAoB,CAAC;QACrD,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,OAAO,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;QAC1F,CAAC;QACD,MAAM,MAAM,GAAG,uBAAuB,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO,MAAM,MAAM,KAAK,iBAAiB,KAAK;cACxC,mBAAmB,MAAM,QAAQ;cACjC,MAAM,KAAK,CAAC,UAAU,EAAE,SAAS,MAAM,KAAK,iBAAiB,KAAK;cAClE,MAAM,MAAM,SAAS,KAAK,CAAC,UAAU,EAAE,KAAK,iBAAiB,KAAK,CAAC;IAC7E,CAAC;CACJ;AArDD,0CAqDC","sourcesContent":["import { dotValue } from './dot-syntax';\n\n/**\n * Level bands for the architecture graph.\n *\n * Every row of architecture/dependencies.html is ONE dependency level: the highest level on top,\n * descending as you read down, L0 — the foundation everything is built on — always last.\n *\n * `{ rank=same; ... }` alone does not deliver that, and assuming it did was the bug. It ties a\n * level's boxes to one row but says nothing about where that row goes, so graphviz derived each\n * row's position from the edges. A level containing a box with no visible edges — a leaf sdk, an\n * api-lib whose only consumers are hidden — is unconstrained, so it was placed at the top rank and\n * dragged its ENTIRE level up with it (verified against graphviz: that is what put the L0 sdks in\n * the top row, and what parked an L0 lib in the same row as the L6 servers).\n *\n * So the ordering is STATED here rather than inferred: one invisible anchor per band, sitting in\n * that band's rank set, chained top-to-bottom with invisible edges. Ranks are then a function of\n * level and nothing else. The anchors — never the real boxes — carry the chain, so no fake\n * dependency is ever drawn between two projects.\n */\n\n/**\n * A band bigger than this gets a blank spacer band inserted next to it, so the long edges leaving a\n * crowded row have vertical room to fan out instead of cutting straight through the row below.\n * Eleven-plus boxes in a row is where that became unreadable in practice.\n */\nconst SPACER_MIN_BAND_SIZE = 10;\n\n/**\n * The class stamped on every node and edge that exists ONLY to pin the layout. Graphviz drops\n * `style=invis` elements from its SVG entirely, so nothing carrying this class reaches the page\n * today; the class is how the DOT tells the browser client which elements are scaffolding, and the\n * client skips them when indexing so an anchor can never be walked as a dependency.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** Attributes shared by every invisible layout node (rank anchors and spacer bands). */\nconst LAYOUT_NODE_ATTRS =\n `[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"${LAYOUT_CLASS}\"]`;\n\n/** Attributes for the invisible edges that force one band strictly above the next. */\nconst LAYOUT_EDGE_ATTRS = `[style=invis, class=\"${LAYOUT_CLASS}\"]`;\n\n/**\n * One horizontal band of the graph: every visible project sharing a dependency level, plus the\n * invisible anchor that pins the band's vertical position.\n */\nexport class LevelBand {\n level: number;\n /** Short names of the visible projects on this level, sorted for deterministic output. */\n nodeNames: string[];\n\n constructor(level: number, nodeNames: string[]) {\n this.level = level;\n this.nodeNames = nodeNames;\n }\n\n anchorName(): string {\n return `__wp_layout_L${this.level}`;\n }\n}\n\n/** Emits the DOT that pins each band to its own rank, in descending level order. */\nexport class LevelBandLayout {\n /**\n * @param bands the visible projects grouped by level, HIGHEST LEVEL FIRST. Levels need not be\n * contiguous — only the levels actually present get a band, and the chain links\n * whichever ones exist.\n */\n dot(bands: LevelBand[]): string {\n let dot = '';\n for (const band of bands) {\n const attrs = band.nodeNames.length === 0 ? this.emptyBandAttrs(band.level) : LAYOUT_NODE_ATTRS;\n dot += ` \"${band.anchorName()}\" ${attrs};\\n`;\n dot += ` { rank=same; \"${band.anchorName()}\"; `;\n for (const name of band.nodeNames) dot += `\"${dotValue(name)}\"; `;\n dot += '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n dot += this.bandGap(bands[i], bands[i + 1]);\n }\n return dot;\n }\n\n /**\n * A level with NO box left in it (the page's Filter removed them all) is never dropped: the next\n * level would slide up into its row, so L0 would stop being the bottom row. Its anchor becomes a\n * thin plain-text band instead — visible, labeled, still in the chain — so every remaining box\n * stays in its own L-row. Still `wp-layout`: never clickable, never walked as a dependency.\n * graph-filter.client.ts mirrors these bytes (a global browser script cannot import).\n */\n emptyBandAttrs(level: number): string {\n return (\n '[shape=plaintext, style=\"\", width=0.01, height=0.15, margin=0, fontsize=9, fontcolor=\"#9a9eab\", ' +\n `label=\"L${level} · no matching projects\", class=\"${LAYOUT_CLASS} wp-empty-level\"]`\n );\n }\n\n /**\n * The ordering chain between two adjacent bands. When either band is crowded\n * (> SPACER_MIN_BAND_SIZE boxes), an empty spacer band goes between them: the edges leaving a\n * wide row then have a whole rank of vertical room to fan out in, instead of being drawn\n * straight through the row below.\n */\n private bandGap(upper: LevelBand, lower: LevelBand): string {\n const crowded = upper.nodeNames.length > SPACER_MIN_BAND_SIZE\n || lower.nodeNames.length > SPACER_MIN_BAND_SIZE;\n if (!crowded) {\n return ` \"${upper.anchorName()}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n return ` \"${spacer}\" ${LAYOUT_NODE_ATTRS};\\n`\n + ` { rank=same; \"${spacer}\"; }\\n`\n + ` \"${upper.anchorName()}\" -> \"${spacer}\" ${LAYOUT_EDGE_ATTRS};\\n`\n + ` \"${spacer}\" -> \"${lower.anchorName()}\" ${LAYOUT_EDGE_ATTRS};\\n`;\n }\n}\n"]}