@webpieces/nx-webpieces-rules 0.3.252 → 0.3.254
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 +37 -7
- package/src/executors/di-graph-generate/executor.js.map +1 -1
- package/src/lib/di-graph/analyzer-strategy.d.ts +21 -4
- package/src/lib/di-graph/analyzer-strategy.js +47 -12
- package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
- package/src/lib/di-graph/analyzer.d.ts +10 -5
- package/src/lib/di-graph/analyzer.js +29 -13
- package/src/lib/di-graph/analyzer.js.map +1 -1
- package/src/lib/di-graph/dot.js +4 -2
- package/src/lib/di-graph/dot.js.map +1 -1
- package/src/lib/di-graph/mermaid.js +10 -1
- package/src/lib/di-graph/mermaid.js.map +1 -1
- package/src/lib/di-graph/model.d.ts +1 -1
- package/src/lib/di-graph/model.js.map +1 -1
- package/src/lib/graph-loader.js +1 -0
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-metadata.d.ts +18 -0
- package/src/lib/graph-metadata.js +49 -1
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-sorter.d.ts +1 -0
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.js +40 -4
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/role-resolver.d.ts +39 -0
- package/src/lib/role-resolver.js +59 -0
- package/src/lib/role-resolver.js.map +1 -0
|
@@ -14,16 +14,18 @@
|
|
|
14
14
|
* a failed run never clobbers dependencies.json.
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.ALL_LIB_TYPE = exports.MetadataValidationError = exports.RESPONSIBILITIES_FILE_NAME = void 0;
|
|
17
|
+
exports.ALL_LIB_TYPE = exports.APP_ROLES = exports.MetadataValidationError = exports.RESPONSIBILITIES_FILE_NAME = void 0;
|
|
18
18
|
exports.collectProjectInfo = collectProjectInfo;
|
|
19
19
|
exports.enrichGraph = enrichGraph;
|
|
20
20
|
exports.validateLibraryTypesMatch = validateLibraryTypesMatch;
|
|
21
|
+
exports.validateRoleDependencies = validateRoleDependencies;
|
|
21
22
|
const tslib_1 = require("tslib");
|
|
22
23
|
const fs = tslib_1.__importStar(require("fs"));
|
|
23
24
|
const path = tslib_1.__importStar(require("path"));
|
|
24
25
|
const devkit_1 = require("@nx/devkit");
|
|
25
26
|
const project_info_1 = require("./project-info");
|
|
26
27
|
const framework_resolver_1 = require("./framework-resolver");
|
|
28
|
+
const role_resolver_1 = require("./role-resolver");
|
|
27
29
|
const responsibilities_1 = require("./responsibilities");
|
|
28
30
|
exports.RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';
|
|
29
31
|
/**
|
|
@@ -72,6 +74,13 @@ function enrichGraph(graph, infos, workspaceRoot) {
|
|
|
72
74
|
else if (resolution.framework !== null) {
|
|
73
75
|
entry.framework = resolution.framework;
|
|
74
76
|
}
|
|
77
|
+
const roleResolution = (0, role_resolver_1.resolveRole)(info);
|
|
78
|
+
if (roleResolution.problem !== null) {
|
|
79
|
+
problems.push(roleResolution.problem);
|
|
80
|
+
}
|
|
81
|
+
else if (roleResolution.role !== null) {
|
|
82
|
+
entry.role = roleResolution.role;
|
|
83
|
+
}
|
|
75
84
|
enrichResponsibilities(entry, info, workspaceRoot, problems);
|
|
76
85
|
// Only project.json projects get a generated design.json (see di-graph-targets.ts)
|
|
77
86
|
if (fs.existsSync(path.join(workspaceRoot, info.root, 'project.json'))) {
|
|
@@ -79,10 +88,17 @@ function enrichGraph(graph, infos, workspaceRoot) {
|
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
validateLibraryTypesMatch(graph, problems);
|
|
91
|
+
validateRoleDependencies(graph, problems);
|
|
82
92
|
if (problems.length > 0) {
|
|
83
93
|
throw new MetadataValidationError(problems);
|
|
84
94
|
}
|
|
85
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Roles that are terminal APPS — nothing may depend on them. A server or a
|
|
98
|
+
* client is a top-level runnable; being depended upon means it is really a
|
|
99
|
+
* library and should be retagged `role:lib`/`role:designed-lib`.
|
|
100
|
+
*/
|
|
101
|
+
exports.APP_ROLES = ['server', 'client'];
|
|
86
102
|
/**
|
|
87
103
|
* libType usable by any side — a dependency of this type is always allowed.
|
|
88
104
|
*/
|
|
@@ -120,6 +136,38 @@ function validateLibraryTypesMatch(graph, problems) {
|
|
|
120
136
|
}
|
|
121
137
|
}
|
|
122
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* `role-dependency` rule.
|
|
141
|
+
*
|
|
142
|
+
* A project's `role` is its function (server | designed-lib | lib | client).
|
|
143
|
+
* Apps are terminal — libraries and clients consume them, never the reverse:
|
|
144
|
+
* - a `client` is fully terminal: NOTHING may depend on it.
|
|
145
|
+
* - a `server` may only be depended upon by another `server` — the one
|
|
146
|
+
* legitimate case is a server-side orchestrator/e2e harness that boots
|
|
147
|
+
* other servers. A `lib`/`designed-lib`/`client` depending on a `server`
|
|
148
|
+
* inverts the dependency direction and is a violation.
|
|
149
|
+
*/
|
|
150
|
+
function validateRoleDependencies(graph, problems) {
|
|
151
|
+
for (const [projectName, entry] of Object.entries(graph)) {
|
|
152
|
+
const fromRole = entry.role;
|
|
153
|
+
for (const dep of entry.dependsOn) {
|
|
154
|
+
const toRole = graph[dep]?.role;
|
|
155
|
+
if (toRole === undefined)
|
|
156
|
+
continue; // role resolution already flagged this project
|
|
157
|
+
if (!exports.APP_ROLES.includes(toRole))
|
|
158
|
+
continue;
|
|
159
|
+
// A server may orchestrate/boot other servers (e.g. an e2e harness).
|
|
160
|
+
if (toRole === 'server' && fromRole === 'server')
|
|
161
|
+
continue;
|
|
162
|
+
const why = toRole === 'client'
|
|
163
|
+
? `a 'client' app is terminal and may never be depended upon`
|
|
164
|
+
: `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;
|
|
165
|
+
problems.push(`role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +
|
|
166
|
+
`'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +
|
|
167
|
+
`is actually a library, or remove the dependency.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
123
171
|
function enrichResponsibilities(entry, info, workspaceRoot, problems) {
|
|
124
172
|
const responsibilitiesFile = toRepoRelative(info.root, exports.RESPONSIBILITIES_FILE_NAME);
|
|
125
173
|
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 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"]}
|
|
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 { resolveRole } from './role-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 const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\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 validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server or a\n * client is a top-level runnable; being depended upon means it is really a\n * library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'client'];\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\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, 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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;
|
|
1
|
+
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAqCH,4DA2CC;AA8CD,wDAgBC;AAlHD;;;;;;;;GAQG;AACH,SAAgB,wBAAwB,CAAC,KAA+B;IACpE,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEvC,OAAO,SAAS,CAAC,IAAI,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;YAChC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;gBAAE,SAAS;YAErC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,uEAAuE;YACvE,MAAM,mBAAmB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,IAAI,mBAAmB,EAAE,CAAC;gBACtB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QAED,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,kDAAkD;YAClD,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAE/D,4BAA4B;YAC5B,MAAM,SAAS,GAAG,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YAE9C,MAAM,IAAI,KAAK,CACX,uCAAuC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;gBAC3D,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,SAAS,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1C,yDAAyD,CAChE,CAAC;QACN,CAAC;QAED,4DAA4D;QAC5D,YAAY,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAE1B,oBAAoB;QACpB,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,SAAS,SAAS,CAAC,KAA+B,EAAE,SAAmB;IACnE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,SAAS,GAAG,CAAC,IAAY;QACrB,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACtC,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAEnC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEhB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1B,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;gBACxB,IAAI,MAAM;oBAAE,OAAO,MAAM,CAAC;YAC9B,CAAC;QACL,CAAC;QAED,IAAI,CAAC,GAAG,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC5B,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,sBAAsB,CAAC,KAA+B;IAClE,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE;QACjC,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC1B,6CAA6C;YAC7C,MAAM,CAAC,OAAO,CAAC,GAAG;gBACd,KAAK,EAAE,UAAU;gBACjB,SAAS,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE;aAC3C,CAAC;QACN,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — angular | react | express | all\n * (\"all\" = library usable by any side); from its `framework:` nx tag\n * (source of truth) or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string;\n role?: string;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
|
|
@@ -29,6 +29,23 @@ const FRAMEWORK_COLORS = {
|
|
|
29
29
|
all: '#F5F5F5', // grey - library usable by any side
|
|
30
30
|
};
|
|
31
31
|
const DEFAULT_FRAMEWORK_COLOR = '#FFF3E0'; // orange - unknown/other libType
|
|
32
|
+
/**
|
|
33
|
+
* Role border styling — fill stays keyed on framework; the border shape shows a
|
|
34
|
+
* project's ROLE at a glance:
|
|
35
|
+
* server → double border (a runnable server app)
|
|
36
|
+
* client → dashed border (a client app, e.g. angular)
|
|
37
|
+
* designed-lib → bold border (a library with a generated @ApiImplementation design)
|
|
38
|
+
* lib / other → plain thin border
|
|
39
|
+
*/
|
|
40
|
+
function roleBorderAttrs(role) {
|
|
41
|
+
if (role === 'server')
|
|
42
|
+
return ', peripheries=2';
|
|
43
|
+
if (role === 'client')
|
|
44
|
+
return ', style="filled,dashed"';
|
|
45
|
+
if (role === 'designed-lib')
|
|
46
|
+
return ', penwidth=2';
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
32
49
|
/**
|
|
33
50
|
* Remove scope from name for display
|
|
34
51
|
* '@scope/name' → 'name'
|
|
@@ -52,12 +69,15 @@ function generateDot(graph, title = 'Monorepo Dependency Architecture') {
|
|
|
52
69
|
levels[info.level] = [];
|
|
53
70
|
levels[info.level].push(project);
|
|
54
71
|
}
|
|
55
|
-
//
|
|
72
|
+
// Nodes: fill colored by framework (libType), border shaped by role; the
|
|
73
|
+
// label shows the framework-role combo (e.g. express-server, all-lib).
|
|
56
74
|
for (const [project, info] of Object.entries(graph)) {
|
|
57
75
|
const shortName = getShortName(project);
|
|
58
76
|
const framework = info.framework ?? 'all';
|
|
77
|
+
const role = info.role ?? 'lib';
|
|
59
78
|
const color = FRAMEWORK_COLORS[framework] ?? DEFAULT_FRAMEWORK_COLOR;
|
|
60
|
-
|
|
79
|
+
const border = roleBorderAttrs(role);
|
|
80
|
+
dot += ` "${shortName}" [fillcolor="${color}"${border}, label="${shortName}\\n(L${info.level} · ${framework}-${role})"];\n`;
|
|
61
81
|
}
|
|
62
82
|
dot += '\n';
|
|
63
83
|
// Create same-rank subgraphs for each level
|
|
@@ -152,7 +172,7 @@ function generateHTMLStyles() {
|
|
|
152
172
|
}
|
|
153
173
|
function generateHTMLLegend() {
|
|
154
174
|
return `<div class="legend">
|
|
155
|
-
<h2>Legend —
|
|
175
|
+
<h2>Legend — fill = framework (libType), border = role</h2>
|
|
156
176
|
<div class="legend-item">
|
|
157
177
|
<span class="legend-box" style="background: #FCE4EC;"></span>
|
|
158
178
|
<strong>angular:</strong> Angular front-end
|
|
@@ -169,8 +189,24 @@ function generateHTMLLegend() {
|
|
|
169
189
|
<span class="legend-box" style="background: #F5F5F5;"></span>
|
|
170
190
|
<strong>all:</strong> Library usable by any side
|
|
171
191
|
</div>
|
|
192
|
+
<div class="legend-item" style="margin-top: 12px;">
|
|
193
|
+
<span class="legend-box" style="border: 3px double #333;"></span>
|
|
194
|
+
<strong>server:</strong> runnable server app (double border)
|
|
195
|
+
</div>
|
|
196
|
+
<div class="legend-item">
|
|
197
|
+
<span class="legend-box" style="border: 1px dashed #333;"></span>
|
|
198
|
+
<strong>client:</strong> client app, e.g. angular (dashed border)
|
|
199
|
+
</div>
|
|
200
|
+
<div class="legend-item">
|
|
201
|
+
<span class="legend-box" style="border: 2px solid #333;"></span>
|
|
202
|
+
<strong>designed-lib:</strong> library with a generated @ApiImplementation design (bold border)
|
|
203
|
+
</div>
|
|
204
|
+
<div class="legend-item">
|
|
205
|
+
<span class="legend-box" style="border: 1px solid #ccc;"></span>
|
|
206
|
+
<strong>lib:</strong> plain library, no generated design (thin border)
|
|
207
|
+
</div>
|
|
172
208
|
<div class="legend-item" style="margin-top: 15px;">
|
|
173
|
-
<em>Each node label shows its dependency level (L#) and framework. Rows are
|
|
209
|
+
<em>Each node label shows its dependency level (L#) and framework-role (e.g. express-server, all-lib). Rows are laid out by level (top = no dependencies). Transitive dependencies are allowed but not shown.</em>
|
|
174
210
|
</div>
|
|
175
211
|
</div>`;
|
|
176
212
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * Output files go to tmp/webpieces/ for easy viewing without committing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { toError } from '../toError';\n\n/**\n * Framework (libType) colors for visualization — nodes are colored by which\n * client side they target so it is obvious at a glance which projects are\n * Angular, which are Express, and which libraries are shared (\"all\").\n */\nconst FRAMEWORK_COLORS: Record<string, string> = {\n angular: '#FCE4EC', // pink - Angular front-end\n react: '#E3F2FD', // blue - React front-end\n express: '#E8F5E9', // green - Express / server side\n all: '#F5F5F5', // grey - library usable by any side\n};\n\nconst DEFAULT_FRAMEWORK_COLOR = '#FFF3E0'; // orange - unknown/other libType\n\n/**\n * Remove scope from name for display\n * '@scope/name' → 'name'\n * 'name' → 'name'\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/**\n * Generate Graphviz DOT format from the graph\n */\nexport function generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n // Group projects by level\n const levels: Record<number, string[]> = {};\n for (const [project, info] of Object.entries(graph)) {\n if (!levels[info.level]) levels[info.level] = [];\n levels[info.level].push(project);\n }\n\n // Create nodes colored by framework (libType); level is kept in the label\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n const framework = info.framework ?? 'all';\n const color = FRAMEWORK_COLORS[framework] ?? DEFAULT_FRAMEWORK_COLOR;\n dot += ` \"${shortName}\" [fillcolor=\"${color}\", label=\"${shortName}\\\\n(L${info.level} · ${framework})\"];\\n`;\n }\n\n dot += '\\n';\n\n // Create same-rank subgraphs for each level\n for (const [level, projects] of Object.entries(levels)) {\n dot += ` { rank=same; `;\n projects.forEach((p) => {\n const shortName = getShortName(p);\n dot += `\"${shortName}\"; `;\n });\n dot += '}\\n';\n }\n\n dot += '\\n';\n\n // Create edges (dependencies)\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n for (const dep of info.dependsOn || []) {\n const depShortName = getShortName(dep);\n dot += ` \"${shortName}\" -> \"${depShortName}\";\\n`;\n }\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n}\n\n/**\n * Generate interactive HTML with embedded SVG using viz.js\n */\nexport function generateHTML(dot: string, title: string = 'Monorepo Dependency Architecture'): string {\n const styles = generateHTMLStyles();\n const legend = generateHTMLLegend();\n const script = generateHTMLScript(dot);\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>${styles}</style>\n</head>\n<body>\n <h1>${title}</h1>\n ${legend}\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nfunction generateHTMLStyles(): string {\n return `\n body {\n margin: 0;\n padding: 20px;\n font-family: Arial, sans-serif;\n background: #f5f5f5;\n }\n h1 {\n text-align: center;\n color: #333;\n }\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 600px;\n padding: 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 {\n margin-top: 0;\n }\n .legend-item {\n margin: 8px 0;\n }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n `;\n}\n\nfunction generateHTMLLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend — colored by framework (libType)</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FCE4EC;\"></span>\n <strong>angular:</strong> Angular front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>react:</strong> React front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>express:</strong> Express / server side\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>all:</strong> Library usable by any side\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Each node label shows its dependency level (L#) and framework. Rows are still laid out by level (top = no dependencies). Transitive dependencies are allowed but not shown.</em>\n </div>\n </div>`;\n}\n\nfunction generateHTMLScript(dot: string): string {\n return `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n\n viz.renderSVGElement(dot)\n .then(element => {\n document.getElementById('graph').appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';\n });\n `;\n}\n\ninterface VisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/**\n * Write visualization files to tmp/webpieces/\n */\nexport function writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'Monorepo Dependency Architecture'\n): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n\n // Ensure directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n // Generate DOT\n const dot = generateDot(graph, title);\n const dotPath = path.join(outputDir, 'architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n // Generate HTML\n const html = generateHTML(dot, title);\n const htmlPath = path.join(outputDir, 'architecture.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return { dotPath, htmlPath };\n}\n\n/**\n * Open the HTML visualization in the default browser\n */\nexport function openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return false;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * Output files go to tmp/webpieces/ for easy viewing without committing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { toError } from '../toError';\n\n/**\n * Framework (libType) colors for visualization — nodes are colored by which\n * client side they target so it is obvious at a glance which projects are\n * Angular, which are Express, and which libraries are shared (\"all\").\n */\nconst FRAMEWORK_COLORS: Record<string, string> = {\n angular: '#FCE4EC', // pink - Angular front-end\n react: '#E3F2FD', // blue - React front-end\n express: '#E8F5E9', // green - Express / server side\n all: '#F5F5F5', // grey - library usable by any side\n};\n\nconst DEFAULT_FRAMEWORK_COLOR = '#FFF3E0'; // orange - unknown/other libType\n\n/**\n * Role border styling — fill stays keyed on framework; the border shape shows a\n * project's ROLE at a glance:\n * server → double border (a runnable server app)\n * client → dashed border (a client app, e.g. angular)\n * designed-lib → bold border (a library with a generated @ApiImplementation design)\n * lib / other → plain thin border\n */\nfunction roleBorderAttrs(role: string): string {\n if (role === 'server') return ', peripheries=2';\n if (role === 'client') return ', style=\"filled,dashed\"';\n if (role === 'designed-lib') return ', penwidth=2';\n return '';\n}\n\n/**\n * Remove scope from name for display\n * '@scope/name' → 'name'\n * 'name' → 'name'\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/**\n * Generate Graphviz DOT format from the graph\n */\nexport function generateDot(graph: EnhancedGraph, title: string = 'Monorepo Dependency Architecture'): string {\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n // Group projects by level\n const levels: Record<number, string[]> = {};\n for (const [project, info] of Object.entries(graph)) {\n if (!levels[info.level]) levels[info.level] = [];\n levels[info.level].push(project);\n }\n\n // Nodes: fill colored by framework (libType), border shaped by role; the\n // label shows the framework-role combo (e.g. express-server, all-lib).\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n const framework = info.framework ?? 'all';\n const role = info.role ?? 'lib';\n const color = FRAMEWORK_COLORS[framework] ?? DEFAULT_FRAMEWORK_COLOR;\n const border = roleBorderAttrs(role);\n dot += ` \"${shortName}\" [fillcolor=\"${color}\"${border}, label=\"${shortName}\\\\n(L${info.level} · ${framework}-${role})\"];\\n`;\n }\n\n dot += '\\n';\n\n // Create same-rank subgraphs for each level\n for (const [level, projects] of Object.entries(levels)) {\n dot += ` { rank=same; `;\n projects.forEach((p) => {\n const shortName = getShortName(p);\n dot += `\"${shortName}\"; `;\n });\n dot += '}\\n';\n }\n\n dot += '\\n';\n\n // Create edges (dependencies)\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n for (const dep of info.dependsOn || []) {\n const depShortName = getShortName(dep);\n dot += ` \"${shortName}\" -> \"${depShortName}\";\\n`;\n }\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n}\n\n/**\n * Generate interactive HTML with embedded SVG using viz.js\n */\nexport function generateHTML(dot: string, title: string = 'Monorepo Dependency Architecture'): string {\n const styles = generateHTMLStyles();\n const legend = generateHTMLLegend();\n const script = generateHTMLScript(dot);\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>${styles}</style>\n</head>\n<body>\n <h1>${title}</h1>\n ${legend}\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nfunction generateHTMLStyles(): string {\n return `\n body {\n margin: 0;\n padding: 20px;\n font-family: Arial, sans-serif;\n background: #f5f5f5;\n }\n h1 {\n text-align: center;\n color: #333;\n }\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 600px;\n padding: 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 {\n margin-top: 0;\n }\n .legend-item {\n margin: 8px 0;\n }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n `;\n}\n\nfunction generateHTMLLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend — fill = framework (libType), border = role</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FCE4EC;\"></span>\n <strong>angular:</strong> Angular front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>react:</strong> React front-end\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>express:</strong> Express / server side\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>all:</strong> Library usable by any side\n </div>\n <div class=\"legend-item\" style=\"margin-top: 12px;\">\n <span class=\"legend-box\" style=\"border: 3px double #333;\"></span>\n <strong>server:</strong> runnable server app (double border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 1px dashed #333;\"></span>\n <strong>client:</strong> client app, e.g. angular (dashed border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 2px solid #333;\"></span>\n <strong>designed-lib:</strong> library with a generated @ApiImplementation design (bold border)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"border: 1px solid #ccc;\"></span>\n <strong>lib:</strong> plain library, no generated design (thin border)\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Each node label shows its dependency level (L#) and framework-role (e.g. express-server, all-lib). Rows are laid out by level (top = no dependencies). Transitive dependencies are allowed but not shown.</em>\n </div>\n </div>`;\n}\n\nfunction generateHTMLScript(dot: string): string {\n return `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n\n viz.renderSVGElement(dot)\n .then(element => {\n document.getElementById('graph').appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';\n });\n `;\n}\n\ninterface VisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/**\n * Write visualization files to tmp/webpieces/\n */\nexport function writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'Monorepo Dependency Architecture'\n): VisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n\n // Ensure directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n // Generate DOT\n const dot = generateDot(graph, title);\n const dotPath = path.join(outputDir, 'architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n // Generate HTML\n const html = generateHTML(dot, title);\n const htmlPath = path.join(outputDir, 'architecture.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return { dotPath, htmlPath };\n}\n\n/**\n * Open the HTML visualization in the default browser\n */\nexport function openVisualization(htmlPath: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return false;\n }\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role Resolver
|
|
3
|
+
*
|
|
4
|
+
* Determines the `role` field written per project into
|
|
5
|
+
* architecture/dependencies.json — a project's ROLE, orthogonal to its
|
|
6
|
+
* `framework` (libType). Known values:
|
|
7
|
+
*
|
|
8
|
+
* - `server` — a runnable server app; DI design roots on `@Controller`.
|
|
9
|
+
* - `designed-lib` — a library whose DI design we generate; roots on
|
|
10
|
+
* `@ApiImplementation` (required to have ≥1).
|
|
11
|
+
* - `lib` — a plain library; no DI design is generated.
|
|
12
|
+
* - `client` — a client app (e.g. angular-site); angular apps keep their
|
|
13
|
+
* component/route design, others get none.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. Explicit nx tag `role:<value>` on the project (project.json tags) — the
|
|
17
|
+
* source of truth; every project should carry one (enforced by the
|
|
18
|
+
* `role-tag` code rule).
|
|
19
|
+
* 2. Fallback: 'lib' (a plain library with no generated design) — the safe
|
|
20
|
+
* default so an untagged project never claims to be a server/client.
|
|
21
|
+
*/
|
|
22
|
+
import { ProjectInfo } from './project-info';
|
|
23
|
+
export declare const ROLE_TAG_PREFIX = "role:";
|
|
24
|
+
/** The roles the `role-tag` rule and the DI-graph analyzer understand. */
|
|
25
|
+
export declare const KNOWN_ROLES: ReadonlyArray<string>;
|
|
26
|
+
/** Default role for a project with no explicit `role:` tag. */
|
|
27
|
+
export declare const DEFAULT_ROLE = "lib";
|
|
28
|
+
export declare class RoleResolution {
|
|
29
|
+
/** Resolved role name, or null when resolution failed */
|
|
30
|
+
readonly role: string | null;
|
|
31
|
+
/** Problem description when resolution failed, otherwise null */
|
|
32
|
+
readonly problem: string | null;
|
|
33
|
+
constructor(
|
|
34
|
+
/** Resolved role name, or null when resolution failed */
|
|
35
|
+
role: string | null,
|
|
36
|
+
/** Problem description when resolution failed, otherwise null */
|
|
37
|
+
problem: string | null);
|
|
38
|
+
}
|
|
39
|
+
export declare function resolveRole(info: ProjectInfo): RoleResolution;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Role Resolver
|
|
4
|
+
*
|
|
5
|
+
* Determines the `role` field written per project into
|
|
6
|
+
* architecture/dependencies.json — a project's ROLE, orthogonal to its
|
|
7
|
+
* `framework` (libType). Known values:
|
|
8
|
+
*
|
|
9
|
+
* - `server` — a runnable server app; DI design roots on `@Controller`.
|
|
10
|
+
* - `designed-lib` — a library whose DI design we generate; roots on
|
|
11
|
+
* `@ApiImplementation` (required to have ≥1).
|
|
12
|
+
* - `lib` — a plain library; no DI design is generated.
|
|
13
|
+
* - `client` — a client app (e.g. angular-site); angular apps keep their
|
|
14
|
+
* component/route design, others get none.
|
|
15
|
+
*
|
|
16
|
+
* Resolution order:
|
|
17
|
+
* 1. Explicit nx tag `role:<value>` on the project (project.json tags) — the
|
|
18
|
+
* source of truth; every project should carry one (enforced by the
|
|
19
|
+
* `role-tag` code rule).
|
|
20
|
+
* 2. Fallback: 'lib' (a plain library with no generated design) — the safe
|
|
21
|
+
* default so an untagged project never claims to be a server/client.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.RoleResolution = exports.DEFAULT_ROLE = exports.KNOWN_ROLES = exports.ROLE_TAG_PREFIX = void 0;
|
|
25
|
+
exports.resolveRole = resolveRole;
|
|
26
|
+
exports.ROLE_TAG_PREFIX = 'role:';
|
|
27
|
+
/** The roles the `role-tag` rule and the DI-graph analyzer understand. */
|
|
28
|
+
exports.KNOWN_ROLES = ['server', 'designed-lib', 'lib', 'client'];
|
|
29
|
+
/** Default role for a project with no explicit `role:` tag. */
|
|
30
|
+
exports.DEFAULT_ROLE = 'lib';
|
|
31
|
+
class RoleResolution {
|
|
32
|
+
role;
|
|
33
|
+
problem;
|
|
34
|
+
constructor(
|
|
35
|
+
/** Resolved role name, or null when resolution failed */
|
|
36
|
+
role,
|
|
37
|
+
/** Problem description when resolution failed, otherwise null */
|
|
38
|
+
problem) {
|
|
39
|
+
this.role = role;
|
|
40
|
+
this.problem = problem;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.RoleResolution = RoleResolution;
|
|
44
|
+
function resolveRole(info) {
|
|
45
|
+
const tagValues = info.tags
|
|
46
|
+
.filter((tag) => tag.startsWith(exports.ROLE_TAG_PREFIX))
|
|
47
|
+
.map((tag) => tag.slice(exports.ROLE_TAG_PREFIX.length).trim());
|
|
48
|
+
if (tagValues.length > 1) {
|
|
49
|
+
return new RoleResolution(null, `${info.name}: has ${tagValues.length} 'role:' tags (${tagValues.join(', ')}) — a project must have at most one`);
|
|
50
|
+
}
|
|
51
|
+
if (tagValues.length === 1) {
|
|
52
|
+
if (tagValues[0].length === 0) {
|
|
53
|
+
return new RoleResolution(null, `${info.name}: 'role:' tag has an empty value`);
|
|
54
|
+
}
|
|
55
|
+
return new RoleResolution(tagValues[0], null);
|
|
56
|
+
}
|
|
57
|
+
return new RoleResolution(exports.DEFAULT_ROLE, null);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=role-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/role-resolver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;AAqBH,kCAmBC;AApCY,QAAA,eAAe,GAAG,OAAO,CAAC;AAEvC,0EAA0E;AAC7D,QAAA,WAAW,GAA0B,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAE9F,+DAA+D;AAClD,QAAA,YAAY,GAAG,KAAK,CAAC;AAElC,MAAa,cAAc;IAGH;IAEA;IAJpB;IACI,yDAAyD;IACzC,IAAmB;IACnC,iEAAiE;IACjD,OAAsB;QAFtB,SAAI,GAAJ,IAAI,CAAe;QAEnB,YAAO,GAAP,OAAO,CAAe;IACvC,CAAC;CACP;AAPD,wCAOC;AAED,SAAgB,WAAW,CAAC,IAAiB;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI;SACtB,MAAM,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,uBAAe,CAAC,CAAC;SACxD,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,uBAAe,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAEpE,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,IAAI,cAAc,CACrB,IAAI,EACJ,GAAG,IAAI,CAAC,IAAI,SAAS,SAAS,CAAC,MAAM,kBAAkB,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,qCAAqC,CACnH,CAAC;IACN,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,OAAO,IAAI,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,kCAAkC,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,IAAI,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,IAAI,cAAc,CAAC,oBAAY,EAAE,IAAI,CAAC,CAAC;AAClD,CAAC","sourcesContent":["/**\n * Role Resolver\n *\n * Determines the `role` field written per project into\n * architecture/dependencies.json — a project's ROLE, orthogonal to its\n * `framework` (libType). Known values:\n *\n * - `server` — a runnable server app; DI design roots on `@Controller`.\n * - `designed-lib` — a library whose DI design we generate; roots on\n * `@ApiImplementation` (required to have ≥1).\n * - `lib` — a plain library; no DI design is generated.\n * - `client` — a client app (e.g. angular-site); angular apps keep their\n * component/route design, others get none.\n *\n * Resolution order:\n * 1. Explicit nx tag `role:<value>` on the project (project.json tags) — the\n * source of truth; every project should carry one (enforced by the\n * `role-tag` code rule).\n * 2. Fallback: 'lib' (a plain library with no generated design) — the safe\n * default so an untagged project never claims to be a server/client.\n */\n\nimport { ProjectInfo } from './project-info';\n\nexport const ROLE_TAG_PREFIX = 'role:';\n\n/** The roles the `role-tag` rule and the DI-graph analyzer understand. */\nexport const KNOWN_ROLES: ReadonlyArray<string> = ['server', 'designed-lib', 'lib', 'client'];\n\n/** Default role for a project with no explicit `role:` tag. */\nexport const DEFAULT_ROLE = 'lib';\n\nexport class RoleResolution {\n constructor(\n /** Resolved role name, or null when resolution failed */\n public readonly role: string | null,\n /** Problem description when resolution failed, otherwise null */\n public readonly problem: string | null\n ) {}\n}\n\nexport function resolveRole(info: ProjectInfo): RoleResolution {\n const tagValues = info.tags\n .filter((tag: string) => tag.startsWith(ROLE_TAG_PREFIX))\n .map((tag: string) => tag.slice(ROLE_TAG_PREFIX.length).trim());\n\n if (tagValues.length > 1) {\n return new RoleResolution(\n null,\n `${info.name}: has ${tagValues.length} 'role:' 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 RoleResolution(null, `${info.name}: 'role:' tag has an empty value`);\n }\n return new RoleResolution(tagValues[0], null);\n }\n\n return new RoleResolution(DEFAULT_ROLE, null);\n}\n"]}
|