@webpieces/nx-webpieces-rules 0.3.136 → 0.3.138
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/executors.json +15 -0
- package/package.json +5 -5
- package/src/executors/generate/executor.js +23 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.d.ts +21 -0
- package/src/executors/validate-runtime-architecture/executor.js +95 -0
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -0
- package/src/executors/validate-runtime-architecture/schema.json +8 -0
- package/src/executors/validate-runtime-markers/executor.d.ts +25 -0
- package/src/executors/validate-runtime-markers/executor.js +90 -0
- package/src/executors/validate-runtime-markers/executor.js.map +1 -0
- package/src/executors/validate-runtime-markers/schema.json +8 -0
- package/src/executors/visualize-runtime/executor.d.ts +15 -0
- package/src/executors/visualize-runtime/executor.js +45 -0
- package/src/executors/visualize-runtime/executor.js.map +1 -0
- package/src/executors/visualize-runtime/schema.json +8 -0
- package/src/lib/runtime-config.d.ts +35 -0
- package/src/lib/runtime-config.js +51 -0
- package/src/lib/runtime-config.js.map +1 -0
- package/src/lib/runtime-cycles.d.ts +25 -0
- package/src/lib/runtime-cycles.js +74 -0
- package/src/lib/runtime-cycles.js.map +1 -0
- package/src/lib/runtime-graph.d.ts +47 -0
- package/src/lib/runtime-graph.js +186 -0
- package/src/lib/runtime-graph.js.map +1 -0
- package/src/lib/runtime-markers.d.ts +58 -0
- package/src/lib/runtime-markers.js +130 -0
- package/src/lib/runtime-markers.js.map +1 -0
- package/src/lib/runtime-visualizer.d.ts +16 -0
- package/src/lib/runtime-visualizer.js +88 -0
- package/src/lib/runtime-visualizer.js.map +1 -0
- package/src/plugin.d.ts +2 -0
- package/src/plugin.js +55 -53
- package/src/plugin.js.map +1 -1
- package/src/runtime-targets.d.ts +11 -0
- package/src/runtime-targets.js +50 -0
- package/src/runtime-targets.js.map +1 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime Cycles
|
|
4
|
+
*
|
|
5
|
+
* Enumerates ALL cycles in the runtime service graph using Tarjan's
|
|
6
|
+
* strongly-connected-components algorithm. The existing graph-sorter `findCycle`
|
|
7
|
+
* reports only one cycle; runtime validation needs every cycle so each can be
|
|
8
|
+
* checked against the per-cycle allowlist independently.
|
|
9
|
+
*
|
|
10
|
+
* A cycle is any SCC with more than one node, or a single node with a self-edge.
|
|
11
|
+
* Each cycle is keyed by its sorted, comma-joined node names so it can be
|
|
12
|
+
* matched against an `allowedCycles` entry regardless of traversal order.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.cycleKey = cycleKey;
|
|
16
|
+
exports.findRuntimeCycles = findRuntimeCycles;
|
|
17
|
+
/** Canonical key for a set of service names (order-independent). */
|
|
18
|
+
function cycleKey(services) {
|
|
19
|
+
return [...services].sort().join(',');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Find every cycle in a directed graph via Tarjan's SCC algorithm.
|
|
23
|
+
* `graph[node]` lists the nodes `node` points to.
|
|
24
|
+
*/
|
|
25
|
+
function findRuntimeCycles(graph) {
|
|
26
|
+
const indexOf = new Map();
|
|
27
|
+
const lowLink = new Map();
|
|
28
|
+
const onStack = new Set();
|
|
29
|
+
const stack = [];
|
|
30
|
+
const sccs = [];
|
|
31
|
+
let counter = 0;
|
|
32
|
+
const nodes = Object.keys(graph);
|
|
33
|
+
const strongConnect = (node) => {
|
|
34
|
+
indexOf.set(node, counter);
|
|
35
|
+
lowLink.set(node, counter);
|
|
36
|
+
counter += 1;
|
|
37
|
+
stack.push(node);
|
|
38
|
+
onStack.add(node);
|
|
39
|
+
for (const next of graph[node] ?? []) {
|
|
40
|
+
if (!indexOf.has(next)) {
|
|
41
|
+
strongConnect(next);
|
|
42
|
+
lowLink.set(node, Math.min(lowLink.get(node), lowLink.get(next)));
|
|
43
|
+
}
|
|
44
|
+
else if (onStack.has(next)) {
|
|
45
|
+
lowLink.set(node, Math.min(lowLink.get(node), indexOf.get(next)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (lowLink.get(node) === indexOf.get(node)) {
|
|
49
|
+
const component = [];
|
|
50
|
+
let member = '';
|
|
51
|
+
do {
|
|
52
|
+
member = stack.pop();
|
|
53
|
+
onStack.delete(member);
|
|
54
|
+
component.push(member);
|
|
55
|
+
} while (member !== node);
|
|
56
|
+
sccs.push(component);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
for (const node of nodes) {
|
|
60
|
+
if (!indexOf.has(node))
|
|
61
|
+
strongConnect(node);
|
|
62
|
+
}
|
|
63
|
+
const cycles = [];
|
|
64
|
+
for (const component of sccs) {
|
|
65
|
+
const isMultiNode = component.length > 1;
|
|
66
|
+
const isSelfLoop = component.length === 1 && (graph[component[0]] ?? []).includes(component[0]);
|
|
67
|
+
if (isMultiNode || isSelfLoop) {
|
|
68
|
+
const services = [...component].sort();
|
|
69
|
+
cycles.push({ services, key: cycleKey(services) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return cycles;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=runtime-cycles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-cycles.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-cycles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Cycles\n *\n * Enumerates ALL cycles in the runtime service graph using Tarjan's\n * strongly-connected-components algorithm. The existing graph-sorter `findCycle`\n * reports only one cycle; runtime validation needs every cycle so each can be\n * checked against the per-cycle allowlist independently.\n *\n * A cycle is any SCC with more than one node, or a single node with a self-edge.\n * Each cycle is keyed by its sorted, comma-joined node names so it can be\n * matched against an `allowedCycles` entry regardless of traversal order.\n */\n\nexport interface RuntimeCycle {\n /** Sorted service names participating in the cycle. */\n services: string[];\n /** Canonical key: services sorted then joined with \",\". */\n key: string;\n}\n\n/** Canonical key for a set of service names (order-independent). */\nexport function cycleKey(services: string[]): string {\n return [...services].sort().join(',');\n}\n\n/**\n * Find every cycle in a directed graph via Tarjan's SCC algorithm.\n * `graph[node]` lists the nodes `node` points to.\n */\nexport function findRuntimeCycles(graph: Record<string, string[]>): RuntimeCycle[] {\n const indexOf = new Map<string, number>();\n const lowLink = new Map<string, number>();\n const onStack = new Set<string>();\n const stack: string[] = [];\n const sccs: string[][] = [];\n let counter = 0;\n\n const nodes = Object.keys(graph);\n\n const strongConnect = (node: string): void => {\n indexOf.set(node, counter);\n lowLink.set(node, counter);\n counter += 1;\n stack.push(node);\n onStack.add(node);\n\n for (const next of graph[node] ?? []) {\n if (!indexOf.has(next)) {\n strongConnect(next);\n lowLink.set(node, Math.min(lowLink.get(node)!, lowLink.get(next)!));\n } else if (onStack.has(next)) {\n lowLink.set(node, Math.min(lowLink.get(node)!, indexOf.get(next)!));\n }\n }\n\n if (lowLink.get(node) === indexOf.get(node)) {\n const component: string[] = [];\n let member = '';\n do {\n member = stack.pop()!;\n onStack.delete(member);\n component.push(member);\n } while (member !== node);\n sccs.push(component);\n }\n };\n\n for (const node of nodes) {\n if (!indexOf.has(node)) strongConnect(node);\n }\n\n const cycles: RuntimeCycle[] = [];\n for (const component of sccs) {\n const isMultiNode = component.length > 1;\n const isSelfLoop = component.length === 1 && (graph[component[0]] ?? []).includes(component[0]);\n if (isMultiNode || isSelfLoop) {\n const services = [...component].sort();\n cycles.push({ services, key: cycleKey(services) });\n }\n }\n return cycles;\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Graph
|
|
3
|
+
*
|
|
4
|
+
* Assembles the runtime microservice graph from the per-service
|
|
5
|
+
* `service-contract.json` files, and saves/loads the committed
|
|
6
|
+
* `architecture/runtime-dependencies.json`.
|
|
7
|
+
*
|
|
8
|
+
* The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
|
|
9
|
+
* Y and X `implements` api Y. This edge does not exist in the compile-time
|
|
10
|
+
* dependencies.json (both Z and X only compile-depend on the api library Y).
|
|
11
|
+
*/
|
|
12
|
+
import type { WorkspaceModel } from './runtime-markers';
|
|
13
|
+
export declare const DEFAULT_RUNTIME_GRAPH_PATH = "architecture/runtime-dependencies.json";
|
|
14
|
+
export interface RuntimeService {
|
|
15
|
+
level: number;
|
|
16
|
+
implements: string[];
|
|
17
|
+
uses: string[];
|
|
18
|
+
dependsOn: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface RuntimeApi {
|
|
21
|
+
implementedBy: string[];
|
|
22
|
+
usedBy: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface RuntimeEdge {
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
via: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface RuntimeUnresolved {
|
|
30
|
+
service: string;
|
|
31
|
+
api: string;
|
|
32
|
+
}
|
|
33
|
+
export interface RuntimeGraph {
|
|
34
|
+
services: Record<string, RuntimeService>;
|
|
35
|
+
apis: Record<string, RuntimeApi>;
|
|
36
|
+
runtimeEdges: RuntimeEdge[];
|
|
37
|
+
unresolvedUses: RuntimeUnresolved[];
|
|
38
|
+
}
|
|
39
|
+
/** Adjacency (service -> [targets]) from a loaded runtime graph. */
|
|
40
|
+
export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]>;
|
|
41
|
+
/** Assemble the full runtime graph from the workspace model + service contracts. */
|
|
42
|
+
export declare function assembleRuntimeGraph(model: WorkspaceModel, workspaceRoot: string): RuntimeGraph;
|
|
43
|
+
export declare function saveRuntimeGraph(graph: RuntimeGraph, workspaceRoot: string, graphPath?: string): void;
|
|
44
|
+
export declare function runtimeGraphFileExists(workspaceRoot: string, graphPath?: string): boolean;
|
|
45
|
+
export declare function loadRuntimeGraph(workspaceRoot: string, graphPath?: string): RuntimeGraph | null;
|
|
46
|
+
/** Serialize for an in-memory equality check (matches the on-disk format). */
|
|
47
|
+
export declare function serializeRuntimeGraph(graph: RuntimeGraph): string;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime Graph
|
|
4
|
+
*
|
|
5
|
+
* Assembles the runtime microservice graph from the per-service
|
|
6
|
+
* `service-contract.json` files, and saves/loads the committed
|
|
7
|
+
* `architecture/runtime-dependencies.json`.
|
|
8
|
+
*
|
|
9
|
+
* The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api
|
|
10
|
+
* Y and X `implements` api Y. This edge does not exist in the compile-time
|
|
11
|
+
* dependencies.json (both Z and X only compile-depend on the api library Y).
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.DEFAULT_RUNTIME_GRAPH_PATH = void 0;
|
|
15
|
+
exports.runtimeAdjacency = runtimeAdjacency;
|
|
16
|
+
exports.assembleRuntimeGraph = assembleRuntimeGraph;
|
|
17
|
+
exports.saveRuntimeGraph = saveRuntimeGraph;
|
|
18
|
+
exports.runtimeGraphFileExists = runtimeGraphFileExists;
|
|
19
|
+
exports.loadRuntimeGraph = loadRuntimeGraph;
|
|
20
|
+
exports.serializeRuntimeGraph = serializeRuntimeGraph;
|
|
21
|
+
const tslib_1 = require("tslib");
|
|
22
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
23
|
+
const path = tslib_1.__importStar(require("path"));
|
|
24
|
+
const graph_sorter_1 = require("./graph-sorter");
|
|
25
|
+
const runtime_markers_1 = require("./runtime-markers");
|
|
26
|
+
const toError_1 = require("../toError");
|
|
27
|
+
exports.DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';
|
|
28
|
+
/** Collect every service project (servicePaths), with its declarations resolved to projects. */
|
|
29
|
+
function collectServiceDecls(model, workspaceRoot) {
|
|
30
|
+
const decls = [];
|
|
31
|
+
for (const info of model.projects.values()) {
|
|
32
|
+
if (!info.isService)
|
|
33
|
+
continue;
|
|
34
|
+
// A service missing its contract still appears as a node (empty edges);
|
|
35
|
+
// validate-runtime-markers fails it separately for the missing file.
|
|
36
|
+
const contract = (0, runtime_markers_1.readServiceContract)(workspaceRoot, info.root);
|
|
37
|
+
const implementsPkgs = contract ? contract.implements : [];
|
|
38
|
+
const usesPkgs = contract ? contract.uses : [];
|
|
39
|
+
decls.push({
|
|
40
|
+
name: info.name,
|
|
41
|
+
implements: (0, runtime_markers_1.resolvePackageNames)(model, implementsPkgs).projects.sort(),
|
|
42
|
+
uses: (0, runtime_markers_1.resolvePackageNames)(model, usesPkgs).projects.sort(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return decls.sort((a, b) => a.name.localeCompare(b.name));
|
|
46
|
+
}
|
|
47
|
+
function buildApiIndex(decls) {
|
|
48
|
+
const apis = new Map();
|
|
49
|
+
const ensure = (api) => {
|
|
50
|
+
let entry = apis.get(api);
|
|
51
|
+
if (!entry) {
|
|
52
|
+
entry = { implementedBy: [], usedBy: [] };
|
|
53
|
+
apis.set(api, entry);
|
|
54
|
+
}
|
|
55
|
+
return entry;
|
|
56
|
+
};
|
|
57
|
+
for (const decl of decls) {
|
|
58
|
+
for (const api of decl.implements)
|
|
59
|
+
ensure(api).implementedBy.push(decl.name);
|
|
60
|
+
for (const api of decl.uses)
|
|
61
|
+
ensure(api).usedBy.push(decl.name);
|
|
62
|
+
}
|
|
63
|
+
for (const entry of apis.values()) {
|
|
64
|
+
entry.implementedBy.sort();
|
|
65
|
+
entry.usedBy.sort();
|
|
66
|
+
}
|
|
67
|
+
return apis;
|
|
68
|
+
}
|
|
69
|
+
/** Build inferred runtime edges (Z -> X via Y) and the unresolved-uses list. */
|
|
70
|
+
function buildEdges(decls, apis) {
|
|
71
|
+
const viaByEdge = new Map();
|
|
72
|
+
const unresolved = [];
|
|
73
|
+
for (const decl of decls) {
|
|
74
|
+
for (const api of decl.uses) {
|
|
75
|
+
const implementers = apis.get(api)?.implementedBy ?? [];
|
|
76
|
+
if (implementers.length === 0) {
|
|
77
|
+
unresolved.push({ service: decl.name, api });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const target of implementers) {
|
|
81
|
+
if (target === decl.name)
|
|
82
|
+
continue;
|
|
83
|
+
const key = `${decl.name} ${target}`;
|
|
84
|
+
if (!viaByEdge.has(key))
|
|
85
|
+
viaByEdge.set(key, new Set());
|
|
86
|
+
viaByEdge.get(key).add(api);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const edges = [];
|
|
91
|
+
for (const key of viaByEdge.keys()) {
|
|
92
|
+
const parts = key.split(' ');
|
|
93
|
+
edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByEdge.get(key)).sort() });
|
|
94
|
+
}
|
|
95
|
+
edges.sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
96
|
+
unresolved.sort((a, b) => a.service.localeCompare(b.service) || a.api.localeCompare(b.api));
|
|
97
|
+
return { edges, unresolved };
|
|
98
|
+
}
|
|
99
|
+
/** Adjacency (service -> [targets]) used for leveling + cycle checks. */
|
|
100
|
+
function adjacencyFromEdges(serviceNames, edges) {
|
|
101
|
+
const adj = {};
|
|
102
|
+
for (const name of serviceNames)
|
|
103
|
+
adj[name] = [];
|
|
104
|
+
for (const edge of edges) {
|
|
105
|
+
if (!adj[edge.from])
|
|
106
|
+
adj[edge.from] = [];
|
|
107
|
+
adj[edge.from].push(edge.to);
|
|
108
|
+
}
|
|
109
|
+
return adj;
|
|
110
|
+
}
|
|
111
|
+
/** Adjacency (service -> [targets]) from a loaded runtime graph. */
|
|
112
|
+
function runtimeAdjacency(graph) {
|
|
113
|
+
return adjacencyFromEdges(Object.keys(graph.services), graph.runtimeEdges);
|
|
114
|
+
}
|
|
115
|
+
/** Assign levels via topological sort; falls back to level 0 when a cycle exists. */
|
|
116
|
+
function assignLevels(adjacency) {
|
|
117
|
+
const levels = {};
|
|
118
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
119
|
+
try {
|
|
120
|
+
const sorted = (0, graph_sorter_1.sortGraphTopologically)(adjacency);
|
|
121
|
+
for (const name of Object.keys(sorted))
|
|
122
|
+
levels[name] = sorted[name].level;
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
const error = (0, toError_1.toError)(err);
|
|
126
|
+
void error;
|
|
127
|
+
for (const name of Object.keys(adjacency))
|
|
128
|
+
levels[name] = 0;
|
|
129
|
+
}
|
|
130
|
+
return levels;
|
|
131
|
+
}
|
|
132
|
+
/** Assemble the full runtime graph from the workspace model + service contracts. */
|
|
133
|
+
function assembleRuntimeGraph(model, workspaceRoot) {
|
|
134
|
+
const decls = collectServiceDecls(model, workspaceRoot);
|
|
135
|
+
const apis = buildApiIndex(decls);
|
|
136
|
+
const edgeResult = buildEdges(decls, apis);
|
|
137
|
+
const services = {};
|
|
138
|
+
for (const decl of decls) {
|
|
139
|
+
const dependsOn = Array.from(new Set(edgeResult.edges.filter((e) => e.from === decl.name).map((e) => e.to))).sort();
|
|
140
|
+
services[decl.name] = { level: 0, implements: decl.implements, uses: decl.uses, dependsOn };
|
|
141
|
+
}
|
|
142
|
+
const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edgeResult.edges));
|
|
143
|
+
for (const name of Object.keys(services))
|
|
144
|
+
services[name].level = levels[name] ?? 0;
|
|
145
|
+
const apisObj = {};
|
|
146
|
+
for (const api of Array.from(apis.keys()).sort())
|
|
147
|
+
apisObj[api] = apis.get(api);
|
|
148
|
+
return {
|
|
149
|
+
services,
|
|
150
|
+
apis: apisObj,
|
|
151
|
+
runtimeEdges: edgeResult.edges,
|
|
152
|
+
unresolvedUses: edgeResult.unresolved,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */
|
|
156
|
+
function formatRuntimeJson(graph) {
|
|
157
|
+
return JSON.stringify(graph, null, 4) + '\n';
|
|
158
|
+
}
|
|
159
|
+
function saveRuntimeGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
160
|
+
const fullPath = path.join(workspaceRoot, graphPath);
|
|
161
|
+
const dir = path.dirname(fullPath);
|
|
162
|
+
if (!fs.existsSync(dir))
|
|
163
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
164
|
+
fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');
|
|
165
|
+
}
|
|
166
|
+
function runtimeGraphFileExists(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
167
|
+
return fs.existsSync(path.join(workspaceRoot, graphPath));
|
|
168
|
+
}
|
|
169
|
+
function loadRuntimeGraph(workspaceRoot, graphPath = exports.DEFAULT_RUNTIME_GRAPH_PATH) {
|
|
170
|
+
const fullPath = path.join(workspaceRoot, graphPath);
|
|
171
|
+
if (!fs.existsSync(fullPath))
|
|
172
|
+
return null;
|
|
173
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
const error = (0, toError_1.toError)(err);
|
|
179
|
+
throw new Error(`Failed to load runtime graph from ${fullPath}: ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Serialize for an in-memory equality check (matches the on-disk format). */
|
|
183
|
+
function serializeRuntimeGraph(graph) {
|
|
184
|
+
return formatRuntimeJson(graph);
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=runtime-graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-graph.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Graph\n *\n * Assembles the runtime microservice graph from the per-service\n * `service-contract.json` files, and saves/loads the committed\n * `architecture/runtime-dependencies.json`.\n *\n * The runtime edge Z -> X (Z depends on X at runtime) is INFERRED: Z `uses` api\n * Y and X `implements` api Y. This edge does not exist in the compile-time\n * dependencies.json (both Z and X only compile-depend on the api library Y).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { sortGraphTopologically } from './graph-sorter';\nimport { readServiceContract, resolvePackageNames } from './runtime-markers';\nimport type { WorkspaceModel } from './runtime-markers';\nimport { toError } from '../toError';\n\nexport const DEFAULT_RUNTIME_GRAPH_PATH = 'architecture/runtime-dependencies.json';\n\nexport interface RuntimeService {\n level: number;\n implements: string[];\n uses: string[];\n dependsOn: string[];\n}\n\nexport interface RuntimeApi {\n implementedBy: string[];\n usedBy: string[];\n}\n\nexport interface RuntimeEdge {\n from: string;\n to: string;\n via: string[];\n}\n\nexport interface RuntimeUnresolved {\n service: string;\n api: string;\n}\n\nexport interface RuntimeGraph {\n services: Record<string, RuntimeService>;\n apis: Record<string, RuntimeApi>;\n runtimeEdges: RuntimeEdge[];\n unresolvedUses: RuntimeUnresolved[];\n}\n\n/** One service's declared relationships, resolved to workspace project names. */\ninterface ServiceDecl {\n name: string;\n implements: string[];\n uses: string[];\n}\n\ninterface EdgeResult {\n edges: RuntimeEdge[];\n unresolved: RuntimeUnresolved[];\n}\n\n/** Collect every service project (servicePaths), with its declarations resolved to projects. */\nfunction collectServiceDecls(model: WorkspaceModel, workspaceRoot: string): ServiceDecl[] {\n const decls: ServiceDecl[] = [];\n for (const info of model.projects.values()) {\n if (!info.isService) continue;\n // A service missing its contract still appears as a node (empty edges);\n // validate-runtime-markers fails it separately for the missing file.\n const contract = readServiceContract(workspaceRoot, info.root);\n const implementsPkgs = contract ? contract.implements : [];\n const usesPkgs = contract ? contract.uses : [];\n decls.push({\n name: info.name,\n implements: resolvePackageNames(model, implementsPkgs).projects.sort(),\n uses: resolvePackageNames(model, usesPkgs).projects.sort(),\n });\n }\n return decls.sort((a: ServiceDecl, b: ServiceDecl) => a.name.localeCompare(b.name));\n}\n\nfunction buildApiIndex(decls: ServiceDecl[]): Map<string, RuntimeApi> {\n const apis = new Map<string, RuntimeApi>();\n const ensure = (api: string): RuntimeApi => {\n let entry = apis.get(api);\n if (!entry) {\n entry = { implementedBy: [], usedBy: [] };\n apis.set(api, entry);\n }\n return entry;\n };\n for (const decl of decls) {\n for (const api of decl.implements) ensure(api).implementedBy.push(decl.name);\n for (const api of decl.uses) ensure(api).usedBy.push(decl.name);\n }\n for (const entry of apis.values()) {\n entry.implementedBy.sort();\n entry.usedBy.sort();\n }\n return apis;\n}\n\n/** Build inferred runtime edges (Z -> X via Y) and the unresolved-uses list. */\nfunction buildEdges(decls: ServiceDecl[], apis: Map<string, RuntimeApi>): EdgeResult {\n const viaByEdge = new Map<string, Set<string>>();\n const unresolved: RuntimeUnresolved[] = [];\n\n for (const decl of decls) {\n for (const api of decl.uses) {\n const implementers = apis.get(api)?.implementedBy ?? [];\n if (implementers.length === 0) {\n unresolved.push({ service: decl.name, api });\n continue;\n }\n for (const target of implementers) {\n if (target === decl.name) continue;\n const key = `${decl.name} ${target}`;\n if (!viaByEdge.has(key)) viaByEdge.set(key, new Set());\n viaByEdge.get(key)!.add(api);\n }\n }\n }\n\n const edges: RuntimeEdge[] = [];\n for (const key of viaByEdge.keys()) {\n const parts = key.split(' ');\n edges.push({ from: parts[0], to: parts[1], via: Array.from(viaByEdge.get(key)!).sort() });\n }\n edges.sort((a: RuntimeEdge, b: RuntimeEdge) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to));\n unresolved.sort(\n (a: RuntimeUnresolved, b: RuntimeUnresolved) =>\n a.service.localeCompare(b.service) || a.api.localeCompare(b.api),\n );\n return { edges, unresolved };\n}\n\n/** Adjacency (service -> [targets]) used for leveling + cycle checks. */\nfunction adjacencyFromEdges(serviceNames: string[], edges: RuntimeEdge[]): Record<string, string[]> {\n const adj: Record<string, string[]> = {};\n for (const name of serviceNames) adj[name] = [];\n for (const edge of edges) {\n if (!adj[edge.from]) adj[edge.from] = [];\n adj[edge.from].push(edge.to);\n }\n return adj;\n}\n\n/** Adjacency (service -> [targets]) from a loaded runtime graph. */\nexport function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]> {\n return adjacencyFromEdges(Object.keys(graph.services), graph.runtimeEdges);\n}\n\n/** Assign levels via topological sort; falls back to level 0 when a cycle exists. */\nfunction assignLevels(adjacency: Record<string, string[]>): Record<string, number> {\n const levels: Record<string, number> = {};\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const sorted = sortGraphTopologically(adjacency);\n for (const name of Object.keys(sorted)) levels[name] = sorted[name].level;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n for (const name of Object.keys(adjacency)) levels[name] = 0;\n }\n return levels;\n}\n\n/** Assemble the full runtime graph from the workspace model + service contracts. */\nexport function assembleRuntimeGraph(model: WorkspaceModel, workspaceRoot: string): RuntimeGraph {\n const decls = collectServiceDecls(model, workspaceRoot);\n const apis = buildApiIndex(decls);\n const edgeResult = buildEdges(decls, apis);\n\n const services: Record<string, RuntimeService> = {};\n for (const decl of decls) {\n const dependsOn = Array.from(\n new Set(edgeResult.edges.filter((e: RuntimeEdge) => e.from === decl.name).map((e: RuntimeEdge) => e.to)),\n ).sort();\n services[decl.name] = { level: 0, implements: decl.implements, uses: decl.uses, dependsOn };\n }\n\n const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edgeResult.edges));\n for (const name of Object.keys(services)) services[name].level = levels[name] ?? 0;\n\n const apisObj: Record<string, RuntimeApi> = {};\n for (const api of Array.from(apis.keys()).sort()) apisObj[api] = apis.get(api)!;\n\n return {\n services,\n apis: apisObj,\n runtimeEdges: edgeResult.edges,\n unresolvedUses: edgeResult.unresolved,\n };\n}\n\n/** Deterministic JSON (sorted keys + arrays already sorted during assembly). */\nfunction formatRuntimeJson(graph: RuntimeGraph): string {\n return JSON.stringify(graph, null, 4) + '\\n';\n}\n\nexport function saveRuntimeGraph(\n graph: RuntimeGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(fullPath, formatRuntimeJson(graph), 'utf-8');\n}\n\nexport function runtimeGraphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): boolean {\n return fs.existsSync(path.join(workspaceRoot, graphPath));\n}\n\nexport function loadRuntimeGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_RUNTIME_GRAPH_PATH,\n): RuntimeGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n if (!fs.existsSync(fullPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as RuntimeGraph;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load runtime graph from ${fullPath}: ${error.message}`);\n }\n}\n\n/** Serialize for an in-memory equality check (matches the on-disk format). */\nexport function serializeRuntimeGraph(graph: RuntimeGraph): string {\n return formatRuntimeJson(graph);\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Markers
|
|
3
|
+
*
|
|
4
|
+
* Reads the per-service `service-contract.json` files and builds a workspace
|
|
5
|
+
* model used to assemble the runtime microservice graph and to validate each
|
|
6
|
+
* service independently.
|
|
7
|
+
*
|
|
8
|
+
* `service-contract.json` (at a service's project root) declares, at api-PROJECT
|
|
9
|
+
* granularity (by package name), which api projects a service IMPLEMENTS (serves)
|
|
10
|
+
* and which it USES (calls as a client):
|
|
11
|
+
*
|
|
12
|
+
* { "implements": ["@scope/checkout-api"], "uses": ["@scope/payments-api"] }
|
|
13
|
+
*
|
|
14
|
+
* Classification (from webpieces.config.json globs):
|
|
15
|
+
* - api project: root matches `apiProjectPaths` (e.g. "libraries/apis/*").
|
|
16
|
+
* - service: root matches `servicePaths` (e.g. "services/*") AND is not an
|
|
17
|
+
* api project (api classification wins, so the lists may overlap
|
|
18
|
+
* harmlessly). Every service must have a service-contract.json.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SERVICE_CONTRACT_FILENAME = "service-contract.json";
|
|
21
|
+
/** Parsed `service-contract.json` contents (api package names). */
|
|
22
|
+
export interface ServiceContract {
|
|
23
|
+
implements: string[];
|
|
24
|
+
uses: string[];
|
|
25
|
+
}
|
|
26
|
+
/** Everything we know about one workspace project for runtime analysis. */
|
|
27
|
+
export interface ProjectInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
root: string;
|
|
30
|
+
packageName: string | null;
|
|
31
|
+
isApi: boolean;
|
|
32
|
+
isService: boolean;
|
|
33
|
+
/** Workspace project names this project depends on (from the Nx graph). */
|
|
34
|
+
deps: string[];
|
|
35
|
+
}
|
|
36
|
+
/** The whole-workspace model used by the generator and validators. */
|
|
37
|
+
export interface WorkspaceModel {
|
|
38
|
+
projects: Map<string, ProjectInfo>;
|
|
39
|
+
byPackageName: Map<string, string>;
|
|
40
|
+
}
|
|
41
|
+
/** Result of mapping package names to workspace projects. */
|
|
42
|
+
export interface ResolvedNames {
|
|
43
|
+
projects: string[];
|
|
44
|
+
unknown: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the workspace model from the Nx project graph. Dependency edges come
|
|
48
|
+
* from Nx (imports + package.json), the same source dependencies.json uses, so
|
|
49
|
+
* this works whether or not a service has its own package.json.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildWorkspaceModel(workspaceRoot: string, apiProjectPaths: string[], servicePaths: string[]): Promise<WorkspaceModel>;
|
|
52
|
+
/** Read and normalize a project's `service-contract.json`, or null if it has none. */
|
|
53
|
+
export declare function readServiceContract(workspaceRoot: string, projectRoot: string): ServiceContract | null;
|
|
54
|
+
/**
|
|
55
|
+
* Map a list of api package names (from a service contract) to workspace project
|
|
56
|
+
* names. Unknown names (not a workspace package) are returned in `unknown`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolvePackageNames(model: WorkspaceModel, packageNames: string[]): ResolvedNames;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime Markers
|
|
4
|
+
*
|
|
5
|
+
* Reads the per-service `service-contract.json` files and builds a workspace
|
|
6
|
+
* model used to assemble the runtime microservice graph and to validate each
|
|
7
|
+
* service independently.
|
|
8
|
+
*
|
|
9
|
+
* `service-contract.json` (at a service's project root) declares, at api-PROJECT
|
|
10
|
+
* granularity (by package name), which api projects a service IMPLEMENTS (serves)
|
|
11
|
+
* and which it USES (calls as a client):
|
|
12
|
+
*
|
|
13
|
+
* { "implements": ["@scope/checkout-api"], "uses": ["@scope/payments-api"] }
|
|
14
|
+
*
|
|
15
|
+
* Classification (from webpieces.config.json globs):
|
|
16
|
+
* - api project: root matches `apiProjectPaths` (e.g. "libraries/apis/*").
|
|
17
|
+
* - service: root matches `servicePaths` (e.g. "services/*") AND is not an
|
|
18
|
+
* api project (api classification wins, so the lists may overlap
|
|
19
|
+
* harmlessly). Every service must have a service-contract.json.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.SERVICE_CONTRACT_FILENAME = void 0;
|
|
23
|
+
exports.buildWorkspaceModel = buildWorkspaceModel;
|
|
24
|
+
exports.readServiceContract = readServiceContract;
|
|
25
|
+
exports.resolvePackageNames = resolvePackageNames;
|
|
26
|
+
const tslib_1 = require("tslib");
|
|
27
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
28
|
+
const path = tslib_1.__importStar(require("path"));
|
|
29
|
+
const devkit_1 = require("@nx/devkit");
|
|
30
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
31
|
+
const toError_1 = require("../toError");
|
|
32
|
+
exports.SERVICE_CONTRACT_FILENAME = 'service-contract.json';
|
|
33
|
+
/** True when a project root matches one of the given globs (segment/glob/dir-prefix). */
|
|
34
|
+
function matchesGlobs(root, globs) {
|
|
35
|
+
if (globs.length === 0)
|
|
36
|
+
return false;
|
|
37
|
+
return (0, rules_config_1.isPathExcluded)(root, globs);
|
|
38
|
+
}
|
|
39
|
+
function readPackageName(workspaceRoot, projectRoot) {
|
|
40
|
+
const pkgPath = path.join(workspaceRoot, projectRoot, 'package.json');
|
|
41
|
+
if (!fs.existsSync(pkgPath))
|
|
42
|
+
return null;
|
|
43
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync(pkgPath, 'utf-8');
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
return typeof parsed.name === 'string' ? parsed.name : null;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const error = (0, toError_1.toError)(err);
|
|
51
|
+
void error;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function collectDeps(name, dependencies, workspaceNames) {
|
|
56
|
+
const deps = [];
|
|
57
|
+
for (const edge of dependencies[name] ?? []) {
|
|
58
|
+
if (edge.target !== name && workspaceNames.has(edge.target)) {
|
|
59
|
+
deps.push(edge.target);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(new Set(deps)).sort();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the workspace model from the Nx project graph. Dependency edges come
|
|
66
|
+
* from Nx (imports + package.json), the same source dependencies.json uses, so
|
|
67
|
+
* this works whether or not a service has its own package.json.
|
|
68
|
+
*/
|
|
69
|
+
async function buildWorkspaceModel(workspaceRoot, apiProjectPaths, servicePaths) {
|
|
70
|
+
const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
|
|
71
|
+
const projectsConfig = (0, devkit_1.readProjectsConfigurationFromProjectGraph)(projectGraph);
|
|
72
|
+
const projects = new Map();
|
|
73
|
+
const byPackageName = new Map();
|
|
74
|
+
const workspaceNames = new Set(Object.keys(projectGraph.nodes));
|
|
75
|
+
for (const [name, cfg] of Object.entries(projectsConfig.projects)) {
|
|
76
|
+
const root = cfg.root;
|
|
77
|
+
if (root === '' || root === '.')
|
|
78
|
+
continue;
|
|
79
|
+
const packageName = readPackageName(workspaceRoot, root);
|
|
80
|
+
const isApi = matchesGlobs(root, apiProjectPaths);
|
|
81
|
+
projects.set(name, {
|
|
82
|
+
name,
|
|
83
|
+
root,
|
|
84
|
+
packageName,
|
|
85
|
+
isApi,
|
|
86
|
+
// api classification wins, so servicePaths and apiProjectPaths may overlap.
|
|
87
|
+
isService: !isApi && matchesGlobs(root, servicePaths),
|
|
88
|
+
deps: collectDeps(name, projectGraph.dependencies, workspaceNames),
|
|
89
|
+
});
|
|
90
|
+
if (packageName)
|
|
91
|
+
byPackageName.set(packageName, name);
|
|
92
|
+
}
|
|
93
|
+
return { projects, byPackageName };
|
|
94
|
+
}
|
|
95
|
+
/** Read and normalize a project's `service-contract.json`, or null if it has none. */
|
|
96
|
+
function readServiceContract(workspaceRoot, projectRoot) {
|
|
97
|
+
const fullPath = path.join(workspaceRoot, projectRoot, exports.SERVICE_CONTRACT_FILENAME);
|
|
98
|
+
if (!fs.existsSync(fullPath))
|
|
99
|
+
return null;
|
|
100
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
101
|
+
try {
|
|
102
|
+
const raw = fs.readFileSync(fullPath, 'utf-8');
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
return {
|
|
105
|
+
implements: Array.isArray(parsed.implements) ? parsed.implements : [],
|
|
106
|
+
uses: Array.isArray(parsed.uses) ? parsed.uses : [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const error = (0, toError_1.toError)(err);
|
|
111
|
+
throw new Error(`Failed to parse ${projectRoot}/${exports.SERVICE_CONTRACT_FILENAME}: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Map a list of api package names (from a service contract) to workspace project
|
|
116
|
+
* names. Unknown names (not a workspace package) are returned in `unknown`.
|
|
117
|
+
*/
|
|
118
|
+
function resolvePackageNames(model, packageNames) {
|
|
119
|
+
const resolved = [];
|
|
120
|
+
const unknown = [];
|
|
121
|
+
for (const pkg of packageNames) {
|
|
122
|
+
const project = model.byPackageName.get(pkg);
|
|
123
|
+
if (project)
|
|
124
|
+
resolved.push(project);
|
|
125
|
+
else
|
|
126
|
+
unknown.push(pkg);
|
|
127
|
+
}
|
|
128
|
+
return { projects: resolved, unknown };
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=runtime-markers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-markers.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-markers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Markers\n *\n * Reads the per-service `service-contract.json` files and builds a workspace\n * model used to assemble the runtime microservice graph and to validate each\n * service independently.\n *\n * `service-contract.json` (at a service's project root) declares, at api-PROJECT\n * granularity (by package name), which api projects a service IMPLEMENTS (serves)\n * and which it USES (calls as a client):\n *\n * { \"implements\": [\"@scope/checkout-api\"], \"uses\": [\"@scope/payments-api\"] }\n *\n * Classification (from webpieces.config.json globs):\n * - api project: root matches `apiProjectPaths` (e.g. \"libraries/apis/*\").\n * - service: root matches `servicePaths` (e.g. \"services/*\") AND is not an\n * api project (api classification wins, so the lists may overlap\n * harmlessly). Every service must have a service-contract.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph } from '@nx/devkit';\nimport { isPathExcluded } from '@webpieces/rules-config';\nimport { toError } from '../toError';\n\nexport const SERVICE_CONTRACT_FILENAME = 'service-contract.json';\n\n/** Parsed `service-contract.json` contents (api package names). */\nexport interface ServiceContract {\n implements: string[];\n uses: string[];\n}\n\n/** Everything we know about one workspace project for runtime analysis. */\nexport interface ProjectInfo {\n name: string;\n root: string;\n packageName: string | null;\n isApi: boolean;\n isService: boolean;\n /** Workspace project names this project depends on (from the Nx graph). */\n deps: string[];\n}\n\n/** The whole-workspace model used by the generator and validators. */\nexport interface WorkspaceModel {\n projects: Map<string, ProjectInfo>;\n byPackageName: Map<string, string>;\n}\n\n/** Result of mapping package names to workspace projects. */\nexport interface ResolvedNames {\n projects: string[];\n unknown: string[];\n}\n\n/** Minimal shapes for the JSON files we parse. */\ninterface PackageJsonShape {\n name?: string;\n}\ninterface ServiceContractShape {\n implements?: string[];\n uses?: string[];\n}\n/** One Nx project-graph dependency edge (the slice we use). */\ninterface GraphEdge {\n target: string;\n}\n\n/** True when a project root matches one of the given globs (segment/glob/dir-prefix). */\nfunction matchesGlobs(root: string, globs: string[]): boolean {\n if (globs.length === 0) return false;\n return isPathExcluded(root, globs);\n}\n\nfunction readPackageName(workspaceRoot: string, projectRoot: string): string | null {\n const pkgPath = path.join(workspaceRoot, projectRoot, 'package.json');\n if (!fs.existsSync(pkgPath)) return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = fs.readFileSync(pkgPath, 'utf-8');\n const parsed = JSON.parse(raw) as PackageJsonShape;\n return typeof parsed.name === 'string' ? parsed.name : null;\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n}\n\nfunction collectDeps(\n name: string,\n dependencies: Record<string, GraphEdge[]>,\n workspaceNames: Set<string>,\n): string[] {\n const deps: string[] = [];\n for (const edge of dependencies[name] ?? []) {\n if (edge.target !== name && workspaceNames.has(edge.target)) {\n deps.push(edge.target);\n }\n }\n return Array.from(new Set(deps)).sort();\n}\n\n/**\n * Build the workspace model from the Nx project graph. Dependency edges come\n * from Nx (imports + package.json), the same source dependencies.json uses, so\n * this works whether or not a service has its own package.json.\n */\nexport async function buildWorkspaceModel(\n workspaceRoot: string,\n apiProjectPaths: string[],\n servicePaths: string[],\n): Promise<WorkspaceModel> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n const projects = new Map<string, ProjectInfo>();\n const byPackageName = new Map<string, string>();\n const workspaceNames = new Set(Object.keys(projectGraph.nodes));\n\n for (const [name, cfg] of Object.entries(projectsConfig.projects)) {\n const root = cfg.root;\n if (root === '' || root === '.') continue;\n\n const packageName = readPackageName(workspaceRoot, root);\n const isApi = matchesGlobs(root, apiProjectPaths);\n projects.set(name, {\n name,\n root,\n packageName,\n isApi,\n // api classification wins, so servicePaths and apiProjectPaths may overlap.\n isService: !isApi && matchesGlobs(root, servicePaths),\n deps: collectDeps(name, projectGraph.dependencies, workspaceNames),\n });\n if (packageName) byPackageName.set(packageName, name);\n }\n\n return { projects, byPackageName };\n}\n\n/** Read and normalize a project's `service-contract.json`, or null if it has none. */\nexport function readServiceContract(workspaceRoot: string, projectRoot: string): ServiceContract | null {\n const fullPath = path.join(workspaceRoot, projectRoot, SERVICE_CONTRACT_FILENAME);\n if (!fs.existsSync(fullPath)) return null;\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const raw = fs.readFileSync(fullPath, 'utf-8');\n const parsed = JSON.parse(raw) as ServiceContractShape;\n return {\n implements: Array.isArray(parsed.implements) ? parsed.implements : [],\n uses: Array.isArray(parsed.uses) ? parsed.uses : [],\n };\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to parse ${projectRoot}/${SERVICE_CONTRACT_FILENAME}: ${error.message}`);\n }\n}\n\n/**\n * Map a list of api package names (from a service contract) to workspace project\n * names. Unknown names (not a workspace package) are returned in `unknown`.\n */\nexport function resolvePackageNames(\n model: WorkspaceModel,\n packageNames: string[],\n): ResolvedNames {\n const resolved: string[] = [];\n const unknown: string[] = [];\n for (const pkg of packageNames) {\n const project = model.byPackageName.get(pkg);\n if (project) resolved.push(project);\n else unknown.push(pkg);\n }\n return { projects: resolved, unknown };\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Visualizer
|
|
3
|
+
*
|
|
4
|
+
* Renders the runtime microservice graph (services + inferred Z -> X edges,
|
|
5
|
+
* each labeled with the api(s) they flow over) to DOT + interactive HTML in
|
|
6
|
+
* tmp/webpieces/runtime-architecture.{dot,html}.
|
|
7
|
+
*/
|
|
8
|
+
import type { RuntimeGraph } from './runtime-graph';
|
|
9
|
+
/** Build the Graphviz DOT for the runtime service graph. */
|
|
10
|
+
export declare function generateRuntimeDot(graph: RuntimeGraph, title?: string): string;
|
|
11
|
+
export interface RuntimeVisualizationPaths {
|
|
12
|
+
dotPath: string;
|
|
13
|
+
htmlPath: string;
|
|
14
|
+
}
|
|
15
|
+
/** Write the DOT + HTML renderings to tmp/webpieces/. */
|
|
16
|
+
export declare function writeRuntimeVisualization(graph: RuntimeGraph, workspaceRoot: string, title?: string): RuntimeVisualizationPaths;
|