@webpieces/nx-webpieces-rules 0.4.881 → 0.4.883
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -13
- package/package.json +7 -7
- package/src/configs/tag-truth-configs.d.ts +15 -0
- package/src/configs/tag-truth-configs.js +20 -1
- package/src/configs/tag-truth-configs.js.map +1 -1
- package/src/executors/generate/executor.js +3 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/index.d.ts +6 -3
- package/src/index.js +15 -1
- package/src/index.js.map +1 -1
- package/src/lib/graph-color-modes.d.ts +50 -6
- package/src/lib/graph-color-modes.js +88 -8
- package/src/lib/graph-color-modes.js.map +1 -1
- package/src/lib/graph-comparator.js +2 -0
- package/src/lib/graph-comparator.js.map +1 -1
- package/src/lib/graph-entry-levels.d.ts +35 -0
- package/src/lib/graph-entry-levels.js +60 -0
- package/src/lib/graph-entry-levels.js.map +1 -0
- package/src/lib/graph-impact.d.ts +5 -5
- package/src/lib/graph-impact.js +5 -5
- package/src/lib/graph-impact.js.map +1 -1
- package/src/lib/graph-legend.d.ts +11 -1
- package/src/lib/graph-legend.js +44 -6
- package/src/lib/graph-legend.js.map +1 -1
- package/src/lib/graph-level-bands.js +3 -1
- package/src/lib/graph-level-bands.js.map +1 -1
- package/src/lib/graph-loader.d.ts +7 -1
- package/src/lib/graph-loader.js +10 -1
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-metadata.d.ts +2 -0
- package/src/lib/graph-metadata.js +19 -0
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-page-shell.d.ts +28 -6
- package/src/lib/graph-page-shell.js +55 -18
- package/src/lib/graph-page-shell.js.map +1 -1
- package/src/lib/graph-page-styles.js +3 -1
- package/src/lib/graph-page-styles.js.map +1 -1
- package/src/lib/graph-products.d.ts +79 -0
- package/src/lib/graph-products.js +199 -0
- package/src/lib/graph-products.js.map +1 -0
- package/src/lib/graph-render-model.d.ts +13 -2
- package/src/lib/graph-render-model.js +36 -4
- package/src/lib/graph-render-model.js.map +1 -1
- package/src/lib/graph-sorter.d.ts +13 -0
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +20 -10
- package/src/lib/graph-visualizer.client.js +67 -31
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.js +7 -6
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/product-resolver.d.ts +38 -0
- package/src/lib/product-resolver.js +63 -0
- package/src/lib/product-resolver.js.map +1 -0
- package/src/lib/runtime-graph-model.d.ts +7 -0
- package/src/lib/runtime-graph-model.js.map +1 -1
- package/src/lib/runtime-graph.js +3 -0
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-html-page.d.ts +7 -0
- package/src/lib/runtime-html-page.js +29 -0
- package/src/lib/runtime-html-page.js.map +1 -1
- package/src/lib/runtime-visualizer.client.d.ts +7 -0
- package/src/lib/runtime-visualizer.client.js +32 -0
- package/src/lib/runtime-visualizer.client.js.map +1 -1
- package/src/lib/runtime-visualizer.js +3 -1
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/lib/saved-snapshot.js +6 -2
- package/src/lib/saved-snapshot.js.map +1 -1
- package/src/lib/tag-truth.d.ts +20 -3
- package/src/lib/tag-truth.js +48 -7
- package/src/lib/tag-truth.js.map +1 -1
- package/src/nx-policy-registry.js +1 -0
- package/src/nx-policy-registry.js.map +1 -1
- package/src/rule-help.js +1 -0
- package/src/rule-help.js.map +1 -1
- package/src/rule-settings.js +12 -1
- package/src/rule-settings.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-render-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-render-model.ts"],"names":[],"mappings":";;;AAAA,6CAAwC;AAExC,2DAA2E;AAE3E
|
|
1
|
+
{"version":3,"file":"graph-render-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-render-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { dotValue } from './dot-syntax';\nimport { LevelBand } from './graph-level-bands';\nimport { LegendFacts, NodeFacts, NodeModeDots } from './graph-color-modes';\n\n/**\n * Renderer-owned records, captured while emitting DOT, never recovered by parsing labels.\n *\n * `dot` is the statement `fullDot` carries. `modes` and `tags` are set only by the architecture\n * graph, whose page switches color modes by re-rendering each box from its per-mode statement and\n * filters boxes by their role and framework tags; the runtime graph has one look and leaves both null.\n *\n * `products` is set on BOTH graphs (#1179): the products a box belongs to, which each page's product\n * filter matches on. Empty for a box in no product.\n */\nexport class RenderNode {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n public readonly modes: NodeModeDots | null,\n public readonly tags: NodeTags | null,\n public readonly products: string[],\n ) {}\n}\n\n/** What the architecture page's Filter matches a box on: its level, role and framework tags. */\nexport class NodeTags {\n constructor(\n public readonly level: number,\n public readonly role: string,\n public readonly frameworks: string[],\n ) {}\n}\n\nexport class RenderEdge {\n constructor(\n public readonly from: string,\n public readonly to: string,\n public readonly dot: string,\n public readonly id: string,\n ) {}\n}\n\n/** The immutable drawable snapshot carried by a generated page. */\nexport class GraphRenderModel {\n readonly nodes: RenderNode[] = [];\n readonly edges: RenderEdge[] = [];\n bands: LevelBand[] = [];\n fullDot = '';\n header = '';\n footer = '';\n /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */\n legend = new LegendFacts();\n\n node(id: string, dot: string, products: string[] = []): string {\n this.nodes.push(new RenderNode(id, dot, null, null, products));\n return dot;\n }\n\n /**\n * Every product a declared box (a queue, a cron trigger, an external system) inherits from the\n * services its edges touch (#1179): it belongs to whichever product's services it is attached to.\n * Called by the runtime graph once every edge is emitted; RenderNode is immutable, so a node gains\n * its products by being replaced.\n */\n attachProducts(isService: (id: string) => boolean): void {\n const byId = new Map<string, RenderNode>();\n for (const node of this.nodes) byId.set(node.id, node);\n for (let index = 0; index < this.nodes.length; index++) {\n const node = this.nodes[index];\n if (isService(node.id)) continue;\n const inherited = new Set<string>();\n for (const edge of this.edges) {\n const other = edge.from === node.id ? edge.to : edge.to === node.id ? edge.from : null;\n if (other === null || !isService(other)) continue;\n for (const product of byId.get(other)?.products ?? []) inherited.add(product);\n }\n if (inherited.size === 0) continue;\n this.nodes[index] = new RenderNode(node.id, node.dot, node.modes, node.tags, [...inherited].sort());\n }\n }\n\n /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */\n styledNode(facts: NodeFacts, modes: NodeModeDots): string {\n this.nodes.push(\n new RenderNode(\n facts.nodeId,\n modes.runtime,\n modes,\n new NodeTags(facts.level, facts.role, facts.frameworks),\n facts.products,\n ),\n );\n return modes.runtime;\n }\n\n completeEndpoints(): void {\n const ids = new Set(this.nodes.map((node) => node.id));\n for (const edge of this.edges) {\n for (const id of [edge.from, edge.to]) {\n if (ids.has(id)) continue;\n ids.add(id);\n this.node(id, ` \"${dotValue(id)}\";\\n`);\n }\n }\n }\n\n edge(from: string, to: string, dot: string): string {\n const id = `wp-real-edge-${this.edges.length}`;\n const statement = `${dot.slice(0, -2)} [id=\"${id}\"];\\n`;\n this.edges.push(new RenderEdge(from, to, statement, id));\n return statement;\n }\n}\n\n/** Shared filter chrome and script-safe JSON for pages opened directly from disk. */\nexport class GraphFilterAssets {\n json<T>(value: T): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n }\n\n html(): string {\n return (\n '<p id=\"wp-filter-status\" hidden>Filtering: <strong id=\"wp-filter-anchor\"></strong> ' +\n '<button id=\"wp-filter-off\" type=\"button\">Turn off Filter</button></p>' +\n '<p id=\"wp-graph-error\" role=\"alert\" hidden></p>'\n );\n }\n\n styles(): string {\n return (\n '#wp-filter-status { text-align:center; } #wp-graph-error { color:#b71c1c; } ' +\n '#graph .wp-filter-anchor polygon, #graph .wp-filter-anchor path, ' +\n '#graph .wp-filter-anchor ellipse { stroke:#1565c0; stroke-width:3; }'\n );\n }\n}\n"]}
|
|
@@ -23,6 +23,11 @@ import type { ProjectApiRelations } from './api-usage/api-relations';
|
|
|
23
23
|
* transport (rpc | pubsub). Derived by scanning source (see api-usage/).
|
|
24
24
|
*/
|
|
25
25
|
export interface GraphEntry {
|
|
26
|
+
/**
|
|
27
|
+
* The project's row: its dependency depth (0 = no deps) — except that every server, client and
|
|
28
|
+
* app (APP_ROLES) is pinned to the TOP level, and a bundle above the apps it aggregates (#1179,
|
|
29
|
+
* graph-entry-levels.ts). Every dependency still sits strictly below its dependent.
|
|
30
|
+
*/
|
|
26
31
|
level: number;
|
|
27
32
|
dependsOn: string[];
|
|
28
33
|
framework?: string[];
|
|
@@ -56,6 +61,14 @@ export interface GraphEntry {
|
|
|
56
61
|
* Absent (the overwhelmingly normal case) means this project cuts nothing.
|
|
57
62
|
*/
|
|
58
63
|
cutLegacyCycle?: string[];
|
|
64
|
+
/**
|
|
65
|
+
* The products this project belongs to (#1179), sorted: every product whose `product:<name>`-tagged
|
|
66
|
+
* project reaches it by a downward walk over `dependsOn` (a tagged project is in its own products).
|
|
67
|
+
* It is the project's blast radius — a change here is a change to each of these products. DERIVED,
|
|
68
|
+
* never declared on a library; absent when no product reaches it (and everywhere when no project
|
|
69
|
+
* carries a `product:` tag). See graph-products.ts.
|
|
70
|
+
*/
|
|
71
|
+
products?: string[];
|
|
59
72
|
shortDescription?: string;
|
|
60
73
|
responsibilitiesFile?: string;
|
|
61
74
|
designFile?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAuFH,4DAyCC;AASD,wDAgBC;AAtJD,iDAAsD;AA2EtD;;;;;;;;GAQG;AACH,SAAgB,wBAAwB,CAAC,KAA+B;IACpE,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEvC,OAAO,SAAS,CAAC,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAErC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,uEAAuE;YACvE,MAAM,mBAAmB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,IAAI,mBAAmB,EAAE,CAAC;gBACtB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,gFAAgF;YAChF,mFAAmF;YACnF,kFAAkF;YAClF,IAAI,mCAAoB,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;YACrE,sFAAsF;YACtF,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAChF,MAAM,IAAI,KAAK,CACX,+EAA+E,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACxG,CAAC;QACN,CAAC;QAED,4DAA4D;QAC5D,YAAY,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,oBAAoB;QACpB,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,sBAAsB,CAAC,KAA+B;IAClE,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC1B,6CAA6C;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG;gBACd,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\nimport { ProjectCycleDetector } from './graph-cycles';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n /**\n * Target service names whose RUNTIME edge from this project is cut out of leveling and cycle\n * detection, from this project's `cutLegacyCycle:<target>` nx tags. Per EDGE, not per project.\n * An admission of legacy debt, never an approved pattern — see cut-legacy-cycle-resolver.ts.\n * Absent (the overwhelmingly normal case) means this project cuts nothing.\n */\n cutLegacyCycle?: string[];\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n runtimeDeclaration?: string;\n runtimeComposition?: boolean;\n /**\n * The @webpieces runtime packages this project's OWN package.json declares — http-routing,\n * http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it\n * declares none, so TOTAL absence across the file means the file predates this field and\n * nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately\n * not markers; see runtime-participant-resolver.ts for why.\n *\n * The runtime graph ORs this over a node's library closure: a server whose whole webpieces\n * stack comes from a shared bootstrap lib participates even though it declares nothing itself.\n */\n webpiecesRuntime?: string[];\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency. ProjectCycleDetector is the ONE cycle\n // implementation in this package — it names every chain, in project keys, with the\n // fix — so this reports through it rather than keeping a second, weaker DFS here.\n new ProjectCycleDetector().assertAcyclic(graph, 'the project graph');\n // Unreachable: Kahn only stalls on a cycle, and the assert above throws on every one.\n const remaining = allProjects.filter((p: string): boolean => !processed.has(p));\n throw new Error(\n `Topological sort stalled with no cycle to report — projects left unlayered: ${remaining.join(', ')}`,\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAoGH,4DAyCC;AASD,wDAgBC;AAnKD,iDAAsD;AAwFtD;;;;;;;;GAQG;AACH,SAAgB,wBAAwB,CAAC,KAA+B;IACpE,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEvC,OAAO,SAAS,CAAC,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAErC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,uEAAuE;YACvE,MAAM,mBAAmB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,IAAI,mBAAmB,EAAE,CAAC;gBACtB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,gFAAgF;YAChF,mFAAmF;YACnF,kFAAkF;YAClF,IAAI,mCAAoB,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;YACrE,sFAAsF;YACtF,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAChF,MAAM,IAAI,KAAK,CACX,+EAA+E,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACxG,CAAC;QACN,CAAC;QAED,4DAA4D;QAC5D,YAAY,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,oBAAoB;QACpB,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,sBAAsB,CAAC,KAA+B;IAClE,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC1B,6CAA6C;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG;gBACd,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\nimport { ProjectCycleDetector } from './graph-cycles';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n /**\n * The project's row: its dependency depth (0 = no deps) — except that every server, client and\n * app (APP_ROLES) is pinned to the TOP level, and a bundle above the apps it aggregates (#1179,\n * graph-entry-levels.ts). Every dependency still sits strictly below its dependent.\n */\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n /**\n * Target service names whose RUNTIME edge from this project is cut out of leveling and cycle\n * detection, from this project's `cutLegacyCycle:<target>` nx tags. Per EDGE, not per project.\n * An admission of legacy debt, never an approved pattern — see cut-legacy-cycle-resolver.ts.\n * Absent (the overwhelmingly normal case) means this project cuts nothing.\n */\n cutLegacyCycle?: string[];\n /**\n * The products this project belongs to (#1179), sorted: every product whose `product:<name>`-tagged\n * project reaches it by a downward walk over `dependsOn` (a tagged project is in its own products).\n * It is the project's blast radius — a change here is a change to each of these products. DERIVED,\n * never declared on a library; absent when no product reaches it (and everywhere when no project\n * carries a `product:` tag). See graph-products.ts.\n */\n products?: string[];\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n runtimeDeclaration?: string;\n runtimeComposition?: boolean;\n /**\n * The @webpieces runtime packages this project's OWN package.json declares — http-routing,\n * http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it\n * declares none, so TOTAL absence across the file means the file predates this field and\n * nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately\n * not markers; see runtime-participant-resolver.ts for why.\n *\n * The runtime graph ORs this over a node's library closure: a server whose whole webpieces\n * stack comes from a shared bootstrap lib participates even though it declares nothing itself.\n */\n webpiecesRuntime?: string[];\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency. ProjectCycleDetector is the ONE cycle\n // implementation in this package — it names every chain, in project keys, with the\n // fix — so this reports through it rather than keeping a second, weaker DFS here.\n new ProjectCycleDetector().assertAcyclic(graph, 'the project graph');\n // Unreachable: Kahn only stalls on a cycle, and the assert above throws on every one.\n const remaining = allProjects.filter((p: string): boolean => !processed.has(p));\n throw new Error(\n `Topological sort stalled with no cycle to report — projects left unlayered: ${remaining.join(', ')}`,\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The four color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.
|
|
3
3
|
*
|
|
4
|
-
* The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the
|
|
5
|
-
* one) and remembered per viewer in localStorage.
|
|
4
|
+
* The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, `#product`, which win over the
|
|
5
|
+
* remembered one) and remembered per viewer in localStorage. Product is unavailable on a page whose
|
|
6
|
+
* workspace declares no `product:` tag. Storage can be missing or throw (a private window,
|
|
6
7
|
* blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
|
|
7
8
|
* without it.
|
|
8
9
|
*
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
* set reads.
|
|
13
14
|
*/
|
|
14
15
|
declare class WpModeState {
|
|
16
|
+
private readonly hasProducts;
|
|
15
17
|
static readonly MODES: string[];
|
|
16
18
|
static readonly NAMES: string[];
|
|
17
19
|
static readonly SUBTITLES: string[];
|
|
@@ -20,7 +22,10 @@ declare class WpModeState {
|
|
|
20
22
|
readonly scans: ImpactJson[];
|
|
21
23
|
kind: string;
|
|
22
24
|
mode: string;
|
|
23
|
-
|
|
25
|
+
/** @param hasProducts some box belongs to a product, so the Product mode has something to show */
|
|
26
|
+
constructor(hasProducts: boolean);
|
|
27
|
+
/** '' when Product can be shown, else why not. */
|
|
28
|
+
productReason(): string;
|
|
24
29
|
/** The active scan; null when the page has no Impact data. */
|
|
25
30
|
get impact(): ImpactJson | null;
|
|
26
31
|
/** '' when Impact can be shown, else the one line the drawer and the Filter popover say instead. */
|
|
@@ -40,25 +45,28 @@ declare class WpModeState {
|
|
|
40
45
|
private read;
|
|
41
46
|
}
|
|
42
47
|
/**
|
|
43
|
-
* One state of the Filter popover: a change scope plus the selected Runtime and
|
|
44
|
-
* holds the APPLIED one; the popover edits a copy until "Show N projects".
|
|
48
|
+
* One state of the Filter popover: a change scope plus the selected Runtime, Role and Product chips.
|
|
49
|
+
* The page holds the APPLIED one; the popover edits a copy until "Show N projects".
|
|
45
50
|
*/
|
|
46
51
|
declare class WpFilterChoice {
|
|
47
52
|
scope: string;
|
|
48
53
|
readonly runtimes: Set<string>;
|
|
49
54
|
readonly roles: Set<string>;
|
|
55
|
+
readonly products: Set<string>;
|
|
50
56
|
copy(): WpFilterChoice;
|
|
51
57
|
/** How many filters are active: the drawer button's badge. */
|
|
52
58
|
count(): number;
|
|
59
|
+
/** The chip set a `data-wp-chip-group` (or a pill's group) names. */
|
|
60
|
+
group(name: string): Set<string>;
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
|
-
* The Filter popover's
|
|
56
|
-
* any selected runtime chip (when one is selected), AND has any selected role chip (likewise)
|
|
63
|
+
* The Filter popover's four groups, INTERSECTED: a box stays when it is in the change scope, AND has
|
|
64
|
+
* any selected runtime chip (when one is selected), AND has any selected role chip (likewise), AND
|
|
65
|
+
* belongs to any selected product (likewise — several products are the UNION of their closures).
|
|
57
66
|
*/
|
|
58
67
|
declare class WpProjectFilter {
|
|
59
68
|
private readonly model;
|
|
60
69
|
private readonly modes;
|
|
61
|
-
static readonly SCOPE_NAMES: Map<string, string>;
|
|
62
70
|
constructor(model: RenderModelJson, modes: WpModeState);
|
|
63
71
|
/** The node ids a change scope keeps; null for "everything" (and whenever Impact is unavailable). */
|
|
64
72
|
scopeSet(scope: string): Set<string> | null;
|
|
@@ -111,7 +119,7 @@ declare class GraphDrawer {
|
|
|
111
119
|
readonly popover: GraphFilterPopover;
|
|
112
120
|
constructor(page: GraphPage);
|
|
113
121
|
wire(): void;
|
|
114
|
-
/** "Color by": the pill trigger opens the menu of
|
|
122
|
+
/** "Color by": the pill trigger opens the menu of four; picking one switches and closes it. */
|
|
115
123
|
private wireModeMenu;
|
|
116
124
|
/** The "Changed on this branch" / "Last commit" toggle, in the drawer and in the Filter popover. */
|
|
117
125
|
private wireImpactKinds;
|
|
@@ -133,6 +141,8 @@ declare class GraphDrawer {
|
|
|
133
141
|
private syncFilterToggle;
|
|
134
142
|
/** The drawer button's count badge, and one removable pill per active filter in the top bar. */
|
|
135
143
|
private syncFilter;
|
|
144
|
+
/** A change scope's name, read from the Filter popover's markup: the shell owns the ONE copy. */
|
|
145
|
+
private scopeLabel;
|
|
136
146
|
/** A pill, built with text nodes so no value is parsed as HTML. */
|
|
137
147
|
private pill;
|
|
138
148
|
private impactNote;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
3
|
+
* The four color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.
|
|
4
4
|
*
|
|
5
|
-
* The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the
|
|
6
|
-
* one) and remembered per viewer in localStorage.
|
|
5
|
+
* The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, `#product`, which win over the
|
|
6
|
+
* remembered one) and remembered per viewer in localStorage. Product is unavailable on a page whose
|
|
7
|
+
* workspace declares no `product:` tag. Storage can be missing or throw (a private window,
|
|
7
8
|
* blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
|
|
8
9
|
* without it.
|
|
9
10
|
*
|
|
@@ -13,18 +14,32 @@
|
|
|
13
14
|
* set reads.
|
|
14
15
|
*/
|
|
15
16
|
class WpModeState {
|
|
16
|
-
|
|
17
|
-
static
|
|
18
|
-
static
|
|
17
|
+
hasProducts;
|
|
18
|
+
static MODES = ['runtime', 'architecture', 'impact', 'product'];
|
|
19
|
+
static NAMES = ['Runtime', 'Architecture', 'Impact', 'Product'];
|
|
20
|
+
static SUBTITLES = [
|
|
21
|
+
'where the code can run',
|
|
22
|
+
'servers · clients · APIs',
|
|
23
|
+
'what changed',
|
|
24
|
+
'which products it belongs to',
|
|
25
|
+
];
|
|
19
26
|
static STORAGE_KEY = 'wp-architecture-graph-mode';
|
|
20
27
|
static KIND_STORAGE_KEY = 'wp-architecture-graph-impact-kind';
|
|
21
28
|
scans = window.__WP_IMPACT__?.scans ?? [];
|
|
22
29
|
kind = '';
|
|
23
30
|
mode = 'runtime';
|
|
24
|
-
|
|
31
|
+
/** @param hasProducts some box belongs to a product, so the Product mode has something to show */
|
|
32
|
+
constructor(hasProducts) {
|
|
33
|
+
this.hasProducts = hasProducts;
|
|
25
34
|
this.kind = this.initialKind();
|
|
26
35
|
this.mode = this.initial();
|
|
27
36
|
}
|
|
37
|
+
/** '' when Product can be shown, else why not. */
|
|
38
|
+
productReason() {
|
|
39
|
+
return this.hasProducts
|
|
40
|
+
? ''
|
|
41
|
+
: 'No products here: tag servers, clients and apps product:<name> in their project.json, then regenerate.';
|
|
42
|
+
}
|
|
28
43
|
/** The active scan; null when the page has no Impact data. */
|
|
29
44
|
get impact() {
|
|
30
45
|
return this.scans.find((scan) => scan.kind === this.kind) ?? null;
|
|
@@ -65,7 +80,13 @@ class WpModeState {
|
|
|
65
80
|
return this.scans[0]?.kind ?? '';
|
|
66
81
|
}
|
|
67
82
|
available(mode) {
|
|
68
|
-
|
|
83
|
+
if (!WpModeState.MODES.includes(mode))
|
|
84
|
+
return false;
|
|
85
|
+
if (mode === 'impact')
|
|
86
|
+
return this.impactReason() === '';
|
|
87
|
+
if (mode === 'product')
|
|
88
|
+
return this.productReason() === '';
|
|
89
|
+
return true;
|
|
69
90
|
}
|
|
70
91
|
name(mode) {
|
|
71
92
|
return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;
|
|
@@ -125,13 +146,14 @@ class WpModeState {
|
|
|
125
146
|
}
|
|
126
147
|
}
|
|
127
148
|
/**
|
|
128
|
-
* One state of the Filter popover: a change scope plus the selected Runtime and
|
|
129
|
-
* holds the APPLIED one; the popover edits a copy until "Show N projects".
|
|
149
|
+
* One state of the Filter popover: a change scope plus the selected Runtime, Role and Product chips.
|
|
150
|
+
* The page holds the APPLIED one; the popover edits a copy until "Show N projects".
|
|
130
151
|
*/
|
|
131
152
|
class WpFilterChoice {
|
|
132
153
|
scope = 'everything';
|
|
133
154
|
runtimes = new Set();
|
|
134
155
|
roles = new Set();
|
|
156
|
+
products = new Set();
|
|
135
157
|
copy() {
|
|
136
158
|
const copy = new WpFilterChoice();
|
|
137
159
|
copy.scope = this.scope;
|
|
@@ -139,27 +161,31 @@ class WpFilterChoice {
|
|
|
139
161
|
copy.runtimes.add(runtime);
|
|
140
162
|
for (const role of this.roles)
|
|
141
163
|
copy.roles.add(role);
|
|
164
|
+
for (const product of this.products)
|
|
165
|
+
copy.products.add(product);
|
|
142
166
|
return copy;
|
|
143
167
|
}
|
|
144
168
|
/** How many filters are active: the drawer button's badge. */
|
|
145
169
|
count() {
|
|
146
|
-
return (this.scope === 'everything' ? 0 : 1) + this.runtimes.size + this.roles.size;
|
|
170
|
+
return (this.scope === 'everything' ? 0 : 1) + this.runtimes.size + this.roles.size + this.products.size;
|
|
171
|
+
}
|
|
172
|
+
/** The chip set a `data-wp-chip-group` (or a pill's group) names. */
|
|
173
|
+
group(name) {
|
|
174
|
+
if (name === 'role')
|
|
175
|
+
return this.roles;
|
|
176
|
+
if (name === 'product')
|
|
177
|
+
return this.products;
|
|
178
|
+
return this.runtimes;
|
|
147
179
|
}
|
|
148
180
|
}
|
|
149
181
|
/**
|
|
150
|
-
* The Filter popover's
|
|
151
|
-
* any selected runtime chip (when one is selected), AND has any selected role chip (likewise)
|
|
182
|
+
* The Filter popover's four groups, INTERSECTED: a box stays when it is in the change scope, AND has
|
|
183
|
+
* any selected runtime chip (when one is selected), AND has any selected role chip (likewise), AND
|
|
184
|
+
* belongs to any selected product (likewise — several products are the UNION of their closures).
|
|
152
185
|
*/
|
|
153
186
|
class WpProjectFilter {
|
|
154
187
|
model;
|
|
155
188
|
modes;
|
|
156
|
-
static SCOPE_NAMES = new Map([
|
|
157
|
-
['everything', 'Everything'],
|
|
158
|
-
['changed', 'Changed'],
|
|
159
|
-
['dependents', 'Changed + what uses them'],
|
|
160
|
-
['dependencies', 'Changed + what they use'],
|
|
161
|
-
['build', 'Whole build of this branch'],
|
|
162
|
-
]);
|
|
163
189
|
constructor(model, modes) {
|
|
164
190
|
this.model = model;
|
|
165
191
|
this.modes = modes;
|
|
@@ -196,6 +222,8 @@ class WpProjectFilter {
|
|
|
196
222
|
continue;
|
|
197
223
|
if (!this.anyOf(choice.roles, node.tags === null ? [] : [node.tags.role]))
|
|
198
224
|
continue;
|
|
225
|
+
if (!this.anyOf(choice.products, node.products))
|
|
226
|
+
continue;
|
|
199
227
|
kept.add(node.id);
|
|
200
228
|
}
|
|
201
229
|
return kept;
|
|
@@ -328,7 +356,7 @@ class GraphFilterPopover {
|
|
|
328
356
|
});
|
|
329
357
|
document.querySelectorAll('.wp-chip[data-wp-chip]').forEach((chip) => {
|
|
330
358
|
chip.addEventListener('click', () => {
|
|
331
|
-
const set = chip.dataset['wpChipGroup']
|
|
359
|
+
const set = this.draft.group(chip.dataset['wpChipGroup'] ?? '');
|
|
332
360
|
const value = chip.dataset['wpChip'] ?? '';
|
|
333
361
|
if (set.has(value))
|
|
334
362
|
set.delete(value);
|
|
@@ -380,7 +408,7 @@ class GraphFilterPopover {
|
|
|
380
408
|
count.textContent = reason === '' ? String(this.page.scopeCount(count.dataset['wpScopeCount'] ?? '')) : '';
|
|
381
409
|
});
|
|
382
410
|
document.querySelectorAll('.wp-chip[data-wp-chip]').forEach((chip) => {
|
|
383
|
-
const set = chip.dataset['wpChipGroup']
|
|
411
|
+
const set = this.draft.group(chip.dataset['wpChipGroup'] ?? '');
|
|
384
412
|
chip.setAttribute('aria-pressed', String(set.has(chip.dataset['wpChip'] ?? '')));
|
|
385
413
|
});
|
|
386
414
|
const apply = this.byId('wp-filter-apply');
|
|
@@ -427,7 +455,7 @@ class GraphDrawer {
|
|
|
427
455
|
this.closeModeMenu(false);
|
|
428
456
|
});
|
|
429
457
|
}
|
|
430
|
-
/** "Color by": the pill trigger opens the menu of
|
|
458
|
+
/** "Color by": the pill trigger opens the menu of four; picking one switches and closes it. */
|
|
431
459
|
wireModeMenu() {
|
|
432
460
|
this.byId('wp-mode-trigger')?.addEventListener('click', (ev) => {
|
|
433
461
|
ev.stopPropagation();
|
|
@@ -540,7 +568,7 @@ class GraphDrawer {
|
|
|
540
568
|
this.lockInput()?.focus();
|
|
541
569
|
return;
|
|
542
570
|
}
|
|
543
|
-
const index = ['1', '2', '3'].indexOf(ev.key);
|
|
571
|
+
const index = ['1', '2', '3', '4'].indexOf(ev.key);
|
|
544
572
|
if (index >= 0 && !menuOpen) {
|
|
545
573
|
this.page.setMode(WpModeState.MODES[index]);
|
|
546
574
|
return;
|
|
@@ -576,6 +604,8 @@ class GraphDrawer {
|
|
|
576
604
|
button.disabled = !modes.available(mode);
|
|
577
605
|
if (mode === 'impact')
|
|
578
606
|
button.title = reason === '' ? 'Key 3' : reason;
|
|
607
|
+
if (mode === 'product')
|
|
608
|
+
button.title = modes.productReason() === '' ? 'Key 4' : modes.productReason();
|
|
579
609
|
});
|
|
580
610
|
this.text('wp-mode-current', modes.name(modes.mode));
|
|
581
611
|
this.text('wp-mode-current-sub', modes.subtitle(modes.mode));
|
|
@@ -621,13 +651,19 @@ class GraphDrawer {
|
|
|
621
651
|
return;
|
|
622
652
|
const items = [];
|
|
623
653
|
if (filter.scope !== 'everything')
|
|
624
|
-
items.push(this.pill(
|
|
654
|
+
items.push(this.pill(this.scopeLabel(filter.scope), 'scope', filter.scope));
|
|
625
655
|
for (const runtime of filter.runtimes)
|
|
626
656
|
items.push(this.pill(runtime, 'runtime', runtime));
|
|
627
657
|
for (const role of filter.roles)
|
|
628
658
|
items.push(this.pill(role, 'role', role));
|
|
659
|
+
for (const product of filter.products)
|
|
660
|
+
items.push(this.pill(`product: ${product}`, 'product', product));
|
|
629
661
|
pills.replaceChildren(...items);
|
|
630
662
|
}
|
|
663
|
+
/** A change scope's name, read from the Filter popover's markup: the shell owns the ONE copy. */
|
|
664
|
+
scopeLabel(scope) {
|
|
665
|
+
return document.querySelector(`[data-wp-scope-label="${scope}"]`)?.textContent ?? scope;
|
|
666
|
+
}
|
|
631
667
|
/** A pill, built with text nodes so no value is parsed as HTML. */
|
|
632
668
|
pill(label, group, value) {
|
|
633
669
|
const pill = document.createElement('span');
|
|
@@ -652,8 +688,8 @@ class GraphDrawer {
|
|
|
652
688
|
// An available scan with a note is an EMPTY one, e.g. "Nothing changed on this branch yet".
|
|
653
689
|
if (impact.reason !== '')
|
|
654
690
|
return `${impact.label}: ${impact.reason}.`;
|
|
655
|
-
const stats = `${impact.label}: ${impact.touched.length}
|
|
656
|
-
`${impact.buildInputs.length}
|
|
691
|
+
const stats = `${impact.label}: ${impact.touched.length} changed · ${impact.affected.length} dependents · ` +
|
|
692
|
+
`${impact.buildInputs.length} dependencies — nx affected, ${impact.changedFiles} changed files.`;
|
|
657
693
|
const cause = this.globalCause(impact);
|
|
658
694
|
return cause === '' ? stats : `${cause}. ${stats}`;
|
|
659
695
|
}
|
|
@@ -702,7 +738,7 @@ class GraphDrawer {
|
|
|
702
738
|
}
|
|
703
739
|
class GraphPage extends WpFilterPage {
|
|
704
740
|
highlighter = null;
|
|
705
|
-
modes = new WpModeState();
|
|
741
|
+
modes = new WpModeState(this.model.nodes.some((node) => node.products.length > 0));
|
|
706
742
|
projects = new WpProjectFilter(this.model, this.modes);
|
|
707
743
|
applied = new WpFilterChoice();
|
|
708
744
|
drawer = new GraphDrawer(this);
|
|
@@ -726,6 +762,8 @@ class GraphPage extends WpFilterPage {
|
|
|
726
762
|
return modes.architecture;
|
|
727
763
|
if (this.modes.mode === 'impact')
|
|
728
764
|
return modes[this.modes.status(node.id)];
|
|
765
|
+
if (this.modes.mode === 'product')
|
|
766
|
+
return modes.product;
|
|
729
767
|
return modes.runtime;
|
|
730
768
|
}
|
|
731
769
|
usesFullDot() {
|
|
@@ -821,10 +859,8 @@ class GraphPage extends WpFilterPage {
|
|
|
821
859
|
const next = this.applied.copy();
|
|
822
860
|
if (group === 'scope')
|
|
823
861
|
next.scope = 'everything';
|
|
824
|
-
else if (group === 'role')
|
|
825
|
-
next.roles.delete(value);
|
|
826
862
|
else
|
|
827
|
-
next.
|
|
863
|
+
next.group(group).delete(value);
|
|
828
864
|
this.applyFilter(next);
|
|
829
865
|
}
|
|
830
866
|
syncControls() {
|