@webpieces/dev-config 0.2.17 → 0.2.21

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 (87) hide show
  1. package/architecture/executors/generate/executor.d.ts +17 -0
  2. package/architecture/executors/generate/executor.js +67 -0
  3. package/architecture/executors/generate/executor.js.map +1 -0
  4. package/architecture/executors/generate/executor.ts +83 -0
  5. package/architecture/executors/generate/schema.json +14 -0
  6. package/architecture/executors/validate-architecture-unchanged/executor.d.ts +17 -0
  7. package/architecture/executors/validate-architecture-unchanged/executor.js +65 -0
  8. package/architecture/executors/validate-architecture-unchanged/executor.js.map +1 -0
  9. package/architecture/executors/validate-architecture-unchanged/executor.ts +81 -0
  10. package/architecture/executors/validate-architecture-unchanged/schema.json +14 -0
  11. package/architecture/executors/validate-no-cycles/executor.d.ts +16 -0
  12. package/architecture/executors/validate-no-cycles/executor.js +48 -0
  13. package/architecture/executors/validate-no-cycles/executor.js.map +1 -0
  14. package/architecture/executors/validate-no-cycles/executor.ts +60 -0
  15. package/architecture/executors/validate-no-cycles/schema.json +8 -0
  16. package/architecture/executors/validate-no-skiplevel-deps/executor.d.ts +19 -0
  17. package/architecture/executors/validate-no-skiplevel-deps/executor.js +227 -0
  18. package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +1 -0
  19. package/architecture/executors/validate-no-skiplevel-deps/executor.ts +267 -0
  20. package/architecture/executors/validate-no-skiplevel-deps/schema.json +8 -0
  21. package/architecture/executors/visualize/executor.d.ts +17 -0
  22. package/architecture/executors/visualize/executor.js +49 -0
  23. package/architecture/executors/visualize/executor.js.map +1 -0
  24. package/architecture/executors/visualize/executor.ts +63 -0
  25. package/architecture/executors/visualize/schema.json +14 -0
  26. package/architecture/index.d.ts +19 -0
  27. package/architecture/index.js +23 -0
  28. package/architecture/index.js.map +1 -0
  29. package/architecture/index.ts +20 -0
  30. package/architecture/lib/graph-comparator.d.ts +39 -0
  31. package/architecture/lib/graph-comparator.js +100 -0
  32. package/architecture/lib/graph-comparator.js.map +1 -0
  33. package/architecture/lib/graph-comparator.ts +141 -0
  34. package/architecture/lib/graph-generator.d.ts +19 -0
  35. package/architecture/lib/graph-generator.js +88 -0
  36. package/architecture/lib/graph-generator.js.map +1 -0
  37. package/architecture/lib/graph-generator.ts +102 -0
  38. package/architecture/lib/graph-loader.d.ts +31 -0
  39. package/architecture/lib/graph-loader.js +70 -0
  40. package/architecture/lib/graph-loader.js.map +1 -0
  41. package/architecture/lib/graph-loader.ts +82 -0
  42. package/architecture/lib/graph-sorter.d.ts +37 -0
  43. package/architecture/lib/graph-sorter.js +110 -0
  44. package/architecture/lib/graph-sorter.js.map +1 -0
  45. package/architecture/lib/graph-sorter.ts +137 -0
  46. package/architecture/lib/graph-visualizer.d.ts +29 -0
  47. package/architecture/lib/graph-visualizer.js +209 -0
  48. package/architecture/lib/graph-visualizer.js.map +1 -0
  49. package/architecture/lib/graph-visualizer.ts +222 -0
  50. package/architecture/lib/package-validator.d.ts +38 -0
  51. package/architecture/lib/package-validator.js +105 -0
  52. package/architecture/lib/package-validator.js.map +1 -0
  53. package/architecture/lib/package-validator.ts +144 -0
  54. package/config/eslint/base.mjs +6 -0
  55. package/eslint-plugin/__tests__/max-file-lines.test.ts +207 -0
  56. package/eslint-plugin/__tests__/max-method-lines.test.ts +258 -0
  57. package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +359 -0
  58. package/eslint-plugin/index.d.ts +11 -0
  59. package/eslint-plugin/index.js +15 -0
  60. package/eslint-plugin/index.js.map +1 -1
  61. package/eslint-plugin/index.ts +15 -0
  62. package/eslint-plugin/rules/enforce-architecture.d.ts +15 -0
  63. package/eslint-plugin/rules/enforce-architecture.js +406 -0
  64. package/eslint-plugin/rules/enforce-architecture.js.map +1 -0
  65. package/eslint-plugin/rules/enforce-architecture.ts +469 -0
  66. package/eslint-plugin/rules/max-file-lines.d.ts +12 -0
  67. package/eslint-plugin/rules/max-file-lines.js +257 -0
  68. package/eslint-plugin/rules/max-file-lines.js.map +1 -0
  69. package/eslint-plugin/rules/max-file-lines.ts +272 -0
  70. package/eslint-plugin/rules/max-method-lines.d.ts +12 -0
  71. package/eslint-plugin/rules/max-method-lines.js +240 -0
  72. package/eslint-plugin/rules/max-method-lines.js.map +1 -0
  73. package/eslint-plugin/rules/max-method-lines.ts +287 -0
  74. package/eslint-plugin/rules/no-unmanaged-exceptions.d.ts +22 -0
  75. package/eslint-plugin/rules/no-unmanaged-exceptions.js +605 -0
  76. package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +1 -0
  77. package/eslint-plugin/rules/no-unmanaged-exceptions.ts +621 -0
  78. package/executors.json +29 -0
  79. package/package.json +13 -3
  80. package/plugins/circular-deps/index.d.ts +8 -0
  81. package/plugins/circular-deps/index.js +14 -0
  82. package/plugins/circular-deps/index.js.map +1 -0
  83. package/plugins/circular-deps/index.ts +9 -0
  84. package/plugins/circular-deps/plugin.d.ts +32 -0
  85. package/plugins/circular-deps/plugin.js +73 -0
  86. package/plugins/circular-deps/plugin.js.map +1 -0
  87. package/plugins/circular-deps/plugin.ts +83 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * ESLint rule to enforce architecture boundaries
3
+ *
4
+ * Validates that imports from @webpieces/* packages comply with the
5
+ * blessed dependency graph in .graphs/dependencies.json
6
+ *
7
+ * Supports transitive dependencies: if A depends on B and B depends on C,
8
+ * then A can import from C.
9
+ *
10
+ * Configuration:
11
+ * '@webpieces/enforce-architecture': 'error'
12
+ */
13
+
14
+ import type { Rule } from 'eslint';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+
18
+ const DEPENDENCIES_DOC_CONTENT = `# AI Agent Instructions: Architecture Dependency Violation
19
+
20
+ **READ THIS FILE FIRST before making any changes!**
21
+
22
+ ## ⚠️ CRITICAL WARNING ⚠️
23
+
24
+ **This is a VERY IMPORTANT change that has LARGE REPERCUSSIONS later!**
25
+
26
+ Adding new dependencies creates technical debt that compounds over time:
27
+ - Creates coupling between packages that may be hard to undo
28
+ - Can create circular dependency tangles
29
+ - Makes packages harder to test in isolation
30
+ - Increases build times and bundle sizes
31
+ - May force unnecessary upgrades across the codebase
32
+
33
+ **DO NOT add dependencies without senior developer approval!**
34
+
35
+ ## Understanding the Error
36
+
37
+ You've attempted to import from a package that is not in your project's allowed dependencies.
38
+ The architecture enforces a layered dependency structure where:
39
+ - Level 0 packages are foundation packages with NO dependencies on other @webpieces packages
40
+ - Higher level packages can only depend on lower level packages
41
+ - All dependencies must be explicitly declared
42
+
43
+ ## Steps to Resolve
44
+
45
+ ### Step 1: Generate Current Dependency Graph
46
+ Run this command to see the current architecture:
47
+ \`\`\`bash
48
+ npx nx run architecture:generate
49
+ \`\`\`
50
+ This creates/updates \`architecture/dependencies.json\` showing all packages and their levels.
51
+
52
+ ### Step 2: Analyze the Proposed Change
53
+ Ask yourself:
54
+ 1. **Is this import truly necessary?** Can you refactor to avoid it?
55
+ 2. **Should the code move instead?** Maybe the code belongs in a different package.
56
+ 3. **Will this create a cycle?** Use \`npx nx graph\` to visualize dependencies.
57
+ 4. **Can you use an interface/abstraction?** Define interface in lower-level package, implement in higher-level.
58
+
59
+ ### Step 3: Get Senior Developer Approval
60
+
61
+ ## 🛑 AI AGENTS: STOP HERE AND ASK FOR HUMAN APPROVAL! 🛑
62
+
63
+ **YOU MUST NOT PROCEED TO STEP 4 WITHOUT EXPLICIT HUMAN APPROVAL!**
64
+
65
+ **REQUIRED**: Discuss this architectural change with a senior developer before proceeding.
66
+ - Explain why the dependency is needed
67
+ - Show you've considered alternatives (Step 2)
68
+ - **WAIT for explicit approval before making ANY changes to project.json or package.json**
69
+
70
+ **AI Agent Instructions:**
71
+ 1. Present your analysis from Step 2 to the human
72
+ 2. Explain which package needs which dependency and why
73
+ 3. ASK: "Do you approve adding this dependency?"
74
+ 4. **DO NOT modify project.json or package.json until you receive explicit "yes" or approval**
75
+
76
+ ### Step 4: If Approved, Add the Dependency
77
+
78
+ ## ⛔ NEVER MODIFY THESE FILES WITHOUT HUMAN APPROVAL FROM STEP 3! ⛔
79
+
80
+ Only after receiving explicit human approval in Step 3, make these changes:
81
+
82
+ 1. **Update project.json** - Add to \`build.dependsOn\`:
83
+ \`\`\`json
84
+ {
85
+ "targets": {
86
+ "build": {
87
+ "dependsOn": ["^build", "dep1:build", "NEW_PACKAGE:build"]
88
+ }
89
+ }
90
+ }
91
+ \`\`\`
92
+
93
+ 2. **Update package.json** - Add to \`dependencies\`:
94
+ \`\`\`json
95
+ {
96
+ "dependencies": {
97
+ "@webpieces/NEW_PACKAGE": "*"
98
+ }
99
+ }
100
+ \`\`\`
101
+
102
+ ### Step 5: Update Architecture Definition
103
+ Run this command to validate and update the architecture:
104
+ \`\`\`bash
105
+ npx nx run architecture:validate --mode=update
106
+ \`\`\`
107
+
108
+ This will:
109
+ - Detect any cycles (which MUST be fixed before proceeding)
110
+ - Update \`architecture/dependencies.json\` with the new dependency
111
+ - Recalculate package levels
112
+
113
+ ### Step 6: Verify No Cycles
114
+ \`\`\`bash
115
+ npx nx run architecture:validate
116
+ \`\`\`
117
+
118
+ If cycles are detected, you MUST refactor to break the cycle. Common strategies:
119
+ - Move shared code to a lower-level package
120
+ - Use dependency inversion (interfaces in low-level, implementations in high-level)
121
+ - Restructure package boundaries
122
+
123
+ ## Alternative Solutions (Preferred over adding dependencies)
124
+
125
+ ### Option A: Move the Code
126
+ If you need functionality from another package, consider moving that code to a shared lower-level package.
127
+
128
+ ### Option B: Dependency Inversion
129
+ Define an interface in the lower-level package, implement it in the higher-level package:
130
+ \`\`\`typescript
131
+ // In foundation package (level 0)
132
+ export interface Logger { log(msg: string): void; }
133
+
134
+ // In higher-level package
135
+ export class ConsoleLogger implements Logger { ... }
136
+ \`\`\`
137
+
138
+ ### Option C: Pass Dependencies as Parameters
139
+ Instead of importing, receive the dependency as a constructor or method parameter.
140
+
141
+ ## Remember
142
+ - Every dependency you add today is technical debt for tomorrow
143
+ - The best dependency is the one you don't need
144
+ - When in doubt, refactor rather than add dependencies
145
+ `;
146
+
147
+ // Module-level flag to prevent redundant file creation
148
+ let dependenciesDocCreated = false;
149
+
150
+ /**
151
+ * Ensure a documentation file exists at the given path.
152
+ */
153
+ function ensureDocFile(docPath: string, content: string): boolean {
154
+ try {
155
+ fs.mkdirSync(path.dirname(docPath), { recursive: true });
156
+ fs.writeFileSync(docPath, content, 'utf-8');
157
+ return true;
158
+ } catch (err: any) {
159
+ void err;
160
+ console.warn(`[webpieces] Could not create doc file: ${docPath}`);
161
+ return false;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Ensure the dependencies documentation file exists.
167
+ * Called when an architecture violation is detected.
168
+ */
169
+ function ensureDependenciesDoc(workspaceRoot: string): void {
170
+ if (dependenciesDocCreated) return;
171
+ const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.dependencies.md');
172
+ if (ensureDocFile(docPath, DEPENDENCIES_DOC_CONTENT)) {
173
+ dependenciesDocCreated = true;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Graph entry format from .graphs/dependencies.json
179
+ */
180
+ interface GraphEntry {
181
+ level: number;
182
+ dependsOn: string[];
183
+ }
184
+
185
+ type EnhancedGraph = Record<string, GraphEntry>;
186
+
187
+ /**
188
+ * Project mapping entry
189
+ */
190
+ interface ProjectMapping {
191
+ root: string;
192
+ name: string;
193
+ }
194
+
195
+ // Cache for blessed graph (loaded once per lint run)
196
+ let cachedGraph: EnhancedGraph | null = null;
197
+ let cachedGraphPath: string | null = null;
198
+
199
+ // Cache for project mappings
200
+ let cachedProjectMappings: ProjectMapping[] | null = null;
201
+
202
+ /**
203
+ * Find workspace root by walking up from file location
204
+ */
205
+ function findWorkspaceRoot(startPath: string): string {
206
+ let currentDir = path.dirname(startPath);
207
+
208
+ for (let i = 0; i < 20; i++) {
209
+ const packagePath = path.join(currentDir, 'package.json');
210
+ if (fs.existsSync(packagePath)) {
211
+ try {
212
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
213
+ if (pkg.workspaces || pkg.name === 'webpieces-ts') {
214
+ return currentDir;
215
+ }
216
+ } catch (err: any) {
217
+ //const error = toError(err);
218
+ void err;
219
+ }
220
+ }
221
+
222
+ const parent = path.dirname(currentDir);
223
+ if (parent === currentDir) break;
224
+ currentDir = parent;
225
+ }
226
+
227
+ return process.cwd();
228
+ }
229
+
230
+ /**
231
+ * Load blessed graph from architecture/dependencies.json
232
+ */
233
+ function loadBlessedGraph(workspaceRoot: string): EnhancedGraph | null {
234
+ const graphPath = path.join(workspaceRoot, 'architecture', 'dependencies.json');
235
+
236
+ // Return cached if same path
237
+ if (cachedGraphPath === graphPath && cachedGraph !== null) {
238
+ return cachedGraph;
239
+ }
240
+
241
+ if (!fs.existsSync(graphPath)) {
242
+ return null;
243
+ }
244
+
245
+ try {
246
+ const content = fs.readFileSync(graphPath, 'utf-8');
247
+ cachedGraph = JSON.parse(content) as EnhancedGraph;
248
+ cachedGraphPath = graphPath;
249
+ return cachedGraph;
250
+ } catch (err: any) {
251
+ //const error = toError(err);
252
+ // err is used below
253
+ console.error(`[ESLint @webpieces/enforce-architecture] Could not load graph: ${err}`);
254
+ return null;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Build project mappings from project.json files in workspace
260
+ */
261
+ function buildProjectMappings(workspaceRoot: string): ProjectMapping[] {
262
+ if (cachedProjectMappings !== null) {
263
+ return cachedProjectMappings;
264
+ }
265
+
266
+ const mappings: ProjectMapping[] = [];
267
+
268
+ // Scan common locations for project.json files
269
+ const searchDirs = ['packages', 'apps', 'libs', 'libraries', 'services'];
270
+
271
+ for (const searchDir of searchDirs) {
272
+ const searchPath = path.join(workspaceRoot, searchDir);
273
+ if (!fs.existsSync(searchPath)) continue;
274
+
275
+ scanForProjects(searchPath, workspaceRoot, mappings);
276
+ }
277
+
278
+ // Sort by path length (longest first) for more specific matching
279
+ mappings.sort((a, b) => b.root.length - a.root.length);
280
+
281
+ cachedProjectMappings = mappings;
282
+ return mappings;
283
+ }
284
+
285
+ /**
286
+ * Recursively scan for project.json files
287
+ */
288
+ function scanForProjects(
289
+ dir: string,
290
+ workspaceRoot: string,
291
+ mappings: ProjectMapping[]
292
+ ): void {
293
+ try {
294
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
295
+
296
+ for (const entry of entries) {
297
+ const fullPath = path.join(dir, entry.name);
298
+
299
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
300
+ // Check for project.json in this directory
301
+ const projectJsonPath = path.join(fullPath, 'project.json');
302
+ if (fs.existsSync(projectJsonPath)) {
303
+ try {
304
+ const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
305
+ const projectRoot = path.relative(workspaceRoot, fullPath);
306
+
307
+ // Determine project name
308
+ let projectName = projectJson.name || entry.name;
309
+
310
+ // Add @webpieces/ prefix if not present
311
+ if (!projectName.startsWith('@webpieces/')) {
312
+ projectName = `@webpieces/${projectName}`;
313
+ }
314
+
315
+ mappings.push({
316
+ root: projectRoot,
317
+ name: projectName,
318
+ });
319
+ } catch (err: any) {
320
+ //const error = toError(err);
321
+ void err;
322
+ }
323
+ }
324
+
325
+ // Continue scanning subdirectories
326
+ scanForProjects(fullPath, workspaceRoot, mappings);
327
+ }
328
+ }
329
+ } catch (err: any) {
330
+ //const error = toError(err);
331
+ void err;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Get project name from file path
337
+ */
338
+ function getProjectFromFile(filePath: string, workspaceRoot: string): string | null {
339
+ const relativePath = path.relative(workspaceRoot, filePath).replace(/\\/g, '/');
340
+ const mappings = buildProjectMappings(workspaceRoot);
341
+
342
+ for (const mapping of mappings) {
343
+ if (relativePath.startsWith(mapping.root + '/') || relativePath.startsWith(mapping.root)) {
344
+ return mapping.name;
345
+ }
346
+ }
347
+
348
+ return null;
349
+ }
350
+
351
+ /**
352
+ * Compute all transitive dependencies for a project
353
+ */
354
+ function computeTransitiveDependencies(project: string, graph: EnhancedGraph): Set<string> {
355
+ const result = new Set<string>();
356
+ const visited = new Set<string>();
357
+
358
+ function visit(currentProject: string): void {
359
+ if (visited.has(currentProject)) return;
360
+ visited.add(currentProject);
361
+
362
+ const entry = graph[currentProject];
363
+ if (!entry || !entry.dependsOn) return;
364
+
365
+ for (const dep of entry.dependsOn) {
366
+ result.add(dep);
367
+ visit(dep);
368
+ }
369
+ }
370
+
371
+ visit(project);
372
+ return result;
373
+ }
374
+
375
+ const rule: Rule.RuleModule = {
376
+ meta: {
377
+ type: 'problem',
378
+ docs: {
379
+ description: 'Enforce blessed architecture dependencies',
380
+ category: 'Best Practices',
381
+ recommended: true,
382
+ url: 'https://github.com/deanhiller/webpieces-ts',
383
+ },
384
+ messages: {
385
+ illegalImport:
386
+ "⚠️ AI Agent: READ tmp/webpieces/webpieces.dependencies.md for resolution steps!\n\n" +
387
+ "Import '{{imported}}' violates architecture boundaries.\n\n" +
388
+ "Project '{{project}}' (level {{level}}) can only import from:\n" +
389
+ '{{allowedList}}',
390
+ noGraph:
391
+ 'No architecture graph found at architecture/dependencies.json\n' +
392
+ 'Run: nx run architecture:validate --mode=update',
393
+ },
394
+ schema: [],
395
+ },
396
+
397
+ create(context: Rule.RuleContext): Rule.RuleListener {
398
+ const filename = context.filename || context.getFilename();
399
+ const workspaceRoot = findWorkspaceRoot(filename);
400
+
401
+ return {
402
+ ImportDeclaration(node: any): void {
403
+ const importPath = node.source.value as string;
404
+
405
+ // Only check @webpieces/* imports
406
+ if (!importPath.startsWith('@webpieces/')) {
407
+ return;
408
+ }
409
+
410
+ // Determine which project this file belongs to
411
+ const project = getProjectFromFile(filename, workspaceRoot);
412
+ if (!project) {
413
+ // File not in any known project (e.g., tools/, scripts/)
414
+ return;
415
+ }
416
+
417
+ // Self-import is always allowed
418
+ if (importPath === project) {
419
+ return;
420
+ }
421
+
422
+ // Load blessed graph
423
+ const graph = loadBlessedGraph(workspaceRoot);
424
+ if (!graph) {
425
+ // No graph file - warn but don't fail (allows gradual adoption)
426
+ // Uncomment below to enforce graph existence:
427
+ // context.report({ node: node.source, messageId: 'noGraph' });
428
+ return;
429
+ }
430
+
431
+ // Get project entry
432
+ const projectEntry = graph[project];
433
+ if (!projectEntry) {
434
+ // Project not in graph (new project?) - allow
435
+ return;
436
+ }
437
+
438
+ // Compute allowed dependencies (direct + transitive)
439
+ const allowedDeps = computeTransitiveDependencies(project, graph);
440
+
441
+ // Check if import is allowed
442
+ if (!allowedDeps.has(importPath)) {
443
+ // Write documentation file for AI/developer to read
444
+ ensureDependenciesDoc(workspaceRoot);
445
+
446
+ const directDeps = projectEntry.dependsOn || [];
447
+ const allowedList =
448
+ directDeps.length > 0
449
+ ? directDeps.map((dep) => ` - ${dep}`).join('\n') +
450
+ '\n (and their transitive dependencies)'
451
+ : ' (none - this is a foundation project)';
452
+
453
+ context.report({
454
+ node: node.source,
455
+ messageId: 'illegalImport',
456
+ data: {
457
+ imported: importPath,
458
+ project: project,
459
+ level: String(projectEntry.level),
460
+ allowedList: allowedList,
461
+ },
462
+ });
463
+ }
464
+ },
465
+ };
466
+ },
467
+ };
468
+
469
+ export = rule;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ESLint rule to enforce maximum file length
3
+ *
4
+ * Enforces a configurable maximum line count for files.
5
+ * Default: 700 lines
6
+ *
7
+ * Configuration:
8
+ * '@webpieces/max-file-lines': ['error', { max: 700 }]
9
+ */
10
+ import type { Rule } from 'eslint';
11
+ declare const rule: Rule.RuleModule;
12
+ export = rule;