@webpieces/nx-webpieces-rules 0.4.460 → 0.4.461

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +6 -6
  2. package/src/executors/generate/executor.js +16 -4
  3. package/src/executors/generate/executor.js.map +1 -1
  4. package/src/executors/validate-runtime-architecture/executor.js +4 -1
  5. package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
  6. package/src/executors/visualize-runtime/executor.js +4 -1
  7. package/src/executors/visualize-runtime/executor.js.map +1 -1
  8. package/src/lib/api-usage/api-relations.d.ts +21 -1
  9. package/src/lib/api-usage/api-relations.js +14 -2
  10. package/src/lib/api-usage/api-relations.js.map +1 -1
  11. package/src/lib/api-usage/api-scanner.d.ts +2 -0
  12. package/src/lib/api-usage/api-scanner.js +45 -4
  13. package/src/lib/api-usage/api-scanner.js.map +1 -1
  14. package/src/lib/graph-loader.js +1 -0
  15. package/src/lib/graph-loader.js.map +1 -1
  16. package/src/lib/graph-metadata.d.ts +2 -0
  17. package/src/lib/graph-metadata.js +16 -0
  18. package/src/lib/graph-metadata.js.map +1 -1
  19. package/src/lib/graph-sorter.d.ts +6 -0
  20. package/src/lib/graph-sorter.js.map +1 -1
  21. package/src/lib/graph-visualizer.d.ts +15 -0
  22. package/src/lib/graph-visualizer.js +37 -5
  23. package/src/lib/graph-visualizer.js.map +1 -1
  24. package/src/lib/runtime-config.d.ts +4 -1
  25. package/src/lib/runtime-config.js +5 -1
  26. package/src/lib/runtime-config.js.map +1 -1
  27. package/src/lib/runtime-graph.d.ts +48 -0
  28. package/src/lib/runtime-graph.js +175 -26
  29. package/src/lib/runtime-graph.js.map +1 -1
  30. package/src/lib/runtime-visualizer.d.ts +29 -2
  31. package/src/lib/runtime-visualizer.js +121 -5
  32. package/src/lib/runtime-visualizer.js.map +1 -1
  33. package/src/lib/service-name-resolver.d.ts +49 -0
  34. package/src/lib/service-name-resolver.js +103 -0
  35. package/src/lib/service-name-resolver.js.map +1 -0
@@ -4,13 +4,40 @@
4
4
  * Renders the runtime microservice graph (services + inferred Z -> X edges,
5
5
  * each labeled with the api(s) they flow over) to DOT + interactive HTML in
6
6
  * tmp/webpieces/runtime-architecture.{dot,html}.
7
+ *
8
+ * Each service node names the contracts it IMPLEMENTS and USES. That list is the
9
+ * single most important fact in a microservice architecture, and it used to be
10
+ * collapsed into a server/client boolean and thrown away — leaving an api that a
11
+ * server serves but nothing in-repo calls completely invisible, and making a
12
+ * correct api design look like a detection failure.
13
+ *
14
+ * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,
15
+ * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that
16
+ * actually page you at 3am stop being missing from the picture. They are
17
+ * RENDER-ONLY: derivation, levels and cycle detection never see them.
7
18
  */
8
19
  import type { RuntimeGraph } from './runtime-graph';
20
+ /** Render options for the runtime graph. */
21
+ export declare class RuntimeVizOptions {
22
+ /**
23
+ * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;
24
+ * a repo whose external surface is noisy can turn them off in webpieces.config.json
25
+ * (runtime-architecture.showExternalNodes).
26
+ */
27
+ readonly showExternalNodes: boolean;
28
+ constructor(
29
+ /**
30
+ * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;
31
+ * a repo whose external surface is noisy can turn them off in webpieces.config.json
32
+ * (runtime-architecture.showExternalNodes).
33
+ */
34
+ showExternalNodes?: boolean);
35
+ }
9
36
  /** Build the Graphviz DOT for the runtime service graph. */
10
- export declare function generateRuntimeDot(graph: RuntimeGraph, title?: string): string;
37
+ export declare function generateRuntimeDot(graph: RuntimeGraph, title?: string, options?: RuntimeVizOptions): string;
11
38
  export interface RuntimeVisualizationPaths {
12
39
  dotPath: string;
13
40
  htmlPath: string;
14
41
  }
15
42
  /** Write the DOT + HTML renderings to tmp/webpieces/. */
16
- export declare function writeRuntimeVisualization(graph: RuntimeGraph, workspaceRoot: string, title?: string): RuntimeVisualizationPaths;
43
+ export declare function writeRuntimeVisualization(graph: RuntimeGraph, workspaceRoot: string, title?: string, options?: RuntimeVizOptions): RuntimeVisualizationPaths;
@@ -5,8 +5,20 @@
5
5
  * Renders the runtime microservice graph (services + inferred Z -> X edges,
6
6
  * each labeled with the api(s) they flow over) to DOT + interactive HTML in
7
7
  * tmp/webpieces/runtime-architecture.{dot,html}.
8
+ *
9
+ * Each service node names the contracts it IMPLEMENTS and USES. That list is the
10
+ * single most important fact in a microservice architecture, and it used to be
11
+ * collapsed into a server/client boolean and thrown away — leaving an api that a
12
+ * server serves but nothing in-repo calls completely invisible, and making a
13
+ * correct api design look like a detection failure.
14
+ *
15
+ * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,
16
+ * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that
17
+ * actually page you at 3am stop being missing from the picture. They are
18
+ * RENDER-ONLY: derivation, levels and cycle detection never see them.
8
19
  */
9
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.RuntimeVizOptions = void 0;
10
22
  exports.generateRuntimeDot = generateRuntimeDot;
11
23
  exports.writeRuntimeVisualization = writeRuntimeVisualization;
12
24
  const tslib_1 = require("tslib");
@@ -19,9 +31,66 @@ const LEVEL_COLORS = {
19
31
  3: '#FCE4EC',
20
32
  };
21
33
  const QUEUE_FILL = '#FFF3E0';
34
+ /** Fill + border for the dashed terminal node standing for a system outside this repo. */
35
+ const EXTERNAL_FILL = '#FAFAFA';
36
+ const EXTERNAL_BORDER = '#9E9E9E';
37
+ /** Apis per line inside a node label — beyond this the box grows wider than it is readable. */
38
+ const APIS_PER_LABEL_LINE = 3;
39
+ /** Separator for the (service, external-library) grouping key; illegal in both project names. */
40
+ const PAIR_SEP = '|';
41
+ /** Render options for the runtime graph. */
42
+ class RuntimeVizOptions {
43
+ showExternalNodes;
44
+ constructor(
45
+ /**
46
+ * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;
47
+ * a repo whose external surface is noisy can turn them off in webpieces.config.json
48
+ * (runtime-architecture.showExternalNodes).
49
+ */
50
+ showExternalNodes = true) {
51
+ this.showExternalNodes = showExternalNodes;
52
+ }
53
+ }
54
+ exports.RuntimeVizOptions = RuntimeVizOptions;
22
55
  function getShortName(name) {
23
56
  return name.includes('/') ? name.split('/').pop() : name;
24
57
  }
58
+ /** Chunk a list into `\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */
59
+ // webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
60
+ function labelList(entries) {
61
+ const lines = [];
62
+ for (let i = 0; i < entries.length; i += APIS_PER_LABEL_LINE) {
63
+ lines.push(entries.slice(i, i + APIS_PER_LABEL_LINE).join(', '));
64
+ }
65
+ return lines.join('\\n');
66
+ }
67
+ /**
68
+ * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is
69
+ * annotated with that library, because "who implements WarmupApi?" otherwise requires knowing that
70
+ * the derivation walks the dependsOn closure and then walking it by hand.
71
+ */
72
+ // webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
73
+ function implementsEntries(svc) {
74
+ return svc.implements.map((api) => {
75
+ const via = svc.implementsVia?.[api];
76
+ return via === undefined ? api : `${api} (via ${getShortName(via)})`;
77
+ });
78
+ }
79
+ /**
80
+ * The full node label: name, role/level/declared service name, then the contracts it serves and
81
+ * the contracts it calls. A node with neither reads exactly as before.
82
+ */
83
+ // webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
84
+ function nodeLabel(name, svc) {
85
+ const role = svc.implements.length > 0 ? 'server' : 'client';
86
+ const declared = svc.serviceName === undefined ? '' : `, "${svc.serviceName}"`;
87
+ let label = `${getShortName(name)}\\n(${role}, L${svc.level}${declared})`;
88
+ if (svc.implements.length > 0)
89
+ label += `\\nimplements: ${labelList(implementsEntries(svc))}`;
90
+ if (svc.uses.length > 0)
91
+ label += `\\nuses: ${labelList(svc.uses)}`;
92
+ return label;
93
+ }
25
94
  /**
26
95
  * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the
27
96
  * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer
@@ -40,8 +109,53 @@ function edgeDot(edge) {
40
109
  ` "${from}" -> "${queueId}" [label="enqueue", style=dashed];\n` +
41
110
  ` "${queueId}" -> "${to}" [label="deliver", style=dashed];\n`);
42
111
  }
112
+ /**
113
+ * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —
114
+ * a contract used by a node and implemented by nobody in-repo — which the derivation already
115
+ * computes and which was, until now, only ever printed as a warning.
116
+ *
117
+ * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts
118
+ * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they
119
+ * are absent from levels, cycle detection and the transitive implements attribution.
120
+ */
121
+ // webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
122
+ function externalDot(graph, hidden) {
123
+ // "service|externalName" -> the apis flowing over it.
124
+ const apisByPair = new Map();
125
+ for (const use of graph.unresolvedUses) {
126
+ if (hidden.has(use.service))
127
+ continue;
128
+ const external = getShortName(graph.apis[use.api]?.owner ?? use.api);
129
+ const key = `${use.service}${PAIR_SEP}${external}`;
130
+ if (!apisByPair.has(key))
131
+ apisByPair.set(key, []);
132
+ apisByPair.get(key).push(use.api);
133
+ }
134
+ if (apisByPair.size === 0)
135
+ return '';
136
+ let dot = '\n // Systems outside this repo — no in-repo service implements these contracts.\n';
137
+ // The node ID is prefixed so an external library can never collide with a service of the same
138
+ // short name; only the label carries the bare name.
139
+ const externals = new Set([...apisByPair.keys()].map((key) => key.split(PAIR_SEP)[1]));
140
+ for (const external of [...externals].sort()) {
141
+ dot +=
142
+ ` "external__${external}" [shape=box, style="dashed,filled", fillcolor="${EXTERNAL_FILL}", ` +
143
+ `color="${EXTERNAL_BORDER}", label="${external}\\n(external)"];\n`;
144
+ }
145
+ for (const key of [...apisByPair.keys()].sort()) {
146
+ const parts = key.split(PAIR_SEP);
147
+ const service = parts[0];
148
+ const external = parts[1];
149
+ const via = labelList(apisByPair.get(key).sort());
150
+ dot +=
151
+ ` "${getShortName(service)}" -> "external__${external}" ` +
152
+ `[label="${via}", style=dashed, color="${EXTERNAL_BORDER}"];\n`;
153
+ }
154
+ return dot;
155
+ }
43
156
  /** Build the Graphviz DOT for the runtime service graph. */
44
- function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture') {
157
+ // webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here
158
+ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', options = new RuntimeVizOptions()) {
45
159
  let dot = 'digraph RuntimeArchitecture {\n';
46
160
  dot += ' rankdir=TB;\n';
47
161
  dot += ' node [shape=box, style="filled,rounded", fontname="Arial"];\n';
@@ -54,8 +168,7 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture') {
54
168
  continue;
55
169
  const svc = graph.services[name];
56
170
  const color = LEVEL_COLORS[svc.level] || '#F5F5F5';
57
- const role = svc.implements.length > 0 ? 'server' : 'client';
58
- dot += ` "${getShortName(name)}" [fillcolor="${color}", label="${getShortName(name)}\\n(${role}, L${svc.level})"];\n`;
171
+ dot += ` "${getShortName(name)}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n`;
59
172
  }
60
173
  dot += '\n';
61
174
  for (const edge of graph.runtimeEdges) {
@@ -63,6 +176,8 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture') {
63
176
  continue;
64
177
  dot += edgeDot(edge);
65
178
  }
179
+ if (options.showExternalNodes)
180
+ dot += externalDot(graph, hidden);
66
181
  dot += '\n labelloc="t";\n';
67
182
  dot += ` label="${title}\\n(from architecture/runtime-dependencies.json)";\n`;
68
183
  dot += ' fontsize=20;\n';
@@ -93,17 +208,18 @@ function generateRuntimeHtml(dot, title) {
93
208
  <body>
94
209
  <h1>${title}</h1>
95
210
  <div class="note">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>pubsub</strong> = producer &rarr; <em>queue</em> (cylinder) &rarr; consumer: the producer enqueues a Cloud Task and the consumer is delivered it later.</div>
211
+ <div class="note">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via &lt;lib&gt;)</em> means the service serves that contract through an embedded library rather than its own source. A <strong>dashed box</strong> is a system OUTSIDE this repo (firestore, gmail, ...): a contract this repo calls and nothing here implements.</div>
96
212
  <div id="graph"></div>
97
213
  <script>${script}</script>
98
214
  </body>
99
215
  </html>`;
100
216
  }
101
217
  /** Write the DOT + HTML renderings to tmp/webpieces/. */
102
- function writeRuntimeVisualization(graph, workspaceRoot, title = 'WebPieces Runtime Architecture') {
218
+ function writeRuntimeVisualization(graph, workspaceRoot, title = 'WebPieces Runtime Architecture', options = new RuntimeVizOptions()) {
103
219
  const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');
104
220
  if (!fs.existsSync(outputDir))
105
221
  fs.mkdirSync(outputDir, { recursive: true });
106
- const dot = generateRuntimeDot(graph, title);
222
+ const dot = generateRuntimeDot(graph, title, options);
107
223
  const dotPath = path.join(outputDir, 'runtime-architecture.dot');
108
224
  fs.writeFileSync(dotPath, dot, 'utf-8');
109
225
  const htmlPath = path.join(outputDir, 'runtime-architecture.html');
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge } from './runtime-graph';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the\n * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer\n * with a cylinder queue node and dashed enqueue/deliver arrows.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = getShortName(edge.from);\n const to = getShortName(edge.to);\n const via = edge.via.map((v: string) => getShortName(v)).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${via}\"];\\n`;\n }\n const queueId = `queue__${from}__${to}`;\n return (\n ` \"${queueId}\" [shape=cylinder, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", label=\"${via}\\\\nqueue\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\nexport function generateRuntimeDot(graph: RuntimeGraph, title: string = 'WebPieces Runtime Architecture'): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n const role = svc.implements.length > 0 ? 'server' : 'client';\n dot += ` \"${getShortName(name)}\" [fillcolor=\"${color}\", label=\"${getShortName(name)}\\\\n(${role}, L${svc.level})\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge);\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n return dot;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n const script = `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(el => document.getElementById('graph').appendChild(el))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }\n .note { max-width: 700px; margin: 12px auto; color: #555; text-align: center; }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div class=\"note\">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>pubsub</strong> = producer &rarr; <em>queue</em> (cylinder) &rarr; consumer: the producer enqueues a Cloud Task and the consumer is delivered it later.</div>\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
1
+ {"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS and USES. That list is the\n * single most important fact in a microservice architecture, and it used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that\n * actually page you at 3am stop being missing from the picture. They are\n * RENDER-ONLY: derivation, levels and cycle detection never see them.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeService } from './runtime-graph';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n for (let i = 0; i < entries.length; i += APIS_PER_LABEL_LINE) {\n lines.push(entries.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it serves and\n * the contracts it calls. A node with neither reads exactly as before.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n const declared = svc.serviceName === undefined ? '' : `, \"${svc.serviceName}\"`;\n let label = `${getShortName(name)}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n if (svc.uses.length > 0) label += `\\\\nuses: ${labelList(svc.uses)}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the\n * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer\n * with a cylinder queue node and dashed enqueue/deliver arrows.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = getShortName(edge.from);\n const to = getShortName(edge.to);\n const via = edge.via.map((v: string) => getShortName(v)).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${via}\"];\\n`;\n }\n const queueId = `queue__${from}__${to}`;\n return (\n ` \"${queueId}\" [shape=cylinder, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", label=\"${via}\\\\nqueue\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n const external = getShortName(graph.apis[use.api]?.owner ?? use.api);\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n dot +=\n ` \"${getShortName(service)}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${getShortName(name)}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge);\n }\n\n if (options.showExternalNodes) dot += externalDot(graph, hidden);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n return dot;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n const script = `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(el => document.getElementById('graph').appendChild(el))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }\n .note { max-width: 700px; margin: 12px auto; color: #555; text-align: center; }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div class=\"note\">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>pubsub</strong> = producer &rarr; <em>queue</em> (cylinder) &rarr; consumer: the producer enqueues a Cloud Task and the consumer is delivered it later.</div>\n <div class=\"note\">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via &lt;lib&gt;)</em> means the service serves that contract through an embedded library rather than its own source. A <strong>dashed box</strong> is a system OUTSIDE this repo (firestore, gmail, ...): a contract this repo calls and nothing here implements.</div>\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Service Name Resolver
3
+ *
4
+ * Resolves the `serviceName` field written per project into
5
+ * architecture/dependencies.json — the name a CLIENT uses to address this app at
6
+ * runtime (`new ClientConfig('helper-fsdb')`), i.e. its deployed Cloud Run
7
+ * service name, NOT its nx project name.
8
+ *
9
+ * It is DECLARED, never derived. The three naming spaces have no mechanical
10
+ * relationship, and any strip-the-suffix rule gets it wrong somewhere:
11
+ *
12
+ * nx project | serviceName | Cloud Run
13
+ * helper-svr | helper-portal | helper-portal
14
+ * helper-fsdb-svr | helper-fsdb | helper-fsdb
15
+ * lang-server | lang | lang
16
+ *
17
+ * Declared in the project's own project.json, next to the code it names:
18
+ *
19
+ * { "metadata": { "webpieces": { "serviceName": "helper-fsdb" } } }
20
+ *
21
+ * Absent is legal and common — a library or a browser app is never addressed by
22
+ * name. A node WITHOUT one simply cannot be the resolved target of a targeted
23
+ * client call, and the runtime graph says so out loud rather than guessing.
24
+ */
25
+ import { ProjectInfo } from './project-info';
26
+ /** The project.json key path holding the declared name: metadata.webpieces.serviceName. */
27
+ export declare const SERVICE_NAME_METADATA_PATH = "metadata.webpieces.serviceName";
28
+ export declare class ServiceNameResolution {
29
+ /** Declared service name, or null when none is declared (or resolution failed). */
30
+ readonly serviceName: string | null;
31
+ /** Problem description when the declaration is present but unusable, otherwise null. */
32
+ readonly problem: string | null;
33
+ constructor(
34
+ /** Declared service name, or null when none is declared (or resolution failed). */
35
+ serviceName: string | null,
36
+ /** Problem description when the declaration is present but unusable, otherwise null. */
37
+ problem: string | null);
38
+ }
39
+ /**
40
+ * Read `metadata.webpieces.serviceName` from the project's project.json. A missing file, a missing
41
+ * key, or an unparseable file all resolve to "not declared" — only a PRESENT-but-wrong value (empty
42
+ * or not a string) is a problem, because that is a typo the author wants told about.
43
+ */
44
+ export declare function resolveServiceName(info: ProjectInfo, workspaceRoot: string): ServiceNameResolution;
45
+ /**
46
+ * Two projects claiming the same service name make every targeted client edge ambiguous, so the
47
+ * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.
48
+ */
49
+ export declare function validateUniqueServiceNames(names: Map<string, string>, problems: string[]): void;
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * Service Name Resolver
4
+ *
5
+ * Resolves the `serviceName` field written per project into
6
+ * architecture/dependencies.json — the name a CLIENT uses to address this app at
7
+ * runtime (`new ClientConfig('helper-fsdb')`), i.e. its deployed Cloud Run
8
+ * service name, NOT its nx project name.
9
+ *
10
+ * It is DECLARED, never derived. The three naming spaces have no mechanical
11
+ * relationship, and any strip-the-suffix rule gets it wrong somewhere:
12
+ *
13
+ * nx project | serviceName | Cloud Run
14
+ * helper-svr | helper-portal | helper-portal
15
+ * helper-fsdb-svr | helper-fsdb | helper-fsdb
16
+ * lang-server | lang | lang
17
+ *
18
+ * Declared in the project's own project.json, next to the code it names:
19
+ *
20
+ * { "metadata": { "webpieces": { "serviceName": "helper-fsdb" } } }
21
+ *
22
+ * Absent is legal and common — a library or a browser app is never addressed by
23
+ * name. A node WITHOUT one simply cannot be the resolved target of a targeted
24
+ * client call, and the runtime graph says so out loud rather than guessing.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.ServiceNameResolution = exports.SERVICE_NAME_METADATA_PATH = void 0;
28
+ exports.resolveServiceName = resolveServiceName;
29
+ exports.validateUniqueServiceNames = validateUniqueServiceNames;
30
+ const tslib_1 = require("tslib");
31
+ const fs = tslib_1.__importStar(require("fs"));
32
+ const path = tslib_1.__importStar(require("path"));
33
+ const toError_1 = require("../toError");
34
+ /** The project.json key path holding the declared name: metadata.webpieces.serviceName. */
35
+ exports.SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';
36
+ class ServiceNameResolution {
37
+ serviceName;
38
+ problem;
39
+ constructor(
40
+ /** Declared service name, or null when none is declared (or resolution failed). */
41
+ serviceName,
42
+ /** Problem description when the declaration is present but unusable, otherwise null. */
43
+ problem) {
44
+ this.serviceName = serviceName;
45
+ this.problem = problem;
46
+ }
47
+ }
48
+ exports.ServiceNameResolution = ServiceNameResolution;
49
+ /**
50
+ * Read `metadata.webpieces.serviceName` from the project's project.json. A missing file, a missing
51
+ * key, or an unparseable file all resolve to "not declared" — only a PRESENT-but-wrong value (empty
52
+ * or not a string) is a problem, because that is a typo the author wants told about.
53
+ */
54
+ // webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph
55
+ function resolveServiceName(info, workspaceRoot) {
56
+ const raw = readServiceNameField(path.join(workspaceRoot, info.root, 'project.json'));
57
+ if (raw === undefined)
58
+ return new ServiceNameResolution(null, null);
59
+ if (typeof raw !== 'string' || raw.trim().length === 0) {
60
+ return new ServiceNameResolution(null, `${info.name}: ${exports.SERVICE_NAME_METADATA_PATH} in project.json must be a non-empty string`);
61
+ }
62
+ return new ServiceNameResolution(raw.trim(), null);
63
+ }
64
+ /**
65
+ * Two projects claiming the same service name make every targeted client edge ambiguous, so the
66
+ * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.
67
+ */
68
+ // webpieces-disable no-function-outside-class -- pure validator, mirrors validateRoleDependencies
69
+ function validateUniqueServiceNames(names, problems) {
70
+ const projectsByName = new Map();
71
+ for (const project of [...names.keys()].sort()) {
72
+ const serviceName = names.get(project);
73
+ const claimants = projectsByName.get(serviceName) ?? [];
74
+ claimants.push(project);
75
+ projectsByName.set(serviceName, claimants);
76
+ }
77
+ for (const serviceName of [...projectsByName.keys()].sort()) {
78
+ const claimants = projectsByName.get(serviceName);
79
+ if (claimants.length < 2)
80
+ continue;
81
+ problems.push(`serviceName '${serviceName}' is declared by ${claimants.length} projects ` +
82
+ `(${claimants.join(', ')}) — a client naming it could not be routed to one of them. ` +
83
+ `Give each project a distinct ${exports.SERVICE_NAME_METADATA_PATH}.`);
84
+ }
85
+ }
86
+ /** The raw `metadata.webpieces.serviceName` value, or undefined when not present at all. */
87
+ // webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here
88
+ // webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers
89
+ function readServiceNameField(projectJsonPath) {
90
+ if (!fs.existsSync(projectJsonPath))
91
+ return undefined;
92
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
93
+ try {
94
+ const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
95
+ return parsed?.metadata?.webpieces?.serviceName;
96
+ }
97
+ catch (err) {
98
+ const error = (0, toError_1.toError)(err);
99
+ console.warn(`⚠️ Skipping unparseable ${projectJsonPath}: ${error.message}`);
100
+ return undefined;
101
+ }
102
+ }
103
+ //# sourceMappingURL=service-name-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-name-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/service-name-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Service Name Resolver\n *\n * Resolves the `serviceName` field written per project into\n * architecture/dependencies.json — the name a CLIENT uses to address this app at\n * runtime (`new ClientConfig('helper-fsdb')`), i.e. its deployed Cloud Run\n * service name, NOT its nx project name.\n *\n * It is DECLARED, never derived. The three naming spaces have no mechanical\n * relationship, and any strip-the-suffix rule gets it wrong somewhere:\n *\n * nx project | serviceName | Cloud Run\n * helper-svr | helper-portal | helper-portal\n * helper-fsdb-svr | helper-fsdb | helper-fsdb\n * lang-server | lang | lang\n *\n * Declared in the project's own project.json, next to the code it names:\n *\n * { \"metadata\": { \"webpieces\": { \"serviceName\": \"helper-fsdb\" } } }\n *\n * Absent is legal and common — a library or a browser app is never addressed by\n * name. A node WITHOUT one simply cannot be the resolved target of a targeted\n * client call, and the runtime graph says so out loud rather than guessing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\n/** The project.json key path holding the declared name: metadata.webpieces.serviceName. */\nexport const SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';\n\nexport class ServiceNameResolution {\n constructor(\n /** Declared service name, or null when none is declared (or resolution failed). */\n public readonly serviceName: string | null,\n /** Problem description when the declaration is present but unusable, otherwise null. */\n public readonly problem: string | null,\n ) {}\n}\n\n/**\n * Read `metadata.webpieces.serviceName` from the project's project.json. A missing file, a missing\n * key, or an unparseable file all resolve to \"not declared\" — only a PRESENT-but-wrong value (empty\n * or not a string) is a problem, because that is a typo the author wants told about.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph\nexport function resolveServiceName(info: ProjectInfo, workspaceRoot: string): ServiceNameResolution {\n const raw = readServiceNameField(path.join(workspaceRoot, info.root, 'project.json'));\n if (raw === undefined) return new ServiceNameResolution(null, null);\n if (typeof raw !== 'string' || raw.trim().length === 0) {\n return new ServiceNameResolution(\n null,\n `${info.name}: ${SERVICE_NAME_METADATA_PATH} in project.json must be a non-empty string`,\n );\n }\n return new ServiceNameResolution(raw.trim(), null);\n}\n\n/**\n * Two projects claiming the same service name make every targeted client edge ambiguous, so the\n * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.\n */\n// webpieces-disable no-function-outside-class -- pure validator, mirrors validateRoleDependencies\nexport function validateUniqueServiceNames(names: Map<string, string>, problems: string[]): void {\n const projectsByName = new Map<string, string[]>();\n for (const project of [...names.keys()].sort()) {\n const serviceName = names.get(project)!;\n const claimants = projectsByName.get(serviceName) ?? [];\n claimants.push(project);\n projectsByName.set(serviceName, claimants);\n }\n for (const serviceName of [...projectsByName.keys()].sort()) {\n const claimants = projectsByName.get(serviceName)!;\n if (claimants.length < 2) continue;\n problems.push(\n `serviceName '${serviceName}' is declared by ${claimants.length} projects ` +\n `(${claimants.join(', ')}) — a client naming it could not be routed to one of them. ` +\n `Give each project a distinct ${SERVICE_NAME_METADATA_PATH}.`,\n );\n }\n}\n\n/** The raw `metadata.webpieces.serviceName` value, or undefined when not present at all. */\n// webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers\nfunction readServiceNameField(projectJsonPath: string): unknown {\n if (!fs.existsSync(projectJsonPath)) return undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));\n return parsed?.metadata?.webpieces?.serviceName;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${projectJsonPath}: ${error.message}`);\n return undefined;\n }\n}\n"]}