@webpieces/dev-config 0.2.29 → 0.2.31
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/README.md +2 -2
- package/architecture/lib/graph-generator.d.ts +1 -1
- package/architecture/lib/graph-generator.js +4 -8
- package/architecture/lib/graph-generator.js.map +1 -1
- package/architecture/lib/graph-generator.ts +4 -9
- package/architecture/lib/graph-visualizer.js +12 -4
- package/architecture/lib/graph-visualizer.js.map +1 -1
- package/architecture/lib/graph-visualizer.ts +13 -4
- package/architecture/lib/package-validator.d.ts +2 -2
- package/architecture/lib/package-validator.js +35 -14
- package/architecture/lib/package-validator.js.map +1 -1
- package/architecture/lib/package-validator.ts +41 -15
- package/eslint-plugin/rules/enforce-architecture.js +70 -19
- package/eslint-plugin/rules/enforce-architecture.js.map +1 -1
- package/eslint-plugin/rules/enforce-architecture.ts +76 -20
- package/executors/help/executor.js +5 -5
- package/executors/help/executor.js.map +1 -1
- package/executors/help/executor.ts +5 -5
- package/package.json +1 -1
- package/plugin/README.md +6 -6
- package/plugin.js +102 -54
- package/src/generators/init/generator.js +5 -5
- package/src/generators/init/generator.js.map +1 -1
package/README.md
CHANGED
|
@@ -93,10 +93,10 @@ Automatically adds architecture validation and circular dependency checking to N
|
|
|
93
93
|
nx add @webpieces/dev-config
|
|
94
94
|
|
|
95
95
|
# Generate dependency graph
|
|
96
|
-
nx run
|
|
96
|
+
nx run architecture:generate
|
|
97
97
|
|
|
98
98
|
# Validate architecture
|
|
99
|
-
nx run
|
|
99
|
+
nx run architecture:validate-no-cycles
|
|
100
100
|
|
|
101
101
|
# Check project for circular dependencies
|
|
102
102
|
nx run my-project:check-circular-deps
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function generateRawGraph(): Promise<Record<string, string[]>>;
|
|
12
12
|
/**
|
|
13
|
-
* Transform project names
|
|
13
|
+
* Transform project names (sorting dependencies only - no scope transformation)
|
|
14
14
|
*/
|
|
15
15
|
export declare function transformGraph(rawGraph: Record<string, string[]>): Record<string, string[]>;
|
|
16
16
|
/**
|
|
@@ -62,18 +62,14 @@ async function generateRawGraph() {
|
|
|
62
62
|
return rawDeps;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* Transform project names
|
|
65
|
+
* Transform project names (sorting dependencies only - no scope transformation)
|
|
66
66
|
*/
|
|
67
67
|
function transformGraph(rawGraph) {
|
|
68
68
|
const result = {};
|
|
69
69
|
for (const [projectName, deps] of Object.entries(rawGraph)) {
|
|
70
|
-
//
|
|
71
|
-
const transformedName = projectName
|
|
72
|
-
|
|
73
|
-
: `@webpieces/${projectName}`;
|
|
74
|
-
const transformedDeps = deps
|
|
75
|
-
.map((d) => (d.startsWith('@webpieces/') ? d : `@webpieces/${d}`))
|
|
76
|
-
.sort();
|
|
70
|
+
// Use project names as-is - don't force @webpieces scope on client projects
|
|
71
|
+
const transformedName = projectName;
|
|
72
|
+
const transformedDeps = deps.sort();
|
|
77
73
|
result[transformedName] = transformedDeps;
|
|
78
74
|
}
|
|
79
75
|
return result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-generator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-generator.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAiDH,4CAiBC;AAKD,
|
|
1
|
+
{"version":3,"file":"graph-generator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-generator.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAiDH,4CAiBC;AAKD,wCAYC;AAKD,sCAGC;AAzFD,uCAIoB;AAEpB;;GAEG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAS,EAAE,CAAC,CAAC;AAE9C;;GAEG;AACH,SAAS,wBAAwB,CAAC,aAAmC;IACjE,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,+BAA+B;IAC/B,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;IACrD,IAAI,WAAW,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;QACvC,KAAK,MAAM,GAAG,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;gBAC1B,0DAA0D;gBAC1D,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;gBAC3C,IAAI,KAAK,EAAE,CAAC;oBACR,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBACxB,CAAC;YACL,CAAC;QACL,CAAC;IACL,CAAC;IAED,yCAAyC;IACzC,IAAI,aAAa,CAAC,oBAAoB,IAAI,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAC1F,KAAK,MAAM,GAAG,IAAI,aAAa,CAAC,oBAAoB,EAAE,CAAC;YACnD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBACjD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnB,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,gBAAgB;IAClC,MAAM,YAAY,GAAG,MAAM,IAAA,gCAAuB,GAAE,CAAC;IACrD,MAAM,cAAc,GAAG,IAAA,kDAAyC,EAAC,YAAY,CAAC,CAAC;IAC/E,MAAM,OAAO,GAA6B,EAAE,CAAC;IAE7C,KAAK,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjF,0CAA0C;QAC1C,IAAI,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YACrC,SAAS;QACb,CAAC;QAED,4DAA4D;QAC5D,MAAM,IAAI,GAAG,wBAAwB,CAAC,aAAa,CAAC,CAAC;QACrD,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;IAChC,CAAC;IAED,OAAO,OAAO,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,SAAgB,cAAc,CAAC,QAAkC;IAC7D,MAAM,MAAM,GAA6B,EAAE,CAAC;IAE5C,KAAK,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzD,4EAA4E;QAC5E,MAAM,eAAe,GAAG,WAAW,CAAC;QACpC,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAEpC,MAAM,CAAC,eAAe,CAAC,GAAG,eAAe,CAAC;IAC9C,CAAC;IAED,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,aAAa;IAC/B,MAAM,QAAQ,GAAG,MAAM,gBAAgB,EAAE,CAAC;IAC1C,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;AACpC,CAAC","sourcesContent":["/**\n * Graph Generator\n *\n * Generates dependency graph from project.json files in the workspace.\n * Reads build.dependsOn and implicitDependencies to determine project relationships.\n */\n\nimport {\n createProjectGraphAsync,\n readProjectsConfigurationFromProjectGraph,\n ProjectConfiguration,\n} from '@nx/devkit';\n\n/**\n * Projects to exclude from graph validation (tools, configs, etc.)\n */\nconst EXCLUDED_PROJECTS = new Set<string>([]);\n\n/**\n * Extract project dependencies from project.json's build.dependsOn and implicitDependencies\n */\nfunction extractBuildDependencies(projectConfig: ProjectConfiguration): string[] {\n const deps: string[] = [];\n\n // 1. Read from build.dependsOn\n const buildTarget = projectConfig.targets?.['build'];\n if (buildTarget && buildTarget.dependsOn) {\n for (const dep of buildTarget.dependsOn) {\n if (typeof dep === 'string') {\n // Format: \"project-name:build\" or just \"build\" (for self)\n const match = dep.match(/^([^:]+):build$/);\n if (match) {\n deps.push(match[1]);\n }\n }\n }\n }\n\n // 2. Also read from implicitDependencies\n if (projectConfig.implicitDependencies && Array.isArray(projectConfig.implicitDependencies)) {\n for (const dep of projectConfig.implicitDependencies) {\n if (typeof dep === 'string' && !deps.includes(dep)) {\n deps.push(dep);\n }\n }\n }\n\n return deps.sort();\n}\n\n/**\n * Generate raw dependency graph from project.json files\n * Returns: { projectName: [dependencyNames] }\n */\nexport async function generateRawGraph(): Promise<Record<string, string[]>> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n const rawDeps: Record<string, string[]> = {};\n\n for (const [projectName, projectConfig] of Object.entries(projectsConfig.projects)) {\n // Skip excluded projects (tools, plugins)\n if (EXCLUDED_PROJECTS.has(projectName)) {\n continue;\n }\n\n // Extract dependencies from build.dependsOn in project.json\n const deps = extractBuildDependencies(projectConfig);\n rawDeps[projectName] = deps;\n }\n\n return rawDeps;\n}\n\n/**\n * Transform project names (sorting dependencies only - no scope transformation)\n */\nexport function transformGraph(rawGraph: Record<string, string[]>): Record<string, string[]> {\n const result: Record<string, string[]> = {};\n\n for (const [projectName, deps] of Object.entries(rawGraph)) {\n // Use project names as-is - don't force @webpieces scope on client projects\n const transformedName = projectName;\n const transformedDeps = deps.sort();\n\n result[transformedName] = transformedDeps;\n }\n\n return result;\n}\n\n/**\n * Generate complete dependency graph with transformations\n */\nexport async function generateGraph(): Promise<Record<string, string[]>> {\n const rawGraph = await generateRawGraph();\n return transformGraph(rawGraph);\n}\n"]}
|
|
@@ -72,20 +72,15 @@ export async function generateRawGraph(): Promise<Record<string, string[]>> {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Transform project names
|
|
75
|
+
* Transform project names (sorting dependencies only - no scope transformation)
|
|
76
76
|
*/
|
|
77
77
|
export function transformGraph(rawGraph: Record<string, string[]>): Record<string, string[]> {
|
|
78
78
|
const result: Record<string, string[]> = {};
|
|
79
79
|
|
|
80
80
|
for (const [projectName, deps] of Object.entries(rawGraph)) {
|
|
81
|
-
//
|
|
82
|
-
const transformedName = projectName
|
|
83
|
-
|
|
84
|
-
: `@webpieces/${projectName}`;
|
|
85
|
-
|
|
86
|
-
const transformedDeps = deps
|
|
87
|
-
.map((d) => (d.startsWith('@webpieces/') ? d : `@webpieces/${d}`))
|
|
88
|
-
.sort();
|
|
81
|
+
// Use project names as-is - don't force @webpieces scope on client projects
|
|
82
|
+
const transformedName = projectName;
|
|
83
|
+
const transformedDeps = deps.sort();
|
|
89
84
|
|
|
90
85
|
result[transformedName] = transformedDeps;
|
|
91
86
|
}
|
|
@@ -26,6 +26,14 @@ const LEVEL_COLORS = {
|
|
|
26
26
|
2: '#FFF3E0', // Light orange - applications
|
|
27
27
|
3: '#FCE4EC', // Light pink - higher level
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Remove scope from name for display
|
|
31
|
+
* '@scope/name' → 'name'
|
|
32
|
+
* 'name' → 'name'
|
|
33
|
+
*/
|
|
34
|
+
function getShortName(name) {
|
|
35
|
+
return name.includes('/') ? name.split('/').pop() : name;
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Generate Graphviz DOT format from the graph
|
|
31
39
|
*/
|
|
@@ -43,7 +51,7 @@ function generateDot(graph, title = 'WebPieces Architecture') {
|
|
|
43
51
|
}
|
|
44
52
|
// Create nodes with level-based colors
|
|
45
53
|
for (const [project, info] of Object.entries(graph)) {
|
|
46
|
-
const shortName = project
|
|
54
|
+
const shortName = getShortName(project);
|
|
47
55
|
const color = LEVEL_COLORS[info.level] || '#F5F5F5';
|
|
48
56
|
dot += ` "${shortName}" [fillcolor="${color}", label="${shortName}\\n(L${info.level})"];\n`;
|
|
49
57
|
}
|
|
@@ -52,7 +60,7 @@ function generateDot(graph, title = 'WebPieces Architecture') {
|
|
|
52
60
|
for (const [level, projects] of Object.entries(levels)) {
|
|
53
61
|
dot += ` { rank=same; `;
|
|
54
62
|
projects.forEach((p) => {
|
|
55
|
-
const shortName = p
|
|
63
|
+
const shortName = getShortName(p);
|
|
56
64
|
dot += `"${shortName}"; `;
|
|
57
65
|
});
|
|
58
66
|
dot += '}\n';
|
|
@@ -60,9 +68,9 @@ function generateDot(graph, title = 'WebPieces Architecture') {
|
|
|
60
68
|
dot += '\n';
|
|
61
69
|
// Create edges (dependencies)
|
|
62
70
|
for (const [project, info] of Object.entries(graph)) {
|
|
63
|
-
const shortName = project
|
|
71
|
+
const shortName = getShortName(project);
|
|
64
72
|
for (const dep of info.dependsOn || []) {
|
|
65
|
-
const depShortName = dep
|
|
73
|
+
const depShortName = getShortName(dep);
|
|
66
74
|
dot += ` "${shortName}" -> "${depShortName}";\n`;
|
|
67
75
|
}
|
|
68
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/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';\n\n/**\n * Level colors for visualization\n */\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9', // Light green - foundation\n 1: '#E3F2FD', // Light blue - middleware\n 2: '#FFF3E0', // Light orange - applications\n 3: '#FCE4EC', // Light pink - higher level\n};\n\n/**\n * Generate Graphviz DOT format from the graph\n */\nexport function generateDot(graph: EnhancedGraph, title: string = 'WebPieces 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 with level-based colors\n for (const [project, info] of Object.entries(graph)) {\n const shortName = project.replace('@webpieces/', '');\n const color = LEVEL_COLORS[info.level] || '#F5F5F5';\n dot += ` \"${shortName}\" [fillcolor=\"${color}\", label=\"${shortName}\\\\n(L${info.level})\"];\\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 = p.replace('@webpieces/', '');\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 = project.replace('@webpieces/', '');\n for (const dep of info.dependsOn || []) {\n const depShortName = dep.replace('@webpieces/', '');\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 = 'WebPieces Architecture'): string {\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>\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 </style>\n</head>\n<body>\n <h1>${title}</h1>\n\n <div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>Level 0:</strong> Foundation libraries (no dependencies)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Level 1:</strong> Middleware libraries (depend on Level 0)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Level 2:</strong> Applications (depend on Level 1)\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Note: Transitive dependencies are allowed but not shown in the graph.</em>\n </div>\n </div>\n\n <div id=\"graph\"></div>\n\n <script>\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 </script>\n</body>\n</html>`;\n}\n\n/**\n * Write visualization files to tmp/webpieces/\n */\nexport function writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Architecture'\n): { dotPath: string; htmlPath: string } {\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 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 return false;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/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';\n\n/**\n * Level colors for visualization\n */\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9', // Light green - foundation\n 1: '#E3F2FD', // Light blue - middleware\n 2: '#FFF3E0', // Light orange - applications\n 3: '#FCE4EC', // Light pink - higher level\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 = 'WebPieces 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 with level-based colors\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n const color = LEVEL_COLORS[info.level] || '#F5F5F5';\n dot += ` \"${shortName}\" [fillcolor=\"${color}\", label=\"${shortName}\\\\n(L${info.level})\"];\\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 = 'WebPieces Architecture'): string {\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>\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 </style>\n</head>\n<body>\n <h1>${title}</h1>\n\n <div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>Level 0:</strong> Foundation libraries (no dependencies)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Level 1:</strong> Middleware libraries (depend on Level 0)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Level 2:</strong> Applications (depend on Level 1)\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Note: Transitive dependencies are allowed but not shown in the graph.</em>\n </div>\n </div>\n\n <div id=\"graph\"></div>\n\n <script>\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 </script>\n</body>\n</html>`;\n}\n\n/**\n * Write visualization files to tmp/webpieces/\n */\nexport function writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Architecture'\n): { dotPath: string; htmlPath: string } {\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 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 return false;\n }\n}\n"]}
|
|
@@ -23,6 +23,15 @@ const LEVEL_COLORS: Record<number, string> = {
|
|
|
23
23
|
3: '#FCE4EC', // Light pink - higher level
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Remove scope from name for display
|
|
28
|
+
* '@scope/name' → 'name'
|
|
29
|
+
* 'name' → 'name'
|
|
30
|
+
*/
|
|
31
|
+
function getShortName(name: string): string {
|
|
32
|
+
return name.includes('/') ? name.split('/').pop()! : name;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Generate Graphviz DOT format from the graph
|
|
28
37
|
*/
|
|
@@ -41,7 +50,7 @@ export function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Arc
|
|
|
41
50
|
|
|
42
51
|
// Create nodes with level-based colors
|
|
43
52
|
for (const [project, info] of Object.entries(graph)) {
|
|
44
|
-
const shortName = project
|
|
53
|
+
const shortName = getShortName(project);
|
|
45
54
|
const color = LEVEL_COLORS[info.level] || '#F5F5F5';
|
|
46
55
|
dot += ` "${shortName}" [fillcolor="${color}", label="${shortName}\\n(L${info.level})"];\n`;
|
|
47
56
|
}
|
|
@@ -52,7 +61,7 @@ export function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Arc
|
|
|
52
61
|
for (const [level, projects] of Object.entries(levels)) {
|
|
53
62
|
dot += ` { rank=same; `;
|
|
54
63
|
projects.forEach((p) => {
|
|
55
|
-
const shortName = p
|
|
64
|
+
const shortName = getShortName(p);
|
|
56
65
|
dot += `"${shortName}"; `;
|
|
57
66
|
});
|
|
58
67
|
dot += '}\n';
|
|
@@ -62,9 +71,9 @@ export function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Arc
|
|
|
62
71
|
|
|
63
72
|
// Create edges (dependencies)
|
|
64
73
|
for (const [project, info] of Object.entries(graph)) {
|
|
65
|
-
const shortName = project
|
|
74
|
+
const shortName = getShortName(project);
|
|
66
75
|
for (const dep of info.dependsOn || []) {
|
|
67
|
-
const depShortName = dep
|
|
76
|
+
const depShortName = getShortName(dep);
|
|
68
77
|
dot += ` "${shortName}" -> "${depShortName}";\n`;
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -26,9 +26,9 @@ export interface ValidationResult {
|
|
|
26
26
|
*
|
|
27
27
|
* For each project in the graph:
|
|
28
28
|
* - Check that all graph dependencies exist in package.json
|
|
29
|
-
* -
|
|
29
|
+
* - Maps project names to package names for accurate comparison
|
|
30
30
|
*
|
|
31
|
-
* @param graph - Enhanced graph with project dependencies
|
|
31
|
+
* @param graph - Enhanced graph with project dependencies (uses project names)
|
|
32
32
|
* @param workspaceRoot - Absolute path to workspace root
|
|
33
33
|
* @returns Validation result with errors if any
|
|
34
34
|
*/
|
|
@@ -23,11 +23,11 @@ function readPackageJsonDeps(workspaceRoot, projectRoot) {
|
|
|
23
23
|
try {
|
|
24
24
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
25
25
|
const deps = [];
|
|
26
|
-
// Collect
|
|
26
|
+
// Collect ALL dependencies from package.json
|
|
27
27
|
for (const depType of ['dependencies', 'peerDependencies']) {
|
|
28
28
|
const depObj = packageJson[depType] || {};
|
|
29
29
|
for (const depName of Object.keys(depObj)) {
|
|
30
|
-
if (
|
|
30
|
+
if (!deps.includes(depName)) {
|
|
31
31
|
deps.push(depName);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -39,42 +39,63 @@ function readPackageJsonDeps(workspaceRoot, projectRoot) {
|
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Build map of project names to their package names
|
|
44
|
+
* e.g., "core-util" → "@webpieces/core-util"
|
|
45
|
+
*/
|
|
46
|
+
function buildProjectToPackageMap(workspaceRoot, projectsConfig) {
|
|
47
|
+
const map = new Map();
|
|
48
|
+
for (const [projectName, config] of Object.entries(projectsConfig.projects)) {
|
|
49
|
+
const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');
|
|
50
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
53
|
+
if (packageJson.name) {
|
|
54
|
+
map.set(projectName, packageJson.name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Ignore parse errors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
42
64
|
/**
|
|
43
65
|
* Validate that package.json dependencies match the dependency graph
|
|
44
66
|
*
|
|
45
67
|
* For each project in the graph:
|
|
46
68
|
* - Check that all graph dependencies exist in package.json
|
|
47
|
-
* -
|
|
69
|
+
* - Maps project names to package names for accurate comparison
|
|
48
70
|
*
|
|
49
|
-
* @param graph - Enhanced graph with project dependencies
|
|
71
|
+
* @param graph - Enhanced graph with project dependencies (uses project names)
|
|
50
72
|
* @param workspaceRoot - Absolute path to workspace root
|
|
51
73
|
* @returns Validation result with errors if any
|
|
52
74
|
*/
|
|
53
75
|
async function validatePackageJsonDependencies(graph, workspaceRoot) {
|
|
54
76
|
const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
|
|
55
77
|
const projectsConfig = (0, devkit_1.readProjectsConfigurationFromProjectGraph)(projectGraph);
|
|
78
|
+
// Build map: project name → package name
|
|
79
|
+
const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);
|
|
56
80
|
const errors = [];
|
|
57
81
|
const projectResults = [];
|
|
58
82
|
for (const [projectName, entry] of Object.entries(graph)) {
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
// Find the project config
|
|
62
|
-
const projectConfig = projectsConfig.projects[baseName];
|
|
83
|
+
// Find the project config using project name directly
|
|
84
|
+
const projectConfig = projectsConfig.projects[projectName];
|
|
63
85
|
if (!projectConfig) {
|
|
64
|
-
// Project not found in Nx config, skip
|
|
65
86
|
continue;
|
|
66
87
|
}
|
|
67
88
|
const projectRoot = projectConfig.root;
|
|
68
89
|
const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectRoot);
|
|
69
|
-
// Skip projects without package.json (common for apps in monorepo)
|
|
70
90
|
if (packageJsonDeps === null) {
|
|
71
91
|
continue;
|
|
72
92
|
}
|
|
73
|
-
//
|
|
93
|
+
// Convert graph dependencies (project names) to package names for comparison
|
|
74
94
|
const missingInPackageJson = [];
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
for (const depProjectName of entry.dependsOn) {
|
|
96
|
+
const depPackageName = projectToPackage.get(depProjectName) || depProjectName;
|
|
97
|
+
if (!packageJsonDeps.includes(depPackageName)) {
|
|
98
|
+
missingInPackageJson.push(depProjectName);
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
101
|
// Check for extra dependencies in package.json (not critical, just informational)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-validator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/package-validator.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;
|
|
1
|
+
{"version":3,"file":"package-validator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/package-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Package Validator\n *\n * Validates that package.json dependencies match the project.json build.dependsOn\n * This ensures the two sources of truth don't drift apart.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n createProjectGraphAsync,\n readProjectsConfigurationFromProjectGraph,\n} from '@nx/devkit';\n\n/**\n * Validation result for a single project\n */\nexport interface ProjectValidationResult {\n project: string;\n valid: boolean;\n missingInPackageJson: string[];\n extraInPackageJson: string[];\n}\n\n/**\n * Overall validation result\n */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[];\n projectResults: ProjectValidationResult[];\n}\n\n/**\n * Read package.json dependencies for a project\n * Returns null if package.json doesn't exist (apps often don't have one)\n */\nfunction readPackageJsonDeps(workspaceRoot: string, projectRoot: string): string[] | null {\n const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');\n\n if (!fs.existsSync(packageJsonPath)) {\n return null; // No package.json - skip validation for this project\n }\n\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const deps: string[] = [];\n\n // Collect ALL dependencies from package.json\n for (const depType of ['dependencies', 'peerDependencies']) {\n const depObj = packageJson[depType] || {};\n for (const depName of Object.keys(depObj)) {\n if (!deps.includes(depName)) {\n deps.push(depName);\n }\n }\n }\n\n return deps.sort();\n } catch (err: unknown) {\n console.warn(`Could not read package.json at ${packageJsonPath}`);\n return [];\n }\n}\n\n/**\n * Build map of project names to their package names\n * e.g., \"core-util\" → \"@webpieces/core-util\"\n */\nfunction buildProjectToPackageMap(\n workspaceRoot: string,\n projectsConfig: any\n): Map<string, string> {\n const map = new Map<string, string>();\n\n for (const [projectName, config] of Object.entries<any>(projectsConfig.projects)) {\n const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');\n if (fs.existsSync(packageJsonPath)) {\n try {\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n if (packageJson.name) {\n map.set(projectName, packageJson.name);\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n return map;\n}\n\n/**\n * Validate that package.json dependencies match the dependency graph\n *\n * For each project in the graph:\n * - Check that all graph dependencies exist in package.json\n * - Maps project names to package names for accurate comparison\n *\n * @param graph - Enhanced graph with project dependencies (uses project names)\n * @param workspaceRoot - Absolute path to workspace root\n * @returns Validation result with errors if any\n */\nexport async function validatePackageJsonDependencies(\n graph: Record<string, { level: number; dependsOn: string[] }>,\n workspaceRoot: string\n): Promise<ValidationResult> {\n const projectGraph = await createProjectGraphAsync();\n const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);\n\n // Build map: project name → package name\n const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);\n\n const errors: string[] = [];\n const projectResults: ProjectValidationResult[] = [];\n\n for (const [projectName, entry] of Object.entries(graph)) {\n // Find the project config using project name directly\n const projectConfig = projectsConfig.projects[projectName];\n if (!projectConfig) {\n continue;\n }\n\n const projectRoot = projectConfig.root;\n const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectRoot);\n\n if (packageJsonDeps === null) {\n continue;\n }\n\n // Convert graph dependencies (project names) to package names for comparison\n const missingInPackageJson: string[] = [];\n for (const depProjectName of entry.dependsOn) {\n const depPackageName = projectToPackage.get(depProjectName) || depProjectName;\n if (!packageJsonDeps.includes(depPackageName)) {\n missingInPackageJson.push(depProjectName);\n }\n }\n\n // Check for extra dependencies in package.json (not critical, just informational)\n const extraInPackageJson: string[] = [];\n for (const dep of packageJsonDeps) {\n if (!entry.dependsOn.includes(dep)) {\n extraInPackageJson.push(dep);\n }\n }\n\n const valid = missingInPackageJson.length === 0;\n\n if (!valid) {\n errors.push(\n `Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${missingInPackageJson.join(', ')}\\n` +\n ` Fix: Add these to package.json dependencies`\n );\n }\n\n projectResults.push({\n project: projectName,\n valid,\n missingInPackageJson,\n extraInPackageJson,\n });\n }\n\n return {\n valid: errors.length === 0,\n errors,\n projectResults,\n };\n}\n"]}
|
|
@@ -46,11 +46,11 @@ function readPackageJsonDeps(workspaceRoot: string, projectRoot: string): string
|
|
|
46
46
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
47
47
|
const deps: string[] = [];
|
|
48
48
|
|
|
49
|
-
// Collect
|
|
49
|
+
// Collect ALL dependencies from package.json
|
|
50
50
|
for (const depType of ['dependencies', 'peerDependencies']) {
|
|
51
51
|
const depObj = packageJson[depType] || {};
|
|
52
52
|
for (const depName of Object.keys(depObj)) {
|
|
53
|
-
if (
|
|
53
|
+
if (!deps.includes(depName)) {
|
|
54
54
|
deps.push(depName);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -63,14 +63,41 @@ function readPackageJsonDeps(workspaceRoot: string, projectRoot: string): string
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Build map of project names to their package names
|
|
68
|
+
* e.g., "core-util" → "@webpieces/core-util"
|
|
69
|
+
*/
|
|
70
|
+
function buildProjectToPackageMap(
|
|
71
|
+
workspaceRoot: string,
|
|
72
|
+
projectsConfig: any
|
|
73
|
+
): Map<string, string> {
|
|
74
|
+
const map = new Map<string, string>();
|
|
75
|
+
|
|
76
|
+
for (const [projectName, config] of Object.entries<any>(projectsConfig.projects)) {
|
|
77
|
+
const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');
|
|
78
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
81
|
+
if (packageJson.name) {
|
|
82
|
+
map.set(projectName, packageJson.name);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Ignore parse errors
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return map;
|
|
91
|
+
}
|
|
92
|
+
|
|
66
93
|
/**
|
|
67
94
|
* Validate that package.json dependencies match the dependency graph
|
|
68
95
|
*
|
|
69
96
|
* For each project in the graph:
|
|
70
97
|
* - Check that all graph dependencies exist in package.json
|
|
71
|
-
* -
|
|
98
|
+
* - Maps project names to package names for accurate comparison
|
|
72
99
|
*
|
|
73
|
-
* @param graph - Enhanced graph with project dependencies
|
|
100
|
+
* @param graph - Enhanced graph with project dependencies (uses project names)
|
|
74
101
|
* @param workspaceRoot - Absolute path to workspace root
|
|
75
102
|
* @returns Validation result with errors if any
|
|
76
103
|
*/
|
|
@@ -81,33 +108,32 @@ export async function validatePackageJsonDependencies(
|
|
|
81
108
|
const projectGraph = await createProjectGraphAsync();
|
|
82
109
|
const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);
|
|
83
110
|
|
|
111
|
+
// Build map: project name → package name
|
|
112
|
+
const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);
|
|
113
|
+
|
|
84
114
|
const errors: string[] = [];
|
|
85
115
|
const projectResults: ProjectValidationResult[] = [];
|
|
86
116
|
|
|
87
117
|
for (const [projectName, entry] of Object.entries(graph)) {
|
|
88
|
-
//
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
// Find the project config
|
|
92
|
-
const projectConfig = projectsConfig.projects[baseName];
|
|
118
|
+
// Find the project config using project name directly
|
|
119
|
+
const projectConfig = projectsConfig.projects[projectName];
|
|
93
120
|
if (!projectConfig) {
|
|
94
|
-
// Project not found in Nx config, skip
|
|
95
121
|
continue;
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
const projectRoot = projectConfig.root;
|
|
99
125
|
const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectRoot);
|
|
100
126
|
|
|
101
|
-
// Skip projects without package.json (common for apps in monorepo)
|
|
102
127
|
if (packageJsonDeps === null) {
|
|
103
128
|
continue;
|
|
104
129
|
}
|
|
105
130
|
|
|
106
|
-
//
|
|
131
|
+
// Convert graph dependencies (project names) to package names for comparison
|
|
107
132
|
const missingInPackageJson: string[] = [];
|
|
108
|
-
for (const
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
for (const depProjectName of entry.dependsOn) {
|
|
134
|
+
const depPackageName = projectToPackage.get(depProjectName) || depProjectName;
|
|
135
|
+
if (!packageJsonDeps.includes(depPackageName)) {
|
|
136
|
+
missingInPackageJson.push(depProjectName);
|
|
111
137
|
}
|
|
112
138
|
}
|
|
113
139
|
|
|
@@ -227,6 +227,61 @@ function loadBlessedGraph(workspaceRoot) {
|
|
|
227
227
|
return null;
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Build set of all workspace package names (from package.json files)
|
|
232
|
+
* Used to detect workspace imports (works for any scope or unscoped)
|
|
233
|
+
*/
|
|
234
|
+
function buildWorkspacePackageNames(workspaceRoot) {
|
|
235
|
+
const packageNames = new Set();
|
|
236
|
+
const mappings = buildProjectMappings(workspaceRoot);
|
|
237
|
+
for (const mapping of mappings) {
|
|
238
|
+
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
239
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
240
|
+
try {
|
|
241
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
242
|
+
if (pkgJson.name) {
|
|
243
|
+
packageNames.add(pkgJson.name);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Ignore parse errors
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return packageNames;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Check if an import path is a workspace project
|
|
255
|
+
* Works for scoped (@scope/name) or unscoped (name) packages
|
|
256
|
+
*/
|
|
257
|
+
function isWorkspaceImport(importPath, workspaceRoot) {
|
|
258
|
+
const workspacePackages = buildWorkspacePackageNames(workspaceRoot);
|
|
259
|
+
return workspacePackages.has(importPath);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get project name from package name
|
|
263
|
+
* e.g., '@webpieces/client' → 'client', 'apis' → 'apis'
|
|
264
|
+
*/
|
|
265
|
+
function getProjectNameFromPackageName(packageName, workspaceRoot) {
|
|
266
|
+
const mappings = buildProjectMappings(workspaceRoot);
|
|
267
|
+
// Try to find by reading package.json files
|
|
268
|
+
for (const mapping of mappings) {
|
|
269
|
+
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
270
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
271
|
+
try {
|
|
272
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
273
|
+
if (pkgJson.name === packageName) {
|
|
274
|
+
return mapping.name; // Return project name
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Ignore parse errors
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Fallback: return package name as-is (might be unscoped project name)
|
|
283
|
+
return packageName;
|
|
284
|
+
}
|
|
230
285
|
/**
|
|
231
286
|
* Build project mappings from project.json files in workspace
|
|
232
287
|
*/
|
|
@@ -263,12 +318,8 @@ function scanForProjects(dir, workspaceRoot, mappings) {
|
|
|
263
318
|
try {
|
|
264
319
|
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
|
265
320
|
const projectRoot = path.relative(workspaceRoot, fullPath);
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
// Add @webpieces/ prefix if not present
|
|
269
|
-
if (!projectName.startsWith('@webpieces/')) {
|
|
270
|
-
projectName = `@webpieces/${projectName}`;
|
|
271
|
-
}
|
|
321
|
+
// Use project name from project.json as-is (no scope forcing)
|
|
322
|
+
const projectName = projectJson.name || entry.name;
|
|
272
323
|
mappings.push({
|
|
273
324
|
root: projectRoot,
|
|
274
325
|
name: projectName,
|
|
@@ -348,38 +399,38 @@ const rule = {
|
|
|
348
399
|
return {
|
|
349
400
|
ImportDeclaration(node) {
|
|
350
401
|
const importPath = node.source.value;
|
|
351
|
-
//
|
|
352
|
-
if (!importPath
|
|
353
|
-
return;
|
|
402
|
+
// Check if this is a workspace import (works for any scope or unscoped)
|
|
403
|
+
if (!isWorkspaceImport(importPath, workspaceRoot)) {
|
|
404
|
+
return; // Not a workspace import, skip validation
|
|
354
405
|
}
|
|
355
406
|
// Determine which project this file belongs to
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
407
|
+
const sourceProject = getProjectFromFile(filename, workspaceRoot);
|
|
408
|
+
if (!sourceProject) {
|
|
358
409
|
// File not in any known project (e.g., tools/, scripts/)
|
|
359
410
|
return;
|
|
360
411
|
}
|
|
412
|
+
// Convert import (package name) to project name
|
|
413
|
+
const targetProject = getProjectNameFromPackageName(importPath, workspaceRoot);
|
|
361
414
|
// Self-import is always allowed
|
|
362
|
-
if (
|
|
415
|
+
if (targetProject === sourceProject) {
|
|
363
416
|
return;
|
|
364
417
|
}
|
|
365
418
|
// Load blessed graph
|
|
366
419
|
const graph = loadBlessedGraph(workspaceRoot);
|
|
367
420
|
if (!graph) {
|
|
368
421
|
// No graph file - warn but don't fail (allows gradual adoption)
|
|
369
|
-
// Uncomment below to enforce graph existence:
|
|
370
|
-
// context.report({ node: node.source, messageId: 'noGraph' });
|
|
371
422
|
return;
|
|
372
423
|
}
|
|
373
424
|
// Get project entry
|
|
374
|
-
const projectEntry = graph[
|
|
425
|
+
const projectEntry = graph[sourceProject];
|
|
375
426
|
if (!projectEntry) {
|
|
376
427
|
// Project not in graph (new project?) - allow
|
|
377
428
|
return;
|
|
378
429
|
}
|
|
379
430
|
// Compute allowed dependencies (direct + transitive)
|
|
380
|
-
const allowedDeps = computeTransitiveDependencies(
|
|
381
|
-
// Check if import is allowed
|
|
382
|
-
if (!allowedDeps.has(
|
|
431
|
+
const allowedDeps = computeTransitiveDependencies(sourceProject, graph);
|
|
432
|
+
// Check if import is allowed (use project name, not package name)
|
|
433
|
+
if (!allowedDeps.has(targetProject)) {
|
|
383
434
|
// Write documentation file for AI/developer to read
|
|
384
435
|
ensureDependenciesDoc(workspaceRoot);
|
|
385
436
|
const directDeps = projectEntry.dependsOn || [];
|
|
@@ -392,7 +443,7 @@ const rule = {
|
|
|
392
443
|
messageId: 'illegalImport',
|
|
393
444
|
data: {
|
|
394
445
|
imported: importPath,
|
|
395
|
-
project:
|
|
446
|
+
project: sourceProject,
|
|
396
447
|
level: String(projectEntry.level),
|
|
397
448
|
allowedList: allowedList,
|
|
398
449
|
},
|