@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,257 @@
1
+ "use strict";
2
+ /**
3
+ * ESLint rule to enforce maximum file length
4
+ *
5
+ * Enforces a configurable maximum line count for files.
6
+ * Default: 700 lines
7
+ *
8
+ * Configuration:
9
+ * '@webpieces/max-file-lines': ['error', { max: 700 }]
10
+ */
11
+ const tslib_1 = require("tslib");
12
+ const fs = tslib_1.__importStar(require("fs"));
13
+ const path = tslib_1.__importStar(require("path"));
14
+ const FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
15
+
16
+ **READ THIS FILE to fix files that are too long**
17
+
18
+ ## Core Principle
19
+ Files should contain a SINGLE COHESIVE UNIT.
20
+ - One class per file (Java convention)
21
+ - If class is too large, extract child responsibilities
22
+ - Use dependency injection to compose functionality
23
+
24
+ ## Command: Reduce File Size
25
+
26
+ ### Step 1: Check for Multiple Classes
27
+ If the file contains multiple classes, **SEPARATE each class into its own file**.
28
+
29
+ \`\`\`typescript
30
+ // BAD: UserController.ts (multiple classes)
31
+ export class UserController { /* ... */ }
32
+ export class UserValidator { /* ... */ }
33
+ export class UserNotifier { /* ... */ }
34
+
35
+ // GOOD: Three separate files
36
+ // UserController.ts
37
+ export class UserController { /* ... */ }
38
+
39
+ // UserValidator.ts
40
+ export class UserValidator { /* ... */ }
41
+
42
+ // UserNotifier.ts
43
+ export class UserNotifier { /* ... */ }
44
+ \`\`\`
45
+
46
+ ### Step 2: Extract Child Responsibilities (if single class is too large)
47
+
48
+ #### Pattern: Create New Service Class with Dependency Injection
49
+
50
+ \`\`\`typescript
51
+ // BAD: UserController.ts (800 lines, single class)
52
+ @provideSingleton()
53
+ @Controller()
54
+ export class UserController {
55
+ // 200 lines: CRUD operations
56
+ // 300 lines: validation logic
57
+ // 200 lines: notification logic
58
+ // 100 lines: analytics logic
59
+ }
60
+
61
+ // GOOD: Extract validation service
62
+ // 1. Create UserValidationService.ts
63
+ @provideSingleton()
64
+ export class UserValidationService {
65
+ validateUserData(data: UserData): ValidationResult {
66
+ // 300 lines of validation logic moved here
67
+ }
68
+
69
+ validateEmail(email: string): boolean { /* ... */ }
70
+ validatePassword(password: string): boolean { /* ... */ }
71
+ }
72
+
73
+ // 2. Inject into UserController.ts
74
+ @provideSingleton()
75
+ @Controller()
76
+ export class UserController {
77
+ constructor(
78
+ @inject(TYPES.UserValidationService)
79
+ private validator: UserValidationService
80
+ ) {}
81
+
82
+ async createUser(data: UserData): Promise<User> {
83
+ const validation = this.validator.validateUserData(data);
84
+ if (!validation.isValid) {
85
+ throw new ValidationError(validation.errors);
86
+ }
87
+ // ... rest of logic
88
+ }
89
+ }
90
+ \`\`\`
91
+
92
+ ## AI Agent Action Steps
93
+
94
+ 1. **ANALYZE** the file structure:
95
+ - Count classes (if >1, separate immediately)
96
+ - Identify logical responsibilities within single class
97
+
98
+ 2. **IDENTIFY** "child code" to extract:
99
+ - Validation logic -> ValidationService
100
+ - Notification logic -> NotificationService
101
+ - Data transformation -> TransformerService
102
+ - External API calls -> ApiService
103
+ - Business rules -> RulesEngine
104
+
105
+ 3. **CREATE** new service file(s):
106
+ - Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
107
+ - Add \`@provideSingleton()\` decorator
108
+ - Move child methods to new class
109
+
110
+ 4. **UPDATE** dependency injection:
111
+ - Add to \`TYPES\` constants (if using symbol-based DI)
112
+ - Inject new service into original class constructor
113
+ - Replace direct method calls with \`this.serviceName.method()\`
114
+
115
+ 5. **RENAME** extracted file:
116
+ - Read the extracted code to understand its purpose
117
+ - Rename \`XXXX.ts\` to logical name (e.g., \`UserValidationService.ts\`)
118
+
119
+ 6. **VERIFY** file sizes:
120
+ - Original file should now be <700 lines
121
+ - Each extracted file should be <700 lines
122
+ - If still too large, extract more services
123
+
124
+ ## Examples of Child Responsibilities to Extract
125
+
126
+ | If File Contains | Extract To | Pattern |
127
+ |-----------------|------------|---------|
128
+ | Validation logic (200+ lines) | \`XValidator.ts\` or \`XValidationService.ts\` | Singleton service |
129
+ | Notification logic (150+ lines) | \`XNotifier.ts\` or \`XNotificationService.ts\` | Singleton service |
130
+ | Data transformation (200+ lines) | \`XTransformer.ts\` | Singleton service |
131
+ | External API calls (200+ lines) | \`XApiClient.ts\` | Singleton service |
132
+ | Complex business rules (300+ lines) | \`XRulesEngine.ts\` | Singleton service |
133
+ | Database queries (200+ lines) | \`XRepository.ts\` | Singleton service |
134
+
135
+ ## WebPieces Dependency Injection Pattern
136
+
137
+ \`\`\`typescript
138
+ // 1. Define service with @provideSingleton
139
+ import { provideSingleton } from '@webpieces/http-routing';
140
+
141
+ @provideSingleton()
142
+ export class MyService {
143
+ doSomething(): void { /* ... */ }
144
+ }
145
+
146
+ // 2. Inject into consumer
147
+ import { inject } from 'inversify';
148
+ import { TYPES } from './types';
149
+
150
+ @provideSingleton()
151
+ @Controller()
152
+ export class MyController {
153
+ constructor(
154
+ @inject(TYPES.MyService) private service: MyService
155
+ ) {}
156
+ }
157
+ \`\`\`
158
+
159
+ Remember: Find the "child code" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.
160
+ `;
161
+ // Module-level flag to prevent redundant file creation
162
+ let fileDocCreated = false;
163
+ function getWorkspaceRoot(context) {
164
+ const filename = context.filename || context.getFilename();
165
+ let dir = path.dirname(filename);
166
+ // Walk up directory tree to find workspace root
167
+ while (dir !== path.dirname(dir)) {
168
+ const pkgPath = path.join(dir, 'package.json');
169
+ if (fs.existsSync(pkgPath)) {
170
+ try {
171
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
172
+ if (pkg.workspaces || pkg.name === 'webpieces-ts') {
173
+ return dir;
174
+ }
175
+ }
176
+ catch (err) {
177
+ //const error = toError(err);
178
+ void err; // Continue searching if JSON parse fails
179
+ }
180
+ }
181
+ dir = path.dirname(dir);
182
+ }
183
+ return process.cwd(); // Fallback
184
+ }
185
+ function ensureDocFile(docPath, content) {
186
+ try {
187
+ fs.mkdirSync(path.dirname(docPath), { recursive: true });
188
+ fs.writeFileSync(docPath, content, 'utf-8');
189
+ return true;
190
+ }
191
+ catch (err) {
192
+ //const error = toError(err);
193
+ // err is used in console.warn below
194
+ console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
195
+ return false;
196
+ }
197
+ }
198
+ function ensureFileDoc(context) {
199
+ if (fileDocCreated)
200
+ return; // Performance: only create once per lint run
201
+ const workspaceRoot = getWorkspaceRoot(context);
202
+ const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.filesize.md');
203
+ if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {
204
+ fileDocCreated = true;
205
+ }
206
+ }
207
+ const rule = {
208
+ meta: {
209
+ type: 'suggestion',
210
+ docs: {
211
+ description: 'Enforce maximum file length',
212
+ category: 'Best Practices',
213
+ recommended: false,
214
+ url: 'https://github.com/deanhiller/webpieces-ts',
215
+ },
216
+ messages: {
217
+ tooLong: 'AI Agent: READ tmp/webpieces/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',
218
+ },
219
+ fixable: undefined,
220
+ schema: [
221
+ {
222
+ type: 'object',
223
+ properties: {
224
+ max: {
225
+ type: 'integer',
226
+ minimum: 1,
227
+ },
228
+ },
229
+ additionalProperties: false,
230
+ },
231
+ ],
232
+ },
233
+ create(context) {
234
+ const options = context.options[0];
235
+ const maxLines = options?.max ?? 700;
236
+ return {
237
+ Program(node) {
238
+ ensureFileDoc(context);
239
+ const sourceCode = context.sourceCode || context.getSourceCode();
240
+ const lines = sourceCode.lines;
241
+ const lineCount = lines.length;
242
+ if (lineCount > maxLines) {
243
+ context.report({
244
+ node,
245
+ messageId: 'tooLong',
246
+ data: {
247
+ actual: String(lineCount),
248
+ max: String(maxLines),
249
+ },
250
+ });
251
+ }
252
+ },
253
+ };
254
+ },
255
+ };
256
+ module.exports = rule;
257
+ //# sourceMappingURL=max-file-lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/max-file-lines.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAGH,+CAAyB;AACzB,mDAA6B;AAM7B,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkJxB,CAAC;AAEF,uDAAuD;AACvD,IAAI,cAAc,GAAG,KAAK,CAAC;AAE3B,SAAS,gBAAgB,CAAC,OAAyB;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAC3D,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEjC,gDAAgD;IAChD,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAC/C,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC;gBACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1D,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;oBAChD,OAAO,GAAG,CAAC;gBACf,CAAC;YACL,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAChB,6BAA6B;gBAC7B,KAAK,GAAG,CAAC,CAAC,yCAAyC;YACvD,CAAC;QACL,CAAC;QACD,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW;AACrC,CAAC;AAED,SAAS,aAAa,CAAC,OAAe,EAAE,OAAe;IACnD,IAAI,CAAC;QACD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAChB,6BAA6B;QAC7B,oCAAoC;QACpC,OAAO,CAAC,IAAI,CAAC,0CAA0C,OAAO,EAAE,EAAE,GAAG,CAAC,CAAC;QACvE,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,OAAyB;IAC5C,IAAI,cAAc;QAAE,OAAO,CAAC,6CAA6C;IAEzE,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,uBAAuB,CAAC,CAAC;IAEtF,IAAI,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAE,CAAC;QAC3C,cAAc,GAAG,IAAI,CAAC;IAC1B,CAAC;AACL,CAAC;AAED,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACF,WAAW,EAAE,6BAA6B;YAC1C,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,KAAK;YAClB,GAAG,EAAE,4CAA4C;SACpD;QACD,QAAQ,EAAE;YACN,OAAO,EACH,mHAAmH;SAC1H;QACD,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE;YACJ;gBACI,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACR,GAAG,EAAE;wBACD,IAAI,EAAE,SAAS;wBACf,OAAO,EAAE,CAAC;qBACb;iBACJ;gBACD,oBAAoB,EAAE,KAAK;aAC9B;SACJ;KACJ;IAED,MAAM,CAAC,OAAyB;QAC5B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAiC,CAAC;QACnE,MAAM,QAAQ,GAAG,OAAO,EAAE,GAAG,IAAI,GAAG,CAAC;QAErC,OAAO;YACH,OAAO,CAAC,IAAS;gBACb,aAAa,CAAC,OAAO,CAAC,CAAC;gBAEvB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;gBACjE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC;gBAC/B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;gBAE/B,IAAI,SAAS,GAAG,QAAQ,EAAE,CAAC;oBACvB,OAAO,CAAC,MAAM,CAAC;wBACX,IAAI;wBACJ,SAAS,EAAE,SAAS;wBACpB,IAAI,EAAE;4BACF,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;4BACzB,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC;yBACxB;qBACJ,CAAC,CAAC;gBACP,CAAC;YACL,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","sourcesContent":["/**\n * ESLint rule to enforce maximum file length\n *\n * Enforces a configurable maximum line count for files.\n * Default: 700 lines\n *\n * Configuration:\n * '@webpieces/max-file-lines': ['error', { max: 700 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\ninterface FileLinesOptions {\n max: number;\n}\n\nconst FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\nFiles should contain a SINGLE COHESIVE UNIT.\n- One class per file (Java convention)\n- If class is too large, extract child responsibilities\n- Use dependency injection to compose functionality\n\n## Command: Reduce File Size\n\n### Step 1: Check for Multiple Classes\nIf the file contains multiple classes, **SEPARATE each class into its own file**.\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (multiple classes)\nexport class UserController { /* ... */ }\nexport class UserValidator { /* ... */ }\nexport class UserNotifier { /* ... */ }\n\n// GOOD: Three separate files\n// UserController.ts\nexport class UserController { /* ... */ }\n\n// UserValidator.ts\nexport class UserValidator { /* ... */ }\n\n// UserNotifier.ts\nexport class UserNotifier { /* ... */ }\n\\`\\`\\`\n\n### Step 2: Extract Child Responsibilities (if single class is too large)\n\n#### Pattern: Create New Service Class with Dependency Injection\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (800 lines, single class)\n@provideSingleton()\n@Controller()\nexport class UserController {\n // 200 lines: CRUD operations\n // 300 lines: validation logic\n // 200 lines: notification logic\n // 100 lines: analytics logic\n}\n\n// GOOD: Extract validation service\n// 1. Create UserValidationService.ts\n@provideSingleton()\nexport class UserValidationService {\n validateUserData(data: UserData): ValidationResult {\n // 300 lines of validation logic moved here\n }\n\n validateEmail(email: string): boolean { /* ... */ }\n validatePassword(password: string): boolean { /* ... */ }\n}\n\n// 2. Inject into UserController.ts\n@provideSingleton()\n@Controller()\nexport class UserController {\n constructor(\n @inject(TYPES.UserValidationService)\n private validator: UserValidationService\n ) {}\n\n async createUser(data: UserData): Promise<User> {\n const validation = this.validator.validateUserData(data);\n if (!validation.isValid) {\n throw new ValidationError(validation.errors);\n }\n // ... rest of logic\n }\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **ANALYZE** the file structure:\n - Count classes (if >1, separate immediately)\n - Identify logical responsibilities within single class\n\n2. **IDENTIFY** \"child code\" to extract:\n - Validation logic -> ValidationService\n - Notification logic -> NotificationService\n - Data transformation -> TransformerService\n - External API calls -> ApiService\n - Business rules -> RulesEngine\n\n3. **CREATE** new service file(s):\n - Start with temporary name: \\`XXXX.ts\\` or \\`ChildService.ts\\`\n - Add \\`@provideSingleton()\\` decorator\n - Move child methods to new class\n\n4. **UPDATE** dependency injection:\n - Add to \\`TYPES\\` constants (if using symbol-based DI)\n - Inject new service into original class constructor\n - Replace direct method calls with \\`this.serviceName.method()\\`\n\n5. **RENAME** extracted file:\n - Read the extracted code to understand its purpose\n - Rename \\`XXXX.ts\\` to logical name (e.g., \\`UserValidationService.ts\\`)\n\n6. **VERIFY** file sizes:\n - Original file should now be <700 lines\n - Each extracted file should be <700 lines\n - If still too large, extract more services\n\n## Examples of Child Responsibilities to Extract\n\n| If File Contains | Extract To | Pattern |\n|-----------------|------------|---------|\n| Validation logic (200+ lines) | \\`XValidator.ts\\` or \\`XValidationService.ts\\` | Singleton service |\n| Notification logic (150+ lines) | \\`XNotifier.ts\\` or \\`XNotificationService.ts\\` | Singleton service |\n| Data transformation (200+ lines) | \\`XTransformer.ts\\` | Singleton service |\n| External API calls (200+ lines) | \\`XApiClient.ts\\` | Singleton service |\n| Complex business rules (300+ lines) | \\`XRulesEngine.ts\\` | Singleton service |\n| Database queries (200+ lines) | \\`XRepository.ts\\` | Singleton service |\n\n## WebPieces Dependency Injection Pattern\n\n\\`\\`\\`typescript\n// 1. Define service with @provideSingleton\nimport { provideSingleton } from '@webpieces/http-routing';\n\n@provideSingleton()\nexport class MyService {\n doSomething(): void { /* ... */ }\n}\n\n// 2. Inject into consumer\nimport { inject } from 'inversify';\nimport { TYPES } from './types';\n\n@provideSingleton()\n@Controller()\nexport class MyController {\n constructor(\n @inject(TYPES.MyService) private service: MyService\n ) {}\n}\n\\`\\`\\`\n\nRemember: Find the \"child code\" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.\n`;\n\n// Module-level flag to prevent redundant file creation\nlet fileDocCreated = false;\n\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n // Walk up directory tree to find workspace root\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: any) {\n //const error = toError(err);\n void err; // Continue searching if JSON parse fails\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd(); // Fallback\n}\n\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n //const error = toError(err);\n // err is used in console.warn below\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);\n return false;\n }\n}\n\nfunction ensureFileDoc(context: Rule.RuleContext): void {\n if (fileDocCreated) return; // Performance: only create once per lint run\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.filesize.md');\n\n if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {\n fileDocCreated = true;\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum file length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ tmp/webpieces/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as FileLinesOptions | undefined;\n const maxLines = options?.max ?? 700;\n\n return {\n Program(node: any): void {\n ensureFileDoc(context);\n\n const sourceCode = context.sourceCode || context.getSourceCode();\n const lines = sourceCode.lines;\n const lineCount = lines.length;\n\n if (lineCount > maxLines) {\n context.report({\n node,\n messageId: 'tooLong',\n data: {\n actual: String(lineCount),\n max: String(maxLines),\n },\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
@@ -0,0 +1,272 @@
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
+
11
+ import type { Rule } from 'eslint';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+
15
+ interface FileLinesOptions {
16
+ max: number;
17
+ }
18
+
19
+ const FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
20
+
21
+ **READ THIS FILE to fix files that are too long**
22
+
23
+ ## Core Principle
24
+ Files should contain a SINGLE COHESIVE UNIT.
25
+ - One class per file (Java convention)
26
+ - If class is too large, extract child responsibilities
27
+ - Use dependency injection to compose functionality
28
+
29
+ ## Command: Reduce File Size
30
+
31
+ ### Step 1: Check for Multiple Classes
32
+ If the file contains multiple classes, **SEPARATE each class into its own file**.
33
+
34
+ \`\`\`typescript
35
+ // BAD: UserController.ts (multiple classes)
36
+ export class UserController { /* ... */ }
37
+ export class UserValidator { /* ... */ }
38
+ export class UserNotifier { /* ... */ }
39
+
40
+ // GOOD: Three separate files
41
+ // UserController.ts
42
+ export class UserController { /* ... */ }
43
+
44
+ // UserValidator.ts
45
+ export class UserValidator { /* ... */ }
46
+
47
+ // UserNotifier.ts
48
+ export class UserNotifier { /* ... */ }
49
+ \`\`\`
50
+
51
+ ### Step 2: Extract Child Responsibilities (if single class is too large)
52
+
53
+ #### Pattern: Create New Service Class with Dependency Injection
54
+
55
+ \`\`\`typescript
56
+ // BAD: UserController.ts (800 lines, single class)
57
+ @provideSingleton()
58
+ @Controller()
59
+ export class UserController {
60
+ // 200 lines: CRUD operations
61
+ // 300 lines: validation logic
62
+ // 200 lines: notification logic
63
+ // 100 lines: analytics logic
64
+ }
65
+
66
+ // GOOD: Extract validation service
67
+ // 1. Create UserValidationService.ts
68
+ @provideSingleton()
69
+ export class UserValidationService {
70
+ validateUserData(data: UserData): ValidationResult {
71
+ // 300 lines of validation logic moved here
72
+ }
73
+
74
+ validateEmail(email: string): boolean { /* ... */ }
75
+ validatePassword(password: string): boolean { /* ... */ }
76
+ }
77
+
78
+ // 2. Inject into UserController.ts
79
+ @provideSingleton()
80
+ @Controller()
81
+ export class UserController {
82
+ constructor(
83
+ @inject(TYPES.UserValidationService)
84
+ private validator: UserValidationService
85
+ ) {}
86
+
87
+ async createUser(data: UserData): Promise<User> {
88
+ const validation = this.validator.validateUserData(data);
89
+ if (!validation.isValid) {
90
+ throw new ValidationError(validation.errors);
91
+ }
92
+ // ... rest of logic
93
+ }
94
+ }
95
+ \`\`\`
96
+
97
+ ## AI Agent Action Steps
98
+
99
+ 1. **ANALYZE** the file structure:
100
+ - Count classes (if >1, separate immediately)
101
+ - Identify logical responsibilities within single class
102
+
103
+ 2. **IDENTIFY** "child code" to extract:
104
+ - Validation logic -> ValidationService
105
+ - Notification logic -> NotificationService
106
+ - Data transformation -> TransformerService
107
+ - External API calls -> ApiService
108
+ - Business rules -> RulesEngine
109
+
110
+ 3. **CREATE** new service file(s):
111
+ - Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
112
+ - Add \`@provideSingleton()\` decorator
113
+ - Move child methods to new class
114
+
115
+ 4. **UPDATE** dependency injection:
116
+ - Add to \`TYPES\` constants (if using symbol-based DI)
117
+ - Inject new service into original class constructor
118
+ - Replace direct method calls with \`this.serviceName.method()\`
119
+
120
+ 5. **RENAME** extracted file:
121
+ - Read the extracted code to understand its purpose
122
+ - Rename \`XXXX.ts\` to logical name (e.g., \`UserValidationService.ts\`)
123
+
124
+ 6. **VERIFY** file sizes:
125
+ - Original file should now be <700 lines
126
+ - Each extracted file should be <700 lines
127
+ - If still too large, extract more services
128
+
129
+ ## Examples of Child Responsibilities to Extract
130
+
131
+ | If File Contains | Extract To | Pattern |
132
+ |-----------------|------------|---------|
133
+ | Validation logic (200+ lines) | \`XValidator.ts\` or \`XValidationService.ts\` | Singleton service |
134
+ | Notification logic (150+ lines) | \`XNotifier.ts\` or \`XNotificationService.ts\` | Singleton service |
135
+ | Data transformation (200+ lines) | \`XTransformer.ts\` | Singleton service |
136
+ | External API calls (200+ lines) | \`XApiClient.ts\` | Singleton service |
137
+ | Complex business rules (300+ lines) | \`XRulesEngine.ts\` | Singleton service |
138
+ | Database queries (200+ lines) | \`XRepository.ts\` | Singleton service |
139
+
140
+ ## WebPieces Dependency Injection Pattern
141
+
142
+ \`\`\`typescript
143
+ // 1. Define service with @provideSingleton
144
+ import { provideSingleton } from '@webpieces/http-routing';
145
+
146
+ @provideSingleton()
147
+ export class MyService {
148
+ doSomething(): void { /* ... */ }
149
+ }
150
+
151
+ // 2. Inject into consumer
152
+ import { inject } from 'inversify';
153
+ import { TYPES } from './types';
154
+
155
+ @provideSingleton()
156
+ @Controller()
157
+ export class MyController {
158
+ constructor(
159
+ @inject(TYPES.MyService) private service: MyService
160
+ ) {}
161
+ }
162
+ \`\`\`
163
+
164
+ Remember: Find the "child code" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.
165
+ `;
166
+
167
+ // Module-level flag to prevent redundant file creation
168
+ let fileDocCreated = false;
169
+
170
+ function getWorkspaceRoot(context: Rule.RuleContext): string {
171
+ const filename = context.filename || context.getFilename();
172
+ let dir = path.dirname(filename);
173
+
174
+ // Walk up directory tree to find workspace root
175
+ while (dir !== path.dirname(dir)) {
176
+ const pkgPath = path.join(dir, 'package.json');
177
+ if (fs.existsSync(pkgPath)) {
178
+ try {
179
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
180
+ if (pkg.workspaces || pkg.name === 'webpieces-ts') {
181
+ return dir;
182
+ }
183
+ } catch (err: any) {
184
+ //const error = toError(err);
185
+ void err; // Continue searching if JSON parse fails
186
+ }
187
+ }
188
+ dir = path.dirname(dir);
189
+ }
190
+ return process.cwd(); // Fallback
191
+ }
192
+
193
+ function ensureDocFile(docPath: string, content: string): boolean {
194
+ try {
195
+ fs.mkdirSync(path.dirname(docPath), { recursive: true });
196
+ fs.writeFileSync(docPath, content, 'utf-8');
197
+ return true;
198
+ } catch (err: any) {
199
+ //const error = toError(err);
200
+ // err is used in console.warn below
201
+ console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
202
+ return false;
203
+ }
204
+ }
205
+
206
+ function ensureFileDoc(context: Rule.RuleContext): void {
207
+ if (fileDocCreated) return; // Performance: only create once per lint run
208
+
209
+ const workspaceRoot = getWorkspaceRoot(context);
210
+ const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.filesize.md');
211
+
212
+ if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {
213
+ fileDocCreated = true;
214
+ }
215
+ }
216
+
217
+ const rule: Rule.RuleModule = {
218
+ meta: {
219
+ type: 'suggestion',
220
+ docs: {
221
+ description: 'Enforce maximum file length',
222
+ category: 'Best Practices',
223
+ recommended: false,
224
+ url: 'https://github.com/deanhiller/webpieces-ts',
225
+ },
226
+ messages: {
227
+ tooLong:
228
+ 'AI Agent: READ tmp/webpieces/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',
229
+ },
230
+ fixable: undefined,
231
+ schema: [
232
+ {
233
+ type: 'object',
234
+ properties: {
235
+ max: {
236
+ type: 'integer',
237
+ minimum: 1,
238
+ },
239
+ },
240
+ additionalProperties: false,
241
+ },
242
+ ],
243
+ },
244
+
245
+ create(context: Rule.RuleContext): Rule.RuleListener {
246
+ const options = context.options[0] as FileLinesOptions | undefined;
247
+ const maxLines = options?.max ?? 700;
248
+
249
+ return {
250
+ Program(node: any): void {
251
+ ensureFileDoc(context);
252
+
253
+ const sourceCode = context.sourceCode || context.getSourceCode();
254
+ const lines = sourceCode.lines;
255
+ const lineCount = lines.length;
256
+
257
+ if (lineCount > maxLines) {
258
+ context.report({
259
+ node,
260
+ messageId: 'tooLong',
261
+ data: {
262
+ actual: String(lineCount),
263
+ max: String(maxLines),
264
+ },
265
+ });
266
+ }
267
+ },
268
+ };
269
+ },
270
+ };
271
+
272
+ export = rule;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ESLint rule to enforce maximum method length
3
+ *
4
+ * Enforces a configurable maximum line count for methods, functions, and arrow functions.
5
+ * Default: 70 lines
6
+ *
7
+ * Configuration:
8
+ * '@webpieces/max-method-lines': ['error', { max: 70 }]
9
+ */
10
+ import type { Rule } from 'eslint';
11
+ declare const rule: Rule.RuleModule;
12
+ export = rule;