@webpieces/nx-webpieces-rules 0.4.459 → 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.
- package/package.json +6 -6
- package/src/executors/generate/executor.js +16 -4
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +4 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/executors/visualize-runtime/executor.js +4 -1
- package/src/executors/visualize-runtime/executor.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +21 -1
- package/src/lib/api-usage/api-relations.js +14 -2
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +2 -0
- package/src/lib/api-usage/api-scanner.js +45 -4
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/graph-loader.js +1 -0
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-metadata.d.ts +2 -0
- package/src/lib/graph-metadata.js +16 -0
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-sorter.d.ts +6 -0
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +15 -0
- package/src/lib/graph-visualizer.js +37 -5
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-config.d.ts +4 -1
- package/src/lib/runtime-config.js +5 -1
- package/src/lib/runtime-config.js.map +1 -1
- package/src/lib/runtime-graph.d.ts +48 -0
- package/src/lib/runtime-graph.js +175 -26
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +29 -2
- package/src/lib/runtime-visualizer.js +121 -5
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/lib/service-name-resolver.d.ts +49 -0
- package/src/lib/service-name-resolver.js +103 -0
- 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
|
|
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
|
-
|
|
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 → <em>queue</em> (cylinder) → 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 <lib>)</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 → <em>queue</em> (cylinder) → 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 → <em>queue</em> (cylinder) → 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 <lib>)</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"]}
|