@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 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 .:arch:generate
96
+ nx run architecture:generate
97
97
 
98
98
  # Validate architecture
99
- nx run .:arch:validate-no-cycles
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 to @webpieces/xxx format
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 to @webpieces/xxx format
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
- // Avoid double prefix if already has @webpieces/
71
- const transformedName = projectName.startsWith('@webpieces/')
72
- ? projectName
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":"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","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 to @webpieces/xxx format\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 // Avoid double prefix if already has @webpieces/\n const transformedName = projectName.startsWith('@webpieces/')\n ? projectName\n : `@webpieces/${projectName}`;\n\n const transformedDeps = deps\n .map((d) => (d.startsWith('@webpieces/') ? d : `@webpieces/${d}`))\n .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"]}
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 to @webpieces/xxx format
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
- // Avoid double prefix if already has @webpieces/
82
- const transformedName = projectName.startsWith('@webpieces/')
83
- ? projectName
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.replace('@webpieces/', '');
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.replace('@webpieces/', '');
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.replace('@webpieces/', '');
71
+ const shortName = getShortName(project);
64
72
  for (const dep of info.dependsOn || []) {
65
- const depShortName = dep.replace('@webpieces/', '');
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.replace('@webpieces/', '');
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.replace('@webpieces/', '');
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.replace('@webpieces/', '');
74
+ const shortName = getShortName(project);
66
75
  for (const dep of info.dependsOn || []) {
67
- const depShortName = dep.replace('@webpieces/', '');
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
- * - Optionally warn about extra deps in package.json not in graph
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 all @webpieces/* dependencies
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 (depName.startsWith('@webpieces/') && !deps.includes(depName)) {
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
- * - Optionally warn about extra deps in package.json not in graph
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
- // Extract base name (remove @webpieces/ prefix)
60
- const baseName = projectName.replace('@webpieces/', '');
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
- // Check for missing dependencies in package.json
93
+ // Convert graph dependencies (project names) to package names for comparison
74
94
  const missingInPackageJson = [];
75
- for (const dep of entry.dependsOn) {
76
- if (!packageJsonDeps.includes(dep)) {
77
- missingInPackageJson.push(dep);
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":"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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 @webpieces/* dependencies\n for (const depType of ['dependencies', 'peerDependencies']) {\n const depObj = packageJson[depType] || {};\n for (const depName of Object.keys(depObj)) {\n if (depName.startsWith('@webpieces/') && !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 * 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 * - Optionally warn about extra deps in package.json not in graph\n *\n * @param graph - Enhanced graph with project dependencies\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 const errors: string[] = [];\n const projectResults: ProjectValidationResult[] = [];\n\n for (const [projectName, entry] of Object.entries(graph)) {\n // Extract base name (remove @webpieces/ prefix)\n const baseName = projectName.replace('@webpieces/', '');\n\n // Find the project config\n const projectConfig = projectsConfig.projects[baseName];\n if (!projectConfig) {\n // Project not found in Nx config, skip\n continue;\n }\n\n const projectRoot = projectConfig.root;\n const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectRoot);\n\n // Skip projects without package.json (common for apps in monorepo)\n if (packageJsonDeps === null) {\n continue;\n }\n\n // Check for missing dependencies in package.json\n const missingInPackageJson: string[] = [];\n for (const dep of entry.dependsOn) {\n if (!packageJsonDeps.includes(dep)) {\n missingInPackageJson.push(dep);\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"]}
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 all @webpieces/* dependencies
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 (depName.startsWith('@webpieces/') && !deps.includes(depName)) {
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
- * - Optionally warn about extra deps in package.json not in graph
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
- // Extract base name (remove @webpieces/ prefix)
89
- const baseName = projectName.replace('@webpieces/', '');
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
- // Check for missing dependencies in package.json
131
+ // Convert graph dependencies (project names) to package names for comparison
107
132
  const missingInPackageJson: string[] = [];
108
- for (const dep of entry.dependsOn) {
109
- if (!packageJsonDeps.includes(dep)) {
110
- missingInPackageJson.push(dep);
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
- // Determine project name
267
- let projectName = projectJson.name || entry.name;
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
- // Only check @webpieces/* imports
352
- if (!importPath.startsWith('@webpieces/')) {
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 project = getProjectFromFile(filename, workspaceRoot);
357
- if (!project) {
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 (importPath === project) {
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[project];
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(project, graph);
381
- // Check if import is allowed
382
- if (!allowedDeps.has(importPath)) {
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: project,
446
+ project: sourceProject,
396
447
  level: String(projectEntry.level),
397
448
  allowedList: allowedList,
398
449
  },