@webpieces/nx-webpieces-rules 0.3.244 → 0.3.246
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/di-graph-generate/executor.js +42 -12
- package/src/executors/di-graph-generate/executor.js.map +1 -1
- package/src/lib/di-graph/analyzer-strategy.d.ts +47 -0
- package/src/lib/di-graph/analyzer-strategy.js +86 -0
- package/src/lib/di-graph/analyzer-strategy.js.map +1 -0
- package/src/lib/di-graph/analyzer.d.ts +128 -5
- package/src/lib/di-graph/analyzer.js +190 -65
- package/src/lib/di-graph/analyzer.js.map +1 -1
- package/src/lib/di-graph/angular-analyzer.d.ts +28 -0
- package/src/lib/di-graph/angular-analyzer.js +139 -0
- package/src/lib/di-graph/angular-analyzer.js.map +1 -0
- package/src/lib/di-graph/angular-providers.d.ts +32 -0
- package/src/lib/di-graph/angular-providers.js +177 -0
- package/src/lib/di-graph/angular-providers.js.map +1 -0
- package/src/lib/di-graph/angular-roots.d.ts +21 -0
- package/src/lib/di-graph/angular-roots.js +150 -0
- package/src/lib/di-graph/angular-roots.js.map +1 -0
- package/src/lib/di-graph/dot.js +8 -5
- package/src/lib/di-graph/dot.js.map +1 -1
- package/src/lib/di-graph/mermaid.js +12 -7
- package/src/lib/di-graph/mermaid.js.map +1 -1
- package/src/lib/di-graph/model.d.ts +17 -3
- package/src/lib/di-graph/model.js +19 -2
- package/src/lib/di-graph/model.js.map +1 -1
- package/src/lib/framework-resolver.d.ts +8 -4
- package/src/lib/framework-resolver.js +10 -6
- package/src/lib/framework-resolver.js.map +1 -1
- package/src/lib/graph-loader.d.ts +5 -0
- package/src/lib/graph-loader.js +5 -0
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-metadata.d.ts +19 -0
- package/src/lib/graph-metadata.js +40 -1
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-sorter.d.ts +3 -1
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.js +24 -16
- package/src/lib/graph-visualizer.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dot.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/dot.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;
|
|
1
|
+
{"version":3,"file":"dot.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/dot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design DOT Emitter\n *\n * Renders one DiDesign (a single controller/root's dependency tree) as a\n * Graphviz DOT digraph, mirroring the look of the architecture graph\n * (lib/graph-visualizer.ts): rankdir=TB with { rank=same } layers per level,\n * so the controller (level 0) sits at the top and injections fan downward.\n */\n\nimport { DiDesign, DiNode } from './model';\n\n/**\n * Node fill colors by DI node kind.\n */\nconst KIND_COLORS: Record<string, string> = {\n controller: '#E3F2FD', // light blue — the root/entry class (Inversify)\n component: '#E8F5E9', // light green — the root/entry Angular component\n class: '#F5F5F5', // neutral — plain injectable class\n constant: '#FFF3E0', // light orange — toConstantValue leaf\n dynamic: '#FFF3E0', // light orange — toDynamicValue leaf\n unresolved: '#FCE4EC', // light pink — token the analyzer could not resolve\n};\n\n/** Escape a string for use inside a double-quoted DOT identifier/label. */\nfunction dotEscape(text: string): string {\n return text.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nfunction nodeStatement(node: DiNode): string {\n const color = KIND_COLORS[node.kind] ?? '#F5F5F5';\n const label = `${dotEscape(node.className)}\\\\n(L${node.level}, ${node.scope})`;\n const styles: string[] = ['filled'];\n if (node.kind === 'constant' || node.kind === 'dynamic') styles.push('rounded');\n if (node.kind === 'unresolved') styles.push('dashed');\n const penwidth = node.kind === 'controller' || node.kind === 'component' ? ', penwidth=2' : '';\n return ` \"${dotEscape(node.id)}\" [fillcolor=\"${color}\", style=\"${styles.join(',')}\", label=\"${label}\"${penwidth}];\\n`;\n}\n\n/**\n * Generate a Graphviz DOT digraph for one design (one controller/root tree).\n * Deterministic for a serializer-sorted design.\n */\nexport function generateDesignDot(design: DiDesign): string {\n let dot = `digraph \"${dotEscape(design.root)}\" {\\n`;\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=11];\\n\\n';\n\n // Nodes, colored by kind, labeled with their level + scope\n for (const node of design.nodes) {\n dot += nodeStatement(node);\n }\n\n dot += '\\n';\n\n // One { rank=same } layer per level so the root stays on top\n const levels = new Map<number, string[]>();\n for (const node of design.nodes) {\n const layer = levels.get(node.level) ?? [];\n layer.push(node.id);\n levels.set(node.level, layer);\n }\n const sortedLevels = Array.from(levels.keys()).sort((a: number, b: number) => a - b);\n for (const level of sortedLevels) {\n const ids = levels.get(level) ?? [];\n dot += ` { rank=same; ${ids.map((id: string) => `\"${dotEscape(id)}\"`).join('; ')}; }\\n`;\n }\n\n dot += '\\n';\n\n // Constructor-injection edges; labels are the declared param/field NAME\n // (B0) — the human-meaningful \"why\", not the raw token expression. The\n // token/tokenKey stay in design.json for tooling. Mirrors design.md.\n for (const edge of design.edges) {\n const name = edge.injection === 'multiInject' ? `multiInject ${edge.paramName}` : edge.paramName;\n const label = name !== '' ? ` [label=\"${dotEscape(name)}\"]` : '';\n dot += ` \"${dotEscape(edge.from)}\" -> \"${dotEscape(edge.to)}\"${label};\\n`;\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotEscape(design.root)}\\\\n(${design.rootKind}, Level 0-${design.maxLevel})\";\\n`;\n dot += ' fontsize=16;\\n';\n dot += '}\\n';\n\n return dot;\n}\n"]}
|
|
@@ -26,6 +26,8 @@ function nodeStatement(node) {
|
|
|
26
26
|
const text = label(node.className);
|
|
27
27
|
if (node.kind === 'controller')
|
|
28
28
|
return ` ${id}["${text}"]:::controller`;
|
|
29
|
+
if (node.kind === 'component')
|
|
30
|
+
return ` ${id}["${text}"]:::component`;
|
|
29
31
|
if (node.kind === 'constant' || node.kind === 'dynamic')
|
|
30
32
|
return ` ${id}(["${text}"])`;
|
|
31
33
|
if (node.kind === 'unresolved')
|
|
@@ -40,21 +42,23 @@ function graphBody(design) {
|
|
|
40
42
|
for (const edge of design.edges) {
|
|
41
43
|
const from = mermaidId(edge.from);
|
|
42
44
|
const to = mermaidId(edge.to);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// B0: label edges by the declared param/field NAME, not the raw token.
|
|
46
|
+
const name = edge.injection === 'multiInject' ? `multiInject ${edge.paramName}` : edge.paramName;
|
|
47
|
+
if (name !== '') {
|
|
48
|
+
lines.push(` ${from} -->|${edgeLabel(name)}| ${to}`);
|
|
46
49
|
}
|
|
47
50
|
else {
|
|
48
51
|
lines.push(` ${from} --> ${to}`);
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
lines.push(' classDef controller fill:#1f6feb,color:#ffffff,stroke:#0d419d');
|
|
55
|
+
lines.push(' classDef component fill:#2da44e,color:#ffffff,stroke:#1a7f37');
|
|
52
56
|
lines.push(' classDef unresolved fill:#f0ad4e,color:#000000,stroke:#b8860b,stroke-dasharray: 5 5');
|
|
53
57
|
return lines;
|
|
54
58
|
}
|
|
55
59
|
/** One `## Root` section with the design's Mermaid diagram. */
|
|
56
60
|
function designSection(design) {
|
|
57
|
-
const kind = design.rootKind === 'controller' ? 'controller' : 'root';
|
|
61
|
+
const kind = design.rootKind === 'controller' ? 'controller' : design.rootKind === 'component' ? 'component' : 'root';
|
|
58
62
|
const heading = `## ${design.root} — ${kind}, Level 0…${design.maxLevel}`;
|
|
59
63
|
return [heading, '', '```mermaid', ...graphBody(design), '```', ''];
|
|
60
64
|
}
|
|
@@ -86,9 +90,10 @@ function toDesignMarkdown(graph) {
|
|
|
86
90
|
}
|
|
87
91
|
const legend = [
|
|
88
92
|
'',
|
|
89
|
-
'Edges are
|
|
90
|
-
'
|
|
91
|
-
'`toDynamicValue` leaves; dashed
|
|
93
|
+
'Edges are injections labeled by the target `constructor param`/`inject()` field',
|
|
94
|
+
'name (`multiInject <name>` for `@multiInject`). Rounded nodes are',
|
|
95
|
+
'`toConstantValue`/`useValue` and `toDynamicValue`/`useFactory` leaves; dashed',
|
|
96
|
+
'nodes are tokens the analyzer could not resolve.',
|
|
92
97
|
'',
|
|
93
98
|
];
|
|
94
99
|
return header.concat(sections).concat(legend).join('\n');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/mermaid.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;
|
|
1
|
+
{"version":3,"file":"mermaid.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/mermaid.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Mermaid Emitter\n *\n * Renders the DI graph as design.md — a Mermaid `graph TD` diagram that GitHub\n * and IDEs render inline, so the DI design is reviewable in every PR. Emitted\n * in the same sorted order as design.json so diffs are equally stable.\n */\n\nimport { DiDesign, DiGraph, DiNode } from './model';\nimport { sortGraph } from './serializer';\n\n/** Mermaid node ids must be simple identifiers — map graph ids to safe ids. */\nfunction mermaidId(id: string): string {\n return id.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/** Escape text destined for a quoted mermaid label. */\nfunction label(text: string): string {\n return text.replace(/\"/g, '#quot;');\n}\n\n/** Escape an edge label (rendered between pipes). */\nfunction edgeLabel(text: string): string {\n return text.replace(/\\|/g, '/').replace(/\"/g, \"'\");\n}\n\nfunction nodeStatement(node: DiNode): string {\n const id = mermaidId(node.id);\n const text = label(node.className);\n if (node.kind === 'controller') return ` ${id}[\"${text}\"]:::controller`;\n if (node.kind === 'component') return ` ${id}[\"${text}\"]:::component`;\n if (node.kind === 'constant' || node.kind === 'dynamic') return ` ${id}([\"${text}\"])`;\n if (node.kind === 'unresolved') return ` ${id}{{\"${text} ?\"}}:::unresolved`;\n return ` ${id}[\"${text}\"]`;\n}\n\nfunction graphBody(design: DiDesign): string[] {\n const lines: string[] = ['graph TD'];\n for (const node of design.nodes) {\n lines.push(nodeStatement(node));\n }\n for (const edge of design.edges) {\n const from = mermaidId(edge.from);\n const to = mermaidId(edge.to);\n // B0: label edges by the declared param/field NAME, not the raw token.\n const name = edge.injection === 'multiInject' ? `multiInject ${edge.paramName}` : edge.paramName;\n if (name !== '') {\n lines.push(` ${from} -->|${edgeLabel(name)}| ${to}`);\n } else {\n lines.push(` ${from} --> ${to}`);\n }\n }\n lines.push(' classDef controller fill:#1f6feb,color:#ffffff,stroke:#0d419d');\n lines.push(' classDef component fill:#2da44e,color:#ffffff,stroke:#1a7f37');\n lines.push(' classDef unresolved fill:#f0ad4e,color:#000000,stroke:#b8860b,stroke-dasharray: 5 5');\n return lines;\n}\n\n/** One `## Root` section with the design's Mermaid diagram. */\nfunction designSection(design: DiDesign): string[] {\n const kind =\n design.rootKind === 'controller' ? 'controller' : design.rootKind === 'component' ? 'component' : 'root';\n const heading = `## ${design.root} — ${kind}, Level 0…${design.maxLevel}`;\n return [heading, '', '```mermaid', ...graphBody(design), '```', ''];\n}\n\n/**\n * Render the design.md content for a project's DI graph — one Mermaid diagram\n * per design (per controller, or per library top-of-DAG root), each rooted at\n * Level 0 with its injections fanning downward.\n */\nexport function toDesignMarkdown(graph: DiGraph): string {\n sortGraph(graph);\n\n const header = [\n `# DI Design Graph — ${graph.project}`,\n '',\n `> GENERATED by \\`nx run ${graph.project}:di-graph-generate\\` — do not edit by hand.`,\n `> Machine-readable version: [design.json](./design.json)`,\n '',\n `Each section below is one root's dependency tree: Level 0 is the root`,\n `(a \\`@Controller\\` or top-of-DAG class), and constructor injections fan`,\n `downward through Levels 1, 2, … A dependency shared by multiple roots`,\n `appears in each root's tree.`,\n '',\n ];\n\n if (graph.designs.length === 0) {\n return header.concat(['No DI-registered classes found in this project.', '']).join('\\n');\n }\n\n const sections: string[] = [];\n for (const design of graph.designs) {\n sections.push(...designSection(design));\n }\n\n const legend = [\n '',\n 'Edges are injections labeled by the target `constructor param`/`inject()` field',\n 'name (`multiInject <name>` for `@multiInject`). Rounded nodes are',\n '`toConstantValue`/`useValue` and `toDynamicValue`/`useFactory` leaves; dashed',\n 'nodes are tokens the analyzer could not resolve.',\n '',\n ];\n\n return header.concat(sections).concat(legend).join('\\n');\n}\n"]}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* data-only structures.
|
|
15
15
|
*/
|
|
16
16
|
import type * as ts from 'typescript';
|
|
17
|
-
export type DiNodeKind = 'controller' | 'class' | 'constant' | 'dynamic' | 'unresolved';
|
|
17
|
+
export type DiNodeKind = 'controller' | 'component' | 'class' | 'constant' | 'dynamic' | 'unresolved';
|
|
18
18
|
export type DiInjectionKind = 'token' | 'type' | 'multiInject';
|
|
19
19
|
export type DiScope = 'singleton' | 'transient' | 'unknown';
|
|
20
20
|
export type BindingKind = 'to' | 'toSelf' | 'toConstantValue' | 'toDynamicValue' | 'decorator';
|
|
@@ -30,7 +30,14 @@ export declare class DiNode {
|
|
|
30
30
|
file: string;
|
|
31
31
|
/** BFS depth from the design's root (root = 0). Filled in after the walk. */
|
|
32
32
|
level: number;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Secondary detail for constant/dynamic/unresolved leaves whose box is now
|
|
35
|
+
* labeled by the declared param type (see B0): the bound expression text
|
|
36
|
+
* (`toConstantValue`/`toDynamicValue`/`.to(X)` source) or resolving token.
|
|
37
|
+
* Omitted (undefined → not serialized) for class/controller/component nodes.
|
|
38
|
+
*/
|
|
39
|
+
detail?: string;
|
|
40
|
+
constructor(id: string, className: string, kind: DiNodeKind, scope: DiScope, file: string, level?: number, detail?: string);
|
|
34
41
|
}
|
|
35
42
|
/**
|
|
36
43
|
* A constructor-injection edge: `from` class injects `to` node.
|
|
@@ -99,5 +106,12 @@ export declare class Binding {
|
|
|
99
106
|
valueText: string;
|
|
100
107
|
/** Workspace-relative posix path of the file the binding appears in. */
|
|
101
108
|
file: string;
|
|
102
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Angular `useFactory` deps: the declared `deps: [A, B]` tokens. The walker
|
|
111
|
+
* emits an edge from the dynamic leaf to each dep so a factory's true DI
|
|
112
|
+
* boundary is visible (e.g. ClientConfig's factory → EnvironmentConfig +
|
|
113
|
+
* MutableContextStore). Empty for every Inversify binding.
|
|
114
|
+
*/
|
|
115
|
+
factoryDeps: TokenRef[];
|
|
116
|
+
constructor(tokenKey: string, tokenDisplay: string, kind: BindingKind, scope: DiScope, implClass: ts.ClassDeclaration | null, valueText: string, file: string, factoryDeps?: TokenRef[]);
|
|
103
117
|
}
|
|
@@ -28,13 +28,22 @@ class DiNode {
|
|
|
28
28
|
file;
|
|
29
29
|
/** BFS depth from the design's root (root = 0). Filled in after the walk. */
|
|
30
30
|
level;
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Secondary detail for constant/dynamic/unresolved leaves whose box is now
|
|
33
|
+
* labeled by the declared param type (see B0): the bound expression text
|
|
34
|
+
* (`toConstantValue`/`toDynamicValue`/`.to(X)` source) or resolving token.
|
|
35
|
+
* Omitted (undefined → not serialized) for class/controller/component nodes.
|
|
36
|
+
*/
|
|
37
|
+
detail;
|
|
38
|
+
constructor(id, className, kind, scope, file, level = 0, detail) {
|
|
32
39
|
this.id = id;
|
|
33
40
|
this.className = className;
|
|
34
41
|
this.kind = kind;
|
|
35
42
|
this.scope = scope;
|
|
36
43
|
this.file = file;
|
|
37
44
|
this.level = level;
|
|
45
|
+
if (detail !== undefined && detail !== '')
|
|
46
|
+
this.detail = detail;
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
exports.DiNode = DiNode;
|
|
@@ -132,7 +141,14 @@ class Binding {
|
|
|
132
141
|
valueText;
|
|
133
142
|
/** Workspace-relative posix path of the file the binding appears in. */
|
|
134
143
|
file;
|
|
135
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Angular `useFactory` deps: the declared `deps: [A, B]` tokens. The walker
|
|
146
|
+
* emits an edge from the dynamic leaf to each dep so a factory's true DI
|
|
147
|
+
* boundary is visible (e.g. ClientConfig's factory → EnvironmentConfig +
|
|
148
|
+
* MutableContextStore). Empty for every Inversify binding.
|
|
149
|
+
*/
|
|
150
|
+
factoryDeps;
|
|
151
|
+
constructor(tokenKey, tokenDisplay, kind, scope, implClass, valueText, file, factoryDeps = []) {
|
|
136
152
|
this.tokenKey = tokenKey;
|
|
137
153
|
this.tokenDisplay = tokenDisplay;
|
|
138
154
|
this.kind = kind;
|
|
@@ -140,6 +156,7 @@ class Binding {
|
|
|
140
156
|
this.implClass = implClass;
|
|
141
157
|
this.valueText = valueText;
|
|
142
158
|
this.file = file;
|
|
159
|
+
this.factoryDeps = factoryDeps;
|
|
143
160
|
}
|
|
144
161
|
}
|
|
145
162
|
exports.Binding = Binding;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAYH;;;GAGG;AACH,MAAa,MAAM;IACf,EAAE,CAAS;IACX,SAAS,CAAS;IAClB,IAAI,CAAa;IACjB,KAAK,CAAU;IACf,IAAI,CAAS;IACb,6EAA6E;IAC7E,KAAK,CAAS;
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/model.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Graph Model\n *\n * Data classes for the per-project Inversify dependency DAG that is generated\n * into <projectRoot>/design.json and <projectRoot>/design.md on every build.\n *\n * A project's design.json is a container (`DiGraph`) holding an ARRAY of\n * per-root designs (`DiDesign`) — one self-contained downward tree per\n * @Controller class (or, for controller-less library projects, per top-of-DAG\n * class). Each node in a design carries its `level` = BFS depth from that\n * design's root (root = level 0, its direct injections = level 1, and so on).\n *\n * All structures are classes (not interfaces) per the repo convention for\n * data-only structures.\n */\n\nimport type * as ts from 'typescript';\n\nexport type DiNodeKind = 'controller' | 'component' | 'class' | 'constant' | 'dynamic' | 'unresolved';\n\nexport type DiInjectionKind = 'token' | 'type' | 'multiInject';\n\nexport type DiScope = 'singleton' | 'transient' | 'unknown';\n\nexport type BindingKind = 'to' | 'toSelf' | 'toConstantValue' | 'toDynamicValue' | 'decorator';\n\n/**\n * A node in the DI graph — a class, a constant/dynamic binding leaf, or an\n * unresolved token placeholder.\n */\nexport class DiNode {\n id: string;\n className: string;\n kind: DiNodeKind;\n scope: DiScope;\n file: string;\n /** BFS depth from the design's root (root = 0). Filled in after the walk. */\n level: number;\n /**\n * Secondary detail for constant/dynamic/unresolved leaves whose box is now\n * labeled by the declared param type (see B0): the bound expression text\n * (`toConstantValue`/`toDynamicValue`/`.to(X)` source) or resolving token.\n * Omitted (undefined → not serialized) for class/controller/component nodes.\n */\n detail?: string;\n\n constructor(\n id: string,\n className: string,\n kind: DiNodeKind,\n scope: DiScope,\n file: string,\n level = 0,\n detail?: string,\n ) {\n this.id = id;\n this.className = className;\n this.kind = kind;\n this.scope = scope;\n this.file = file;\n this.level = level;\n if (detail !== undefined && detail !== '') this.detail = detail;\n }\n}\n\n/**\n * A constructor-injection edge: `from` class injects `to` node.\n */\nexport class DiEdge {\n from: string;\n to: string;\n injection: DiInjectionKind;\n token: string;\n tokenKey: string;\n paramName: string;\n paramType: string;\n\n constructor(\n from: string,\n to: string,\n injection: DiInjectionKind,\n token: string,\n tokenKey: string,\n paramName: string,\n paramType: string,\n ) {\n this.from = from;\n this.to = to;\n this.injection = injection;\n this.token = token;\n this.tokenKey = tokenKey;\n this.paramName = paramName;\n this.paramType = paramType;\n }\n}\n\n/**\n * One self-contained DI design tree: a single root (a @Controller, or a\n * library top-of-DAG class) and everything it injects \"on down\". Shared\n * dependencies are duplicated across designs — each design is its own tree so\n * it can be reviewed in isolation.\n */\nexport class DiDesign {\n /** Node id of this design's root (level 0). */\n root: string;\n rootKind: DiNodeKind;\n /** Workspace-relative posix path of the root class's file. */\n file: string;\n /** Deepest level reached from the root (0 when the root has no injections). */\n maxLevel: number;\n nodes: DiNode[];\n edges: DiEdge[];\n unresolved: string[];\n\n constructor(root: string, rootKind: DiNodeKind, file: string) {\n this.root = root;\n this.rootKind = rootKind;\n this.file = file;\n this.maxLevel = 0;\n this.nodes = [];\n this.edges = [];\n this.unresolved = [];\n }\n}\n\n/**\n * The full per-project DI graph, serialized to design.json — a container over\n * one `DiDesign` per root (controller or library top-of-DAG class).\n */\nexport class DiGraph {\n schemaVersion: number;\n project: string;\n designs: DiDesign[];\n\n constructor(project: string) {\n this.schemaVersion = 2;\n this.project = project;\n this.designs = [];\n }\n}\n\n/**\n * Canonical identity of a DI token — the key links `Symbol.for('X')` token\n * definitions to their bind() sites even across packages; display is what a\n * human reads in design.md edge labels (e.g. \"TYPES.Counter\").\n */\nexport class TokenRef {\n key: string;\n display: string;\n\n constructor(key: string, display: string) {\n this.key = key;\n this.display = display;\n }\n}\n\n/**\n * One binding discovered in pass 1 — either a ContainerModule bind() call or a\n * @provideSingleton/@provideSingletonAs/@provideTransient decorator.\n */\nexport class Binding {\n tokenKey: string;\n tokenDisplay: string;\n kind: BindingKind;\n scope: DiScope;\n /** Implementation class for to/toSelf/decorator bindings; null for constant/dynamic. */\n implClass: ts.ClassDeclaration | null;\n /** Source text of the bound expression for constant/dynamic leaves; '' otherwise. */\n valueText: string;\n /** Workspace-relative posix path of the file the binding appears in. */\n file: string;\n /**\n * Angular `useFactory` deps: the declared `deps: [A, B]` tokens. The walker\n * emits an edge from the dynamic leaf to each dep so a factory's true DI\n * boundary is visible (e.g. ClientConfig's factory → EnvironmentConfig +\n * MutableContextStore). Empty for every Inversify binding.\n */\n factoryDeps: TokenRef[];\n\n constructor(\n tokenKey: string,\n tokenDisplay: string,\n kind: BindingKind,\n scope: DiScope,\n implClass: ts.ClassDeclaration | null,\n valueText: string,\n file: string,\n factoryDeps: TokenRef[] = [],\n ) {\n this.tokenKey = tokenKey;\n this.tokenDisplay = tokenDisplay;\n this.kind = kind;\n this.scope = scope;\n this.implClass = implClass;\n this.valueText = valueText;\n this.file = file;\n this.factoryDeps = factoryDeps;\n }\n}\n"]}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Framework Resolver
|
|
3
3
|
*
|
|
4
|
-
* Determines the `framework` field
|
|
5
|
-
* architecture/dependencies.json
|
|
4
|
+
* Determines the `framework` field (a project's "libType" — which client side
|
|
5
|
+
* it targets) written per project into architecture/dependencies.json. Known
|
|
6
|
+
* values: angular | react | express | all ("all" = a library usable by any
|
|
7
|
+
* side). This is the field the `library-types-match-client` rule reads to keep
|
|
8
|
+
* an express project from depending on an angular-only lib (and vice-versa).
|
|
6
9
|
*
|
|
7
10
|
* Resolution order:
|
|
8
11
|
* 1. Explicit nx tag `framework:<value>` on the project (project.json tags) —
|
|
9
|
-
* any value is allowed so new frameworks need no code change here.
|
|
12
|
+
* any value is allowed so new frameworks need no code change here. This is
|
|
13
|
+
* the source of truth; every project should carry one.
|
|
10
14
|
* 2. Inference from the project's package.json dependencies.
|
|
11
|
-
* 3. Fallback: 'all
|
|
15
|
+
* 3. Fallback: 'all' (plain TypeScript library usable by any side).
|
|
12
16
|
*/
|
|
13
17
|
import { ProjectInfo } from './project-info';
|
|
14
18
|
export declare const FRAMEWORK_TAG_PREFIX = "framework:";
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Framework Resolver
|
|
4
4
|
*
|
|
5
|
-
* Determines the `framework` field
|
|
6
|
-
* architecture/dependencies.json
|
|
5
|
+
* Determines the `framework` field (a project's "libType" — which client side
|
|
6
|
+
* it targets) written per project into architecture/dependencies.json. Known
|
|
7
|
+
* values: angular | react | express | all ("all" = a library usable by any
|
|
8
|
+
* side). This is the field the `library-types-match-client` rule reads to keep
|
|
9
|
+
* an express project from depending on an angular-only lib (and vice-versa).
|
|
7
10
|
*
|
|
8
11
|
* Resolution order:
|
|
9
12
|
* 1. Explicit nx tag `framework:<value>` on the project (project.json tags) —
|
|
10
|
-
* any value is allowed so new frameworks need no code change here.
|
|
13
|
+
* any value is allowed so new frameworks need no code change here. This is
|
|
14
|
+
* the source of truth; every project should carry one.
|
|
11
15
|
* 2. Inference from the project's package.json dependencies.
|
|
12
|
-
* 3. Fallback: 'all
|
|
16
|
+
* 3. Fallback: 'all' (plain TypeScript library usable by any side).
|
|
13
17
|
*/
|
|
14
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
19
|
exports.FrameworkResolution = exports.FRAMEWORK_TAG_PREFIX = void 0;
|
|
@@ -69,7 +73,7 @@ function resolveFramework(info, workspaceRoot) {
|
|
|
69
73
|
function inferFromPackageJson(info, workspaceRoot) {
|
|
70
74
|
const pkgJsonPath = path.join(workspaceRoot, info.root, 'package.json');
|
|
71
75
|
if (!fs.existsSync(pkgJsonPath)) {
|
|
72
|
-
return 'all
|
|
76
|
+
return 'all';
|
|
73
77
|
}
|
|
74
78
|
let allDeps;
|
|
75
79
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
@@ -92,6 +96,6 @@ function inferFromPackageJson(info, workspaceRoot) {
|
|
|
92
96
|
return marker.framework;
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
|
-
return 'all
|
|
99
|
+
return 'all';
|
|
96
100
|
}
|
|
97
101
|
//# sourceMappingURL=framework-resolver.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/framework-resolver.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"framework-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/framework-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework Resolver\n *\n * Determines the `framework` field (a project's \"libType\" — which client side\n * it targets) written per project into architecture/dependencies.json. Known\n * values: angular | react | express | all (\"all\" = a library usable by any\n * side). This is the field the `library-types-match-client` rule reads to keep\n * an express project from depending on an angular-only lib (and vice-versa).\n *\n * Resolution order:\n * 1. Explicit nx tag `framework:<value>` on the project (project.json tags) —\n * any value is allowed so new frameworks need no code change here. This is\n * the source of truth; every project should carry one.\n * 2. Inference from the project's package.json dependencies.\n * 3. Fallback: 'all' (plain TypeScript library usable by any side).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\nexport const FRAMEWORK_TAG_PREFIX = 'framework:';\n\n/**\n * A package.json dependency name that identifies a framework.\n */\nclass FrameworkMarker {\n constructor(\n public readonly dependency: string,\n public readonly framework: string\n ) {}\n}\n\n/**\n * Dependency name → framework, checked in order (first match wins).\n */\nconst FRAMEWORK_DEPENDENCY_MARKERS: ReadonlyArray<FrameworkMarker> = [\n new FrameworkMarker('@angular/core', 'angular'),\n new FrameworkMarker('react', 'react'),\n new FrameworkMarker('express', 'express'),\n];\n\nexport class FrameworkResolution {\n constructor(\n /** Resolved framework name, or null when resolution failed */\n public readonly framework: string | null,\n /** Problem description when resolution failed, otherwise null */\n public readonly problem: string | null\n ) {}\n}\n\nexport function resolveFramework(info: ProjectInfo, workspaceRoot: string): FrameworkResolution {\n const tagValues = info.tags\n .filter((tag: string) => tag.startsWith(FRAMEWORK_TAG_PREFIX))\n .map((tag: string) => tag.slice(FRAMEWORK_TAG_PREFIX.length).trim());\n\n if (tagValues.length > 1) {\n return new FrameworkResolution(\n null,\n `${info.name}: has ${tagValues.length} 'framework:' tags (${tagValues.join(', ')}) — a project must have at most one`\n );\n }\n if (tagValues.length === 1) {\n if (tagValues[0].length === 0) {\n return new FrameworkResolution(null, `${info.name}: 'framework:' tag has an empty value`);\n }\n return new FrameworkResolution(tagValues[0], null);\n }\n\n return new FrameworkResolution(inferFromPackageJson(info, workspaceRoot), null);\n}\n\nfunction inferFromPackageJson(info: ProjectInfo, workspaceRoot: string): string {\n const pkgJsonPath = path.join(workspaceRoot, info.root, 'package.json');\n if (!fs.existsSync(pkgJsonPath)) {\n return 'all';\n }\n\n let allDeps: Record<string, string>;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n allDeps = {\n ...(pkgJson.dependencies ?? {}),\n ...(pkgJson.devDependencies ?? {}),\n ...(pkgJson.peerDependencies ?? {}),\n };\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to parse ${pkgJsonPath} while inferring framework for ${info.name}`, {\n cause: error,\n });\n }\n\n for (const marker of FRAMEWORK_DEPENDENCY_MARKERS) {\n if (marker.dependency in allDeps) {\n return marker.framework;\n }\n }\n return 'all';\n}\n"]}
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* }
|
|
14
14
|
* }
|
|
15
15
|
*
|
|
16
|
+
* `framework` is the project's libType — which client side it targets:
|
|
17
|
+
* angular | react | express | all ("all" = usable by any side). It comes from
|
|
18
|
+
* the project's `framework:` nx tag and is enforced across edges by the
|
|
19
|
+
* `library-types-match-client` rule.
|
|
20
|
+
*
|
|
16
21
|
* The legacy format (flat { "<project>": { level, dependsOn } } map) is still
|
|
17
22
|
* readable so validation against a pre-upgrade file produces a clean
|
|
18
23
|
* "re-run architecture:generate" diff instead of a parse failure.
|
package/src/lib/graph-loader.js
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* }
|
|
15
15
|
* }
|
|
16
16
|
*
|
|
17
|
+
* `framework` is the project's libType — which client side it targets:
|
|
18
|
+
* angular | react | express | all ("all" = usable by any side). It comes from
|
|
19
|
+
* the project's `framework:` nx tag and is enforced across edges by the
|
|
20
|
+
* `library-types-match-client` rule.
|
|
21
|
+
*
|
|
17
22
|
* The legacy format (flat { "<project>": { level, dependsOn } } map) is still
|
|
18
23
|
* readable so validation against a pre-upgrade file produces a clean
|
|
19
24
|
* "re-run architecture:generate" diff instead of a parse failure.
|
|
@@ -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 * 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 { 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.';\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}\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 );\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(` \"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 * 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 pushOptionalField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\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 * 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): 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(new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph));\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 — which client side it targets:\n * angular | react | express | all (\"all\" = usable by any side). It comes from\n * the project's `framework:` nx tag and is enforced across edges by the\n * `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 { 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.';\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}\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 );\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(` \"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 * 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 pushOptionalField(lines, 'framework', entry.framework);\n pushOptionalField(lines, 'shortDescription', entry.shortDescription);\n pushOptionalField(lines, 'responsibilitiesFile', entry.responsibilitiesFile);\n pushOptionalField(lines, 'designFile', entry.designFile);\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 * 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): 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(new DependenciesFile(AI_INSTRUCTIONS, GRAPH_COMMANDS, sortedGraph));\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"]}
|
|
@@ -34,3 +34,22 @@ export declare function collectProjectInfo(): Promise<Map<string, ProjectInfo>>;
|
|
|
34
34
|
* every problem when any project fails validation.
|
|
35
35
|
*/
|
|
36
36
|
export declare function enrichGraph(graph: EnhancedGraph, infos: Map<string, ProjectInfo>, workspaceRoot: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* libType usable by any side — a dependency of this type is always allowed.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ALL_LIB_TYPE = "all";
|
|
41
|
+
/**
|
|
42
|
+
* `library-types-match-client` rule.
|
|
43
|
+
*
|
|
44
|
+
* A project's `framework` field is its libType — which client side it targets
|
|
45
|
+
* (angular | react | express | all). This keeps side-specific code from
|
|
46
|
+
* crossing sides: an `express` project must not pull in an `angular`-only
|
|
47
|
+
* library (and vice-versa), and an `all` library — one that claims to be usable
|
|
48
|
+
* by everyone — must not depend on a side-specific library (which would quietly
|
|
49
|
+
* make it un-usable by the other sides).
|
|
50
|
+
*
|
|
51
|
+
* For every dependency edge A → B: allowed iff B is `all` or B has the same
|
|
52
|
+
* libType as A. Every violation is appended to `problems` so `arch:generate`
|
|
53
|
+
* fails with the full list.
|
|
54
|
+
*/
|
|
55
|
+
export declare function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void;
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* a failed run never clobbers dependencies.json.
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.MetadataValidationError = exports.RESPONSIBILITIES_FILE_NAME = void 0;
|
|
17
|
+
exports.ALL_LIB_TYPE = exports.MetadataValidationError = exports.RESPONSIBILITIES_FILE_NAME = void 0;
|
|
18
18
|
exports.collectProjectInfo = collectProjectInfo;
|
|
19
19
|
exports.enrichGraph = enrichGraph;
|
|
20
|
+
exports.validateLibraryTypesMatch = validateLibraryTypesMatch;
|
|
20
21
|
const tslib_1 = require("tslib");
|
|
21
22
|
const fs = tslib_1.__importStar(require("fs"));
|
|
22
23
|
const path = tslib_1.__importStar(require("path"));
|
|
@@ -77,10 +78,48 @@ function enrichGraph(graph, infos, workspaceRoot) {
|
|
|
77
78
|
entry.designFile = toRepoRelative(info.root, 'design.json');
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
validateLibraryTypesMatch(graph, problems);
|
|
80
82
|
if (problems.length > 0) {
|
|
81
83
|
throw new MetadataValidationError(problems);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* libType usable by any side — a dependency of this type is always allowed.
|
|
88
|
+
*/
|
|
89
|
+
exports.ALL_LIB_TYPE = 'all';
|
|
90
|
+
/**
|
|
91
|
+
* `library-types-match-client` rule.
|
|
92
|
+
*
|
|
93
|
+
* A project's `framework` field is its libType — which client side it targets
|
|
94
|
+
* (angular | react | express | all). This keeps side-specific code from
|
|
95
|
+
* crossing sides: an `express` project must not pull in an `angular`-only
|
|
96
|
+
* library (and vice-versa), and an `all` library — one that claims to be usable
|
|
97
|
+
* by everyone — must not depend on a side-specific library (which would quietly
|
|
98
|
+
* make it un-usable by the other sides).
|
|
99
|
+
*
|
|
100
|
+
* For every dependency edge A → B: allowed iff B is `all` or B has the same
|
|
101
|
+
* libType as A. Every violation is appended to `problems` so `arch:generate`
|
|
102
|
+
* fails with the full list.
|
|
103
|
+
*/
|
|
104
|
+
function validateLibraryTypesMatch(graph, problems) {
|
|
105
|
+
for (const [projectName, entry] of Object.entries(graph)) {
|
|
106
|
+
const fromType = entry.framework;
|
|
107
|
+
if (fromType === undefined)
|
|
108
|
+
continue; // framework resolution already flagged this project
|
|
109
|
+
for (const dep of entry.dependsOn) {
|
|
110
|
+
const depEntry = graph[dep];
|
|
111
|
+
const toType = depEntry?.framework;
|
|
112
|
+
if (toType === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
if (toType === exports.ALL_LIB_TYPE || toType === fromType)
|
|
115
|
+
continue;
|
|
116
|
+
problems.push(`library-types-match-client: '${projectName}' (${fromType}) must not depend on ` +
|
|
117
|
+
`'${dep}' (${toType}) — a '${fromType}' project may depend only on '${fromType}' ` +
|
|
118
|
+
`or '${exports.ALL_LIB_TYPE}' libraries. Fix the tag on one of them (framework:<angular|react|express|all>) ` +
|
|
119
|
+
`or remove the dependency.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
84
123
|
function enrichResponsibilities(entry, info, workspaceRoot, problems) {
|
|
85
124
|
const responsibilitiesFile = toRepoRelative(info.root, exports.RESPONSIBILITIES_FILE_NAME);
|
|
86
125
|
entry.responsibilitiesFile = responsibilitiesFile;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.framework !== null) {\n entry.framework = resolution.framework;\n }\n\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Only project.json projects get a generated design.json (see di-graph-targets.ts)\n if (fs.existsSync(path.join(workspaceRoot, info.root, 'project.json'))) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;AA8BH,gDAOC;AAOD,kCAkCC;AAqBD,8DAoBC;;AArHD,+CAAyB;AACzB,mDAA6B;AAC7B,uCAAqD;AAErD,iDAA6C;AAC7C,6DAAwD;AACxD,yDAAuF;AAE1E,QAAA,0BAA0B,GAAG,qBAAqB,CAAC;AAEhE;;;;GAIG;AACH,MAAa,uBAAwB,SAAQ,KAAK;IAClB;IAA5B,YAA4B,QAAkB;QAC1C,KAAK,CACD,4CAA4C,QAAQ,CAAC,MAAM,iBAAiB;YACxE,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CACrE,CAAC;QAJsB,aAAQ,GAAR,QAAQ,CAAU;QAK1C,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IAC1C,CAAC;CACJ;AARD,0DAQC;AAED;;GAEG;AACI,KAAK,UAAU,kBAAkB;IACpC,MAAM,YAAY,GAAG,MAAM,IAAA,gCAAuB,GAAE,CAAC;IACrD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5D,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,0BAAW,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,SAAgB,WAAW,CACvB,KAAoB,EACpB,KAA+B,EAC/B,aAAqB;IAErB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,EAAE,CAAC;YACR,QAAQ,CAAC,IAAI,CAAC,GAAG,WAAW,iCAAiC,CAAC,CAAC;YAC/D,SAAS;QACb,CAAC;QAED,MAAM,UAAU,GAAG,IAAA,qCAAgB,EAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACzD,IAAI,UAAU,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC9B,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;aAAM,IAAI,UAAU,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YACvC,KAAK,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC;QAC3C,CAAC;QAED,sBAAsB,CAAC,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC;QAE7D,mFAAmF;QACnF,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;YACrE,KAAK,CAAC,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QAChE,CAAC;IACL,CAAC;IAED,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAE3C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC;AACL,CAAC;AAED;;GAEG;AACU,QAAA,YAAY,GAAG,KAAK,CAAC;AAElC;;;;;;;;;;;;;GAaG;AACH,SAAgB,yBAAyB,CAAC,KAAoB,EAAE,QAAkB;IAC9E,KAAK,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC;QACjC,IAAI,QAAQ,KAAK,SAAS;YAAE,SAAS,CAAC,oDAAoD;QAE1F,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,QAAQ,EAAE,SAAS,CAAC;YACnC,IAAI,MAAM,KAAK,SAAS;gBAAE,SAAS;YAEnC,IAAI,MAAM,KAAK,oBAAY,IAAI,MAAM,KAAK,QAAQ;gBAAE,SAAS;YAE7D,QAAQ,CAAC,IAAI,CACT,gCAAgC,WAAW,MAAM,QAAQ,uBAAuB;gBAC5E,IAAI,GAAG,MAAM,MAAM,UAAU,QAAQ,iCAAiC,QAAQ,IAAI;gBAClF,OAAO,oBAAY,kFAAkF;gBACrG,2BAA2B,CAClC,CAAC;QACN,CAAC;IACL,CAAC;AACL,CAAC;AAED,SAAS,sBAAsB,CAC3B,KAA4B,EAC5B,IAAiB,EACjB,aAAqB,EACrB,QAAkB;IAElB,MAAM,oBAAoB,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,kCAA0B,CAAC,CAAC;IACnF,KAAK,CAAC,oBAAoB,GAAG,oBAAoB,CAAC;IAElD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,kCAA0B,CAAC,CAAC;IACrF,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/B,QAAQ,CAAC,IAAI,CACT,GAAG,IAAI,CAAC,IAAI,sBAAsB,oBAAoB,+BAA+B;YACjF,2EAA2E,CAClF,CAAC;QACF,OAAO;IACX,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,0CAAuB,EAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,MAAM,cAAc,GAAG,IAAA,2CAAwB,EAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;IAC/E,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC,CAAC;QACjD,OAAO;IACX,CAAC;IACD,KAAK,CAAC,gBAAgB,GAAG,OAAO,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc,CAAC,WAAmB,EAAE,QAAgB;IACzD,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACrF,CAAC","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.framework !== null) {\n entry.framework = resolution.framework;\n }\n\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Only project.json projects get a generated design.json (see di-graph-targets.ts)\n if (fs.existsSync(path.join(workspaceRoot, info.root, 'project.json'))) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateLibraryTypesMatch(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * libType usable by any side — a dependency of this type is always allowed.\n */\nexport const ALL_LIB_TYPE = 'all';\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — which client side it targets\n * (angular | react | express | all). This keeps side-specific code from\n * crossing sides: an `express` project must not pull in an `angular`-only\n * library (and vice-versa), and an `all` library — one that claims to be usable\n * by everyone — must not depend on a side-specific library (which would quietly\n * make it un-usable by the other sides).\n *\n * For every dependency edge A → B: allowed iff B is `all` or B has the same\n * libType as A. Every violation is appended to `problems` so `arch:generate`\n * fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromType = entry.framework;\n if (fromType === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toType = depEntry?.framework;\n if (toType === undefined) continue;\n\n if (toType === ALL_LIB_TYPE || toType === fromType) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' (${fromType}) must not depend on ` +\n `'${dep}' (${toType}) — a '${fromType}' project may depend only on '${fromType}' ` +\n `or '${ALL_LIB_TYPE}' libraries. Fix the tag on one of them (framework:<angular|react|express|all>) ` +\n `or remove the dependency.`\n );\n }\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n"]}
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* Graph entry with level metadata plus AI-oriented metadata filled in by
|
|
11
11
|
* enrichGraph() (lib/graph-metadata.ts) before the graph is saved:
|
|
12
|
-
* - framework: angular | react | express | all
|
|
12
|
+
* - framework: the project's libType — angular | react | express | all
|
|
13
|
+
* ("all" = library usable by any side); from its `framework:` nx tag
|
|
14
|
+
* (source of truth) or inferred from package.json deps
|
|
13
15
|
* - shortDescription: summary extracted from the project's responsibilities.md
|
|
14
16
|
* - responsibilitiesFile: repo-relative path to the FULL responsibilities doc
|
|
15
17
|
* - designFile: repo-relative path to the generated DI design.json (only for
|