@webpieces/nx-webpieces-rules 0.3.242 → 0.3.243
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 +9 -6
- package/src/index.d.ts +3 -0
- package/src/index.js +3 -0
- package/src/index.js.map +1 -1
- package/src/lib/di-graph/design-finder.d.ts +36 -0
- package/src/lib/di-graph/design-finder.js +121 -0
- package/src/lib/di-graph/design-finder.js.map +1 -0
- package/src/lib/di-graph/design-visualizer.d.ts +24 -0
- package/src/lib/di-graph/design-visualizer.js +138 -0
- package/src/lib/di-graph/design-visualizer.js.map +1 -0
- package/src/lib/di-graph/dot.d.ts +14 -0
- package/src/lib/di-graph/dot.js +76 -0
- package/src/lib/di-graph/dot.js.map +1 -0
- package/src/lib/graph-loader.d.ts +12 -1
- package/src/lib/graph-loader.js +33 -6
- package/src/lib/graph-loader.js.map +1 -1
- package/src/scripts/wp-design-visualize.d.ts +16 -0
- package/src/scripts/wp-design-visualize.js +99 -0
- package/src/scripts/wp-design-visualize.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.243",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"executors": "./executors.json",
|
|
8
|
+
"bin": {
|
|
9
|
+
"wp-design-visualize": "./src/scripts/wp-design-visualize.js"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": "./src/index.js",
|
|
10
13
|
"./package.json": "./package.json",
|
|
@@ -18,11 +21,11 @@
|
|
|
18
21
|
"README.md"
|
|
19
22
|
],
|
|
20
23
|
"dependencies": {
|
|
21
|
-
"@webpieces/ai-hook-rules": "0.3.
|
|
22
|
-
"@webpieces/code-rules": "0.3.
|
|
23
|
-
"@webpieces/eslint-rules": "0.3.
|
|
24
|
-
"@webpieces/pr-gate": "0.3.
|
|
25
|
-
"@webpieces/rules-config": "0.3.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.3.243",
|
|
25
|
+
"@webpieces/code-rules": "0.3.243",
|
|
26
|
+
"@webpieces/eslint-rules": "0.3.243",
|
|
27
|
+
"@webpieces/pr-gate": "0.3.243",
|
|
28
|
+
"@webpieces/rules-config": "0.3.243",
|
|
26
29
|
"madge": "8.0.0"
|
|
27
30
|
},
|
|
28
31
|
"peerDependencies": {
|
package/src/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export * from './lib/graph-metadata';
|
|
|
8
8
|
export * from './lib/framework-resolver';
|
|
9
9
|
export * from './lib/project-info';
|
|
10
10
|
export * from './lib/responsibilities';
|
|
11
|
+
export * from './lib/di-graph/dot';
|
|
12
|
+
export * from './lib/di-graph/design-visualizer';
|
|
13
|
+
export * from './lib/di-graph/design-finder';
|
|
11
14
|
import { createNodesV2 } from './plugin';
|
|
12
15
|
export { createNodesV2 };
|
|
13
16
|
declare const _default: {
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,9 @@ tslib_1.__exportStar(require("./lib/graph-metadata"), exports);
|
|
|
12
12
|
tslib_1.__exportStar(require("./lib/framework-resolver"), exports);
|
|
13
13
|
tslib_1.__exportStar(require("./lib/project-info"), exports);
|
|
14
14
|
tslib_1.__exportStar(require("./lib/responsibilities"), exports);
|
|
15
|
+
tslib_1.__exportStar(require("./lib/di-graph/dot"), exports);
|
|
16
|
+
tslib_1.__exportStar(require("./lib/di-graph/design-visualizer"), exports);
|
|
17
|
+
tslib_1.__exportStar(require("./lib/di-graph/design-finder"), exports);
|
|
15
18
|
const plugin_1 = require("./plugin");
|
|
16
19
|
Object.defineProperty(exports, "createNodesV2", { enumerable: true, get: function () { return plugin_1.createNodesV2; } });
|
|
17
20
|
exports.default = { name: '@webpieces/nx-webpieces-rules', createNodesV2: plugin_1.createNodesV2 };
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/nx-webpieces-rules/src/index.ts"],"names":[],"mappings":";;;;AAAA,gEAAsC;AACtC,6DAAmC;AACnC,iEAAuC;AACvC,kEAAwC;AACxC,6DAAmC;AACnC,iEAAuC;AACvC,+DAAqC;AACrC,mEAAyC;AACzC,6DAAmC;AACnC,iEAAuC;AACvC,qCAAyC;AAChC,8FADA,sBAAa,OACA;AACtB,kBAAe,EAAE,IAAI,EAAE,+BAA+B,EAAE,aAAa,EAAb,sBAAa,EAAE,CAAC","sourcesContent":["export * from './lib/graph-generator';\nexport * from './lib/graph-sorter';\nexport * from './lib/graph-comparator';\nexport * from './lib/package-validator';\nexport * from './lib/graph-loader';\nexport * from './lib/graph-visualizer';\nexport * from './lib/graph-metadata';\nexport * from './lib/framework-resolver';\nexport * from './lib/project-info';\nexport * from './lib/responsibilities';\nimport { createNodesV2 } from './plugin';\nexport { createNodesV2 };\nexport default { name: '@webpieces/nx-webpieces-rules', createNodesV2 };\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/nx-webpieces-rules/src/index.ts"],"names":[],"mappings":";;;;AAAA,gEAAsC;AACtC,6DAAmC;AACnC,iEAAuC;AACvC,kEAAwC;AACxC,6DAAmC;AACnC,iEAAuC;AACvC,+DAAqC;AACrC,mEAAyC;AACzC,6DAAmC;AACnC,iEAAuC;AACvC,6DAAmC;AACnC,2EAAiD;AACjD,uEAA6C;AAC7C,qCAAyC;AAChC,8FADA,sBAAa,OACA;AACtB,kBAAe,EAAE,IAAI,EAAE,+BAA+B,EAAE,aAAa,EAAb,sBAAa,EAAE,CAAC","sourcesContent":["export * from './lib/graph-generator';\nexport * from './lib/graph-sorter';\nexport * from './lib/graph-comparator';\nexport * from './lib/package-validator';\nexport * from './lib/graph-loader';\nexport * from './lib/graph-visualizer';\nexport * from './lib/graph-metadata';\nexport * from './lib/framework-resolver';\nexport * from './lib/project-info';\nexport * from './lib/responsibilities';\nexport * from './lib/di-graph/dot';\nexport * from './lib/di-graph/design-visualizer';\nexport * from './lib/di-graph/design-finder';\nimport { createNodesV2 } from './plugin';\nexport { createNodesV2 };\nexport default { name: '@webpieces/nx-webpieces-rules', createNodesV2 };\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design File Finder
|
|
3
|
+
*
|
|
4
|
+
* Locates committed <projectRoot>/design.json files across the workspace and
|
|
5
|
+
* resolves user selections (names, substrings, numbers, 'all') against them.
|
|
6
|
+
* Core logic of the wp-design-visualize CLI, kept here so it is unit-testable.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* One discovered design.json file.
|
|
10
|
+
*/
|
|
11
|
+
export declare class DesignFileRef {
|
|
12
|
+
/** Project name from the design.json contents */
|
|
13
|
+
readonly project: string;
|
|
14
|
+
/** Workspace-relative posix path to the design.json */
|
|
15
|
+
readonly relPath: string;
|
|
16
|
+
/** Absolute path to the design.json */
|
|
17
|
+
readonly absPath: string;
|
|
18
|
+
constructor(
|
|
19
|
+
/** Project name from the design.json contents */
|
|
20
|
+
project: string,
|
|
21
|
+
/** Workspace-relative posix path to the design.json */
|
|
22
|
+
relPath: string,
|
|
23
|
+
/** Absolute path to the design.json */
|
|
24
|
+
absPath: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Recursively find every design.json in the workspace (skipping build/vcs
|
|
28
|
+
* dirs), sorted by project name.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findDesignFiles(workspaceRoot: string): DesignFileRef[];
|
|
31
|
+
/**
|
|
32
|
+
* Resolve user selections against the discovered files.
|
|
33
|
+
* Accepts: 'all', 1-based numbers, exact project names, or substrings of the
|
|
34
|
+
* project name / path. Throws on a selection that matches nothing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveSelections(selections: string[], files: DesignFileRef[]): DesignFileRef[];
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Design File Finder
|
|
4
|
+
*
|
|
5
|
+
* Locates committed <projectRoot>/design.json files across the workspace and
|
|
6
|
+
* resolves user selections (names, substrings, numbers, 'all') against them.
|
|
7
|
+
* Core logic of the wp-design-visualize CLI, kept here so it is unit-testable.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DesignFileRef = void 0;
|
|
11
|
+
exports.findDesignFiles = findDesignFiles;
|
|
12
|
+
exports.resolveSelections = resolveSelections;
|
|
13
|
+
const tslib_1 = require("tslib");
|
|
14
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
15
|
+
const path = tslib_1.__importStar(require("path"));
|
|
16
|
+
const toError_1 = require("../../toError");
|
|
17
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.nx', 'tmp', 'coverage']);
|
|
18
|
+
/**
|
|
19
|
+
* One discovered design.json file.
|
|
20
|
+
*/
|
|
21
|
+
class DesignFileRef {
|
|
22
|
+
project;
|
|
23
|
+
relPath;
|
|
24
|
+
absPath;
|
|
25
|
+
constructor(
|
|
26
|
+
/** Project name from the design.json contents */
|
|
27
|
+
project,
|
|
28
|
+
/** Workspace-relative posix path to the design.json */
|
|
29
|
+
relPath,
|
|
30
|
+
/** Absolute path to the design.json */
|
|
31
|
+
absPath) {
|
|
32
|
+
this.project = project;
|
|
33
|
+
this.relPath = relPath;
|
|
34
|
+
this.absPath = absPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.DesignFileRef = DesignFileRef;
|
|
38
|
+
/**
|
|
39
|
+
* Recursively find every design.json in the workspace (skipping build/vcs
|
|
40
|
+
* dirs), sorted by project name.
|
|
41
|
+
*/
|
|
42
|
+
function findDesignFiles(workspaceRoot) {
|
|
43
|
+
const refs = [];
|
|
44
|
+
scanDir(workspaceRoot, workspaceRoot, refs);
|
|
45
|
+
refs.sort((a, b) => a.project.localeCompare(b.project));
|
|
46
|
+
return refs;
|
|
47
|
+
}
|
|
48
|
+
function scanDir(dir, workspaceRoot, refs) {
|
|
49
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
50
|
+
try {
|
|
51
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const fullPath = path.join(dir, entry.name);
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
|
|
56
|
+
continue;
|
|
57
|
+
scanDir(fullPath, workspaceRoot, refs);
|
|
58
|
+
}
|
|
59
|
+
else if (entry.name === 'design.json') {
|
|
60
|
+
addRef(fullPath, workspaceRoot, refs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const error = (0, toError_1.toError)(err);
|
|
66
|
+
void error; // unreadable dir — skip it
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function addRef(absPath, workspaceRoot, refs) {
|
|
70
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(fs.readFileSync(absPath, 'utf-8'));
|
|
73
|
+
const relPath = path.relative(workspaceRoot, absPath).replace(/\\/g, '/');
|
|
74
|
+
const project = typeof parsed.project === 'string' ? parsed.project : relPath;
|
|
75
|
+
refs.push(new DesignFileRef(project, relPath, absPath));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const error = (0, toError_1.toError)(err);
|
|
79
|
+
console.warn(`⚠️ Skipping unparseable ${absPath}: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve user selections against the discovered files.
|
|
84
|
+
* Accepts: 'all', 1-based numbers, exact project names, or substrings of the
|
|
85
|
+
* project name / path. Throws on a selection that matches nothing.
|
|
86
|
+
*/
|
|
87
|
+
function resolveSelections(selections, files) {
|
|
88
|
+
const picked = new Map();
|
|
89
|
+
for (const rawSelection of selections) {
|
|
90
|
+
const selection = rawSelection.trim();
|
|
91
|
+
if (selection.length === 0)
|
|
92
|
+
continue;
|
|
93
|
+
if (selection.toLowerCase() === 'all') {
|
|
94
|
+
for (const file of files)
|
|
95
|
+
picked.set(file.absPath, file);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const file of matchSelection(selection, files)) {
|
|
99
|
+
picked.set(file.absPath, file);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Array.from(picked.values());
|
|
103
|
+
}
|
|
104
|
+
function matchSelection(selection, files) {
|
|
105
|
+
const asNumber = Number(selection);
|
|
106
|
+
if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= files.length) {
|
|
107
|
+
return [files[asNumber - 1]];
|
|
108
|
+
}
|
|
109
|
+
const exact = files.filter((file) => file.project === selection);
|
|
110
|
+
if (exact.length > 0)
|
|
111
|
+
return exact;
|
|
112
|
+
const lower = selection.toLowerCase();
|
|
113
|
+
const partial = files.filter((file) => file.project.toLowerCase().includes(lower) || file.relPath.toLowerCase().includes(lower));
|
|
114
|
+
if (partial.length === 0) {
|
|
115
|
+
throw new Error(`No design.json matches '${selection}'. Known projects: ${files
|
|
116
|
+
.map((file) => file.project)
|
|
117
|
+
.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
return partial;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=design-finder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-finder.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-finder.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Design File Finder\n *\n * Locates committed <projectRoot>/design.json files across the workspace and\n * resolves user selections (names, substrings, numbers, 'all') against them.\n * Core logic of the wp-design-visualize CLI, kept here so it is unit-testable.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../../toError';\n\nconst SKIP_DIRS = new Set<string>(['node_modules', 'dist', '.git', '.nx', 'tmp', 'coverage']);\n\n/**\n * One discovered design.json file.\n */\nexport class DesignFileRef {\n constructor(\n /** Project name from the design.json contents */\n public readonly project: string,\n /** Workspace-relative posix path to the design.json */\n public readonly relPath: string,\n /** Absolute path to the design.json */\n public readonly absPath: string\n ) {}\n}\n\n/**\n * Recursively find every design.json in the workspace (skipping build/vcs\n * dirs), sorted by project name.\n */\nexport function findDesignFiles(workspaceRoot: string): DesignFileRef[] {\n const refs: DesignFileRef[] = [];\n scanDir(workspaceRoot, workspaceRoot, refs);\n refs.sort((a: DesignFileRef, b: DesignFileRef) => a.project.localeCompare(b.project));\n return refs;\n}\n\nfunction scanDir(dir: string, workspaceRoot: string, refs: DesignFileRef[]): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;\n scanDir(fullPath, workspaceRoot, refs);\n } else if (entry.name === 'design.json') {\n addRef(fullPath, workspaceRoot, refs);\n }\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error; // unreadable dir — skip it\n }\n}\n\nfunction addRef(absPath: string, workspaceRoot: string, refs: DesignFileRef[]): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(absPath, 'utf-8'));\n const relPath = path.relative(workspaceRoot, absPath).replace(/\\\\/g, '/');\n const project = typeof parsed.project === 'string' ? parsed.project : relPath;\n refs.push(new DesignFileRef(project, relPath, absPath));\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${absPath}: ${error.message}`);\n }\n}\n\n/**\n * Resolve user selections against the discovered files.\n * Accepts: 'all', 1-based numbers, exact project names, or substrings of the\n * project name / path. Throws on a selection that matches nothing.\n */\nexport function resolveSelections(selections: string[], files: DesignFileRef[]): DesignFileRef[] {\n const picked = new Map<string, DesignFileRef>();\n\n for (const rawSelection of selections) {\n const selection = rawSelection.trim();\n if (selection.length === 0) continue;\n\n if (selection.toLowerCase() === 'all') {\n for (const file of files) picked.set(file.absPath, file);\n continue;\n }\n\n for (const file of matchSelection(selection, files)) {\n picked.set(file.absPath, file);\n }\n }\n\n return Array.from(picked.values());\n}\n\nfunction matchSelection(selection: string, files: DesignFileRef[]): DesignFileRef[] {\n const asNumber = Number(selection);\n if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= files.length) {\n return [files[asNumber - 1]];\n }\n\n const exact = files.filter((file: DesignFileRef) => file.project === selection);\n if (exact.length > 0) return exact;\n\n const lower = selection.toLowerCase();\n const partial = files.filter(\n (file: DesignFileRef) =>\n file.project.toLowerCase().includes(lower) || file.relPath.toLowerCase().includes(lower)\n );\n if (partial.length === 0) {\n throw new Error(\n `No design.json matches '${selection}'. Known projects: ${files\n .map((file: DesignFileRef) => file.project)\n .join(', ')}`\n );\n }\n return partial;\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI Design HTML Visualizer
|
|
3
|
+
*
|
|
4
|
+
* Builds one HTML page per project showing EVERY controller/root design as
|
|
5
|
+
* its own Graphviz graph (rendered client-side with viz.js, same pipeline as
|
|
6
|
+
* the architecture visualization). Output goes to tmp/webpieces/ — a view,
|
|
7
|
+
* never committed (the committed artifacts are design.json/design.md).
|
|
8
|
+
*/
|
|
9
|
+
import { DiGraph } from './model';
|
|
10
|
+
export declare class DesignVisualizationPaths {
|
|
11
|
+
readonly dotPath: string;
|
|
12
|
+
readonly htmlPath: string;
|
|
13
|
+
constructor(dotPath: string, htmlPath: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the full HTML page for a project's DI designs — one section (heading
|
|
17
|
+
* + meta + rendered graph) per controller/root design.
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateDesignHTML(graph: DiGraph): string;
|
|
20
|
+
/**
|
|
21
|
+
* Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs
|
|
22
|
+
* concatenated, for debugging) and return the paths.
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeDesignVisualization(graph: DiGraph, workspaceRoot: string): DesignVisualizationPaths;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DI Design HTML Visualizer
|
|
4
|
+
*
|
|
5
|
+
* Builds one HTML page per project showing EVERY controller/root design as
|
|
6
|
+
* its own Graphviz graph (rendered client-side with viz.js, same pipeline as
|
|
7
|
+
* the architecture visualization). Output goes to tmp/webpieces/ — a view,
|
|
8
|
+
* never committed (the committed artifacts are design.json/design.md).
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.DesignVisualizationPaths = void 0;
|
|
12
|
+
exports.generateDesignHTML = generateDesignHTML;
|
|
13
|
+
exports.writeDesignVisualization = writeDesignVisualization;
|
|
14
|
+
const tslib_1 = require("tslib");
|
|
15
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
16
|
+
const path = tslib_1.__importStar(require("path"));
|
|
17
|
+
const serializer_1 = require("./serializer");
|
|
18
|
+
const dot_1 = require("./dot");
|
|
19
|
+
class DesignVisualizationPaths {
|
|
20
|
+
dotPath;
|
|
21
|
+
htmlPath;
|
|
22
|
+
constructor(dotPath, htmlPath) {
|
|
23
|
+
this.dotPath = dotPath;
|
|
24
|
+
this.htmlPath = htmlPath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.DesignVisualizationPaths = DesignVisualizationPaths;
|
|
28
|
+
class DesignGraphEntry {
|
|
29
|
+
id;
|
|
30
|
+
dot;
|
|
31
|
+
constructor(id, dot) {
|
|
32
|
+
this.id = id;
|
|
33
|
+
this.dot = dot;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function htmlEscape(text) {
|
|
37
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
38
|
+
}
|
|
39
|
+
function pageStyles() {
|
|
40
|
+
return `
|
|
41
|
+
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }
|
|
42
|
+
h1 { text-align: center; color: #333; }
|
|
43
|
+
h2 { color: #333; margin-bottom: 4px; }
|
|
44
|
+
.meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }
|
|
45
|
+
.section { background: white; padding: 20px; border-radius: 8px;
|
|
46
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }
|
|
47
|
+
.graph { text-align: center; overflow-x: auto; }
|
|
48
|
+
.legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;
|
|
49
|
+
border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
50
|
+
.legend-item { margin: 8px 0; }
|
|
51
|
+
.legend-box { display: inline-block; width: 20px; height: 20px;
|
|
52
|
+
border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
function pageLegend() {
|
|
56
|
+
return `<div class="legend">
|
|
57
|
+
<h2>Legend</h2>
|
|
58
|
+
<div class="legend-item"><span class="legend-box" style="background: #E3F2FD;"></span>
|
|
59
|
+
<strong>Controller / root</strong> — level 0, the entry class of the tree</div>
|
|
60
|
+
<div class="legend-item"><span class="legend-box" style="background: #F5F5F5;"></span>
|
|
61
|
+
<strong>Class</strong> — injectable class (constructor injection)</div>
|
|
62
|
+
<div class="legend-item"><span class="legend-box" style="background: #FFF3E0;"></span>
|
|
63
|
+
<strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>
|
|
64
|
+
<div class="legend-item"><span class="legend-box" style="background: #FCE4EC; border-style: dashed;"></span>
|
|
65
|
+
<strong>Unresolved</strong> — token the analyzer could not resolve</div>
|
|
66
|
+
<div class="legend-item" style="margin-top: 15px;">
|
|
67
|
+
<em>One graph per controller/root; a shared dependency appears in each root's tree.
|
|
68
|
+
Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>
|
|
69
|
+
</div>`;
|
|
70
|
+
}
|
|
71
|
+
function renderScript(entries) {
|
|
72
|
+
return `
|
|
73
|
+
const graphs = ${JSON.stringify(entries)};
|
|
74
|
+
const viz = new Viz();
|
|
75
|
+
for (const g of graphs) {
|
|
76
|
+
viz.renderSVGElement(g.dot)
|
|
77
|
+
.then(element => { document.getElementById(g.id).appendChild(element); })
|
|
78
|
+
.catch(err => {
|
|
79
|
+
console.error(err);
|
|
80
|
+
document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build the full HTML page for a project's DI designs — one section (heading
|
|
87
|
+
* + meta + rendered graph) per controller/root design.
|
|
88
|
+
*/
|
|
89
|
+
function generateDesignHTML(graph) {
|
|
90
|
+
const title = `DI Designs — ${graph.project}`;
|
|
91
|
+
const entries = [];
|
|
92
|
+
const sections = [];
|
|
93
|
+
graph.designs.forEach((design, index) => {
|
|
94
|
+
const id = `graph-${index}`;
|
|
95
|
+
entries.push(new DesignGraphEntry(id, (0, dot_1.generateDesignDot)(design)));
|
|
96
|
+
sections.push(`<div class="section">
|
|
97
|
+
<h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>
|
|
98
|
+
<div class="meta">${htmlEscape(design.file)}</div>
|
|
99
|
+
<div id="${id}" class="graph"></div>
|
|
100
|
+
</div>`);
|
|
101
|
+
});
|
|
102
|
+
const body = sections.length > 0
|
|
103
|
+
? sections.join('\n ')
|
|
104
|
+
: '<div class="section"><em>No DI-registered classes found in this project.</em></div>';
|
|
105
|
+
return `<!DOCTYPE html>
|
|
106
|
+
<html>
|
|
107
|
+
<head>
|
|
108
|
+
<title>${htmlEscape(title)}</title>
|
|
109
|
+
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js"></script>
|
|
110
|
+
<script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js"></script>
|
|
111
|
+
<style>${pageStyles()}</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body>
|
|
114
|
+
<h1>${htmlEscape(title)}</h1>
|
|
115
|
+
${pageLegend()}
|
|
116
|
+
${body}
|
|
117
|
+
<script>${renderScript(entries)}</script>
|
|
118
|
+
</body>
|
|
119
|
+
</html>`;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs
|
|
123
|
+
* concatenated, for debugging) and return the paths.
|
|
124
|
+
*/
|
|
125
|
+
function writeDesignVisualization(graph, workspaceRoot) {
|
|
126
|
+
(0, serializer_1.sortGraph)(graph);
|
|
127
|
+
const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');
|
|
128
|
+
if (!fs.existsSync(outputDir)) {
|
|
129
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
const allDot = graph.designs.map((design) => (0, dot_1.generateDesignDot)(design)).join('\n');
|
|
132
|
+
const dotPath = path.join(outputDir, `design-${graph.project}.dot`);
|
|
133
|
+
fs.writeFileSync(dotPath, allDot, 'utf-8');
|
|
134
|
+
const htmlPath = path.join(outputDir, `design-${graph.project}.html`);
|
|
135
|
+
fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');
|
|
136
|
+
return new DesignVisualizationPaths(dotPath, htmlPath);
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=design-visualizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-visualizer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design HTML Visualizer\n *\n * Builds one HTML page per project showing EVERY controller/root design as\n * its own Graphviz graph (rendered client-side with viz.js, same pipeline as\n * the architecture visualization). Output goes to tmp/webpieces/ — a view,\n * never committed (the committed artifacts are design.json/design.md).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DiDesign, DiGraph } from './model';\nimport { sortGraph } from './serializer';\nimport { generateDesignDot } from './dot';\n\nexport class DesignVisualizationPaths {\n constructor(\n public readonly dotPath: string,\n public readonly htmlPath: string\n ) {}\n}\n\nclass DesignGraphEntry {\n constructor(\n public readonly id: string,\n public readonly dot: string\n ) {}\n}\n\nfunction htmlEscape(text: string): string {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n}\n\nfunction pageStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n h2 { color: #333; margin-bottom: 4px; }\n .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }\n .section { background: white; padding: 20px; border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }\n .graph { text-align: center; overflow-x: auto; }\n .legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;\n border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n .legend-item { margin: 8px 0; }\n .legend-box { display: inline-block; width: 20px; height: 20px;\n border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }\n `;\n}\n\nfunction pageLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Controller / root</strong> — level 0, the entry class of the tree</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>Class</strong> — injectable class (constructor injection)</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FCE4EC; border-style: dashed;\"></span>\n <strong>Unresolved</strong> — token the analyzer could not resolve</div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>One graph per controller/root; a shared dependency appears in each root's tree.\n Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>\n </div>`;\n}\n\nfunction renderScript(entries: DesignGraphEntry[]): string {\n return `\n const graphs = ${JSON.stringify(entries)};\n const viz = new Viz();\n for (const g of graphs) {\n viz.renderSVGElement(g.dot)\n .then(element => { document.getElementById(g.id).appendChild(element); })\n .catch(err => {\n console.error(err);\n document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';\n });\n }\n `;\n}\n\n/**\n * Build the full HTML page for a project's DI designs — one section (heading\n * + meta + rendered graph) per controller/root design.\n */\nexport function generateDesignHTML(graph: DiGraph): string {\n const title = `DI Designs — ${graph.project}`;\n const entries: DesignGraphEntry[] = [];\n const sections: string[] = [];\n\n graph.designs.forEach((design: DiDesign, index: number) => {\n const id = `graph-${index}`;\n entries.push(new DesignGraphEntry(id, generateDesignDot(design)));\n sections.push(`<div class=\"section\">\n <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>\n <div class=\"meta\">${htmlEscape(design.file)}</div>\n <div id=\"${id}\" class=\"graph\"></div>\n </div>`);\n });\n\n const body =\n sections.length > 0\n ? sections.join('\\n ')\n : '<div class=\"section\"><em>No DI-registered classes found in this project.</em></div>';\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${htmlEscape(title)}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>${pageStyles()}</style>\n</head>\n<body>\n <h1>${htmlEscape(title)}</h1>\n ${pageLegend()}\n ${body}\n <script>${renderScript(entries)}</script>\n</body>\n</html>`;\n}\n\n/**\n * Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs\n * concatenated, for debugging) and return the paths.\n */\nexport function writeDesignVisualization(\n graph: DiGraph,\n workspaceRoot: string\n): DesignVisualizationPaths {\n sortGraph(graph);\n\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const allDot = graph.designs.map((design: DiDesign) => generateDesignDot(design)).join('\\n');\n const dotPath = path.join(outputDir, `design-${graph.project}.dot`);\n fs.writeFileSync(dotPath, allDot, 'utf-8');\n\n const htmlPath = path.join(outputDir, `design-${graph.project}.html`);\n fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');\n\n return new DesignVisualizationPaths(dotPath, htmlPath);\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI Design DOT Emitter
|
|
3
|
+
*
|
|
4
|
+
* Renders one DiDesign (a single controller/root's dependency tree) as a
|
|
5
|
+
* Graphviz DOT digraph, mirroring the look of the architecture graph
|
|
6
|
+
* (lib/graph-visualizer.ts): rankdir=TB with { rank=same } layers per level,
|
|
7
|
+
* so the controller (level 0) sits at the top and injections fan downward.
|
|
8
|
+
*/
|
|
9
|
+
import { DiDesign } from './model';
|
|
10
|
+
/**
|
|
11
|
+
* Generate a Graphviz DOT digraph for one design (one controller/root tree).
|
|
12
|
+
* Deterministic for a serializer-sorted design.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateDesignDot(design: DiDesign): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DI Design DOT Emitter
|
|
4
|
+
*
|
|
5
|
+
* Renders one DiDesign (a single controller/root's dependency tree) as a
|
|
6
|
+
* Graphviz DOT digraph, mirroring the look of the architecture graph
|
|
7
|
+
* (lib/graph-visualizer.ts): rankdir=TB with { rank=same } layers per level,
|
|
8
|
+
* so the controller (level 0) sits at the top and injections fan downward.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.generateDesignDot = generateDesignDot;
|
|
12
|
+
/**
|
|
13
|
+
* Node fill colors by DI node kind.
|
|
14
|
+
*/
|
|
15
|
+
const KIND_COLORS = {
|
|
16
|
+
controller: '#E3F2FD', // light blue — the root/entry class
|
|
17
|
+
class: '#F5F5F5', // neutral — plain injectable class
|
|
18
|
+
constant: '#FFF3E0', // light orange — toConstantValue leaf
|
|
19
|
+
dynamic: '#FFF3E0', // light orange — toDynamicValue leaf
|
|
20
|
+
unresolved: '#FCE4EC', // light pink — token the analyzer could not resolve
|
|
21
|
+
};
|
|
22
|
+
/** Escape a string for use inside a double-quoted DOT identifier/label. */
|
|
23
|
+
function dotEscape(text) {
|
|
24
|
+
return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
25
|
+
}
|
|
26
|
+
function nodeStatement(node) {
|
|
27
|
+
const color = KIND_COLORS[node.kind] ?? '#F5F5F5';
|
|
28
|
+
const label = `${dotEscape(node.className)}\\n(L${node.level}, ${node.scope})`;
|
|
29
|
+
const styles = ['filled'];
|
|
30
|
+
if (node.kind === 'constant' || node.kind === 'dynamic')
|
|
31
|
+
styles.push('rounded');
|
|
32
|
+
if (node.kind === 'unresolved')
|
|
33
|
+
styles.push('dashed');
|
|
34
|
+
const penwidth = node.kind === 'controller' ? ', penwidth=2' : '';
|
|
35
|
+
return ` "${dotEscape(node.id)}" [fillcolor="${color}", style="${styles.join(',')}", label="${label}"${penwidth}];\n`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a Graphviz DOT digraph for one design (one controller/root tree).
|
|
39
|
+
* Deterministic for a serializer-sorted design.
|
|
40
|
+
*/
|
|
41
|
+
function generateDesignDot(design) {
|
|
42
|
+
let dot = `digraph "${dotEscape(design.root)}" {\n`;
|
|
43
|
+
dot += ' rankdir=TB;\n';
|
|
44
|
+
dot += ' node [shape=box, fontname="Arial"];\n';
|
|
45
|
+
dot += ' edge [fontname="Arial", fontsize=11];\n\n';
|
|
46
|
+
// Nodes, colored by kind, labeled with their level + scope
|
|
47
|
+
for (const node of design.nodes) {
|
|
48
|
+
dot += nodeStatement(node);
|
|
49
|
+
}
|
|
50
|
+
dot += '\n';
|
|
51
|
+
// One { rank=same } layer per level so the root stays on top
|
|
52
|
+
const levels = new Map();
|
|
53
|
+
for (const node of design.nodes) {
|
|
54
|
+
const layer = levels.get(node.level) ?? [];
|
|
55
|
+
layer.push(node.id);
|
|
56
|
+
levels.set(node.level, layer);
|
|
57
|
+
}
|
|
58
|
+
const sortedLevels = Array.from(levels.keys()).sort((a, b) => a - b);
|
|
59
|
+
for (const level of sortedLevels) {
|
|
60
|
+
const ids = levels.get(level) ?? [];
|
|
61
|
+
dot += ` { rank=same; ${ids.map((id) => `"${dotEscape(id)}"`).join('; ')}; }\n`;
|
|
62
|
+
}
|
|
63
|
+
dot += '\n';
|
|
64
|
+
// Constructor-injection edges; labels mirror design.md (mermaid.ts)
|
|
65
|
+
for (const edge of design.edges) {
|
|
66
|
+
const token = edge.injection === 'multiInject' ? `multiInject ${edge.token}` : edge.token;
|
|
67
|
+
const label = token !== '' ? ` [label="${dotEscape(token)}"]` : '';
|
|
68
|
+
dot += ` "${dotEscape(edge.from)}" -> "${dotEscape(edge.to)}"${label};\n`;
|
|
69
|
+
}
|
|
70
|
+
dot += '\n labelloc="t";\n';
|
|
71
|
+
dot += ` label="${dotEscape(design.root)}\\n(${design.rootKind}, Level 0-${design.maxLevel})";\n`;
|
|
72
|
+
dot += ' fontsize=16;\n';
|
|
73
|
+
dot += '}\n';
|
|
74
|
+
return dot;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=dot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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\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' ? ', 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 mirror design.md (mermaid.ts)\n for (const edge of design.edges) {\n const token = edge.injection === 'multiInject' ? `multiInject ${edge.token}` : edge.token;\n const label = token !== '' ? ` [label=\"${dotEscape(token)}\"]` : '';\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"]}
|
|
@@ -27,13 +27,24 @@ export declare const DEFAULT_GRAPH_PATH = "architecture/dependencies.json";
|
|
|
27
27
|
* the per-project metadata fields.
|
|
28
28
|
*/
|
|
29
29
|
export declare const AI_INSTRUCTIONS: string;
|
|
30
|
+
/**
|
|
31
|
+
* Named command → "command — what it does" map embedded in dependencies.json.
|
|
32
|
+
*/
|
|
33
|
+
export type CommandMap = Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Commands embedded in dependencies.json so AI (and humans) know how to
|
|
36
|
+
* regenerate and DISPLAY the architecture + design graphs. These work in any
|
|
37
|
+
* repo consuming @webpieces/nx-webpieces-rules.
|
|
38
|
+
*/
|
|
39
|
+
export declare const GRAPH_COMMANDS: CommandMap;
|
|
30
40
|
/**
|
|
31
41
|
* The full contents of architecture/dependencies.json.
|
|
32
42
|
*/
|
|
33
43
|
export declare class DependenciesFile {
|
|
34
44
|
readonly aiInstructions: string;
|
|
45
|
+
readonly commands: CommandMap;
|
|
35
46
|
readonly projects: EnhancedGraph;
|
|
36
|
-
constructor(aiInstructions: string, projects: EnhancedGraph);
|
|
47
|
+
constructor(aiInstructions: string, commands: CommandMap, projects: EnhancedGraph);
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
39
50
|
* Load the blessed graph from disk. Understands both the current wrapper
|
package/src/lib/graph-loader.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* "re-run architecture:generate" diff instead of a parse failure.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.DependenciesFile = exports.AI_INSTRUCTIONS = exports.DEFAULT_GRAPH_PATH = void 0;
|
|
22
|
+
exports.DependenciesFile = exports.GRAPH_COMMANDS = exports.AI_INSTRUCTIONS = exports.DEFAULT_GRAPH_PATH = void 0;
|
|
23
23
|
exports.loadBlessedGraph = loadBlessedGraph;
|
|
24
24
|
exports.saveGraph = saveGraph;
|
|
25
25
|
exports.graphFileExists = graphFileExists;
|
|
@@ -38,15 +38,35 @@ exports.DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';
|
|
|
38
38
|
exports.AI_INSTRUCTIONS = "Each project's shortDescription is only a summary. BEFORE adding code to a project, " +
|
|
39
39
|
'read its responsibilitiesFile for the full responsibilities (what belongs in that ' +
|
|
40
40
|
'project and what does not), and read its designFile to understand the DI design ' +
|
|
41
|
-
'before reading the code.'
|
|
41
|
+
'before reading the code. Use the entries in `commands` to regenerate these files ' +
|
|
42
|
+
'or display any of the graphs in a browser.';
|
|
43
|
+
/**
|
|
44
|
+
* Commands embedded in dependencies.json so AI (and humans) know how to
|
|
45
|
+
* regenerate and DISPLAY the architecture + design graphs. These work in any
|
|
46
|
+
* repo consuming @webpieces/nx-webpieces-rules.
|
|
47
|
+
*/
|
|
48
|
+
exports.GRAPH_COMMANDS = {
|
|
49
|
+
regenerateArchitecture: 'pnpm nx run architecture:generate — rewrites architecture/dependencies.json and ' +
|
|
50
|
+
'architecture/runtime-dependencies.json; run after adding/removing project dependencies',
|
|
51
|
+
visualizeArchitecture: 'pnpm nx run architecture:visualize — opens the monorepo dependency graph (this file) ' +
|
|
52
|
+
'as HTML in a browser',
|
|
53
|
+
visualizeRuntimeArchitecture: 'pnpm nx run architecture:visualize-runtime — opens the runtime microservice call graph ' +
|
|
54
|
+
'as HTML in a browser',
|
|
55
|
+
regenerateDesigns: "pnpm nx run-many --target=di-graph-generate — rewrites every project's " +
|
|
56
|
+
'design.json/design.md (also runs automatically on every build)',
|
|
57
|
+
visualizeDesign: "pnpm wp-design-visualize <project> — opens a project's DI designs (its designFile) as " +
|
|
58
|
+
'HTML, one graph per controller with the controller at the top; no args = interactive picker',
|
|
59
|
+
};
|
|
42
60
|
/**
|
|
43
61
|
* The full contents of architecture/dependencies.json.
|
|
44
62
|
*/
|
|
45
63
|
class DependenciesFile {
|
|
46
64
|
aiInstructions;
|
|
65
|
+
commands;
|
|
47
66
|
projects;
|
|
48
|
-
constructor(aiInstructions, projects) {
|
|
67
|
+
constructor(aiInstructions, commands, projects) {
|
|
49
68
|
this.aiInstructions = aiInstructions;
|
|
69
|
+
this.commands = commands;
|
|
50
70
|
this.projects = projects;
|
|
51
71
|
}
|
|
52
72
|
}
|
|
@@ -69,10 +89,10 @@ function loadBlessedGraph(workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
|
|
|
69
89
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
70
90
|
const parsed = JSON.parse(content);
|
|
71
91
|
if (parsed !== null && typeof parsed === 'object' && 'projects' in parsed) {
|
|
72
|
-
return new DependenciesFile(typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '', parsed.projects);
|
|
92
|
+
return new DependenciesFile(typeof parsed.aiInstructions === 'string' ? parsed.aiInstructions : '', parsed.commands !== null && typeof parsed.commands === 'object' ? parsed.commands : {}, parsed.projects);
|
|
73
93
|
}
|
|
74
94
|
// Legacy flat format: the whole object is the project map
|
|
75
|
-
return new DependenciesFile('', parsed);
|
|
95
|
+
return new DependenciesFile('', {}, parsed);
|
|
76
96
|
}
|
|
77
97
|
catch (err) {
|
|
78
98
|
const error = (0, toError_1.toError)(err);
|
|
@@ -85,6 +105,13 @@ function loadBlessedGraph(workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
|
|
|
85
105
|
function formatGraphJson(file) {
|
|
86
106
|
const lines = ['{'];
|
|
87
107
|
lines.push(` "aiInstructions": ${JSON.stringify(file.aiInstructions)},`);
|
|
108
|
+
lines.push(` "commands": {`);
|
|
109
|
+
const commandNames = Object.keys(file.commands);
|
|
110
|
+
commandNames.forEach((name, index) => {
|
|
111
|
+
const comma = index === commandNames.length - 1 ? '' : ',';
|
|
112
|
+
lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(file.commands[name])}${comma}`);
|
|
113
|
+
});
|
|
114
|
+
lines.push(` },`);
|
|
88
115
|
lines.push(` "projects": {`);
|
|
89
116
|
const keys = Object.keys(file.projects).sort();
|
|
90
117
|
keys.forEach((key, index) => {
|
|
@@ -151,7 +178,7 @@ function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
|
|
|
151
178
|
for (const key of sortedKeys) {
|
|
152
179
|
sortedGraph[key] = graph[key];
|
|
153
180
|
}
|
|
154
|
-
const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, sortedGraph));
|
|
181
|
+
const content = formatGraphJson(new DependenciesFile(exports.AI_INSTRUCTIONS, exports.GRAPH_COMMANDS, sortedGraph));
|
|
155
182
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
156
183
|
}
|
|
157
184
|
/**
|
|
@@ -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.';\n\n/**\n * The full contents of architecture/dependencies.json.\n */\nexport class DependenciesFile {\n constructor(\n public readonly aiInstructions: string,\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.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(` \"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, 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 * 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"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* wp-design-visualize — open per-project DI design graphs as Graphviz HTML.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* wp-design-visualize interactive picker (numbered list)
|
|
7
|
+
* wp-design-visualize <name|substring>… render matching project(s)
|
|
8
|
+
* wp-design-visualize all render every project
|
|
9
|
+
*
|
|
10
|
+
* Reads each project's committed design.json (schemaVersion 2 — one design
|
|
11
|
+
* per controller/root) and writes tmp/webpieces/design-<project>.html with
|
|
12
|
+
* one graph PER CONTROLLER, the controller (level 0) at the top. Replaces the
|
|
13
|
+
* old mermaid-based scripts/design-visualize.sh (which only showed the first
|
|
14
|
+
* controller).
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* wp-design-visualize — open per-project DI design graphs as Graphviz HTML.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* wp-design-visualize interactive picker (numbered list)
|
|
8
|
+
* wp-design-visualize <name|substring>… render matching project(s)
|
|
9
|
+
* wp-design-visualize all render every project
|
|
10
|
+
*
|
|
11
|
+
* Reads each project's committed design.json (schemaVersion 2 — one design
|
|
12
|
+
* per controller/root) and writes tmp/webpieces/design-<project>.html with
|
|
13
|
+
* one graph PER CONTROLLER, the controller (level 0) at the top. Replaces the
|
|
14
|
+
* old mermaid-based scripts/design-visualize.sh (which only showed the first
|
|
15
|
+
* controller).
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const tslib_1 = require("tslib");
|
|
19
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
20
|
+
const path = tslib_1.__importStar(require("path"));
|
|
21
|
+
const readline = tslib_1.__importStar(require("readline"));
|
|
22
|
+
const design_finder_1 = require("../lib/di-graph/design-finder");
|
|
23
|
+
const design_visualizer_1 = require("../lib/di-graph/design-visualizer");
|
|
24
|
+
const graph_visualizer_1 = require("../lib/graph-visualizer");
|
|
25
|
+
const toError_1 = require("../toError");
|
|
26
|
+
/** Walk up from cwd to the workspace root (dir containing nx.json). */
|
|
27
|
+
function findWorkspaceRoot() {
|
|
28
|
+
let dir = process.cwd();
|
|
29
|
+
for (let i = 0; i < 20; i++) {
|
|
30
|
+
if (fs.existsSync(path.join(dir, 'nx.json')))
|
|
31
|
+
return dir;
|
|
32
|
+
const parent = path.dirname(dir);
|
|
33
|
+
if (parent === dir)
|
|
34
|
+
break;
|
|
35
|
+
dir = parent;
|
|
36
|
+
}
|
|
37
|
+
return process.cwd();
|
|
38
|
+
}
|
|
39
|
+
function printList(files) {
|
|
40
|
+
console.log('\nDI design graphs found:\n');
|
|
41
|
+
files.forEach((file, index) => {
|
|
42
|
+
console.log(` ${String(index + 1).padStart(3)}. ${file.project.padEnd(24)} ${file.relPath}`);
|
|
43
|
+
});
|
|
44
|
+
console.log('');
|
|
45
|
+
}
|
|
46
|
+
/** Prompt for selections: numbers, names/substrings, or "all" (blank = cancel). */
|
|
47
|
+
function promptSelections() {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
+
rl.question('Which designs? (numbers / names / all, blank to cancel): ', (answer) => {
|
|
51
|
+
rl.close();
|
|
52
|
+
resolve(answer.split(/[\s,]+/).filter((part) => part.length > 0));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function loadDiGraph(file) {
|
|
57
|
+
const parsed = JSON.parse(fs.readFileSync(file.absPath, 'utf-8'));
|
|
58
|
+
if (!Array.isArray(parsed.designs)) {
|
|
59
|
+
throw new Error(`${file.relPath} is an old-format design.json (schemaVersion ${parsed.schemaVersion ?? 1}). ` +
|
|
60
|
+
`Regenerate it with: pnpm nx run ${file.project}:di-graph-generate`);
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
function renderOne(file, workspaceRoot) {
|
|
65
|
+
const graph = loadDiGraph(file);
|
|
66
|
+
const paths = (0, design_visualizer_1.writeDesignVisualization)(graph, workspaceRoot);
|
|
67
|
+
const designCount = graph.designs.length;
|
|
68
|
+
console.log(`✅ ${file.project}: ${designCount} design(s) → ${paths.htmlPath}`);
|
|
69
|
+
if (!(0, graph_visualizer_1.openVisualization)(paths.htmlPath)) {
|
|
70
|
+
console.log(`⚠️ Could not auto-open. Open manually: ${paths.htmlPath}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function main() {
|
|
74
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
75
|
+
const files = (0, design_finder_1.findDesignFiles)(workspaceRoot);
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
console.error('❌ No design.json files found. Run a build or: pnpm nx run-many --target=di-graph-generate');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
let selections = process.argv.slice(2);
|
|
81
|
+
if (selections.length === 0) {
|
|
82
|
+
printList(files);
|
|
83
|
+
selections = await promptSelections();
|
|
84
|
+
if (selections.length === 0) {
|
|
85
|
+
console.log('Cancelled.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const picked = (0, design_finder_1.resolveSelections)(selections, files);
|
|
90
|
+
for (const file of picked) {
|
|
91
|
+
renderOne(file, workspaceRoot);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
main().catch((err) => {
|
|
95
|
+
const error = (0, toError_1.toError)(err);
|
|
96
|
+
console.error('❌', error.message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=wp-design-visualize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wp-design-visualize.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/scripts/wp-design-visualize.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * wp-design-visualize — open per-project DI design graphs as Graphviz HTML.\n *\n * Usage:\n * wp-design-visualize interactive picker (numbered list)\n * wp-design-visualize <name|substring>… render matching project(s)\n * wp-design-visualize all render every project\n *\n * Reads each project's committed design.json (schemaVersion 2 — one design\n * per controller/root) and writes tmp/webpieces/design-<project>.html with\n * one graph PER CONTROLLER, the controller (level 0) at the top. Replaces the\n * old mermaid-based scripts/design-visualize.sh (which only showed the first\n * controller).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as readline from 'readline';\nimport { DesignFileRef, findDesignFiles, resolveSelections } from '../lib/di-graph/design-finder';\nimport { writeDesignVisualization } from '../lib/di-graph/design-visualizer';\nimport { openVisualization } from '../lib/graph-visualizer';\nimport { DiGraph } from '../lib/di-graph/model';\nimport { toError } from '../toError';\n\n/** Walk up from cwd to the workspace root (dir containing nx.json). */\nfunction findWorkspaceRoot(): string {\n let dir = process.cwd();\n for (let i = 0; i < 20; i++) {\n if (fs.existsSync(path.join(dir, 'nx.json'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return process.cwd();\n}\n\nfunction printList(files: DesignFileRef[]): void {\n console.log('\\nDI design graphs found:\\n');\n files.forEach((file: DesignFileRef, index: number) => {\n console.log(` ${String(index + 1).padStart(3)}. ${file.project.padEnd(24)} ${file.relPath}`);\n });\n console.log('');\n}\n\n/** Prompt for selections: numbers, names/substrings, or \"all\" (blank = cancel). */\nfunction promptSelections(): Promise<string[]> {\n return new Promise((resolve: (selections: string[]) => void) => {\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n rl.question('Which designs? (numbers / names / all, blank to cancel): ', (answer: string) => {\n rl.close();\n resolve(answer.split(/[\\s,]+/).filter((part: string) => part.length > 0));\n });\n });\n}\n\nfunction loadDiGraph(file: DesignFileRef): DiGraph {\n const parsed = JSON.parse(fs.readFileSync(file.absPath, 'utf-8'));\n if (!Array.isArray(parsed.designs)) {\n throw new Error(\n `${file.relPath} is an old-format design.json (schemaVersion ${parsed.schemaVersion ?? 1}). ` +\n `Regenerate it with: pnpm nx run ${file.project}:di-graph-generate`\n );\n }\n return parsed as DiGraph;\n}\n\nfunction renderOne(file: DesignFileRef, workspaceRoot: string): void {\n const graph = loadDiGraph(file);\n const paths = writeDesignVisualization(graph, workspaceRoot);\n const designCount = graph.designs.length;\n console.log(`✅ ${file.project}: ${designCount} design(s) → ${paths.htmlPath}`);\n if (!openVisualization(paths.htmlPath)) {\n console.log(`⚠️ Could not auto-open. Open manually: ${paths.htmlPath}`);\n }\n}\n\nasync function main(): Promise<void> {\n const workspaceRoot = findWorkspaceRoot();\n const files = findDesignFiles(workspaceRoot);\n\n if (files.length === 0) {\n console.error('❌ No design.json files found. Run a build or: pnpm nx run-many --target=di-graph-generate');\n process.exit(1);\n }\n\n let selections = process.argv.slice(2);\n if (selections.length === 0) {\n printList(files);\n selections = await promptSelections();\n if (selections.length === 0) {\n console.log('Cancelled.');\n return;\n }\n }\n\n const picked = resolveSelections(selections, files);\n for (const file of picked) {\n renderOne(file, workspaceRoot);\n }\n}\n\nmain().catch((err: Error) => {\n const error = toError(err);\n console.error('❌', error.message);\n process.exit(1);\n});\n"]}
|