@webpieces/nx-webpieces-rules 0.4.897 → 0.4.899
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 +5 -1
- package/package.json +7 -7
- package/src/configs/tag-truth-configs.d.ts +31 -5
- package/src/configs/tag-truth-configs.js +37 -6
- package/src/configs/tag-truth-configs.js.map +1 -1
- package/src/executors/generate/executor.js +3 -3
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/visualize/executor.js +3 -1
- package/src/executors/visualize/executor.js.map +1 -1
- package/src/index.d.ts +3 -2
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
- package/src/lib/graph-color-modes.d.ts +8 -0
- package/src/lib/graph-color-modes.js +7 -0
- package/src/lib/graph-color-modes.js.map +1 -1
- package/src/lib/graph-filter.client.d.ts +5 -2
- package/src/lib/graph-filter.client.js +9 -4
- package/src/lib/graph-filter.client.js.map +1 -1
- package/src/lib/graph-legend.d.ts +8 -0
- package/src/lib/graph-legend.js +24 -2
- package/src/lib/graph-legend.js.map +1 -1
- package/src/lib/graph-products.d.ts +14 -0
- package/src/lib/graph-products.js +22 -1
- package/src/lib/graph-products.js.map +1 -1
- package/src/lib/graph-render-model.d.ts +19 -2
- package/src/lib/graph-render-model.js +19 -4
- package/src/lib/graph-render-model.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +2 -0
- package/src/lib/graph-visualizer.client.js +6 -0
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +11 -1
- package/src/lib/graph-visualizer.js +26 -7
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/product-dependencies.d.ts +52 -0
- package/src/lib/product-dependencies.js +128 -0
- package/src/lib/product-dependencies.js.map +1 -0
- package/src/lib/tag-truth.d.ts +12 -3
- package/src/lib/tag-truth.js +27 -4
- package/src/lib/tag-truth.js.map +1 -1
- package/src/rule-help.js +1 -1
- package/src/rule-help.js.map +1 -1
- package/src/rule-settings.js +1 -0
- package/src/rule-settings.js.map +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EnhancedGraph } from './graph-sorter';
|
|
2
2
|
import { GraphRenderModel } from './graph-render-model';
|
|
3
|
+
import { ProductDependencyMatrix } from './product-dependencies';
|
|
3
4
|
/**
|
|
4
5
|
* One node's design page, handed to the browser so the node menu can offer "View Design" for the
|
|
5
6
|
* boxes that HAVE one and omit the item entirely for the boxes that do not.
|
|
@@ -48,6 +49,7 @@ export declare class GraphVisualizer {
|
|
|
48
49
|
private readonly shell;
|
|
49
50
|
private readonly pageStyles;
|
|
50
51
|
private readonly navigation;
|
|
52
|
+
private readonly boundary;
|
|
51
53
|
/**
|
|
52
54
|
* How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
|
|
53
55
|
* ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
|
|
@@ -122,6 +124,11 @@ export declare class GraphVisualizer {
|
|
|
122
124
|
private levelBands;
|
|
123
125
|
private dotNodes;
|
|
124
126
|
private dotEdges;
|
|
127
|
+
/**
|
|
128
|
+
* The Product color mode's extra attributes for one dependency: dashed, thicker and in the color
|
|
129
|
+
* of the product it crosses into, or '' for an edge that stays inside its source's products.
|
|
130
|
+
*/
|
|
131
|
+
private crossProductAttrs;
|
|
125
132
|
/**
|
|
126
133
|
* Generate the interactive page: the drawer shell around a viz.js-rendered graph.
|
|
127
134
|
*
|
|
@@ -162,8 +169,11 @@ export declare class GraphVisualizer {
|
|
|
162
169
|
* come and go. The DOT is embedded in the HTML (rendered client-side by
|
|
163
170
|
* viz.js). Output is deterministic (sorted graph in → same bytes out) so git
|
|
164
171
|
* only shows a diff when the architecture actually changed.
|
|
172
|
+
*
|
|
173
|
+
* `productDependencies` is the `product-tags` rule's matrix, listed in the Product legend; null
|
|
174
|
+
* when the rule is OFF, and the legend then lists no matrix.
|
|
165
175
|
*/
|
|
166
|
-
writeVisualization(graph: EnhancedGraph, workspaceRoot: string, title?: string): VisualizationPaths;
|
|
176
|
+
writeVisualization(graph: EnhancedGraph, workspaceRoot: string, productDependencies: ProductDependencyMatrix | null, title?: string): VisualizationPaths;
|
|
167
177
|
/**
|
|
168
178
|
* Open the HTML visualization in the default browser
|
|
169
179
|
*/
|
|
@@ -105,6 +105,7 @@ class GraphVisualizer {
|
|
|
105
105
|
shell = new graph_page_shell_1.GraphPageShell();
|
|
106
106
|
pageStyles = new graph_page_styles_1.GraphPageStyles();
|
|
107
107
|
navigation = new graph_navigation_1.GraphNavigation(graph_navigation_1.NavigationLayout.FLOATING);
|
|
108
|
+
boundary = new graph_products_1.ProductBoundary();
|
|
108
109
|
/**
|
|
109
110
|
* How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
|
|
110
111
|
* ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
|
|
@@ -215,11 +216,12 @@ class GraphVisualizer {
|
|
|
215
216
|
model.header = dot;
|
|
216
217
|
const bands = this.levelBands(graph);
|
|
217
218
|
model.bands = bands;
|
|
218
|
-
|
|
219
|
+
const palette = graph_products_1.ProductPalette.fromGraph(graph);
|
|
220
|
+
dot += this.dotNodes(graph, model, palette);
|
|
219
221
|
dot += '\n';
|
|
220
222
|
dot += this.bandLayout.dot(bands);
|
|
221
223
|
dot += '\n';
|
|
222
|
-
dot += this.dotEdges(graph, model);
|
|
224
|
+
dot += this.dotEdges(graph, model, palette);
|
|
223
225
|
const footerStart = dot.length;
|
|
224
226
|
dot += '\n labelloc="t";\n';
|
|
225
227
|
dot += ` label="${(0, dot_syntax_1.dotValue)(title)}\\n(from architecture/dependencies.json)";\n`;
|
|
@@ -289,8 +291,9 @@ class GraphVisualizer {
|
|
|
289
291
|
}
|
|
290
292
|
// Edge lines (dependencies). An edge to an api-lib is styled by WHY it exists
|
|
291
293
|
// (implements/uses/uses-implements, from apiRelations); every other dependency
|
|
292
|
-
// keeps the default plain arrow.
|
|
293
|
-
|
|
294
|
+
// keeps the default plain arrow. In the Product color mode an edge crossing into another
|
|
295
|
+
// product is ALSO dashed in that product's color (#1204), so product boundaries read at a glance.
|
|
296
|
+
dotEdges(graph, model, palette) {
|
|
294
297
|
let dot = '';
|
|
295
298
|
for (const project of Object.keys(graph)) {
|
|
296
299
|
const info = graph[project];
|
|
@@ -302,11 +305,22 @@ class GraphVisualizer {
|
|
|
302
305
|
// is dropped so no connection dangles into empty space.
|
|
303
306
|
if (graph[dep] !== undefined && this.isHidden(graph[dep]))
|
|
304
307
|
continue;
|
|
305
|
-
dot += model.edge(nodeId, this.names.getNodeId(dep), this.edgeDot(nodeId, this.names.getNodeId(dep), info.apiRelations?.[dep]));
|
|
308
|
+
dot += model.edge(nodeId, this.names.getNodeId(dep), this.edgeDot(nodeId, this.names.getNodeId(dep), info.apiRelations?.[dep]), this.crossProductAttrs(info.products ?? [], graph[dep]?.products ?? [], model, palette));
|
|
306
309
|
}
|
|
307
310
|
}
|
|
308
311
|
return dot;
|
|
309
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* The Product color mode's extra attributes for one dependency: dashed, thicker and in the color
|
|
315
|
+
* of the product it crosses into, or '' for an edge that stays inside its source's products.
|
|
316
|
+
*/
|
|
317
|
+
crossProductAttrs(from, to, model, palette) {
|
|
318
|
+
const into = this.boundary.crossedInto(from, to);
|
|
319
|
+
if (into === null)
|
|
320
|
+
return '';
|
|
321
|
+
model.legend.crossProductEdges++;
|
|
322
|
+
return `style=dashed, color="${palette.colorOf(into).color}", penwidth=2`;
|
|
323
|
+
}
|
|
310
324
|
/**
|
|
311
325
|
* Generate the interactive page: the drawer shell around a viz.js-rendered graph.
|
|
312
326
|
*
|
|
@@ -400,15 +414,20 @@ class GraphVisualizer {
|
|
|
400
414
|
* come and go. The DOT is embedded in the HTML (rendered client-side by
|
|
401
415
|
* viz.js). Output is deterministic (sorted graph in → same bytes out) so git
|
|
402
416
|
* only shows a diff when the architecture actually changed.
|
|
417
|
+
*
|
|
418
|
+
* `productDependencies` is the `product-tags` rule's matrix, listed in the Product legend; null
|
|
419
|
+
* when the rule is OFF, and the legend then lists no matrix.
|
|
403
420
|
*/
|
|
404
|
-
writeVisualization(graph, workspaceRoot, title = 'Monorepo Dependency Architecture') {
|
|
421
|
+
writeVisualization(graph, workspaceRoot, productDependencies, title = 'Monorepo Dependency Architecture') {
|
|
405
422
|
const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);
|
|
406
423
|
if (!fs.existsSync(outputDir)) {
|
|
407
424
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
408
425
|
}
|
|
409
426
|
const lockControl = this.lockControl(graph);
|
|
410
427
|
const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);
|
|
411
|
-
const
|
|
428
|
+
const model = this.generateRenderModel(graph, title);
|
|
429
|
+
model.legend.productDependencies = productDependencies === null ? null : [...productDependencies.entries];
|
|
430
|
+
const html = this.generateHTML(model, this.designLinks(graph), title, lockControl, responsibilities);
|
|
412
431
|
const htmlPath = path.join(outputDir, 'dependencies.html');
|
|
413
432
|
fs.writeFileSync(htmlPath, html, 'utf-8');
|
|
414
433
|
return new VisualizationPaths(htmlPath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["import { GraphNavigation, NavigationLayout } from './graph-navigation';\n/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * All behavior lives on the injectable GraphVisualizer class so webpieces DI +\n * @DocumentDesign can wire it — module-scope functions are a dead end the DI\n * graph can't reach.\n */\n\nimport * as fs from 'fs';\nimport { SavedSnapshot } from './saved-snapshot';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiRelation, ApiRelationKind } from './api-usage/api-relations';\nimport { GraphNames, NodeIdOwner } from './graph-names';\nimport { LevelBand, LevelBandLayout } from './graph-level-bands';\nimport { ProjectAdjacency, ProjectCycleDetector } from './graph-cycles';\nimport { ResponsibilitiesRenderer } from './graph-responsibilities';\nimport { GraphNodeMenu } from './graph-node-menu';\nimport { dotValue } from './dot-syntax';\nimport { GraphRenderModel, GraphFilterAssets } from './graph-render-model';\nimport { NodeFacts, NodeModeStyler, ProductCount } from './graph-color-modes';\nimport { ProductPalette } from './graph-products';\nimport { GraphLegend } from './graph-legend';\nimport { GraphPageShell, ShellParts } from './graph-page-shell';\nimport { GraphPageStyles } from './graph-page-styles';\nimport { IMPACT_SIDECAR_SRC } from './graph-impact';\nimport { toError } from '../toError';\n\n/**\n * Directory (repo-relative) that the committed architecture HTML lives in.\n * Node click-through links are computed relative to this so they resolve when\n * the file is opened straight from the checkout.\n */\nconst ARCH_OUTPUT_DIR = 'architecture';\n\n/** Contracts named on one `implements` edge label before it truncates to \"+N more\". */\nconst MAX_EDGE_LABEL_APIS = 4;\n\n/**\n * One node's design page, handed to the browser so the node menu can offer \"View Design\" for the\n * boxes that HAVE one and omit the item entirely for the boxes that do not.\n *\n * The link is data on the PAGE now, not a `URL=` attribute in the DOT: a URL made viz.js wrap the\n * box in an `<a>` and clicking navigated straight out, which the menu replaces.\n */\nexport class DesignLink {\n constructor(\n public readonly nodeId: string,\n public readonly href: string,\n ) {}\n}\n\nexport class VisualizationPaths {\n htmlPath: string;\n\n constructor(htmlPath: string) {\n this.htmlPath = htmlPath;\n }\n}\n\n/**\n * The token the client scripts carry where the DOT belongs. It must appear EXACTLY ONCE in a compiled\n * client — the inliner is a blind split/join, so a second occurrence (in a comment, say) would be\n * replaced by the entire DOT too, bloating every generated page.\n */\nexport const CLIENT_MODEL_PLACEHOLDER = '__' + 'RENDER_MODEL' + '__';\n\n/** Same contract as CLIENT_MODEL_PLACEHOLDER, for the node → design.html links the menu offers. */\nexport const CLIENT_DESIGN_LINKS_PLACEHOLDER = '__' + 'DESIGN_LINKS' + '__';\n\n/**\n * Read a compiled browser client sitting beside this file.\n *\n * It is generated from the matching `.client.ts` by tsc, so it exists in `dist` and in the published\n * tarball but NOT in a source checkout. The error says that outright rather than surfacing a bare\n * ENOENT, because \"the build has not run\" and \"the file is missing\" look identical otherwise.\n */\n// webpieces-disable no-function-outside-class -- module-level resolver for the default injected into GraphVisualizer/RuntimeHtmlPage; making it a class would need a container in a module both of them load eagerly\nexport function readCompiledClient(name: string): string {\n const file = path.join(__dirname, name);\n if (!fs.existsSync(file)) {\n throw new Error(\n `${name} not found beside ${__dirname}. It is COMPILED from ${name.replace(/\\.js$/, '.ts')} ` +\n 'by tsc, so it only exists after a build — run the package build, or inject the text.',\n );\n }\n const source = fs.readFileSync(file, 'utf-8');\n return name === 'graph-visualizer.client.js' ? readCompiledClient('graph-mode-state.client.js') + source : source;\n}\n\nexport class GraphVisualizer {\n private readonly filterAssets = new GraphFilterAssets();\n private readonly snapshot = new SavedSnapshot();\n private readonly names = new GraphNames();\n private readonly responsibilities = new ResponsibilitiesRenderer();\n private readonly bandLayout = new LevelBandLayout();\n private readonly cycles = new ProjectCycleDetector();\n /** The ONE floating-node-menu implementation, shared with the per-project design pages. */\n private readonly nodeMenu = new GraphNodeMenu();\n private readonly styler = new NodeModeStyler();\n private readonly legend = new GraphLegend();\n private readonly shell = new GraphPageShell();\n private readonly pageStyles = new GraphPageStyles();\n private readonly navigation = new GraphNavigation(NavigationLayout.FLOATING);\n\n /**\n * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD\n * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in\n * a source checkout (the source there is .client.ts). A unit test running from source supplies the\n * text itself rather than requiring the package to have been built first.\n */\n constructor(\n private readonly clientJs: () => string = (): string =>\n readCompiledClient('graph-visualizer.client.js'),\n private readonly filterJs: () => string = (): string =>\n readCompiledClient('graph-filter.client.js'),\n ) {}\n\n /**\n * A project tagged `drawOnGraph:false` is hidden from the rendered graph —\n * its node, its rank placement, its lock-search option, its responsibilities\n * card, and every edge touching it are all omitted. It stays in the JSON.\n */\n private isHidden(entry: EnhancedGraph[string]): boolean {\n return entry.drawOnGraph === false;\n }\n\n /**\n * Edge styling by API-relation kind (why the edge exists):\n * implements → BLACK dashed (a controller serves this api-lib's contract)\n * uses → BLACK solid (a generated client calls it) — same as a\n * plain library import, since a plain dependency IS a use.\n * uses-implements → BLUE dashed, thicker (does both — implements some\n * contracts of the api-lib, uses others)\n * plain lib (none) → the default thin black solid arrow, unchanged.\n * `kind` is undefined for every non-api-lib dependency edge.\n */\n private edgeAttrs(kind: ApiRelationKind | undefined): string {\n if (kind === 'implements') return 'style=dashed';\n if (kind === 'uses-implements') return 'style=dashed, color=\"#1976d2\", penwidth=2';\n return '';\n }\n\n /**\n * The DOT attribute list for ONE dependency edge. An edge that IMPLEMENTS contracts is also\n * LABELED with them: the dashed line alone says \"something here is implemented\", which reads as\n * \"the tool did not detect anything\" to everyone who has not memorized the legend. Naming the\n * contracts makes `auth-store-api` visibly resolve to the server that serves it — the single\n * most important relationship in a microservice architecture, and the one the diagram was\n * silent about.\n */\n private edgeDot(from: string, to: string, relation: ApiRelation | undefined): string {\n const attrs: string[] = [];\n const styling = this.edgeAttrs(relation?.kind);\n if (styling !== '') attrs.push(styling);\n const served = (relation?.implements ?? []).map((ref: ApiRef) => ref.api);\n if (served.length > 0)\n attrs.push(`label=\"implements: ${this.labelledApis(served)}\", fontsize=9`);\n const suffix = attrs.length === 0 ? '' : ` [${attrs.join(', ')}]`;\n return ` \"${dotValue(from)}\" -> \"${dotValue(to)}\"${suffix};\\n`;\n }\n\n /**\n * The contract names for an edge label, sorted and capped so a shared api-lib serving a dozen\n * contracts cannot blow the edge label up into a wall of text. The truncation is stated in the\n * label (\"+N more\") rather than silent — the full list is in dependencies.json.\n */\n private labelledApis(apis: string[]): string {\n const sorted = [...apis].sort();\n if (sorted.length <= MAX_EDGE_LABEL_APIS) return sorted.join(', ');\n const shown = sorted.slice(0, MAX_EDGE_LABEL_APIS).join(', ');\n return `${shown} +${sorted.length - MAX_EDGE_LABEL_APIS} more`;\n }\n\n /**\n * Href for a node's design page: the project's committed design.html, made\n * relative to architecture/dependencies.html. Returns null when the project\n * has no generated DI design (no design.json → no design page exists).\n */\n private designHtmlHref(designFile: string | undefined): string | null {\n if (!designFile) return null;\n const designHtml = designFile.replace(/design\\.json$/, 'design.html');\n return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);\n }\n\n /**\n * The design pages that EXIST, one entry per visible node that has one. A node absent from this\n * list gets no \"View Design\" item at all — the item is never rendered dead or greyed out.\n */\n designLinks(graph: EnhancedGraph): DesignLink[] {\n const links: DesignLink[] = [];\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const href = this.designHtmlHref(info.designFile);\n if (href === null) continue;\n links.push(new DesignLink(this.names.getNodeId(project), href));\n }\n return links;\n }\n\n /**\n * Generate Graphviz DOT format from the graph\n */\n generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n return this.generateRenderModel(graph, title).fullDot;\n }\n\n generateRenderModel(\n graph: EnhancedGraph,\n title: string = 'Monorepo Dependency Architecture',\n ): GraphRenderModel {\n this.assertDrawable(graph);\n const model = new GraphRenderModel();\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n model.header = dot;\n const bands = this.levelBands(graph);\n model.bands = bands;\n\n dot += this.dotNodes(graph, model, ProductPalette.fromGraph(graph));\n dot += '\\n';\n dot += this.bandLayout.dot(bands);\n dot += '\\n';\n dot += this.dotEdges(graph, model);\n\n const footerStart = dot.length;\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n model.footer = dot.slice(footerStart);\n model.completeEndpoints();\n model.fullDot = dot;\n return model;\n }\n\n /**\n * Refuse to draw a graph that would render something false.\n *\n * Two conditions, both of which produced a confidently-wrong picture with no warning at all:\n * - two projects sharing a node id fuse into one box, which UNIONS their two rank sets and\n * collapses two whole dependency levels onto one row (see graph-names.ts);\n * - a cycle makes the level numbers every row is keyed on meaningless (see graph-cycles.ts).\n */\n private assertDrawable(graph: EnhancedGraph): void {\n const owners: NodeIdOwner[] = [];\n const adjacency: ProjectAdjacency = {};\n for (const project of Object.keys(graph)) {\n owners.push(new NodeIdOwner(project, graph[project].level));\n adjacency[project] = graph[project].dependsOn ?? [];\n }\n this.names.assertUniqueNodeIds(owners);\n this.cycles.assertAcyclic(adjacency, 'architecture/dependencies.json');\n }\n\n /**\n * The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST\n * so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no\n * stray rank=same name is emitted for an absent node). LevelBandLayout turns these into the\n * rank sets and the invisible chain that pins them — see graph-level-bands.ts for why the\n * chain is required at all.\n */\n private levelBands(graph: EnhancedGraph): LevelBand[] {\n const byLevel = new Map<number, string[]>();\n for (const project of Object.keys(graph)) {\n if (this.isHidden(graph[project])) continue;\n const level = graph[project].level;\n const nodeIds = byLevel.get(level);\n if (nodeIds === undefined) byLevel.set(level, [this.names.getNodeId(project)]);\n else nodeIds.push(this.names.getNodeId(project));\n }\n const levels = [...byLevel.keys()].sort((a: number, b: number): number => b - a);\n return levels.map(\n (level: number): LevelBand =>\n new LevelBand(level, [...(byLevel.get(level) as string[])].sort()),\n );\n }\n\n // Node lines: one statement per color mode (graph-color-modes.ts) — the label is the short\n // name plus a short meta line, because the fill now shows what the env list used to spell out.\n // No node carries a URL: EVERY box is clickable and opens the floating node menu instead, which\n // is where a design page is reached (see designLinks).\n private dotNodes(graph: EnhancedGraph, model: GraphRenderModel, palette: ProductPalette): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n // Identity is the project key; the LABEL is the pretty short name.\n const facts = new NodeFacts(\n this.names.getNodeId(project),\n this.names.getShortName(project),\n info.level,\n info.role ?? 'lib',\n info.framework ?? [],\n info.products ?? [],\n );\n this.styler.record(model.legend, facts, palette);\n dot += model.styledNode(facts, this.styler.dots(facts, palette));\n }\n return dot;\n }\n\n // Edge lines (dependencies). An edge to an api-lib is styled by WHY it exists\n // (implements/uses/uses-implements, from apiRelations); every other dependency\n // keeps the default plain arrow.\n private dotEdges(graph: EnhancedGraph, model: GraphRenderModel): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const nodeId = this.names.getNodeId(project);\n for (const dep of info.dependsOn || []) {\n // Both endpoints must be visible — an edge to/from a hidden box\n // is dropped so no connection dangles into empty space.\n if (graph[dep] !== undefined && this.isHidden(graph[dep])) continue;\n dot += model.edge(\n nodeId,\n this.names.getNodeId(dep),\n this.edgeDot(nodeId, this.names.getNodeId(dep), info.apiRelations?.[dep]),\n );\n }\n }\n return dot;\n }\n\n /**\n * Generate the interactive page: the drawer shell around a viz.js-rendered graph.\n *\n * The Impact sidecar is a plain `<script src>` loaded BEFORE the page scripts, so the page knows\n * the branch's impact when it first draws. It is per-branch and gitignored (graph-impact.ts):\n * when it is absent the browser skips it and Impact is disabled with its reason.\n */\n generateHTML(\n model: GraphRenderModel,\n links: DesignLink[],\n title: string = 'Monorepo Dependency Architecture',\n lockControl: string = '',\n responsibilitiesHtml: string = '',\n ): string {\n const parts = new ShellParts(\n title,\n lockControl,\n this.legend.sections(model.legend),\n this.legend.edgeKey(),\n this.filterAssets.html(),\n this.snapshot.html(),\n responsibilitiesHtml,\n model.legend.products.map((count: ProductCount): string => count.product),\n );\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <script src=\"${IMPACT_SIDECAR_SRC}\"></script>\n <style>${this.styles()}</style>\n</head>\n<body>\n ${this.shell.body(parts)}\n <script>${this.nodeMenu.script()}</script>\n <script>${this.filterJs()}</script>\n <script>${this.navigation.script()}</script>\n <script>${this.script(model, links)}</script>\n</body>\n</html>`;\n }\n\n /** Shared menu/dim/filter/zoom rules first, then the page's own, so its `#graph` rules win. */\n private styles(): string {\n return (\n this.nodeMenu.styles() +\n this.nodeMenu.dimStyles('#graph') +\n this.filterAssets.styles() +\n this.navigation.styles() +\n this.pageStyles.css()\n );\n }\n\n /**\n * The drawer's type-to-search lock field, replacing a `<select>` that listed every project and\n * was unusable at hundreds of options. Picking a project LOCKS the graph into that box's hover\n * view — its full ancestor + descendant chain stays lit while everything else dims — and narrows\n * the responsibilities panel to that chain. Clearing the field, or Esc, unlocks.\n *\n * Each option's VALUE is the node id the SVG is keyed on; its label is the level and short name.\n * Options are ordered by level DESCENDING to match the responsibilities cards.\n */\n lockControl(graph: EnhancedGraph): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n let options = '';\n for (const project of projects) {\n if (this.isHidden(graph[project])) continue;\n const nodeId = this.escapeHtml(this.names.getNodeId(project));\n const shortName = this.escapeHtml(this.names.getShortName(project));\n options += `<option value=\"${nodeId}\">L${graph[project].level} · ${shortName}</option>`;\n }\n return (\n '<input type=\"search\" id=\"wp-lock\" list=\"wp-lock-options\" placeholder=\"Lock a project… ( / )\" ' +\n 'autocomplete=\"off\" spellcheck=\"false\">' +\n `<datalist id=\"wp-lock-options\">${options}</datalist>`\n );\n }\n\n private escapeHtml(text: string): string {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n }\n\n /**\n * The page script. The browser code lives in graph-visualizer.client.ts — real, linted TypeScript\n * that tsc compiles in place, so what this inlines is its COMPILED .js sitting beside this file.\n *\n * The substitution is a blind split/join, which is why the placeholder token must appear EXACTLY\n * ONCE in the client (never in one of its comments): every literal occurrence would otherwise be\n * replaced by the whole DOT.\n */\n private script(model: GraphRenderModel, links: DesignLink[]): string {\n return this.clientJs()\n .split(CLIENT_MODEL_PLACEHOLDER)\n .join(this.filterAssets.json(model))\n .split(CLIENT_DESIGN_LINKS_PLACEHOLDER)\n .join(this.filterAssets.json(links));\n }\n\n /**\n * Write the committed architecture visualization to\n * architecture/dependencies.html, next to dependencies.json.\n *\n * This is a checked-in artifact, regenerated deterministically by\n * architecture:generate so every box keeps its menu, and the \"View Design\"\n * item keeps pointing at each project's committed design.html as designs\n * come and go. The DOT is embedded in the HTML (rendered client-side by\n * viz.js). Output is deterministic (sorted graph in → same bytes out) so git\n * only shows a diff when the architecture actually changed.\n */\n writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'Monorepo Dependency Architecture',\n ): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const lockControl = this.lockControl(graph);\n const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);\n const html = this.generateHTML(\n this.generateRenderModel(graph, title),\n this.designLinks(graph),\n title,\n lockControl,\n responsibilities,\n );\n const htmlPath = path.join(outputDir, 'dependencies.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return new VisualizationPaths(htmlPath);\n }\n\n /**\n * Open the HTML visualization in the default browser\n */\n openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not open browser: ${error.message}`);\n return false;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["import { GraphNavigation, NavigationLayout } from './graph-navigation';\n/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * All behavior lives on the injectable GraphVisualizer class so webpieces DI +\n * @DocumentDesign can wire it — module-scope functions are a dead end the DI\n * graph can't reach.\n */\n\nimport * as fs from 'fs';\nimport { SavedSnapshot } from './saved-snapshot';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { ApiRef, ApiRelation, ApiRelationKind } from './api-usage/api-relations';\nimport { GraphNames, NodeIdOwner } from './graph-names';\nimport { LevelBand, LevelBandLayout } from './graph-level-bands';\nimport { ProjectAdjacency, ProjectCycleDetector } from './graph-cycles';\nimport { ResponsibilitiesRenderer } from './graph-responsibilities';\nimport { GraphNodeMenu } from './graph-node-menu';\nimport { dotValue } from './dot-syntax';\nimport { GraphRenderModel, GraphFilterAssets } from './graph-render-model';\nimport { NodeFacts, NodeModeStyler, ProductCount } from './graph-color-modes';\nimport { ProductBoundary, ProductPalette } from './graph-products';\nimport { ProductDependencyMatrix } from './product-dependencies';\nimport { GraphLegend } from './graph-legend';\nimport { GraphPageShell, ShellParts } from './graph-page-shell';\nimport { GraphPageStyles } from './graph-page-styles';\nimport { IMPACT_SIDECAR_SRC } from './graph-impact';\nimport { toError } from '../toError';\n\n/**\n * Directory (repo-relative) that the committed architecture HTML lives in.\n * Node click-through links are computed relative to this so they resolve when\n * the file is opened straight from the checkout.\n */\nconst ARCH_OUTPUT_DIR = 'architecture';\n\n/** Contracts named on one `implements` edge label before it truncates to \"+N more\". */\nconst MAX_EDGE_LABEL_APIS = 4;\n\n/**\n * One node's design page, handed to the browser so the node menu can offer \"View Design\" for the\n * boxes that HAVE one and omit the item entirely for the boxes that do not.\n *\n * The link is data on the PAGE now, not a `URL=` attribute in the DOT: a URL made viz.js wrap the\n * box in an `<a>` and clicking navigated straight out, which the menu replaces.\n */\nexport class DesignLink {\n constructor(\n public readonly nodeId: string,\n public readonly href: string,\n ) {}\n}\n\nexport class VisualizationPaths {\n htmlPath: string;\n\n constructor(htmlPath: string) {\n this.htmlPath = htmlPath;\n }\n}\n\n/**\n * The token the client scripts carry where the DOT belongs. It must appear EXACTLY ONCE in a compiled\n * client — the inliner is a blind split/join, so a second occurrence (in a comment, say) would be\n * replaced by the entire DOT too, bloating every generated page.\n */\nexport const CLIENT_MODEL_PLACEHOLDER = '__' + 'RENDER_MODEL' + '__';\n\n/** Same contract as CLIENT_MODEL_PLACEHOLDER, for the node → design.html links the menu offers. */\nexport const CLIENT_DESIGN_LINKS_PLACEHOLDER = '__' + 'DESIGN_LINKS' + '__';\n\n/**\n * Read a compiled browser client sitting beside this file.\n *\n * It is generated from the matching `.client.ts` by tsc, so it exists in `dist` and in the published\n * tarball but NOT in a source checkout. The error says that outright rather than surfacing a bare\n * ENOENT, because \"the build has not run\" and \"the file is missing\" look identical otherwise.\n */\n// webpieces-disable no-function-outside-class -- module-level resolver for the default injected into GraphVisualizer/RuntimeHtmlPage; making it a class would need a container in a module both of them load eagerly\nexport function readCompiledClient(name: string): string {\n const file = path.join(__dirname, name);\n if (!fs.existsSync(file)) {\n throw new Error(\n `${name} not found beside ${__dirname}. It is COMPILED from ${name.replace(/\\.js$/, '.ts')} ` +\n 'by tsc, so it only exists after a build — run the package build, or inject the text.',\n );\n }\n const source = fs.readFileSync(file, 'utf-8');\n return name === 'graph-visualizer.client.js' ? readCompiledClient('graph-mode-state.client.js') + source : source;\n}\n\nexport class GraphVisualizer {\n private readonly filterAssets = new GraphFilterAssets();\n private readonly snapshot = new SavedSnapshot();\n private readonly names = new GraphNames();\n private readonly responsibilities = new ResponsibilitiesRenderer();\n private readonly bandLayout = new LevelBandLayout();\n private readonly cycles = new ProjectCycleDetector();\n /** The ONE floating-node-menu implementation, shared with the per-project design pages. */\n private readonly nodeMenu = new GraphNodeMenu();\n private readonly styler = new NodeModeStyler();\n private readonly legend = new GraphLegend();\n private readonly shell = new GraphPageShell();\n private readonly pageStyles = new GraphPageStyles();\n private readonly navigation = new GraphNavigation(NavigationLayout.FLOATING);\n private readonly boundary = new ProductBoundary();\n\n /**\n * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD\n * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in\n * a source checkout (the source there is .client.ts). A unit test running from source supplies the\n * text itself rather than requiring the package to have been built first.\n */\n constructor(\n private readonly clientJs: () => string = (): string =>\n readCompiledClient('graph-visualizer.client.js'),\n private readonly filterJs: () => string = (): string =>\n readCompiledClient('graph-filter.client.js'),\n ) {}\n\n /**\n * A project tagged `drawOnGraph:false` is hidden from the rendered graph —\n * its node, its rank placement, its lock-search option, its responsibilities\n * card, and every edge touching it are all omitted. It stays in the JSON.\n */\n private isHidden(entry: EnhancedGraph[string]): boolean {\n return entry.drawOnGraph === false;\n }\n\n /**\n * Edge styling by API-relation kind (why the edge exists):\n * implements → BLACK dashed (a controller serves this api-lib's contract)\n * uses → BLACK solid (a generated client calls it) — same as a\n * plain library import, since a plain dependency IS a use.\n * uses-implements → BLUE dashed, thicker (does both — implements some\n * contracts of the api-lib, uses others)\n * plain lib (none) → the default thin black solid arrow, unchanged.\n * `kind` is undefined for every non-api-lib dependency edge.\n */\n private edgeAttrs(kind: ApiRelationKind | undefined): string {\n if (kind === 'implements') return 'style=dashed';\n if (kind === 'uses-implements') return 'style=dashed, color=\"#1976d2\", penwidth=2';\n return '';\n }\n\n /**\n * The DOT attribute list for ONE dependency edge. An edge that IMPLEMENTS contracts is also\n * LABELED with them: the dashed line alone says \"something here is implemented\", which reads as\n * \"the tool did not detect anything\" to everyone who has not memorized the legend. Naming the\n * contracts makes `auth-store-api` visibly resolve to the server that serves it — the single\n * most important relationship in a microservice architecture, and the one the diagram was\n * silent about.\n */\n private edgeDot(from: string, to: string, relation: ApiRelation | undefined): string {\n const attrs: string[] = [];\n const styling = this.edgeAttrs(relation?.kind);\n if (styling !== '') attrs.push(styling);\n const served = (relation?.implements ?? []).map((ref: ApiRef) => ref.api);\n if (served.length > 0)\n attrs.push(`label=\"implements: ${this.labelledApis(served)}\", fontsize=9`);\n const suffix = attrs.length === 0 ? '' : ` [${attrs.join(', ')}]`;\n return ` \"${dotValue(from)}\" -> \"${dotValue(to)}\"${suffix};\\n`;\n }\n\n /**\n * The contract names for an edge label, sorted and capped so a shared api-lib serving a dozen\n * contracts cannot blow the edge label up into a wall of text. The truncation is stated in the\n * label (\"+N more\") rather than silent — the full list is in dependencies.json.\n */\n private labelledApis(apis: string[]): string {\n const sorted = [...apis].sort();\n if (sorted.length <= MAX_EDGE_LABEL_APIS) return sorted.join(', ');\n const shown = sorted.slice(0, MAX_EDGE_LABEL_APIS).join(', ');\n return `${shown} +${sorted.length - MAX_EDGE_LABEL_APIS} more`;\n }\n\n /**\n * Href for a node's design page: the project's committed design.html, made\n * relative to architecture/dependencies.html. Returns null when the project\n * has no generated DI design (no design.json → no design page exists).\n */\n private designHtmlHref(designFile: string | undefined): string | null {\n if (!designFile) return null;\n const designHtml = designFile.replace(/design\\.json$/, 'design.html');\n return path.posix.relative(ARCH_OUTPUT_DIR, designHtml);\n }\n\n /**\n * The design pages that EXIST, one entry per visible node that has one. A node absent from this\n * list gets no \"View Design\" item at all — the item is never rendered dead or greyed out.\n */\n designLinks(graph: EnhancedGraph): DesignLink[] {\n const links: DesignLink[] = [];\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const href = this.designHtmlHref(info.designFile);\n if (href === null) continue;\n links.push(new DesignLink(this.names.getNodeId(project), href));\n }\n return links;\n }\n\n /**\n * Generate Graphviz DOT format from the graph\n */\n generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n return this.generateRenderModel(graph, title).fullDot;\n }\n\n generateRenderModel(\n graph: EnhancedGraph,\n title: string = 'Monorepo Dependency Architecture',\n ): GraphRenderModel {\n this.assertDrawable(graph);\n const model = new GraphRenderModel();\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n model.header = dot;\n const bands = this.levelBands(graph);\n model.bands = bands;\n\n const palette = ProductPalette.fromGraph(graph);\n dot += this.dotNodes(graph, model, palette);\n dot += '\\n';\n dot += this.bandLayout.dot(bands);\n dot += '\\n';\n dot += this.dotEdges(graph, model, palette);\n\n const footerStart = dot.length;\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n model.footer = dot.slice(footerStart);\n model.completeEndpoints();\n model.fullDot = dot;\n return model;\n }\n\n /**\n * Refuse to draw a graph that would render something false.\n *\n * Two conditions, both of which produced a confidently-wrong picture with no warning at all:\n * - two projects sharing a node id fuse into one box, which UNIONS their two rank sets and\n * collapses two whole dependency levels onto one row (see graph-names.ts);\n * - a cycle makes the level numbers every row is keyed on meaningless (see graph-cycles.ts).\n */\n private assertDrawable(graph: EnhancedGraph): void {\n const owners: NodeIdOwner[] = [];\n const adjacency: ProjectAdjacency = {};\n for (const project of Object.keys(graph)) {\n owners.push(new NodeIdOwner(project, graph[project].level));\n adjacency[project] = graph[project].dependsOn ?? [];\n }\n this.names.assertUniqueNodeIds(owners);\n this.cycles.assertAcyclic(adjacency, 'architecture/dependencies.json');\n }\n\n /**\n * The visible projects grouped into one band per dependency level, ordered HIGHEST LEVEL FIRST\n * so the emitted bands read top-to-bottom with L0 last (hidden projects are omitted, so no\n * stray rank=same name is emitted for an absent node). LevelBandLayout turns these into the\n * rank sets and the invisible chain that pins them — see graph-level-bands.ts for why the\n * chain is required at all.\n */\n private levelBands(graph: EnhancedGraph): LevelBand[] {\n const byLevel = new Map<number, string[]>();\n for (const project of Object.keys(graph)) {\n if (this.isHidden(graph[project])) continue;\n const level = graph[project].level;\n const nodeIds = byLevel.get(level);\n if (nodeIds === undefined) byLevel.set(level, [this.names.getNodeId(project)]);\n else nodeIds.push(this.names.getNodeId(project));\n }\n const levels = [...byLevel.keys()].sort((a: number, b: number): number => b - a);\n return levels.map(\n (level: number): LevelBand =>\n new LevelBand(level, [...(byLevel.get(level) as string[])].sort()),\n );\n }\n\n // Node lines: one statement per color mode (graph-color-modes.ts) — the label is the short\n // name plus a short meta line, because the fill now shows what the env list used to spell out.\n // No node carries a URL: EVERY box is clickable and opens the floating node menu instead, which\n // is where a design page is reached (see designLinks).\n private dotNodes(graph: EnhancedGraph, model: GraphRenderModel, palette: ProductPalette): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n // Identity is the project key; the LABEL is the pretty short name.\n const facts = new NodeFacts(\n this.names.getNodeId(project),\n this.names.getShortName(project),\n info.level,\n info.role ?? 'lib',\n info.framework ?? [],\n info.products ?? [],\n );\n this.styler.record(model.legend, facts, palette);\n dot += model.styledNode(facts, this.styler.dots(facts, palette));\n }\n return dot;\n }\n\n // Edge lines (dependencies). An edge to an api-lib is styled by WHY it exists\n // (implements/uses/uses-implements, from apiRelations); every other dependency\n // keeps the default plain arrow. In the Product color mode an edge crossing into another\n // product is ALSO dashed in that product's color (#1204), so product boundaries read at a glance.\n private dotEdges(graph: EnhancedGraph, model: GraphRenderModel, palette: ProductPalette): string {\n let dot = '';\n for (const project of Object.keys(graph)) {\n const info = graph[project];\n if (this.isHidden(info)) continue;\n const nodeId = this.names.getNodeId(project);\n for (const dep of info.dependsOn || []) {\n // Both endpoints must be visible — an edge to/from a hidden box\n // is dropped so no connection dangles into empty space.\n if (graph[dep] !== undefined && this.isHidden(graph[dep])) continue;\n dot += model.edge(\n nodeId,\n this.names.getNodeId(dep),\n this.edgeDot(nodeId, this.names.getNodeId(dep), info.apiRelations?.[dep]),\n this.crossProductAttrs(info.products ?? [], graph[dep]?.products ?? [], model, palette),\n );\n }\n }\n return dot;\n }\n\n /**\n * The Product color mode's extra attributes for one dependency: dashed, thicker and in the color\n * of the product it crosses into, or '' for an edge that stays inside its source's products.\n */\n private crossProductAttrs(\n from: readonly string[],\n to: readonly string[],\n model: GraphRenderModel,\n palette: ProductPalette,\n ): string {\n const into = this.boundary.crossedInto(from, to);\n if (into === null) return '';\n model.legend.crossProductEdges++;\n return `style=dashed, color=\"${palette.colorOf(into).color}\", penwidth=2`;\n }\n\n /**\n * Generate the interactive page: the drawer shell around a viz.js-rendered graph.\n *\n * The Impact sidecar is a plain `<script src>` loaded BEFORE the page scripts, so the page knows\n * the branch's impact when it first draws. It is per-branch and gitignored (graph-impact.ts):\n * when it is absent the browser skips it and Impact is disabled with its reason.\n */\n generateHTML(\n model: GraphRenderModel,\n links: DesignLink[],\n title: string = 'Monorepo Dependency Architecture',\n lockControl: string = '',\n responsibilitiesHtml: string = '',\n ): string {\n const parts = new ShellParts(\n title,\n lockControl,\n this.legend.sections(model.legend),\n this.legend.edgeKey(),\n this.filterAssets.html(),\n this.snapshot.html(),\n responsibilitiesHtml,\n model.legend.products.map((count: ProductCount): string => count.product),\n );\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@viz-js/viz@3.28.0/dist/viz-global.js\"></script>\n <script src=\"${IMPACT_SIDECAR_SRC}\"></script>\n <style>${this.styles()}</style>\n</head>\n<body>\n ${this.shell.body(parts)}\n <script>${this.nodeMenu.script()}</script>\n <script>${this.filterJs()}</script>\n <script>${this.navigation.script()}</script>\n <script>${this.script(model, links)}</script>\n</body>\n</html>`;\n }\n\n /** Shared menu/dim/filter/zoom rules first, then the page's own, so its `#graph` rules win. */\n private styles(): string {\n return (\n this.nodeMenu.styles() +\n this.nodeMenu.dimStyles('#graph') +\n this.filterAssets.styles() +\n this.navigation.styles() +\n this.pageStyles.css()\n );\n }\n\n /**\n * The drawer's type-to-search lock field, replacing a `<select>` that listed every project and\n * was unusable at hundreds of options. Picking a project LOCKS the graph into that box's hover\n * view — its full ancestor + descendant chain stays lit while everything else dims — and narrows\n * the responsibilities panel to that chain. Clearing the field, or Esc, unlocks.\n *\n * Each option's VALUE is the node id the SVG is keyed on; its label is the level and short name.\n * Options are ordered by level DESCENDING to match the responsibilities cards.\n */\n lockControl(graph: EnhancedGraph): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n let options = '';\n for (const project of projects) {\n if (this.isHidden(graph[project])) continue;\n const nodeId = this.escapeHtml(this.names.getNodeId(project));\n const shortName = this.escapeHtml(this.names.getShortName(project));\n options += `<option value=\"${nodeId}\">L${graph[project].level} · ${shortName}</option>`;\n }\n return (\n '<input type=\"search\" id=\"wp-lock\" list=\"wp-lock-options\" placeholder=\"Lock a project… ( / )\" ' +\n 'autocomplete=\"off\" spellcheck=\"false\">' +\n `<datalist id=\"wp-lock-options\">${options}</datalist>`\n );\n }\n\n private escapeHtml(text: string): string {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n }\n\n /**\n * The page script. The browser code lives in graph-visualizer.client.ts — real, linted TypeScript\n * that tsc compiles in place, so what this inlines is its COMPILED .js sitting beside this file.\n *\n * The substitution is a blind split/join, which is why the placeholder token must appear EXACTLY\n * ONCE in the client (never in one of its comments): every literal occurrence would otherwise be\n * replaced by the whole DOT.\n */\n private script(model: GraphRenderModel, links: DesignLink[]): string {\n return this.clientJs()\n .split(CLIENT_MODEL_PLACEHOLDER)\n .join(this.filterAssets.json(model))\n .split(CLIENT_DESIGN_LINKS_PLACEHOLDER)\n .join(this.filterAssets.json(links));\n }\n\n /**\n * Write the committed architecture visualization to\n * architecture/dependencies.html, next to dependencies.json.\n *\n * This is a checked-in artifact, regenerated deterministically by\n * architecture:generate so every box keeps its menu, and the \"View Design\"\n * item keeps pointing at each project's committed design.html as designs\n * come and go. The DOT is embedded in the HTML (rendered client-side by\n * viz.js). Output is deterministic (sorted graph in → same bytes out) so git\n * only shows a diff when the architecture actually changed.\n *\n * `productDependencies` is the `product-tags` rule's matrix, listed in the Product legend; null\n * when the rule is OFF, and the legend then lists no matrix.\n */\n writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n productDependencies: ProductDependencyMatrix | null,\n title: string = 'Monorepo Dependency Architecture',\n ): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, ARCH_OUTPUT_DIR);\n\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const lockControl = this.lockControl(graph);\n const responsibilities = this.responsibilities.generateSection(graph, workspaceRoot);\n const model = this.generateRenderModel(graph, title);\n model.legend.productDependencies = productDependencies === null ? null : [...productDependencies.entries];\n const html = this.generateHTML(\n model,\n this.designLinks(graph),\n title,\n lockControl,\n responsibilities,\n );\n const htmlPath = path.join(outputDir, 'dependencies.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return new VisualizationPaths(htmlPath);\n }\n\n /**\n * Open the HTML visualization in the default browser\n */\n openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not open browser: ${error.message}`);\n return false;\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The product dependency matrix (#1204): which products may depend on which.
|
|
3
|
+
*
|
|
4
|
+
* Product membership (graph-products.ts) says which product OWNS each box; this says which products
|
|
5
|
+
* may DEPEND on which, so a platform library quietly importing example code is a generation failure
|
|
6
|
+
* instead of one more edge on the graph. It is the second half of the `product-tags` rule — one switch
|
|
7
|
+
* turns the tags requirement and this matrix on and off together.
|
|
8
|
+
*
|
|
9
|
+
* "productDependencies": [
|
|
10
|
+
* { "name": "example", "allowedToDependOn": ["platform", "openapi"] },
|
|
11
|
+
* { "name": "openapi", "allowedToDependOn": ["platform"] }
|
|
12
|
+
* ]
|
|
13
|
+
*
|
|
14
|
+
* Semantics:
|
|
15
|
+
* - A product may always depend on itself (implied, never listed). A product with no entry may depend
|
|
16
|
+
* only on itself; `[]` as the whole field means no product depends on another.
|
|
17
|
+
* - An edge A -> B of the DEPENDENCY graph is legal when, for EVERY product p of A, B carries p or a
|
|
18
|
+
* product p's entry allows. A multi-product B satisfies p when ANY of its products is allowed.
|
|
19
|
+
* - The check reads the RESOLVED membership (`products` in dependencies.json): a project's own tags
|
|
20
|
+
* are final, an untagged project inherits from the tagged projects that reach it. A project in no
|
|
21
|
+
* product constrains nothing.
|
|
22
|
+
* - An entry `name`, or an `allowedToDependOn` value, that no `product:` tag uses fails naming it, and
|
|
23
|
+
* so does a duplicate `name`: a typo must never silently allow or forbid nothing.
|
|
24
|
+
*
|
|
25
|
+
* The runtime graph (HTTP / queue calls between products' services) is deliberately NOT checked:
|
|
26
|
+
* RuntimeProductMembership shows a cross-product call as coupling for a reviewer to see.
|
|
27
|
+
*/
|
|
28
|
+
import type { EnhancedGraph } from './graph-sorter';
|
|
29
|
+
import { ProductDependencyEntry } from '../configs/tag-truth-configs';
|
|
30
|
+
export declare class ProductDependencyMatrix {
|
|
31
|
+
readonly entries: readonly ProductDependencyEntry[];
|
|
32
|
+
/** product → the products it may depend on, itself included. */
|
|
33
|
+
private readonly allowed;
|
|
34
|
+
constructor(entries: readonly ProductDependencyEntry[]);
|
|
35
|
+
/** The products a project of `product` may depend on: itself plus its entry's list. */
|
|
36
|
+
allowedFor(product: string): Set<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Every problem: the matrix's own (unknown names, duplicates), then every illegal edge.
|
|
39
|
+
*
|
|
40
|
+
* @param declared every product some project's `product:` tag names
|
|
41
|
+
*/
|
|
42
|
+
problems(graph: EnhancedGraph, declared: ReadonlySet<string>): string[];
|
|
43
|
+
/** A `name` or `allowedToDependOn` value no `product:` tag uses, and a duplicate `name`. */
|
|
44
|
+
configProblems(declared: ReadonlySet<string>): string[];
|
|
45
|
+
/** Every dependency edge, once per product of its source that none of its target's products is allowed for. */
|
|
46
|
+
edgeProblems(graph: EnhancedGraph): string[];
|
|
47
|
+
/** True when `to` carries `product` or some product `product` may depend on. */
|
|
48
|
+
private satisfies;
|
|
49
|
+
private violation;
|
|
50
|
+
/** The config edit that would allow the edge: one of the target's products, any of which is enough. */
|
|
51
|
+
private cure;
|
|
52
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The product dependency matrix (#1204): which products may depend on which.
|
|
4
|
+
*
|
|
5
|
+
* Product membership (graph-products.ts) says which product OWNS each box; this says which products
|
|
6
|
+
* may DEPEND on which, so a platform library quietly importing example code is a generation failure
|
|
7
|
+
* instead of one more edge on the graph. It is the second half of the `product-tags` rule — one switch
|
|
8
|
+
* turns the tags requirement and this matrix on and off together.
|
|
9
|
+
*
|
|
10
|
+
* "productDependencies": [
|
|
11
|
+
* { "name": "example", "allowedToDependOn": ["platform", "openapi"] },
|
|
12
|
+
* { "name": "openapi", "allowedToDependOn": ["platform"] }
|
|
13
|
+
* ]
|
|
14
|
+
*
|
|
15
|
+
* Semantics:
|
|
16
|
+
* - A product may always depend on itself (implied, never listed). A product with no entry may depend
|
|
17
|
+
* only on itself; `[]` as the whole field means no product depends on another.
|
|
18
|
+
* - An edge A -> B of the DEPENDENCY graph is legal when, for EVERY product p of A, B carries p or a
|
|
19
|
+
* product p's entry allows. A multi-product B satisfies p when ANY of its products is allowed.
|
|
20
|
+
* - The check reads the RESOLVED membership (`products` in dependencies.json): a project's own tags
|
|
21
|
+
* are final, an untagged project inherits from the tagged projects that reach it. A project in no
|
|
22
|
+
* product constrains nothing.
|
|
23
|
+
* - An entry `name`, or an `allowedToDependOn` value, that no `product:` tag uses fails naming it, and
|
|
24
|
+
* so does a duplicate `name`: a typo must never silently allow or forbid nothing.
|
|
25
|
+
*
|
|
26
|
+
* The runtime graph (HTTP / queue calls between products' services) is deliberately NOT checked:
|
|
27
|
+
* RuntimeProductMembership shows a cross-product call as coupling for a reviewer to see.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.ProductDependencyMatrix = void 0;
|
|
31
|
+
const RULE = 'product-tags';
|
|
32
|
+
const FIELD = `${RULE}.productDependencies`;
|
|
33
|
+
class ProductDependencyMatrix {
|
|
34
|
+
entries;
|
|
35
|
+
/** product → the products it may depend on, itself included. */
|
|
36
|
+
allowed = new Map();
|
|
37
|
+
constructor(entries) {
|
|
38
|
+
this.entries = entries;
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const set = this.allowed.get(entry.name) ?? new Set([entry.name]);
|
|
41
|
+
for (const other of entry.allowedToDependOn)
|
|
42
|
+
set.add(other);
|
|
43
|
+
this.allowed.set(entry.name, set);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The products a project of `product` may depend on: itself plus its entry's list. */
|
|
47
|
+
allowedFor(product) {
|
|
48
|
+
return this.allowed.get(product) ?? new Set([product]);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Every problem: the matrix's own (unknown names, duplicates), then every illegal edge.
|
|
52
|
+
*
|
|
53
|
+
* @param declared every product some project's `product:` tag names
|
|
54
|
+
*/
|
|
55
|
+
problems(graph, declared) {
|
|
56
|
+
return [...this.configProblems(declared), ...this.edgeProblems(graph)];
|
|
57
|
+
}
|
|
58
|
+
/** A `name` or `allowedToDependOn` value no `product:` tag uses, and a duplicate `name`. */
|
|
59
|
+
configProblems(declared) {
|
|
60
|
+
const problems = [];
|
|
61
|
+
const known = [...declared].sort();
|
|
62
|
+
const knownText = known.length === 0 ? 'none — no project carries a product: tag' : known.join(', ');
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const entry of this.entries) {
|
|
65
|
+
if (seen.has(entry.name)) {
|
|
66
|
+
problems.push(`${FIELD}: product '${entry.name}' has two entries — merge them into one ` +
|
|
67
|
+
`{ "name": "${entry.name}", "allowedToDependOn": [...] } so its allowance is stated once.`);
|
|
68
|
+
}
|
|
69
|
+
seen.add(entry.name);
|
|
70
|
+
if (!declared.has(entry.name)) {
|
|
71
|
+
problems.push(`${FIELD}: entry "name": "${entry.name}" matches no product: tag (products in use: ${knownText}) — ` +
|
|
72
|
+
'fix the typo, or delete the entry.');
|
|
73
|
+
}
|
|
74
|
+
for (const other of entry.allowedToDependOn) {
|
|
75
|
+
if (declared.has(other))
|
|
76
|
+
continue;
|
|
77
|
+
problems.push(`${FIELD}: "${entry.name}" allowedToDependOn "${other}", which matches no product: tag ` +
|
|
78
|
+
`(products in use: ${knownText}) — fix the typo, or remove it from the list.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return problems;
|
|
82
|
+
}
|
|
83
|
+
/** Every dependency edge, once per product of its source that none of its target's products is allowed for. */
|
|
84
|
+
edgeProblems(graph) {
|
|
85
|
+
const problems = [];
|
|
86
|
+
for (const name of Object.keys(graph).sort()) {
|
|
87
|
+
const from = graph[name].products ?? [];
|
|
88
|
+
if (from.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
for (const dep of [...(graph[name].dependsOn ?? [])].sort()) {
|
|
91
|
+
const to = graph[dep]?.products ?? [];
|
|
92
|
+
// Membership is inherited down every edge, so a product's dependency always carries a
|
|
93
|
+
// product; a target with none is outside the graph and constrains nothing.
|
|
94
|
+
if (to.length === 0)
|
|
95
|
+
continue;
|
|
96
|
+
for (const product of from) {
|
|
97
|
+
if (this.satisfies(product, to))
|
|
98
|
+
continue;
|
|
99
|
+
problems.push(this.violation(name, from, dep, to, product));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return problems;
|
|
104
|
+
}
|
|
105
|
+
/** True when `to` carries `product` or some product `product` may depend on. */
|
|
106
|
+
satisfies(product, to) {
|
|
107
|
+
const allowed = this.allowedFor(product);
|
|
108
|
+
return to.some((other) => allowed.has(other));
|
|
109
|
+
}
|
|
110
|
+
violation(name, from, dep, to, product) {
|
|
111
|
+
const allowed = [...this.allowedFor(product)].sort();
|
|
112
|
+
return (`${RULE}: '${name}' [${from.join(', ')}] -> '${dep}' [${to.join(', ')}] crosses a product boundary — product ` +
|
|
113
|
+
`'${product}' may depend only on [${allowed.join(', ')}]. Either move the code so the dependency stays ` +
|
|
114
|
+
`inside an allowed product, or ${this.cure(to, product)} in webpieces.config.json (a reviewed one-line change).`);
|
|
115
|
+
}
|
|
116
|
+
/** The config edit that would allow the edge: one of the target's products, any of which is enough. */
|
|
117
|
+
cure(to, product) {
|
|
118
|
+
const pick = to[0];
|
|
119
|
+
const anyOf = to.length > 1 ? ` (any one of [${to.join(', ')}] is enough)` : '';
|
|
120
|
+
const entry = this.entries.find((each) => each.name === product);
|
|
121
|
+
const edit = entry === undefined
|
|
122
|
+
? `add { "name": "${product}", "allowedToDependOn": ["${pick}"] } to ${FIELD}`
|
|
123
|
+
: `add "${pick}" to the "${product}" entry's allowedToDependOn in ${FIELD}`;
|
|
124
|
+
return edit + anyOf;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.ProductDependencyMatrix = ProductDependencyMatrix;
|
|
128
|
+
//# sourceMappingURL=product-dependencies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-dependencies.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/product-dependencies.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The product dependency matrix (#1204): which products may depend on which.\n *\n * Product membership (graph-products.ts) says which product OWNS each box; this says which products\n * may DEPEND on which, so a platform library quietly importing example code is a generation failure\n * instead of one more edge on the graph. It is the second half of the `product-tags` rule — one switch\n * turns the tags requirement and this matrix on and off together.\n *\n * \"productDependencies\": [\n * { \"name\": \"example\", \"allowedToDependOn\": [\"platform\", \"openapi\"] },\n * { \"name\": \"openapi\", \"allowedToDependOn\": [\"platform\"] }\n * ]\n *\n * Semantics:\n * - A product may always depend on itself (implied, never listed). A product with no entry may depend\n * only on itself; `[]` as the whole field means no product depends on another.\n * - An edge A -> B of the DEPENDENCY graph is legal when, for EVERY product p of A, B carries p or a\n * product p's entry allows. A multi-product B satisfies p when ANY of its products is allowed.\n * - The check reads the RESOLVED membership (`products` in dependencies.json): a project's own tags\n * are final, an untagged project inherits from the tagged projects that reach it. A project in no\n * product constrains nothing.\n * - An entry `name`, or an `allowedToDependOn` value, that no `product:` tag uses fails naming it, and\n * so does a duplicate `name`: a typo must never silently allow or forbid nothing.\n *\n * The runtime graph (HTTP / queue calls between products' services) is deliberately NOT checked:\n * RuntimeProductMembership shows a cross-product call as coupling for a reviewer to see.\n */\n\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProductDependencyEntry } from '../configs/tag-truth-configs';\n\nconst RULE = 'product-tags';\nconst FIELD = `${RULE}.productDependencies`;\n\nexport class ProductDependencyMatrix {\n /** product → the products it may depend on, itself included. */\n private readonly allowed = new Map<string, Set<string>>();\n\n constructor(public readonly entries: readonly ProductDependencyEntry[]) {\n for (const entry of entries) {\n const set = this.allowed.get(entry.name) ?? new Set<string>([entry.name]);\n for (const other of entry.allowedToDependOn) set.add(other);\n this.allowed.set(entry.name, set);\n }\n }\n\n /** The products a project of `product` may depend on: itself plus its entry's list. */\n allowedFor(product: string): Set<string> {\n return this.allowed.get(product) ?? new Set<string>([product]);\n }\n\n /**\n * Every problem: the matrix's own (unknown names, duplicates), then every illegal edge.\n *\n * @param declared every product some project's `product:` tag names\n */\n problems(graph: EnhancedGraph, declared: ReadonlySet<string>): string[] {\n return [...this.configProblems(declared), ...this.edgeProblems(graph)];\n }\n\n /** A `name` or `allowedToDependOn` value no `product:` tag uses, and a duplicate `name`. */\n configProblems(declared: ReadonlySet<string>): string[] {\n const problems: string[] = [];\n const known = [...declared].sort();\n const knownText = known.length === 0 ? 'none — no project carries a product: tag' : known.join(', ');\n const seen = new Set<string>();\n for (const entry of this.entries) {\n if (seen.has(entry.name)) {\n problems.push(\n `${FIELD}: product '${entry.name}' has two entries — merge them into one ` +\n `{ \"name\": \"${entry.name}\", \"allowedToDependOn\": [...] } so its allowance is stated once.`,\n );\n }\n seen.add(entry.name);\n if (!declared.has(entry.name)) {\n problems.push(\n `${FIELD}: entry \"name\": \"${entry.name}\" matches no product: tag (products in use: ${knownText}) — ` +\n 'fix the typo, or delete the entry.',\n );\n }\n for (const other of entry.allowedToDependOn) {\n if (declared.has(other)) continue;\n problems.push(\n `${FIELD}: \"${entry.name}\" allowedToDependOn \"${other}\", which matches no product: tag ` +\n `(products in use: ${knownText}) — fix the typo, or remove it from the list.`,\n );\n }\n }\n return problems;\n }\n\n /** Every dependency edge, once per product of its source that none of its target's products is allowed for. */\n edgeProblems(graph: EnhancedGraph): string[] {\n const problems: string[] = [];\n for (const name of Object.keys(graph).sort()) {\n const from = graph[name].products ?? [];\n if (from.length === 0) continue;\n for (const dep of [...(graph[name].dependsOn ?? [])].sort()) {\n const to = graph[dep]?.products ?? [];\n // Membership is inherited down every edge, so a product's dependency always carries a\n // product; a target with none is outside the graph and constrains nothing.\n if (to.length === 0) continue;\n for (const product of from) {\n if (this.satisfies(product, to)) continue;\n problems.push(this.violation(name, from, dep, to, product));\n }\n }\n }\n return problems;\n }\n\n /** True when `to` carries `product` or some product `product` may depend on. */\n private satisfies(product: string, to: readonly string[]): boolean {\n const allowed = this.allowedFor(product);\n return to.some((other: string): boolean => allowed.has(other));\n }\n\n private violation(name: string, from: readonly string[], dep: string, to: readonly string[], product: string): string {\n const allowed = [...this.allowedFor(product)].sort();\n return (\n `${RULE}: '${name}' [${from.join(', ')}] -> '${dep}' [${to.join(', ')}] crosses a product boundary — product ` +\n `'${product}' may depend only on [${allowed.join(', ')}]. Either move the code so the dependency stays ` +\n `inside an allowed product, or ${this.cure(to, product)} in webpieces.config.json (a reviewed one-line change).`\n );\n }\n\n /** The config edit that would allow the edge: one of the target's products, any of which is enough. */\n private cure(to: readonly string[], product: string): string {\n const pick = to[0];\n const anyOf = to.length > 1 ? ` (any one of [${to.join(', ')}] is enough)` : '';\n const entry = this.entries.find((each: ProductDependencyEntry): boolean => each.name === product);\n const edit =\n entry === undefined\n ? `add { \"name\": \"${product}\", \"allowedToDependOn\": [\"${pick}\"] } to ${FIELD}`\n : `add \"${pick}\" to the \"${product}\" entry's allowedToDependOn in ${FIELD}`;\n return edit + anyOf;\n }\n}\n"]}
|
package/src/lib/tag-truth.d.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* both directions.
|
|
12
12
|
* product-tags (#1179) — every project whose role is listed (seed: server, client, app)
|
|
13
13
|
* carries a well-formed `product:<name>` tag, the seed the product
|
|
14
|
-
* filter and Product color mode derive membership from
|
|
14
|
+
* filter and Product color mode derive membership from; and (#1204)
|
|
15
|
+
* every dependency edge respects the product dependency matrix
|
|
16
|
+
* (product-dependencies.ts), read on the RESOLVED membership.
|
|
15
17
|
*
|
|
16
18
|
* They run beside `library-types-match-client` and `role-dependency` (graph-metadata.ts), in
|
|
17
19
|
* `architecture:generate` and `validate-architecture-unchanged`, AFTER enrichGraph has resolved every
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
import { FrameworkFolderEntry } from "../configs/tag-truth-configs";
|
|
26
28
|
import type { EnhancedGraph } from './graph-sorter';
|
|
27
29
|
import { ProjectInfo } from './project-info';
|
|
30
|
+
import { ProductDependencyMatrix } from './product-dependencies';
|
|
28
31
|
export declare const API_LIB_ROLE = "api-lib";
|
|
29
32
|
export declare const API_CLIENT_ROLE = "api-client";
|
|
30
33
|
/** `api-lib-dependencies`, resolved: what an api library may import from outside the api-lib set. */
|
|
@@ -59,10 +62,14 @@ export declare class FrameworkFolderRule {
|
|
|
59
62
|
readonly folders: readonly FrameworkFolder[];
|
|
60
63
|
constructor(folders: readonly FrameworkFolder[]);
|
|
61
64
|
}
|
|
62
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* `product-tags`, resolved: the roles whose every project must declare a `product:<name>` tag, and the
|
|
67
|
+
* product dependency matrix every dependency edge must respect (#1204).
|
|
68
|
+
*/
|
|
63
69
|
export declare class ProductTagsRule {
|
|
64
70
|
readonly roles: readonly string[];
|
|
65
|
-
|
|
71
|
+
readonly dependencies: ProductDependencyMatrix;
|
|
72
|
+
constructor(roles: readonly string[], dependencies: ProductDependencyMatrix);
|
|
66
73
|
}
|
|
67
74
|
/**
|
|
68
75
|
* The four rules' switches, read from webpieces.config.json once. A rule that is OFF (or time-boxed /
|
|
@@ -98,6 +105,8 @@ export declare class TagTruthValidator {
|
|
|
98
105
|
private readonly rules;
|
|
99
106
|
constructor(rules: TagTruthRules);
|
|
100
107
|
problems(graph: EnhancedGraph, infos: Map<string, ProjectInfo>, imports: ReadonlyMap<string, ProjectImports>): string[];
|
|
108
|
+
/** Every product some project's `product:` tag names — what a matrix entry may refer to. */
|
|
109
|
+
private declaredProducts;
|
|
101
110
|
/**
|
|
102
111
|
* #1179 — a project whose role the rule lists declares at least one well-formed `product:<name>`.
|
|
103
112
|
* The DECLARED tags are read, never the derived `products`: a server inheriting membership from an
|
package/src/lib/tag-truth.js
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* both directions.
|
|
13
13
|
* product-tags (#1179) — every project whose role is listed (seed: server, client, app)
|
|
14
14
|
* carries a well-formed `product:<name>` tag, the seed the product
|
|
15
|
-
* filter and Product color mode derive membership from
|
|
15
|
+
* filter and Product color mode derive membership from; and (#1204)
|
|
16
|
+
* every dependency edge respects the product dependency matrix
|
|
17
|
+
* (product-dependencies.ts), read on the RESOLVED membership.
|
|
16
18
|
*
|
|
17
19
|
* They run beside `library-types-match-client` and `role-dependency` (graph-metadata.ts), in
|
|
18
20
|
* `architecture:generate` and `validate-architecture-unchanged`, AFTER enrichGraph has resolved every
|
|
@@ -31,6 +33,7 @@ const path = tslib_1.__importStar(require("path"));
|
|
|
31
33
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
32
34
|
const graph_metadata_1 = require("./graph-metadata");
|
|
33
35
|
const product_resolver_1 = require("./product-resolver");
|
|
36
|
+
const product_dependencies_1 = require("./product-dependencies");
|
|
34
37
|
const rule_gate_1 = require("./rule-gate");
|
|
35
38
|
const dep_usage_scanner_1 = require("./dep-usage-scanner");
|
|
36
39
|
const toError_1 = require("../toError");
|
|
@@ -90,11 +93,16 @@ class FrameworkFolderRule {
|
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
exports.FrameworkFolderRule = FrameworkFolderRule;
|
|
93
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* `product-tags`, resolved: the roles whose every project must declare a `product:<name>` tag, and the
|
|
98
|
+
* product dependency matrix every dependency edge must respect (#1204).
|
|
99
|
+
*/
|
|
94
100
|
class ProductTagsRule {
|
|
95
101
|
roles;
|
|
96
|
-
|
|
102
|
+
dependencies;
|
|
103
|
+
constructor(roles, dependencies) {
|
|
97
104
|
this.roles = roles;
|
|
105
|
+
this.dependencies = dependencies;
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
exports.ProductTagsRule = ProductTagsRule;
|
|
@@ -137,7 +145,9 @@ class TagTruthRules {
|
|
|
137
145
|
const products = optionsOf("product-tags");
|
|
138
146
|
return new TagTruthRules(deps === null ? null : TagTruthRules.dependenciesRule(deps), where === null ? null : new ApiLibPathRule(where['paths']), folder === null
|
|
139
147
|
? null
|
|
140
|
-
: new FrameworkFolderRule(folder['entries'].map(FrameworkFolder.fromEntry)), products === null
|
|
148
|
+
: new FrameworkFolderRule(folder['entries'].map(FrameworkFolder.fromEntry)), products === null
|
|
149
|
+
? null
|
|
150
|
+
: new ProductTagsRule(products['roles'], new product_dependencies_1.ProductDependencyMatrix(products['productDependencies'])));
|
|
141
151
|
}
|
|
142
152
|
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
143
153
|
static dependenciesRule(options) {
|
|
@@ -189,8 +199,21 @@ class TagTruthValidator {
|
|
|
189
199
|
problems.push(...this.productTagProblems(info, root, entry, this.rules.productTags));
|
|
190
200
|
}
|
|
191
201
|
}
|
|
202
|
+
if (this.rules.productTags !== null) {
|
|
203
|
+
problems.push(...this.rules.productTags.dependencies.problems(graph, this.declaredProducts(infos)));
|
|
204
|
+
}
|
|
192
205
|
return problems;
|
|
193
206
|
}
|
|
207
|
+
/** Every product some project's `product:` tag names — what a matrix entry may refer to. */
|
|
208
|
+
declaredProducts(infos) {
|
|
209
|
+
const resolver = new product_resolver_1.ProductResolver();
|
|
210
|
+
const declared = new Set();
|
|
211
|
+
for (const info of infos.values()) {
|
|
212
|
+
for (const product of resolver.resolve(info).products)
|
|
213
|
+
declared.add(product);
|
|
214
|
+
}
|
|
215
|
+
return declared;
|
|
216
|
+
}
|
|
194
217
|
/**
|
|
195
218
|
* #1179 — a project whose role the rule lists declares at least one well-formed `product:<name>`.
|
|
196
219
|
* The DECLARED tags are read, never the derived `products`: a server inheriting membership from an
|