@webpieces/nx-webpieces-rules 0.0.1

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.
Files changed (61) hide show
  1. package/LICENSE +373 -0
  2. package/executors.json +124 -0
  3. package/package.json +36 -0
  4. package/src/executor-result.ts +7 -0
  5. package/src/executors/generate/executor.ts +61 -0
  6. package/src/executors/generate/schema.json +14 -0
  7. package/src/executors/help/executor.ts +63 -0
  8. package/src/executors/help/schema.json +7 -0
  9. package/src/executors/validate-architecture-unchanged/executor.ts +253 -0
  10. package/src/executors/validate-architecture-unchanged/schema.json +14 -0
  11. package/src/executors/validate-catch-error-pattern/executor.ts +11 -0
  12. package/src/executors/validate-catch-error-pattern/schema.json +24 -0
  13. package/src/executors/validate-code/executor.ts +11 -0
  14. package/src/executors/validate-code/schema.json +287 -0
  15. package/src/executors/validate-dtos/executor.ts +11 -0
  16. package/src/executors/validate-dtos/schema.json +33 -0
  17. package/src/executors/validate-eslint-sync/executor.ts +87 -0
  18. package/src/executors/validate-eslint-sync/schema.json +7 -0
  19. package/src/executors/validate-modified-files/executor.ts +11 -0
  20. package/src/executors/validate-modified-files/schema.json +25 -0
  21. package/src/executors/validate-modified-methods/executor.ts +11 -0
  22. package/src/executors/validate-modified-methods/schema.json +25 -0
  23. package/src/executors/validate-new-methods/executor.ts +11 -0
  24. package/src/executors/validate-new-methods/schema.json +25 -0
  25. package/src/executors/validate-no-any-unknown/executor.ts +11 -0
  26. package/src/executors/validate-no-any-unknown/schema.json +24 -0
  27. package/src/executors/validate-no-architecture-cycles/executor.ts +63 -0
  28. package/src/executors/validate-no-architecture-cycles/schema.json +8 -0
  29. package/src/executors/validate-no-destructure/executor.ts +11 -0
  30. package/src/executors/validate-no-destructure/schema.json +24 -0
  31. package/src/executors/validate-no-direct-api-resolver/executor.ts +11 -0
  32. package/src/executors/validate-no-direct-api-resolver/schema.json +29 -0
  33. package/src/executors/validate-no-implicit-any/executor.ts +11 -0
  34. package/src/executors/validate-no-implicit-any/schema.json +24 -0
  35. package/src/executors/validate-no-inline-types/executor.ts +11 -0
  36. package/src/executors/validate-no-inline-types/schema.json +24 -0
  37. package/src/executors/validate-no-skiplevel-deps/executor.ts +274 -0
  38. package/src/executors/validate-no-skiplevel-deps/schema.json +8 -0
  39. package/src/executors/validate-no-unmanaged-exceptions/executor.ts +11 -0
  40. package/src/executors/validate-no-unmanaged-exceptions/schema.json +24 -0
  41. package/src/executors/validate-packagejson/executor.ts +76 -0
  42. package/src/executors/validate-packagejson/schema.json +8 -0
  43. package/src/executors/validate-prisma-converters/executor.ts +11 -0
  44. package/src/executors/validate-prisma-converters/schema.json +38 -0
  45. package/src/executors/validate-return-types/executor.ts +11 -0
  46. package/src/executors/validate-return-types/schema.json +24 -0
  47. package/src/executors/validate-ts-in-src/executor.ts +283 -0
  48. package/src/executors/validate-ts-in-src/schema.json +25 -0
  49. package/src/executors/validate-versions-locked/executor.ts +376 -0
  50. package/src/executors/validate-versions-locked/schema.json +8 -0
  51. package/src/executors/visualize/executor.ts +65 -0
  52. package/src/executors/visualize/schema.json +14 -0
  53. package/src/index.ts +9 -0
  54. package/src/lib/graph-comparator.ts +154 -0
  55. package/src/lib/graph-generator.ts +97 -0
  56. package/src/lib/graph-loader.ts +119 -0
  57. package/src/lib/graph-sorter.ts +137 -0
  58. package/src/lib/graph-visualizer.ts +253 -0
  59. package/src/lib/package-validator.ts +184 -0
  60. package/src/plugin.ts +666 -0
  61. package/src/toError.ts +36 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Graph Visualizer
3
+ *
4
+ * Generates visual representations of the architecture graph:
5
+ * - DOT format (for Graphviz)
6
+ * - Interactive HTML (using viz.js)
7
+ *
8
+ * Output files go to tmp/webpieces/ for easy viewing without committing.
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { execSync } from 'child_process';
14
+ import type { EnhancedGraph } from './graph-sorter';
15
+ import { toError } from '../toError';
16
+
17
+ /**
18
+ * Level colors for visualization
19
+ */
20
+ const LEVEL_COLORS: Record<number, string> = {
21
+ 0: '#E8F5E9', // Light green - foundation
22
+ 1: '#E3F2FD', // Light blue - middleware
23
+ 2: '#FFF3E0', // Light orange - applications
24
+ 3: '#FCE4EC', // Light pink - higher level
25
+ };
26
+
27
+ /**
28
+ * Remove scope from name for display
29
+ * '@scope/name' → 'name'
30
+ * 'name' → 'name'
31
+ */
32
+ function getShortName(name: string): string {
33
+ return name.includes('/') ? name.split('/').pop()! : name;
34
+ }
35
+
36
+ /**
37
+ * Generate Graphviz DOT format from the graph
38
+ */
39
+ export function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Architecture'): string {
40
+ let dot = 'digraph Architecture {\n';
41
+ dot += ' rankdir=TB;\n';
42
+ dot += ' node [shape=box, style=filled, fontname="Arial"];\n';
43
+ dot += ' edge [fontname="Arial"];\n\n';
44
+
45
+ // Group projects by level
46
+ const levels: Record<number, string[]> = {};
47
+ for (const [project, info] of Object.entries(graph)) {
48
+ if (!levels[info.level]) levels[info.level] = [];
49
+ levels[info.level].push(project);
50
+ }
51
+
52
+ // Create nodes with level-based colors
53
+ for (const [project, info] of Object.entries(graph)) {
54
+ const shortName = getShortName(project);
55
+ const color = LEVEL_COLORS[info.level] || '#F5F5F5';
56
+ dot += ` "${shortName}" [fillcolor="${color}", label="${shortName}\\n(L${info.level})"];\n`;
57
+ }
58
+
59
+ dot += '\n';
60
+
61
+ // Create same-rank subgraphs for each level
62
+ for (const [level, projects] of Object.entries(levels)) {
63
+ dot += ` { rank=same; `;
64
+ projects.forEach((p) => {
65
+ const shortName = getShortName(p);
66
+ dot += `"${shortName}"; `;
67
+ });
68
+ dot += '}\n';
69
+ }
70
+
71
+ dot += '\n';
72
+
73
+ // Create edges (dependencies)
74
+ for (const [project, info] of Object.entries(graph)) {
75
+ const shortName = getShortName(project);
76
+ for (const dep of info.dependsOn || []) {
77
+ const depShortName = getShortName(dep);
78
+ dot += ` "${shortName}" -> "${depShortName}";\n`;
79
+ }
80
+ }
81
+
82
+ dot += '\n labelloc="t";\n';
83
+ dot += ` label="${title}\\n(from architecture/dependencies.json)";\n`;
84
+ dot += ' fontsize=20;\n';
85
+ dot += '}\n';
86
+
87
+ return dot;
88
+ }
89
+
90
+ /**
91
+ * Generate interactive HTML with embedded SVG using viz.js
92
+ */
93
+ export function generateHTML(dot: string, title: string = 'WebPieces Architecture'): string {
94
+ const styles = generateHTMLStyles();
95
+ const legend = generateHTMLLegend();
96
+ const script = generateHTMLScript(dot);
97
+
98
+ return `<!DOCTYPE html>
99
+ <html>
100
+ <head>
101
+ <title>${title}</title>
102
+ <script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js"></script>
103
+ <script src="https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js"></script>
104
+ <style>${styles}</style>
105
+ </head>
106
+ <body>
107
+ <h1>${title}</h1>
108
+ ${legend}
109
+ <div id="graph"></div>
110
+ <script>${script}</script>
111
+ </body>
112
+ </html>`;
113
+ }
114
+
115
+ function generateHTMLStyles(): string {
116
+ return `
117
+ body {
118
+ margin: 0;
119
+ padding: 20px;
120
+ font-family: Arial, sans-serif;
121
+ background: #f5f5f5;
122
+ }
123
+ h1 {
124
+ text-align: center;
125
+ color: #333;
126
+ }
127
+ #graph {
128
+ text-align: center;
129
+ background: white;
130
+ padding: 20px;
131
+ border-radius: 8px;
132
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
133
+ }
134
+ .legend {
135
+ margin: 20px auto;
136
+ max-width: 600px;
137
+ padding: 15px;
138
+ background: white;
139
+ border-radius: 8px;
140
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
141
+ }
142
+ .legend h2 {
143
+ margin-top: 0;
144
+ }
145
+ .legend-item {
146
+ margin: 8px 0;
147
+ }
148
+ .legend-box {
149
+ display: inline-block;
150
+ width: 20px;
151
+ height: 20px;
152
+ border: 1px solid #ccc;
153
+ margin-right: 10px;
154
+ vertical-align: middle;
155
+ }
156
+ `;
157
+ }
158
+
159
+ function generateHTMLLegend(): string {
160
+ return `<div class="legend">
161
+ <h2>Legend</h2>
162
+ <div class="legend-item">
163
+ <span class="legend-box" style="background: #E8F5E9;"></span>
164
+ <strong>Level 0:</strong> Foundation libraries (no dependencies)
165
+ </div>
166
+ <div class="legend-item">
167
+ <span class="legend-box" style="background: #E3F2FD;"></span>
168
+ <strong>Level 1:</strong> Middleware libraries (depend on Level 0)
169
+ </div>
170
+ <div class="legend-item">
171
+ <span class="legend-box" style="background: #FFF3E0;"></span>
172
+ <strong>Level 2:</strong> Applications (depend on Level 1)
173
+ </div>
174
+ <div class="legend-item" style="margin-top: 15px;">
175
+ <em>Note: Transitive dependencies are allowed but not shown in the graph.</em>
176
+ </div>
177
+ </div>`;
178
+ }
179
+
180
+ function generateHTMLScript(dot: string): string {
181
+ return `
182
+ const dot = ${JSON.stringify(dot)};
183
+ const viz = new Viz();
184
+
185
+ viz.renderSVGElement(dot)
186
+ .then(element => {
187
+ document.getElementById('graph').appendChild(element);
188
+ })
189
+ .catch(err => {
190
+ console.error(err);
191
+ document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';
192
+ });
193
+ `;
194
+ }
195
+
196
+ interface VisualizationPaths {
197
+ dotPath: string;
198
+ htmlPath: string;
199
+ }
200
+
201
+ /**
202
+ * Write visualization files to tmp/webpieces/
203
+ */
204
+ export function writeVisualization(
205
+ graph: EnhancedGraph,
206
+ workspaceRoot: string,
207
+ title: string = 'WebPieces Architecture'
208
+ ): VisualizationPaths {
209
+ const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');
210
+
211
+ // Ensure directory exists
212
+ if (!fs.existsSync(outputDir)) {
213
+ fs.mkdirSync(outputDir, { recursive: true });
214
+ }
215
+
216
+ // Generate DOT
217
+ const dot = generateDot(graph, title);
218
+ const dotPath = path.join(outputDir, 'architecture.dot');
219
+ fs.writeFileSync(dotPath, dot, 'utf-8');
220
+
221
+ // Generate HTML
222
+ const html = generateHTML(dot, title);
223
+ const htmlPath = path.join(outputDir, 'architecture.html');
224
+ fs.writeFileSync(htmlPath, html, 'utf-8');
225
+
226
+ return { dotPath, htmlPath };
227
+ }
228
+
229
+ /**
230
+ * Open the HTML visualization in the default browser
231
+ */
232
+ export function openVisualization(htmlPath: string): boolean {
233
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
234
+ try {
235
+ const platform = process.platform;
236
+ let openCommand: string;
237
+
238
+ if (platform === 'darwin') {
239
+ openCommand = `open "${htmlPath}"`;
240
+ } else if (platform === 'win32') {
241
+ openCommand = `start "" "${htmlPath}"`;
242
+ } else {
243
+ openCommand = `xdg-open "${htmlPath}"`;
244
+ }
245
+
246
+ execSync(openCommand, { stdio: 'ignore' });
247
+ return true;
248
+ } catch (err: unknown) {
249
+ //const error = toError(err);
250
+ void err;
251
+ return false;
252
+ }
253
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Package Validator
3
+ *
4
+ * Validates that package.json dependencies match the project.json build.dependsOn
5
+ * This ensures the two sources of truth don't drift apart.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import {
11
+ createProjectGraphAsync,
12
+ readProjectsConfigurationFromProjectGraph,
13
+ } from '@nx/devkit';
14
+ import { toError } from '../toError';
15
+
16
+ /**
17
+ * Validation result for a single project
18
+ */
19
+ export interface ProjectValidationResult {
20
+ project: string;
21
+ valid: boolean;
22
+ missingInPackageJson: string[];
23
+ extraInPackageJson: string[];
24
+ }
25
+
26
+ /**
27
+ * Overall validation result
28
+ */
29
+ export interface ValidationResult {
30
+ valid: boolean;
31
+ errors: string[];
32
+ projectResults: ProjectValidationResult[];
33
+ }
34
+
35
+ /**
36
+ * Read package.json dependencies for a project
37
+ * Returns null if package.json doesn't exist (apps often don't have one)
38
+ */
39
+ function readPackageJsonDeps(workspaceRoot: string, projectRoot: string): string[] | null {
40
+ const packageJsonPath = path.join(workspaceRoot, projectRoot, 'package.json');
41
+
42
+ if (!fs.existsSync(packageJsonPath)) {
43
+ return null; // No package.json - skip validation for this project
44
+ }
45
+
46
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
47
+ try {
48
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
49
+ const deps: string[] = [];
50
+
51
+ // Collect ALL dependencies from package.json
52
+ for (const depType of ['dependencies', 'peerDependencies']) {
53
+ const depObj = packageJson[depType] || {};
54
+ for (const depName of Object.keys(depObj)) {
55
+ if (!deps.includes(depName)) {
56
+ deps.push(depName);
57
+ }
58
+ }
59
+ }
60
+
61
+ return deps.sort();
62
+ } catch (err: unknown) {
63
+ //const error = toError(err);
64
+ void err;
65
+ console.warn(`Could not read package.json at ${packageJsonPath}`);
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Build map of project names to their package names
72
+ * e.g., "core-util" → "@webpieces/core-util"
73
+ */
74
+ function buildProjectToPackageMap(
75
+ workspaceRoot: string,
76
+ // webpieces-disable no-any-unknown -- Nx devkit projectsConfig type is dynamic and not strongly typed
77
+ projectsConfig: any
78
+ ): Map<string, string> {
79
+ const map = new Map<string, string>();
80
+
81
+ // webpieces-disable no-any-unknown -- Nx devkit projects config entries are untyped
82
+ for (const [projectName, config] of Object.entries<any>(projectsConfig.projects)) {
83
+ const packageJsonPath = path.join(workspaceRoot, config.root, 'package.json');
84
+ if (fs.existsSync(packageJsonPath)) {
85
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
86
+ try {
87
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
88
+ if (packageJson.name) {
89
+ map.set(projectName, packageJson.name);
90
+ }
91
+ } catch (err: unknown) {
92
+ //const error = toError(err);
93
+ void err;
94
+ // Ignore parse errors
95
+ }
96
+ }
97
+ }
98
+
99
+ return map;
100
+ }
101
+
102
+ /**
103
+ * Validate that package.json dependencies match the dependency graph
104
+ *
105
+ * For each project in the graph:
106
+ * - Check that all graph dependencies exist in package.json
107
+ * - Maps project names to package names for accurate comparison
108
+ *
109
+ * @param graph - Enhanced graph with project dependencies (uses project names)
110
+ * @param workspaceRoot - Absolute path to workspace root
111
+ * @returns Validation result with errors if any
112
+ */
113
+ interface GraphEntry {
114
+ level: number;
115
+ dependsOn: string[];
116
+ }
117
+
118
+ export async function validatePackageJsonDependencies(
119
+ graph: Record<string, GraphEntry>,
120
+ workspaceRoot: string
121
+ ): Promise<ValidationResult> {
122
+ const projectGraph = await createProjectGraphAsync();
123
+ const projectsConfig = readProjectsConfigurationFromProjectGraph(projectGraph);
124
+
125
+ // Build map: project name → package name
126
+ const projectToPackage = buildProjectToPackageMap(workspaceRoot, projectsConfig);
127
+
128
+ const errors: string[] = [];
129
+ const projectResults: ProjectValidationResult[] = [];
130
+
131
+ for (const [projectName, entry] of Object.entries(graph)) {
132
+ // Find the project config using project name directly
133
+ const projectConfig = projectsConfig.projects[projectName];
134
+ if (!projectConfig) {
135
+ continue;
136
+ }
137
+
138
+ const projectRoot = projectConfig.root;
139
+ const packageJsonDeps = readPackageJsonDeps(workspaceRoot, projectRoot);
140
+
141
+ if (packageJsonDeps === null) {
142
+ continue;
143
+ }
144
+
145
+ // Convert graph dependencies (project names) to package names for comparison
146
+ const missingInPackageJson: string[] = [];
147
+ for (const depProjectName of entry.dependsOn) {
148
+ const depPackageName = projectToPackage.get(depProjectName) || depProjectName;
149
+ if (!packageJsonDeps.includes(depPackageName)) {
150
+ missingInPackageJson.push(depProjectName);
151
+ }
152
+ }
153
+
154
+ // Check for extra dependencies in package.json (not critical, just informational)
155
+ const extraInPackageJson: string[] = [];
156
+ for (const dep of packageJsonDeps) {
157
+ if (!entry.dependsOn.includes(dep)) {
158
+ extraInPackageJson.push(dep);
159
+ }
160
+ }
161
+
162
+ const valid = missingInPackageJson.length === 0;
163
+
164
+ if (!valid) {
165
+ errors.push(
166
+ `Project ${projectName} (${projectRoot}/package.json) is missing dependencies: ${missingInPackageJson.join(', ')}\n` +
167
+ ` Fix: Add these to package.json dependencies`
168
+ );
169
+ }
170
+
171
+ projectResults.push({
172
+ project: projectName,
173
+ valid,
174
+ missingInPackageJson,
175
+ extraInPackageJson,
176
+ });
177
+ }
178
+
179
+ return {
180
+ valid: errors.length === 0,
181
+ errors,
182
+ projectResults,
183
+ };
184
+ }