@webpieces/nx-webpieces-rules 0.4.519 → 0.4.521
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 +54 -34
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.js +46 -10
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +1 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/lib/api-usage/api-ast.d.ts +17 -1
- package/src/lib/api-usage/api-ast.js +39 -1
- package/src/lib/api-usage/api-ast.js.map +1 -1
- package/src/lib/api-usage/api-relations.d.ts +41 -0
- package/src/lib/api-usage/api-relations.js +15 -1
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/api-usage/external-systems.d.ts +61 -0
- package/src/lib/api-usage/external-systems.js +158 -0
- package/src/lib/api-usage/external-systems.js.map +1 -0
- package/src/lib/dot-syntax.d.ts +13 -0
- package/src/lib/dot-syntax.js +17 -0
- package/src/lib/dot-syntax.js.map +1 -1
- package/src/lib/graph-loader.d.ts +15 -3
- package/src/lib/graph-loader.js +36 -3
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-visualizer.client.js +7 -4
- package/src/lib/graph-visualizer.js +1 -2
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-graph-model.d.ts +38 -1
- package/src/lib/runtime-graph-model.js.map +1 -1
- package/src/lib/runtime-graph.d.ts +3 -3
- package/src/lib/runtime-graph.js +12 -3
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +14 -6
- package/src/lib/runtime-visualizer.js +238 -31
- package/src/lib/runtime-visualizer.js.map +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External system declarations
|
|
3
|
+
*
|
|
4
|
+
* A system OUTSIDE this repo that a service talks to — a database, a bucket, a cache. Until now
|
|
5
|
+
* every one of them collapsed into the same grey dashed box, so `lib-firestore` (a datastore) was
|
|
6
|
+
* indistinguishable from an HTTP service the repo happens not to implement. Declaring what a system
|
|
7
|
+
* IS lets the runtime viz draw it as the thing it is.
|
|
8
|
+
*
|
|
9
|
+
* Split out of api-scanner.ts and runtime-graph.ts (both already at their file-size limit) so the
|
|
10
|
+
* two halves of the feature — DECLARING a system and RESOLVING it to arrows — sit together.
|
|
11
|
+
*
|
|
12
|
+
* The two declaration sites exist because the two real cases differ in whether a contract exists:
|
|
13
|
+
*
|
|
14
|
+
* - **wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an
|
|
15
|
+
* `@externalSystem <kind> [label]` JSDoc tag on the CONTRACT. A TS `interface` cannot carry a
|
|
16
|
+
* decorator, and these seams are interfaces, so JSDoc is the only marker that fits in place.
|
|
17
|
+
* - **unwrapped** — the service opens the connection itself (a `pg.Pool`, a TypeORM `DataSource`)
|
|
18
|
+
* and there is no contract to mark, so the declaration is an `external:<kind>:<identity>` nx tag
|
|
19
|
+
* on that PROJECT. Wrapping such a datastore purely to gain a marker is not worth it: a TypeORM
|
|
20
|
+
* facade never closes, unlike the ~8 hand-picked methods a firestore seam needs.
|
|
21
|
+
*
|
|
22
|
+
* Both resolve to the same `(kind, identity)` pair, and identity is the NODE identity — two projects
|
|
23
|
+
* declaring `postgres` converge on one cylinder with an arrow each, rather than drawing a database
|
|
24
|
+
* apiece.
|
|
25
|
+
*/
|
|
26
|
+
import type { ApiClassInfo, ExternalSystemDecls } from './api-relations';
|
|
27
|
+
import type { ProjectInfo } from '../project-info';
|
|
28
|
+
import type { RuntimeExternalSystem, RuntimeGraph, RuntimeService } from '../runtime-graph-model';
|
|
29
|
+
/**
|
|
30
|
+
* The committed `externalSystems` table for architecture/dependencies.json, merging both declaration
|
|
31
|
+
* sites into one identity-keyed map.
|
|
32
|
+
*
|
|
33
|
+
* A system may legitimately carry both: a repo can wrap a datastore behind a contract in one service
|
|
34
|
+
* and open it directly in another. Takes the api index rather than the whole scan result so this
|
|
35
|
+
* module never has to import api-scanner, which imports it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildExternalSystems(apiIndex: Map<string, ApiClassInfo>, projectInfos: Map<string, ProjectInfo>): ExternalSystemDecls;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve declarations into drawable nodes: which services actually get an arrow to each system.
|
|
40
|
+
*
|
|
41
|
+
* Two resolutions, matching the two declaration sites. A CONTRACT-declared system is reached by
|
|
42
|
+
* every service that `uses` one of its contracts, so the arrows follow real call sites. A
|
|
43
|
+
* TAG-declared system is reached ONLY by the tagged project itself — a tag asserts "I open this
|
|
44
|
+
* connection", and fanning it out to dependents would invent arrows nobody wrote (a service that
|
|
45
|
+
* depends on the entity library only for a DTO type does not talk to the database).
|
|
46
|
+
*
|
|
47
|
+
* A system nothing reaches is dropped rather than drawn floating: a declaration whose users all
|
|
48
|
+
* disappeared is stale, and an unconnected node on the graph reads as a live dependency.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveExternalSystems(decls: ExternalSystemDecls, services: Record<string, RuntimeService>): Record<string, RuntimeExternalSystem>;
|
|
51
|
+
/**
|
|
52
|
+
* Hang the resolved systems off the graph, and STAMP each declaring contract with its declaration.
|
|
53
|
+
*
|
|
54
|
+
* The stamp is what stops the same system being drawn twice: the visualizer skips an
|
|
55
|
+
* `unresolvedUses` entry whose contract carries one, because that contract has already been drawn
|
|
56
|
+
* with a real shape rather than as the generic grey box it would otherwise fall back to.
|
|
57
|
+
*
|
|
58
|
+
* A graph with nothing declared is left completely untouched — no empty key is written — so a repo
|
|
59
|
+
* that adopts none of this keeps a byte-identical runtime-dependencies.json.
|
|
60
|
+
*/
|
|
61
|
+
export declare function attachExternalSystems(graph: RuntimeGraph, systems: Record<string, RuntimeExternalSystem>): void;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* External system declarations
|
|
4
|
+
*
|
|
5
|
+
* A system OUTSIDE this repo that a service talks to — a database, a bucket, a cache. Until now
|
|
6
|
+
* every one of them collapsed into the same grey dashed box, so `lib-firestore` (a datastore) was
|
|
7
|
+
* indistinguishable from an HTTP service the repo happens not to implement. Declaring what a system
|
|
8
|
+
* IS lets the runtime viz draw it as the thing it is.
|
|
9
|
+
*
|
|
10
|
+
* Split out of api-scanner.ts and runtime-graph.ts (both already at their file-size limit) so the
|
|
11
|
+
* two halves of the feature — DECLARING a system and RESOLVING it to arrows — sit together.
|
|
12
|
+
*
|
|
13
|
+
* The two declaration sites exist because the two real cases differ in whether a contract exists:
|
|
14
|
+
*
|
|
15
|
+
* - **wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an
|
|
16
|
+
* `@externalSystem <kind> [label]` JSDoc tag on the CONTRACT. A TS `interface` cannot carry a
|
|
17
|
+
* decorator, and these seams are interfaces, so JSDoc is the only marker that fits in place.
|
|
18
|
+
* - **unwrapped** — the service opens the connection itself (a `pg.Pool`, a TypeORM `DataSource`)
|
|
19
|
+
* and there is no contract to mark, so the declaration is an `external:<kind>:<identity>` nx tag
|
|
20
|
+
* on that PROJECT. Wrapping such a datastore purely to gain a marker is not worth it: a TypeORM
|
|
21
|
+
* facade never closes, unlike the ~8 hand-picked methods a firestore seam needs.
|
|
22
|
+
*
|
|
23
|
+
* Both resolve to the same `(kind, identity)` pair, and identity is the NODE identity — two projects
|
|
24
|
+
* declaring `postgres` converge on one cylinder with an arrow each, rather than drawing a database
|
|
25
|
+
* apiece.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.buildExternalSystems = buildExternalSystems;
|
|
29
|
+
exports.resolveExternalSystems = resolveExternalSystems;
|
|
30
|
+
exports.attachExternalSystems = attachExternalSystems;
|
|
31
|
+
const api_relations_1 = require("./api-relations");
|
|
32
|
+
/** nx tag prefix declaring an external system: `external:<kind>:<identity>`. */
|
|
33
|
+
const EXTERNAL_TAG_PREFIX = 'external:';
|
|
34
|
+
/** The two halves of a parsed `external:<kind>:<identity>` nx tag. */
|
|
35
|
+
class ExternalTag {
|
|
36
|
+
kind;
|
|
37
|
+
identity;
|
|
38
|
+
constructor(kind, identity) {
|
|
39
|
+
this.kind = kind;
|
|
40
|
+
this.identity = identity;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `external:<kind>:<identity>` -> its parts, or null for any other tag.
|
|
45
|
+
*
|
|
46
|
+
* Returns null (rather than throwing) on an unknown kind: this prefix shares a tag list with
|
|
47
|
+
* `framework:` and `role:`, and hard-failing the whole graph generation over one malformed tag is a
|
|
48
|
+
* worse outcome than not drawing one node.
|
|
49
|
+
*/
|
|
50
|
+
// webpieces-disable no-function-outside-class -- pure parser, matching the sibling builders in this file
|
|
51
|
+
function parseExternalTag(tag) {
|
|
52
|
+
if (!tag.startsWith(EXTERNAL_TAG_PREFIX))
|
|
53
|
+
return null;
|
|
54
|
+
const parts = tag.slice(EXTERNAL_TAG_PREFIX.length).split(':');
|
|
55
|
+
if (parts.length !== 2)
|
|
56
|
+
return null;
|
|
57
|
+
const kind = parts[0].toLowerCase();
|
|
58
|
+
const identity = parts[1].trim();
|
|
59
|
+
if (!(0, api_relations_1.isExternalSystemKind)(kind) || identity === '')
|
|
60
|
+
return null;
|
|
61
|
+
return new ExternalTag(kind, identity);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The committed `externalSystems` table for architecture/dependencies.json, merging both declaration
|
|
65
|
+
* sites into one identity-keyed map.
|
|
66
|
+
*
|
|
67
|
+
* A system may legitimately carry both: a repo can wrap a datastore behind a contract in one service
|
|
68
|
+
* and open it directly in another. Takes the api index rather than the whole scan result so this
|
|
69
|
+
* module never has to import api-scanner, which imports it.
|
|
70
|
+
*/
|
|
71
|
+
// webpieces-disable no-function-outside-class -- table builder, mirrors buildApiContracts in api-scanner.ts
|
|
72
|
+
function buildExternalSystems(apiIndex, projectInfos) {
|
|
73
|
+
const systems = {};
|
|
74
|
+
// First declaration wins the kind, so a later typo cannot silently reshape an already-drawn node.
|
|
75
|
+
const ensure = (identity, kind, label) => {
|
|
76
|
+
if (systems[identity] === undefined)
|
|
77
|
+
systems[identity] = { kind, label, apis: [], projects: [] };
|
|
78
|
+
return systems[identity];
|
|
79
|
+
};
|
|
80
|
+
for (const api of [...apiIndex.keys()].sort()) {
|
|
81
|
+
const declared = apiIndex.get(api).externalSystem;
|
|
82
|
+
if (declared === undefined)
|
|
83
|
+
continue;
|
|
84
|
+
ensure(declared.label, declared.kind, declared.label).apis.push(api);
|
|
85
|
+
}
|
|
86
|
+
for (const name of [...projectInfos.keys()].sort()) {
|
|
87
|
+
for (const tag of projectInfos.get(name).tags) {
|
|
88
|
+
const parsed = parseExternalTag(tag);
|
|
89
|
+
if (parsed === null)
|
|
90
|
+
continue;
|
|
91
|
+
ensure(parsed.identity, parsed.kind, parsed.identity).projects.push(name);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return systems;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolve declarations into drawable nodes: which services actually get an arrow to each system.
|
|
98
|
+
*
|
|
99
|
+
* Two resolutions, matching the two declaration sites. A CONTRACT-declared system is reached by
|
|
100
|
+
* every service that `uses` one of its contracts, so the arrows follow real call sites. A
|
|
101
|
+
* TAG-declared system is reached ONLY by the tagged project itself — a tag asserts "I open this
|
|
102
|
+
* connection", and fanning it out to dependents would invent arrows nobody wrote (a service that
|
|
103
|
+
* depends on the entity library only for a DTO type does not talk to the database).
|
|
104
|
+
*
|
|
105
|
+
* A system nothing reaches is dropped rather than drawn floating: a declaration whose users all
|
|
106
|
+
* disappeared is stale, and an unconnected node on the graph reads as a live dependency.
|
|
107
|
+
*/
|
|
108
|
+
// webpieces-disable no-function-outside-class -- pure resolver, matching the sibling builders in this file
|
|
109
|
+
function resolveExternalSystems(decls, services) {
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const identity of Object.keys(decls).sort()) {
|
|
112
|
+
const decl = decls[identity];
|
|
113
|
+
const usedBy = new Set();
|
|
114
|
+
for (const name of Object.keys(services)) {
|
|
115
|
+
if (services[name].uses.some((api) => decl.apis.includes(api)))
|
|
116
|
+
usedBy.add(name);
|
|
117
|
+
}
|
|
118
|
+
// Only when the tagged project is itself a runtime node — tagging a plain library would
|
|
119
|
+
// otherwise draw an arrow leaving a box that does not exist on this graph.
|
|
120
|
+
for (const project of decl.projects) {
|
|
121
|
+
if (services[project] !== undefined)
|
|
122
|
+
usedBy.add(project);
|
|
123
|
+
}
|
|
124
|
+
if (usedBy.size === 0)
|
|
125
|
+
continue;
|
|
126
|
+
out[identity] = {
|
|
127
|
+
kind: decl.kind,
|
|
128
|
+
label: decl.label,
|
|
129
|
+
usedBy: [...usedBy].sort(),
|
|
130
|
+
apis: [...decl.apis].sort(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Hang the resolved systems off the graph, and STAMP each declaring contract with its declaration.
|
|
137
|
+
*
|
|
138
|
+
* The stamp is what stops the same system being drawn twice: the visualizer skips an
|
|
139
|
+
* `unresolvedUses` entry whose contract carries one, because that contract has already been drawn
|
|
140
|
+
* with a real shape rather than as the generic grey box it would otherwise fall back to.
|
|
141
|
+
*
|
|
142
|
+
* A graph with nothing declared is left completely untouched — no empty key is written — so a repo
|
|
143
|
+
* that adopts none of this keeps a byte-identical runtime-dependencies.json.
|
|
144
|
+
*/
|
|
145
|
+
// webpieces-disable no-function-outside-class -- pure graph mutator, matching the sibling builders in this file
|
|
146
|
+
function attachExternalSystems(graph, systems) {
|
|
147
|
+
if (Object.keys(systems).length === 0)
|
|
148
|
+
return;
|
|
149
|
+
graph.externalSystems = systems;
|
|
150
|
+
for (const system of Object.values(systems)) {
|
|
151
|
+
for (const api of system.apis) {
|
|
152
|
+
const entry = graph.apis[api];
|
|
153
|
+
if (entry !== undefined)
|
|
154
|
+
entry.externalSystem = { kind: system.kind, label: system.label };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=external-systems.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-systems.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/external-systems.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * External system declarations\n *\n * A system OUTSIDE this repo that a service talks to — a database, a bucket, a cache. Until now\n * every one of them collapsed into the same grey dashed box, so `lib-firestore` (a datastore) was\n * indistinguishable from an HTTP service the repo happens not to implement. Declaring what a system\n * IS lets the runtime viz draw it as the thing it is.\n *\n * Split out of api-scanner.ts and runtime-graph.ts (both already at their file-size limit) so the\n * two halves of the feature — DECLARING a system and RESOLVING it to arrows — sit together.\n *\n * The two declaration sites exist because the two real cases differ in whether a contract exists:\n *\n * - **wrapped** — the repo has a vendor seam (`FirestoreAdminApi`), so the kind is declared with an\n * `@externalSystem <kind> [label]` JSDoc tag on the CONTRACT. A TS `interface` cannot carry a\n * decorator, and these seams are interfaces, so JSDoc is the only marker that fits in place.\n * - **unwrapped** — the service opens the connection itself (a `pg.Pool`, a TypeORM `DataSource`)\n * and there is no contract to mark, so the declaration is an `external:<kind>:<identity>` nx tag\n * on that PROJECT. Wrapping such a datastore purely to gain a marker is not worth it: a TypeORM\n * facade never closes, unlike the ~8 hand-picked methods a firestore seam needs.\n *\n * Both resolve to the same `(kind, identity)` pair, and identity is the NODE identity — two projects\n * declaring `postgres` converge on one cylinder with an arrow each, rather than drawing a database\n * apiece.\n */\n\nimport type { ApiClassInfo, ExternalSystemDecl, ExternalSystemDecls, ExternalSystemKind } from './api-relations';\nimport { isExternalSystemKind } from './api-relations';\nimport type { ProjectInfo } from '../project-info';\nimport type { RuntimeExternalSystem, RuntimeGraph, RuntimeService } from '../runtime-graph-model';\n\n/** nx tag prefix declaring an external system: `external:<kind>:<identity>`. */\nconst EXTERNAL_TAG_PREFIX = 'external:';\n\n/** The two halves of a parsed `external:<kind>:<identity>` nx tag. */\nclass ExternalTag {\n constructor(\n public readonly kind: ExternalSystemKind,\n public readonly identity: string,\n ) {}\n}\n\n/**\n * `external:<kind>:<identity>` -> its parts, or null for any other tag.\n *\n * Returns null (rather than throwing) on an unknown kind: this prefix shares a tag list with\n * `framework:` and `role:`, and hard-failing the whole graph generation over one malformed tag is a\n * worse outcome than not drawing one node.\n */\n// webpieces-disable no-function-outside-class -- pure parser, matching the sibling builders in this file\nfunction parseExternalTag(tag: string): ExternalTag | null {\n if (!tag.startsWith(EXTERNAL_TAG_PREFIX)) return null;\n const parts = tag.slice(EXTERNAL_TAG_PREFIX.length).split(':');\n if (parts.length !== 2) return null;\n const kind = parts[0].toLowerCase();\n const identity = parts[1].trim();\n if (!isExternalSystemKind(kind) || identity === '') return null;\n return new ExternalTag(kind, identity);\n}\n\n/**\n * The committed `externalSystems` table for architecture/dependencies.json, merging both declaration\n * sites into one identity-keyed map.\n *\n * A system may legitimately carry both: a repo can wrap a datastore behind a contract in one service\n * and open it directly in another. Takes the api index rather than the whole scan result so this\n * module never has to import api-scanner, which imports it.\n */\n// webpieces-disable no-function-outside-class -- table builder, mirrors buildApiContracts in api-scanner.ts\nexport function buildExternalSystems(\n apiIndex: Map<string, ApiClassInfo>,\n projectInfos: Map<string, ProjectInfo>,\n): ExternalSystemDecls {\n const systems: ExternalSystemDecls = {};\n // First declaration wins the kind, so a later typo cannot silently reshape an already-drawn node.\n const ensure = (identity: string, kind: ExternalSystemKind, label: string): ExternalSystemDecl => {\n if (systems[identity] === undefined) systems[identity] = { kind, label, apis: [], projects: [] };\n return systems[identity];\n };\n\n for (const api of [...apiIndex.keys()].sort()) {\n const declared = apiIndex.get(api)!.externalSystem;\n if (declared === undefined) continue;\n ensure(declared.label, declared.kind, declared.label).apis.push(api);\n }\n\n for (const name of [...projectInfos.keys()].sort()) {\n for (const tag of projectInfos.get(name)!.tags) {\n const parsed = parseExternalTag(tag);\n if (parsed === null) continue;\n ensure(parsed.identity, parsed.kind, parsed.identity).projects.push(name);\n }\n }\n return systems;\n}\n\n/**\n * Resolve declarations into drawable nodes: which services actually get an arrow to each system.\n *\n * Two resolutions, matching the two declaration sites. A CONTRACT-declared system is reached by\n * every service that `uses` one of its contracts, so the arrows follow real call sites. A\n * TAG-declared system is reached ONLY by the tagged project itself — a tag asserts \"I open this\n * connection\", and fanning it out to dependents would invent arrows nobody wrote (a service that\n * depends on the entity library only for a DTO type does not talk to the database).\n *\n * A system nothing reaches is dropped rather than drawn floating: a declaration whose users all\n * disappeared is stale, and an unconnected node on the graph reads as a live dependency.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, matching the sibling builders in this file\nexport function resolveExternalSystems(\n decls: ExternalSystemDecls,\n services: Record<string, RuntimeService>,\n): Record<string, RuntimeExternalSystem> {\n const out: Record<string, RuntimeExternalSystem> = {};\n for (const identity of Object.keys(decls).sort()) {\n const decl = decls[identity];\n const usedBy = new Set<string>();\n for (const name of Object.keys(services)) {\n if (services[name].uses.some((api: string) => decl.apis.includes(api))) usedBy.add(name);\n }\n // Only when the tagged project is itself a runtime node — tagging a plain library would\n // otherwise draw an arrow leaving a box that does not exist on this graph.\n for (const project of decl.projects) {\n if (services[project] !== undefined) usedBy.add(project);\n }\n if (usedBy.size === 0) continue;\n out[identity] = {\n kind: decl.kind,\n label: decl.label,\n usedBy: [...usedBy].sort(),\n apis: [...decl.apis].sort(),\n };\n }\n return out;\n}\n\n/**\n * Hang the resolved systems off the graph, and STAMP each declaring contract with its declaration.\n *\n * The stamp is what stops the same system being drawn twice: the visualizer skips an\n * `unresolvedUses` entry whose contract carries one, because that contract has already been drawn\n * with a real shape rather than as the generic grey box it would otherwise fall back to.\n *\n * A graph with nothing declared is left completely untouched — no empty key is written — so a repo\n * that adopts none of this keeps a byte-identical runtime-dependencies.json.\n */\n// webpieces-disable no-function-outside-class -- pure graph mutator, matching the sibling builders in this file\nexport function attachExternalSystems(graph: RuntimeGraph, systems: Record<string, RuntimeExternalSystem>): void {\n if (Object.keys(systems).length === 0) return;\n graph.externalSystems = systems;\n for (const system of Object.values(systems)) {\n for (const api of system.apis) {\n const entry = graph.apis[api];\n if (entry !== undefined) entry.externalSystem = { kind: system.kind, label: system.label };\n }\n }\n}\n"]}
|
package/src/lib/dot-syntax.d.ts
CHANGED
|
@@ -20,6 +20,19 @@
|
|
|
20
20
|
* escaping their values — the separator is ours, the value is theirs.
|
|
21
21
|
*/
|
|
22
22
|
export declare function dotValue(value: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Escape a runtime value for a `record`/`Mrecord` label, on TOP of {@link dotValue}.
|
|
25
|
+
*
|
|
26
|
+
* A record label is not ordinary label text: `|` splits fields, `{}` toggles the layout direction
|
|
27
|
+
* and `<>` delimit port names. A queue name carrying any of them would not merely look wrong — it
|
|
28
|
+
* would restructure the node, splitting one queue box into two or rotating it. Graphviz's record
|
|
29
|
+
* grammar escapes those with a backslash, and since {@link dotValue} has already doubled real
|
|
30
|
+
* backslashes, the `\\` emitted here survives into the record parser as a single one.
|
|
31
|
+
*
|
|
32
|
+
* Leading and trailing spaces are also dropped by the record parser, which is exactly why the
|
|
33
|
+
* horizontal-cylinder queue node uses a `" |"` prefix — that empty first field is deliberate.
|
|
34
|
+
*/
|
|
35
|
+
export declare function recordValue(value: string): string;
|
|
23
36
|
/** Thrown when the generator produces DOT that Graphviz could not parse. */
|
|
24
37
|
export declare class InvalidDotError extends Error {
|
|
25
38
|
constructor(message: string);
|
package/src/lib/dot-syntax.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.InvalidDotError = void 0;
|
|
17
17
|
exports.dotValue = dotValue;
|
|
18
|
+
exports.recordValue = recordValue;
|
|
18
19
|
exports.assertValidDot = assertValidDot;
|
|
19
20
|
/** Chars a quoted string may legally sit directly after, ignoring whitespace. */
|
|
20
21
|
const LEGAL_BEFORE_STRING = new Set(['=', '[', ',', ';', '{', '}', '>', '-']);
|
|
@@ -32,6 +33,22 @@ const LEGAL_AFTER_STRING = new Set(['=', '[', ']', ',', ';', '{', '}', '-', '>']
|
|
|
32
33
|
function dotValue(value) {
|
|
33
34
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Escape a runtime value for a `record`/`Mrecord` label, on TOP of {@link dotValue}.
|
|
38
|
+
*
|
|
39
|
+
* A record label is not ordinary label text: `|` splits fields, `{}` toggles the layout direction
|
|
40
|
+
* and `<>` delimit port names. A queue name carrying any of them would not merely look wrong — it
|
|
41
|
+
* would restructure the node, splitting one queue box into two or rotating it. Graphviz's record
|
|
42
|
+
* grammar escapes those with a backslash, and since {@link dotValue} has already doubled real
|
|
43
|
+
* backslashes, the `\\` emitted here survives into the record parser as a single one.
|
|
44
|
+
*
|
|
45
|
+
* Leading and trailing spaces are also dropped by the record parser, which is exactly why the
|
|
46
|
+
* horizontal-cylinder queue node uses a `" |"` prefix — that empty first field is deliberate.
|
|
47
|
+
*/
|
|
48
|
+
// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
|
|
49
|
+
function recordValue(value) {
|
|
50
|
+
return dotValue(value).replace(/[|{}<>]/g, (char) => `\\${char}`);
|
|
51
|
+
}
|
|
35
52
|
/** Thrown when the generator produces DOT that Graphviz could not parse. */
|
|
36
53
|
class InvalidDotError extends Error {
|
|
37
54
|
constructor(message) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dot-syntax.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dot-syntax.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DOT syntax helpers\n *\n * Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:\n *\n * 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)\n * becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how\n * an unescaped `\"` shipped and took the whole diagram down: in DOT a bare `\"` TERMINATES the\n * string it appears in, so one bad node line makes the entire graph fail to parse.\n * 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of\n * mistake into a thrown error at generation time, instead of a blank page with a Graphviz\n * \"syntax error in line N\" that only a human opening the HTML ever sees.\n */\n\n/** Chars a quoted string may legally sit directly after, ignoring whitespace. */\nconst LEGAL_BEFORE_STRING = new Set(['=', '[', ',', ';', '{', '}', '>', '-']);\n\n/** Chars a quoted string may legally be followed by, ignoring whitespace. */\nconst LEGAL_AFTER_STRING = new Set(['=', '[', ']', ',', ';', '{', '}', '-', '>']);\n\n/**\n * Escape a runtime value for use INSIDE a quoted DOT string.\n *\n * Only `\\` and `\"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once\n * it is inside quotes. Note this deliberately escapes `\\` FIRST, so a value containing a backslash\n * cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\\\n` AFTER\n * escaping their values — the separator is ours, the value is theirs.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function dotValue(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\n/** Thrown when the generator produces DOT that Graphviz could not parse. */\nexport class InvalidDotError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidDotError';\n }\n}\n\n/**\n * Fail loudly on structurally broken DOT.\n *\n * This is not a full Graphviz parser — it is the check that catches the failure mode a string\n * builder actually has: a quote that ends a string early (or never ends it). It scans the quoted\n * strings honouring `\\\"` escapes and asserts each one is terminated and is bounded by DOT\n * punctuation rather than by bare text. An unescaped `\"` inside a label always violates that: the\n * string ends mid-label, and the remaining label text becomes stray tokens.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function assertValidDot(dot: string, source: string): void {\n // Comment text is not code: a `\"` or a word in it must never be read as a DOT token. Blanking\n // it (offsets preserved) keeps every reported line number the one Graphviz would report.\n const code = blankComments(dot);\n let index = 0;\n while (index < code.length) {\n if (code[index] !== '\"') {\n index++;\n continue;\n }\n const start = index;\n index++;\n while (index < code.length && code[index] !== '\"') {\n index += code[index] === '\\\\' ? 2 : 1;\n }\n if (index >= code.length) {\n throw new InvalidDotError(`${source}: unterminated string starting at ${describe(dot, start)}`);\n }\n const end = index;\n index++;\n checkNeighbor(code, dot, start, end, source);\n }\n}\n\n/** Replace `//`, `#` and `/* *\\/` comment bodies with spaces, preserving length and newlines. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction blankComments(dot: string): string {\n const out = dot.split('');\n let index = 0;\n let inString = false;\n while (index < out.length) {\n const two = dot.slice(index, index + 2);\n if (inString) {\n if (dot[index] === '\\\\') index++;\n else if (dot[index] === '\"') inString = false;\n index++;\n } else if (dot[index] === '\"') {\n inString = true;\n index++;\n } else if (two === '//' || dot[index] === '#') {\n while (index < out.length && out[index] !== '\\n') out[index++] = ' ';\n } else if (two === '/*') {\n while (index < out.length && dot.slice(index, index + 2) !== '*/') {\n if (out[index] !== '\\n') out[index] = ' ';\n index++;\n }\n if (index < out.length) {\n out[index] = ' ';\n out[index + 1] = ' ';\n index += 2;\n }\n } else {\n index++;\n }\n }\n return out.join('');\n}\n\n/** Verify the non-whitespace chars bracketing a quoted string are DOT punctuation, not stray text. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction checkNeighbor(code: string, dot: string, start: number, end: number, source: string): void {\n const before = nonSpaceChar(code, start - 1, -1);\n if (before !== undefined && !LEGAL_BEFORE_STRING.has(before)) {\n throw new InvalidDotError(\n `${source}: a quoted string starts right after '${before}' at ${describe(dot, start)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended the previous string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n const after = nonSpaceChar(code, end + 1, 1);\n if (after !== undefined && !LEGAL_AFTER_STRING.has(after)) {\n throw new InvalidDotError(\n `${source}: a quoted string is followed by '${after}' at ${describe(dot, end)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended this string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n}\n\n/** The first non-whitespace char walking `step` from `from`, or undefined at either end. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction nonSpaceChar(dot: string, from: number, step: number): string | undefined {\n for (let i = from; i >= 0 && i < dot.length; i += step) {\n if (!/\\s/.test(dot[i])) return dot[i];\n }\n return undefined;\n}\n\n/** `line N: <the line>` for the offset, so the error names the same line Graphviz would. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction describe(dot: string, offset: number): string {\n const lineNumber = dot.slice(0, offset).split('\\n').length;\n const line = dot.split('\\n')[lineNumber - 1];\n return `line ${lineNumber}: ${line.trim()}`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dot-syntax.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dot-syntax.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DOT syntax helpers\n *\n * Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:\n *\n * 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)\n * becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how\n * an unescaped `\"` shipped and took the whole diagram down: in DOT a bare `\"` TERMINATES the\n * string it appears in, so one bad node line makes the entire graph fail to parse.\n * 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of\n * mistake into a thrown error at generation time, instead of a blank page with a Graphviz\n * \"syntax error in line N\" that only a human opening the HTML ever sees.\n */\n\n/** Chars a quoted string may legally sit directly after, ignoring whitespace. */\nconst LEGAL_BEFORE_STRING = new Set(['=', '[', ',', ';', '{', '}', '>', '-']);\n\n/** Chars a quoted string may legally be followed by, ignoring whitespace. */\nconst LEGAL_AFTER_STRING = new Set(['=', '[', ']', ',', ';', '{', '}', '-', '>']);\n\n/**\n * Escape a runtime value for use INSIDE a quoted DOT string.\n *\n * Only `\\` and `\"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once\n * it is inside quotes. Note this deliberately escapes `\\` FIRST, so a value containing a backslash\n * cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\\\n` AFTER\n * escaping their values — the separator is ours, the value is theirs.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function dotValue(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\n/**\n * Escape a runtime value for a `record`/`Mrecord` label, on TOP of {@link dotValue}.\n *\n * A record label is not ordinary label text: `|` splits fields, `{}` toggles the layout direction\n * and `<>` delimit port names. A queue name carrying any of them would not merely look wrong — it\n * would restructure the node, splitting one queue box into two or rotating it. Graphviz's record\n * grammar escapes those with a backslash, and since {@link dotValue} has already doubled real\n * backslashes, the `\\\\` emitted here survives into the record parser as a single one.\n *\n * Leading and trailing spaces are also dropped by the record parser, which is exactly why the\n * horizontal-cylinder queue node uses a `\" |\"` prefix — that empty first field is deliberate.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function recordValue(value: string): string {\n return dotValue(value).replace(/[|{}<>]/g, (char: string) => `\\\\${char}`);\n}\n\n/** Thrown when the generator produces DOT that Graphviz could not parse. */\nexport class InvalidDotError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidDotError';\n }\n}\n\n/**\n * Fail loudly on structurally broken DOT.\n *\n * This is not a full Graphviz parser — it is the check that catches the failure mode a string\n * builder actually has: a quote that ends a string early (or never ends it). It scans the quoted\n * strings honouring `\\\"` escapes and asserts each one is terminated and is bounded by DOT\n * punctuation rather than by bare text. An unescaped `\"` inside a label always violates that: the\n * string ends mid-label, and the remaining label text becomes stray tokens.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function assertValidDot(dot: string, source: string): void {\n // Comment text is not code: a `\"` or a word in it must never be read as a DOT token. Blanking\n // it (offsets preserved) keeps every reported line number the one Graphviz would report.\n const code = blankComments(dot);\n let index = 0;\n while (index < code.length) {\n if (code[index] !== '\"') {\n index++;\n continue;\n }\n const start = index;\n index++;\n while (index < code.length && code[index] !== '\"') {\n index += code[index] === '\\\\' ? 2 : 1;\n }\n if (index >= code.length) {\n throw new InvalidDotError(`${source}: unterminated string starting at ${describe(dot, start)}`);\n }\n const end = index;\n index++;\n checkNeighbor(code, dot, start, end, source);\n }\n}\n\n/** Replace `//`, `#` and `/* *\\/` comment bodies with spaces, preserving length and newlines. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction blankComments(dot: string): string {\n const out = dot.split('');\n let index = 0;\n let inString = false;\n while (index < out.length) {\n const two = dot.slice(index, index + 2);\n if (inString) {\n if (dot[index] === '\\\\') index++;\n else if (dot[index] === '\"') inString = false;\n index++;\n } else if (dot[index] === '\"') {\n inString = true;\n index++;\n } else if (two === '//' || dot[index] === '#') {\n while (index < out.length && out[index] !== '\\n') out[index++] = ' ';\n } else if (two === '/*') {\n while (index < out.length && dot.slice(index, index + 2) !== '*/') {\n if (out[index] !== '\\n') out[index] = ' ';\n index++;\n }\n if (index < out.length) {\n out[index] = ' ';\n out[index + 1] = ' ';\n index += 2;\n }\n } else {\n index++;\n }\n }\n return out.join('');\n}\n\n/** Verify the non-whitespace chars bracketing a quoted string are DOT punctuation, not stray text. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction checkNeighbor(code: string, dot: string, start: number, end: number, source: string): void {\n const before = nonSpaceChar(code, start - 1, -1);\n if (before !== undefined && !LEGAL_BEFORE_STRING.has(before)) {\n throw new InvalidDotError(\n `${source}: a quoted string starts right after '${before}' at ${describe(dot, start)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended the previous string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n const after = nonSpaceChar(code, end + 1, 1);\n if (after !== undefined && !LEGAL_AFTER_STRING.has(after)) {\n throw new InvalidDotError(\n `${source}: a quoted string is followed by '${after}' at ${describe(dot, end)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended this string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n}\n\n/** The first non-whitespace char walking `step` from `from`, or undefined at either end. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction nonSpaceChar(dot: string, from: number, step: number): string | undefined {\n for (let i = from; i >= 0 && i < dot.length; i += step) {\n if (!/\\s/.test(dot[i])) return dot[i];\n }\n return undefined;\n}\n\n/** `line N: <the line>` for the offset, so the error names the same line Graphviz would. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction describe(dot: string, offset: number): string {\n const lineNumber = dot.slice(0, offset).split('\\n').length;\n const line = dot.split('\\n')[lineNumber - 1];\n return `line ${lineNumber}: ${line.trim()}`;\n}\n"]}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* "re-run architecture:generate" diff instead of a parse failure.
|
|
24
24
|
*/
|
|
25
25
|
import type { EnhancedGraph } from './graph-sorter';
|
|
26
|
-
import type { ApiContracts } from './api-usage/api-relations';
|
|
26
|
+
import type { ApiContracts, ExternalSystemDecls } from './api-usage/api-relations';
|
|
27
27
|
/**
|
|
28
28
|
* Default path for the dependencies file (relative to workspace root)
|
|
29
29
|
*/
|
|
@@ -59,6 +59,12 @@ export declare class DependenciesFile {
|
|
|
59
59
|
* makes those two inputs differ, and the validator reports a diff no one can fix.
|
|
60
60
|
*/
|
|
61
61
|
readonly apiContracts: ApiContracts;
|
|
62
|
+
/**
|
|
63
|
+
* Declared external systems (databases, buckets, ...) keyed by identity. Persisted for the
|
|
64
|
+
* same reason apiContracts is: the runtime graph is derived SOLELY from this file, so a
|
|
65
|
+
* declaration that is scanned but not written would make generate and validate disagree.
|
|
66
|
+
*/
|
|
67
|
+
readonly externalSystems: ExternalSystemDecls;
|
|
62
68
|
constructor(aiInstructions: string, commands: CommandMap, projects: EnhancedGraph,
|
|
63
69
|
/**
|
|
64
70
|
* Every API contract's per-method trigger table (kind + queue name + path).
|
|
@@ -68,7 +74,13 @@ export declare class DependenciesFile {
|
|
|
68
74
|
* while generate derives from the in-memory one. A field that is scanned but not written
|
|
69
75
|
* makes those two inputs differ, and the validator reports a diff no one can fix.
|
|
70
76
|
*/
|
|
71
|
-
apiContracts?: ApiContracts
|
|
77
|
+
apiContracts?: ApiContracts,
|
|
78
|
+
/**
|
|
79
|
+
* Declared external systems (databases, buckets, ...) keyed by identity. Persisted for the
|
|
80
|
+
* same reason apiContracts is: the runtime graph is derived SOLELY from this file, so a
|
|
81
|
+
* declaration that is scanned but not written would make generate and validate disagree.
|
|
82
|
+
*/
|
|
83
|
+
externalSystems?: ExternalSystemDecls);
|
|
72
84
|
}
|
|
73
85
|
/**
|
|
74
86
|
* Load the blessed graph from disk. Understands both the current wrapper
|
|
@@ -86,7 +98,7 @@ export declare function loadBlessedGraph(workspaceRoot: string, graphPath?: stri
|
|
|
86
98
|
* @param workspaceRoot - Absolute path to workspace root
|
|
87
99
|
* @param graphPath - Relative path to graph file (default: architecture/dependencies.json)
|
|
88
100
|
*/
|
|
89
|
-
export declare function saveGraph(graph: EnhancedGraph, workspaceRoot: string, graphPath?: string, apiContracts?: ApiContracts): void;
|
|
101
|
+
export declare function saveGraph(graph: EnhancedGraph, workspaceRoot: string, graphPath?: string, apiContracts?: ApiContracts, externalSystems?: ExternalSystemDecls): void;
|
|
90
102
|
/**
|
|
91
103
|
* Check if the graph file exists
|
|
92
104
|
*/
|
package/src/lib/graph-loader.js
CHANGED
|
@@ -73,6 +73,7 @@ class DependenciesFile {
|
|
|
73
73
|
commands;
|
|
74
74
|
projects;
|
|
75
75
|
apiContracts;
|
|
76
|
+
externalSystems;
|
|
76
77
|
constructor(aiInstructions, commands, projects,
|
|
77
78
|
/**
|
|
78
79
|
* Every API contract's per-method trigger table (kind + queue name + path).
|
|
@@ -82,11 +83,18 @@ class DependenciesFile {
|
|
|
82
83
|
* while generate derives from the in-memory one. A field that is scanned but not written
|
|
83
84
|
* makes those two inputs differ, and the validator reports a diff no one can fix.
|
|
84
85
|
*/
|
|
85
|
-
apiContracts = {}
|
|
86
|
+
apiContracts = {},
|
|
87
|
+
/**
|
|
88
|
+
* Declared external systems (databases, buckets, ...) keyed by identity. Persisted for the
|
|
89
|
+
* same reason apiContracts is: the runtime graph is derived SOLELY from this file, so a
|
|
90
|
+
* declaration that is scanned but not written would make generate and validate disagree.
|
|
91
|
+
*/
|
|
92
|
+
externalSystems = {}) {
|
|
86
93
|
this.aiInstructions = aiInstructions;
|
|
87
94
|
this.commands = commands;
|
|
88
95
|
this.projects = projects;
|
|
89
96
|
this.apiContracts = apiContracts;
|
|
97
|
+
this.externalSystems = externalSystems;
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
exports.DependenciesFile = DependenciesFile;
|
|
@@ -113,6 +121,11 @@ function loadBlessedGraph(workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
|
|
|
113
121
|
// runtime graph to unnamed per-pair queues rather than failing to load.
|
|
114
122
|
parsed.apiContracts !== null && typeof parsed.apiContracts === 'object'
|
|
115
123
|
? parsed.apiContracts
|
|
124
|
+
: {},
|
|
125
|
+
// Absent in any file written before external systems could be declared; an empty
|
|
126
|
+
// table simply draws no shaped nodes, which is exactly the old rendering.
|
|
127
|
+
parsed.externalSystems !== null && typeof parsed.externalSystems === 'object'
|
|
128
|
+
? parsed.externalSystems
|
|
116
129
|
: {});
|
|
117
130
|
}
|
|
118
131
|
// Legacy flat format: the whole object is the project map
|
|
@@ -137,6 +150,7 @@ function formatGraphJson(file) {
|
|
|
137
150
|
});
|
|
138
151
|
lines.push(` },`);
|
|
139
152
|
lines.push(...apiContractsLines(file.apiContracts));
|
|
153
|
+
lines.push(...externalSystemsLines(file.externalSystems));
|
|
140
154
|
lines.push(` "projects": {`);
|
|
141
155
|
const keys = Object.keys(file.projects).sort();
|
|
142
156
|
keys.forEach((key, index) => {
|
|
@@ -168,6 +182,25 @@ function apiContractsLines(contracts) {
|
|
|
168
182
|
return ` ${prefix}${line}${suffix}`;
|
|
169
183
|
});
|
|
170
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* The `"externalSystems": {...}` block (4-space indent), with a trailing comma since `projects`
|
|
187
|
+
* always follows. Omitted entirely when empty, so a repo that declares none keeps the exact file
|
|
188
|
+
* shape it had before this existed — which is what makes adopting it a no-op diff.
|
|
189
|
+
*/
|
|
190
|
+
// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here
|
|
191
|
+
function externalSystemsLines(systems) {
|
|
192
|
+
if (Object.keys(systems).length === 0)
|
|
193
|
+
return [];
|
|
194
|
+
const sorted = {};
|
|
195
|
+
for (const identity of Object.keys(systems).sort())
|
|
196
|
+
sorted[identity] = systems[identity];
|
|
197
|
+
const pretty = JSON.stringify(sorted, null, 4).split('\n');
|
|
198
|
+
return pretty.map((line, index) => {
|
|
199
|
+
const prefix = index === 0 ? '"externalSystems": ' : '';
|
|
200
|
+
const suffix = index === pretty.length - 1 ? ',' : '';
|
|
201
|
+
return ` ${prefix}${line}${suffix}`;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
171
204
|
/**
|
|
172
205
|
* Format one project entry's fields (12-space indent). Optional metadata
|
|
173
206
|
* fields are only emitted when present.
|
|
@@ -261,7 +294,7 @@ function pushApiRelationsField(lines, value) {
|
|
|
261
294
|
* @param workspaceRoot - Absolute path to workspace root
|
|
262
295
|
* @param graphPath - Relative path to graph file (default: architecture/dependencies.json)
|
|
263
296
|
*/
|
|
264
|
-
function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH, apiContracts = {}) {
|
|
297
|
+
function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH, apiContracts = {}, externalSystems = {}) {
|
|
265
298
|
const fullPath = path.join(workspaceRoot, graphPath);
|
|
266
299
|
const dir = path.dirname(fullPath);
|
|
267
300
|
// Ensure directory exists
|
|
@@ -274,7 +307,7 @@ function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH,
|
|
|
274
307
|
for (const key of sortedKeys) {
|
|
275
308
|
sortedGraph[key] = graph[key];
|
|
276
309
|
}
|
|
277
|
-
const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, exports.GRAPH_COMMANDS, sortedGraph, apiContracts));
|
|
310
|
+
const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, exports.GRAPH_COMMANDS, sortedGraph, apiContracts, externalSystems));
|
|
278
311
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
279
312
|
}
|
|
280
313
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n *\n * File format (schema aimed at AI consumers):\n * {\n * \"aiInstructions\": \"...how AI should use the per-project fields...\",\n * \"projects\": {\n * \"<project>\": { level, framework, shortDescription,\n * responsibilitiesFile, designFile, dependsOn }\n * }\n * }\n *\n * `framework` is the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]). It comes from the project's `framework:` nx tags\n * and is enforced across edges by the `library-types-match-client` rule.\n *\n * The legacy format (flat { \"<project>\": { level, dependsOn } } map) is still\n * readable so validation against a pre-upgrade file produces a clean\n * \"re-run architecture:generate\" diff instead of a parse failure.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport type { ApiContracts, ProjectApiRelations } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Top-level instructions embedded in dependencies.json telling AI how to use\n * the per-project metadata fields.\n */\nexport const AI_INSTRUCTIONS =\n \"Each project's shortDescription is only a summary. BEFORE adding code to a project, \" +\n 'read its responsibilitiesFile for the full responsibilities (what belongs in that ' +\n 'project and what does not), and read its designFile to understand the DI design ' +\n 'before reading the code. Use the entries in `commands` to regenerate these files ' +\n 'or display any of the graphs in a browser. To hide a project (its box AND every ' +\n \"edge touching it) from BOTH rendered architecture graphs, add the nx tag \" +\n \"'drawOnGraph:false' to that project's project.json tags and regenerate; it stays \" +\n 'in this file (marked \"drawOnGraph\": false) but is omitted from the HTML.';\n\n/**\n * Named command → \"command — what it does\" map embedded in dependencies.json.\n */\nexport type CommandMap = Record<string, string>;\n\n/**\n * Commands embedded in dependencies.json so AI (and humans) know how to\n * regenerate and DISPLAY the architecture + design graphs. These work in any\n * repo consuming @webpieces/nx-webpieces-rules.\n */\nexport const GRAPH_COMMANDS: CommandMap = {\n regenerateArchitecture:\n 'pnpm nx run architecture:generate — rewrites architecture/dependencies.json and ' +\n 'architecture/runtime-dependencies.json; run after adding/removing project dependencies',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser',\n regenerateDesigns:\n \"pnpm nx run-many --target=di-graph-generate — rewrites every project's \" +\n 'design.json/design.md (also runs automatically on every build)',\n visualizeDesign:\n \"pnpm wp-design-visualize <project> — opens a project's DI designs (its designFile) as \" +\n 'HTML, one graph per controller with the controller at the top; no args = interactive picker',\n};\n\n/**\n * The full contents of architecture/dependencies.json.\n */\nexport class DependenciesFile {\n constructor(\n public readonly aiInstructions: string,\n public readonly commands: CommandMap,\n public readonly projects: EnhancedGraph,\n /**\n * Every API contract's per-method trigger table (kind + queue name + path).\n *\n * MUST be persisted, for the same reason `callsService` must: the runtime graph is derived\n * SOLELY from this file, and `validate-runtime-architecture` re-derives from the LOADED copy\n * while generate derives from the in-memory one. A field that is scanned but not written\n * makes those two inputs differ, and the validator reports a diff no one can fix.\n */\n public readonly apiContracts: ApiContracts = {}\n ) {}\n}\n\n/**\n * Load the blessed graph from disk. Understands both the current wrapper\n * format and the legacy flat map (which loads with empty aiInstructions).\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n * @returns The blessed graph file, or null if it doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): DependenciesFile | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n const parsed = JSON.parse(content);\n if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object' ? (parsed.commands as CommandMap) : {},\n parsed.projects as EnhancedGraph,\n // Absent in a file written before apiContracts existed; an empty table degrades the\n // runtime graph to unnamed per-pair queues rather than failing to load.\n parsed.apiContracts !== null && typeof parsed.apiContracts === 'object'\n ? (parsed.apiContracts as ApiContracts)\n : {}\n );\n }\n // Legacy flat format: the whole object is the project map\n return new DependenciesFile('', {}, parsed as EnhancedGraph);\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\n }\n}\n\n/**\n * Format the dependencies file as JSON with multi-line arrays for readability\n */\nfunction formatGraphJson(file: DependenciesFile): string {\n const lines: string[] = ['{'];\n lines.push(` \"aiInstructions\": ${JSON.stringify(file.aiInstructions)},`);\n lines.push(` \"commands\": {`);\n const commandNames = Object.keys(file.commands);\n commandNames.forEach((name: string, index: number) => {\n const comma = index === commandNames.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);\n });\n lines.push(` },`);\n lines.push(...apiContractsLines(file.apiContracts));\n lines.push(` \"projects\": {`);\n\n const keys = Object.keys(file.projects).sort();\n keys.forEach((key: string, index: number) => {\n const entry = file.projects[key];\n const isLast = index === keys.length - 1;\n const comma = isLast ? '' : ',';\n\n lines.push(` ${JSON.stringify(key)}: {`);\n lines.push(...formatEntryLines(entry));\n lines.push(` }${comma}`);\n });\n\n lines.push(' }');\n lines.push('}');\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * The `\"apiContracts\": {...}` block (4-space indent), with a trailing comma since `projects` always\n * follows it. Omitted entirely when empty, so a repo with no api contracts keeps the old file shape.\n * The scanner already sorts contracts by name and leaves methods in declaration order, so pretty\n * JSON.stringify is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction apiContractsLines(contracts: ApiContracts): string[] {\n if (Object.keys(contracts).length === 0) return [];\n const pretty = JSON.stringify(contracts, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiContracts\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * Format one project entry's fields (12-space indent). Optional metadata\n * fields are only emitted when present.\n */\nfunction formatEntryLines(entry: GraphEntry): string[] {\n const lines: string[] = [];\n lines.push(` \"level\": ${entry.level},`);\n\n pushOptionalArrayField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'role', entry.role);\n pushOptionalField(lines, 'serviceName', entry.serviceName);\n pushOptionalCallsServiceField(lines, entry.callsService);\n pushOptionalBooleanField(lines, 'drawOnGraph', entry.drawOnGraph);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n pushApiRelationsField(lines, entry.apiRelations);\n\n if (entry.dependsOn.length === 0) {\n lines.push(` \"dependsOn\": []`);\n } else {\n lines.push(` \"dependsOn\": [`);\n entry.dependsOn.forEach((dep: string, depIndex: number) => {\n const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(dep)}${depComma}`);\n });\n lines.push(` ]`);\n }\n return lines;\n}\n\n/**\n * Emit one optional string field (12-space indent), skipped when undefined.\n */\nfunction pushOptionalField(lines: string[], field: string, value: string | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `callsService` field (12-space indent). Unlike serviceName it may be EITHER a\n * single service name (string) OR an { apiClassName: serviceName } map, so it is emitted with\n * JSON.stringify to cover both inline. MUST be persisted here: the runtime graph's target resolution\n * reads it back from the committed dependencies.json during validate — dropping it would make\n * generate (which enriches it in-memory) and validate (which loads this file) derive different graphs.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalCallsServiceField(\n lines: string[],\n value: string | Record<string, string> | undefined,\n): void {\n if (value !== undefined) {\n lines.push(` \"callsService\": ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional boolean field (12-space indent) as a raw JSON boolean\n * (e.g. `\"drawOnGraph\": false,`), skipped when undefined.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalBooleanField(lines: string[], field: string, value: boolean | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional string-array field (12-space indent) as a compact inline\n * JSON array (e.g. `\"framework\": [\"browser\",\"node\"],`), skipped when undefined.\n */\nfunction pushOptionalArrayField(lines: string[], field: string, value: string[] | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `apiRelations` object (pretty, multi-line, reindented under\n * the 12-space entry block) with a trailing comma, since `dependsOn` always\n * follows it. Skipped when absent/empty so plain-lib-only projects stay compact.\n * The scanner already sorts owners + refs, so the JSON is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushApiRelationsField(lines: string[], value: ProjectApiRelations | undefined): void {\n if (value === undefined || Object.keys(value).length === 0) return;\n const pretty = JSON.stringify(value, null, 4).split('\\n');\n pretty.forEach((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiRelations\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n lines.push(` ${prefix}${line}${suffix}`);\n });\n}\n\n/**\n * Save the graph to disk in the wrapper format with the standard aiInstructions.\n *\n * @param graph - The enriched project graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n apiContracts: ApiContracts = {}\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = formatGraphJson(\n new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph, apiContracts)\n );\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n *\n * File format (schema aimed at AI consumers):\n * {\n * \"aiInstructions\": \"...how AI should use the per-project fields...\",\n * \"projects\": {\n * \"<project>\": { level, framework, shortDescription,\n * responsibilitiesFile, designFile, dependsOn }\n * }\n * }\n *\n * `framework` is the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]). It comes from the project's `framework:` nx tags\n * and is enforced across edges by the `library-types-match-client` rule.\n *\n * The legacy format (flat { \"<project>\": { level, dependsOn } } map) is still\n * readable so validation against a pre-upgrade file produces a clean\n * \"re-run architecture:generate\" diff instead of a parse failure.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport type { ApiContracts, ExternalSystemDecls, ProjectApiRelations } from './api-usage/api-relations';\nimport { toError } from '../toError';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Top-level instructions embedded in dependencies.json telling AI how to use\n * the per-project metadata fields.\n */\nexport const AI_INSTRUCTIONS =\n \"Each project's shortDescription is only a summary. BEFORE adding code to a project, \" +\n 'read its responsibilitiesFile for the full responsibilities (what belongs in that ' +\n 'project and what does not), and read its designFile to understand the DI design ' +\n 'before reading the code. Use the entries in `commands` to regenerate these files ' +\n 'or display any of the graphs in a browser. To hide a project (its box AND every ' +\n \"edge touching it) from BOTH rendered architecture graphs, add the nx tag \" +\n \"'drawOnGraph:false' to that project's project.json tags and regenerate; it stays \" +\n 'in this file (marked \"drawOnGraph\": false) but is omitted from the HTML.';\n\n/**\n * Named command → \"command — what it does\" map embedded in dependencies.json.\n */\nexport type CommandMap = Record<string, string>;\n\n/**\n * Commands embedded in dependencies.json so AI (and humans) know how to\n * regenerate and DISPLAY the architecture + design graphs. These work in any\n * repo consuming @webpieces/nx-webpieces-rules.\n */\nexport const GRAPH_COMMANDS: CommandMap = {\n regenerateArchitecture:\n 'pnpm nx run architecture:generate — rewrites architecture/dependencies.json and ' +\n 'architecture/runtime-dependencies.json; run after adding/removing project dependencies',\n visualizeArchitecture:\n 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +\n 'as HTML in a browser',\n visualizeRuntimeArchitecture:\n 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +\n 'as HTML in a browser',\n regenerateDesigns:\n \"pnpm nx run-many --target=di-graph-generate — rewrites every project's \" +\n 'design.json/design.md (also runs automatically on every build)',\n visualizeDesign:\n \"pnpm wp-design-visualize <project> — opens a project's DI designs (its designFile) as \" +\n 'HTML, one graph per controller with the controller at the top; no args = interactive picker',\n};\n\n/**\n * The full contents of architecture/dependencies.json.\n */\nexport class DependenciesFile {\n constructor(\n public readonly aiInstructions: string,\n public readonly commands: CommandMap,\n public readonly projects: EnhancedGraph,\n /**\n * Every API contract's per-method trigger table (kind + queue name + path).\n *\n * MUST be persisted, for the same reason `callsService` must: the runtime graph is derived\n * SOLELY from this file, and `validate-runtime-architecture` re-derives from the LOADED copy\n * while generate derives from the in-memory one. A field that is scanned but not written\n * makes those two inputs differ, and the validator reports a diff no one can fix.\n */\n public readonly apiContracts: ApiContracts = {},\n /**\n * Declared external systems (databases, buckets, ...) keyed by identity. Persisted for the\n * same reason apiContracts is: the runtime graph is derived SOLELY from this file, so a\n * declaration that is scanned but not written would make generate and validate disagree.\n */\n public readonly externalSystems: ExternalSystemDecls = {}\n ) {}\n}\n\n/**\n * Load the blessed graph from disk. Understands both the current wrapper\n * format and the legacy flat map (which loads with empty aiInstructions).\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n * @returns The blessed graph file, or null if it doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): DependenciesFile | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n const parsed = JSON.parse(content);\n if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {\n return new DependenciesFile(\n typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '',\n parsed.commands !== null && typeof parsed.commands === 'object' ? (parsed.commands as CommandMap) : {},\n parsed.projects as EnhancedGraph,\n // Absent in a file written before apiContracts existed; an empty table degrades the\n // runtime graph to unnamed per-pair queues rather than failing to load.\n parsed.apiContracts !== null && typeof parsed.apiContracts === 'object'\n ? (parsed.apiContracts as ApiContracts)\n : {},\n // Absent in any file written before external systems could be declared; an empty\n // table simply draws no shaped nodes, which is exactly the old rendering.\n parsed.externalSystems !== null && typeof parsed.externalSystems === 'object'\n ? (parsed.externalSystems as ExternalSystemDecls)\n : {}\n );\n }\n // Legacy flat format: the whole object is the project map\n return new DependenciesFile('', {}, parsed as EnhancedGraph);\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to load graph from ${fullPath}`, { cause: error });\n }\n}\n\n/**\n * Format the dependencies file as JSON with multi-line arrays for readability\n */\nfunction formatGraphJson(file: DependenciesFile): string {\n const lines: string[] = ['{'];\n lines.push(` \"aiInstructions\": ${JSON.stringify(file.aiInstructions)},`);\n lines.push(` \"commands\": {`);\n const commandNames = Object.keys(file.commands);\n commandNames.forEach((name: string, index: number) => {\n const comma = index === commandNames.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);\n });\n lines.push(` },`);\n lines.push(...apiContractsLines(file.apiContracts));\n lines.push(...externalSystemsLines(file.externalSystems));\n lines.push(` \"projects\": {`);\n\n const keys = Object.keys(file.projects).sort();\n keys.forEach((key: string, index: number) => {\n const entry = file.projects[key];\n const isLast = index === keys.length - 1;\n const comma = isLast ? '' : ',';\n\n lines.push(` ${JSON.stringify(key)}: {`);\n lines.push(...formatEntryLines(entry));\n lines.push(` }${comma}`);\n });\n\n lines.push(' }');\n lines.push('}');\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * The `\"apiContracts\": {...}` block (4-space indent), with a trailing comma since `projects` always\n * follows it. Omitted entirely when empty, so a repo with no api contracts keeps the old file shape.\n * The scanner already sorts contracts by name and leaves methods in declaration order, so pretty\n * JSON.stringify is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction apiContractsLines(contracts: ApiContracts): string[] {\n if (Object.keys(contracts).length === 0) return [];\n const pretty = JSON.stringify(contracts, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiContracts\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * The `\"externalSystems\": {...}` block (4-space indent), with a trailing comma since `projects`\n * always follows. Omitted entirely when empty, so a repo that declares none keeps the exact file\n * shape it had before this existed — which is what makes adopting it a no-op diff.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling formatters here\nfunction externalSystemsLines(systems: ExternalSystemDecls): string[] {\n if (Object.keys(systems).length === 0) return [];\n const sorted: ExternalSystemDecls = {};\n for (const identity of Object.keys(systems).sort()) sorted[identity] = systems[identity];\n const pretty = JSON.stringify(sorted, null, 4).split('\\n');\n return pretty.map((line: string, index: number) => {\n const prefix = index === 0 ? '\"externalSystems\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n return ` ${prefix}${line}${suffix}`;\n });\n}\n\n/**\n * Format one project entry's fields (12-space indent). Optional metadata\n * fields are only emitted when present.\n */\nfunction formatEntryLines(entry: GraphEntry): string[] {\n const lines: string[] = [];\n lines.push(` \"level\": ${entry.level},`);\n\n pushOptionalArrayField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'role', entry.role);\n pushOptionalField(lines, 'serviceName', entry.serviceName);\n pushOptionalCallsServiceField(lines, entry.callsService);\n pushOptionalBooleanField(lines, 'drawOnGraph', entry.drawOnGraph);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\n pushApiRelationsField(lines, entry.apiRelations);\n\n if (entry.dependsOn.length === 0) {\n lines.push(` \"dependsOn\": []`);\n } else {\n lines.push(` \"dependsOn\": [`);\n entry.dependsOn.forEach((dep: string, depIndex: number) => {\n const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';\n lines.push(` ${JSON.stringify(dep)}${depComma}`);\n });\n lines.push(` ]`);\n }\n return lines;\n}\n\n/**\n * Emit one optional string field (12-space indent), skipped when undefined.\n */\nfunction pushOptionalField(lines: string[], field: string, value: string | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `callsService` field (12-space indent). Unlike serviceName it may be EITHER a\n * single service name (string) OR an { apiClassName: serviceName } map, so it is emitted with\n * JSON.stringify to cover both inline. MUST be persisted here: the runtime graph's target resolution\n * reads it back from the committed dependencies.json during validate — dropping it would make\n * generate (which enriches it in-memory) and validate (which loads this file) derive different graphs.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalCallsServiceField(\n lines: string[],\n value: string | Record<string, string> | undefined,\n): void {\n if (value !== undefined) {\n lines.push(` \"callsService\": ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional boolean field (12-space indent) as a raw JSON boolean\n * (e.g. `\"drawOnGraph\": false,`), skipped when undefined.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushOptionalBooleanField(lines: string[], field: string, value: boolean | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit one optional string-array field (12-space indent) as a compact inline\n * JSON array (e.g. `\"framework\": [\"browser\",\"node\"],`), skipped when undefined.\n */\nfunction pushOptionalArrayField(lines: string[], field: string, value: string[] | undefined): void {\n if (value !== undefined) {\n lines.push(` ${JSON.stringify(field)}: ${JSON.stringify(value)},`);\n }\n}\n\n/**\n * Emit the optional `apiRelations` object (pretty, multi-line, reindented under\n * the 12-space entry block) with a trailing comma, since `dependsOn` always\n * follows it. Skipped when absent/empty so plain-lib-only projects stay compact.\n * The scanner already sorts owners + refs, so the JSON is deterministic.\n */\n// webpieces-disable no-function-outside-class -- module-scope formatter, matches the sibling push*Field helpers here\nfunction pushApiRelationsField(lines: string[], value: ProjectApiRelations | undefined): void {\n if (value === undefined || Object.keys(value).length === 0) return;\n const pretty = JSON.stringify(value, null, 4).split('\\n');\n pretty.forEach((line: string, index: number) => {\n const prefix = index === 0 ? '\"apiRelations\": ' : '';\n const suffix = index === pretty.length - 1 ? ',' : '';\n lines.push(` ${prefix}${line}${suffix}`);\n });\n}\n\n/**\n * Save the graph to disk in the wrapper format with the standard aiInstructions.\n *\n * @param graph - The enriched project graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: architecture/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH,\n apiContracts: ApiContracts = {},\n externalSystems: ExternalSystemDecls = {}\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = formatGraphJson(\n new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph, apiContracts, externalSystems)\n );\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
|