@webpieces/dev-config 0.2.94 → 0.2.97

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 (183) hide show
  1. package/config/eslint/base.mjs +1 -1
  2. package/executors.json +6 -91
  3. package/package.json +6 -19
  4. package/{executors → src/executors}/help/executor.d.ts +4 -2
  5. package/src/executors/help/executor.js.map +1 -0
  6. package/{executors → src/executors}/validate-eslint-sync/executor.d.ts +3 -2
  7. package/src/executors/validate-eslint-sync/executor.js.map +1 -0
  8. package/{executors → src/executors}/validate-versions-locked/executor.js +5 -3
  9. package/src/executors/validate-versions-locked/executor.js.map +1 -0
  10. package/src/generators/init/generator.js.map +1 -1
  11. package/src/index.d.ts +1 -1
  12. package/src/index.js +1 -1
  13. package/src/index.js.map +1 -1
  14. package/src/plugin.d.ts +86 -0
  15. package/{plugin.js → src/plugin.js} +31 -15
  16. package/src/plugin.js.map +1 -0
  17. package/src/toError.d.ts +5 -0
  18. package/src/toError.js +37 -0
  19. package/src/toError.js.map +1 -0
  20. package/templates/eslint.webpieces.config.mjs +1 -1
  21. package/templates/webpieces.exceptions.md +15 -15
  22. package/architecture/executors/diff-utils.d.ts +0 -24
  23. package/architecture/executors/diff-utils.js +0 -119
  24. package/architecture/executors/diff-utils.js.map +0 -1
  25. package/architecture/executors/diff-utils.ts +0 -127
  26. package/architecture/executors/generate/executor.d.ts +0 -16
  27. package/architecture/executors/generate/executor.js +0 -44
  28. package/architecture/executors/generate/executor.js.map +0 -1
  29. package/architecture/executors/generate/executor.ts +0 -59
  30. package/architecture/executors/generate/schema.json +0 -14
  31. package/architecture/executors/validate-architecture-unchanged/executor.d.ts +0 -17
  32. package/architecture/executors/validate-architecture-unchanged/executor.js +0 -229
  33. package/architecture/executors/validate-architecture-unchanged/executor.js.map +0 -1
  34. package/architecture/executors/validate-architecture-unchanged/executor.ts +0 -251
  35. package/architecture/executors/validate-architecture-unchanged/schema.json +0 -14
  36. package/architecture/executors/validate-code/executor.d.ts +0 -78
  37. package/architecture/executors/validate-code/executor.js +0 -243
  38. package/architecture/executors/validate-code/executor.js.map +0 -1
  39. package/architecture/executors/validate-code/executor.ts +0 -406
  40. package/architecture/executors/validate-code/schema.json +0 -227
  41. package/architecture/executors/validate-dtos/executor.d.ts +0 -42
  42. package/architecture/executors/validate-dtos/executor.js +0 -561
  43. package/architecture/executors/validate-dtos/executor.js.map +0 -1
  44. package/architecture/executors/validate-dtos/executor.ts +0 -689
  45. package/architecture/executors/validate-dtos/schema.json +0 -33
  46. package/architecture/executors/validate-modified-files/executor.d.ts +0 -25
  47. package/architecture/executors/validate-modified-files/executor.js +0 -501
  48. package/architecture/executors/validate-modified-files/executor.js.map +0 -1
  49. package/architecture/executors/validate-modified-files/executor.ts +0 -571
  50. package/architecture/executors/validate-modified-files/schema.json +0 -25
  51. package/architecture/executors/validate-modified-methods/executor.d.ts +0 -31
  52. package/architecture/executors/validate-modified-methods/executor.js +0 -694
  53. package/architecture/executors/validate-modified-methods/executor.js.map +0 -1
  54. package/architecture/executors/validate-modified-methods/executor.ts +0 -797
  55. package/architecture/executors/validate-modified-methods/schema.json +0 -25
  56. package/architecture/executors/validate-new-methods/executor.d.ts +0 -28
  57. package/architecture/executors/validate-new-methods/executor.js +0 -513
  58. package/architecture/executors/validate-new-methods/executor.js.map +0 -1
  59. package/architecture/executors/validate-new-methods/executor.ts +0 -584
  60. package/architecture/executors/validate-new-methods/schema.json +0 -25
  61. package/architecture/executors/validate-no-any-unknown/executor.d.ts +0 -42
  62. package/architecture/executors/validate-no-any-unknown/executor.js +0 -462
  63. package/architecture/executors/validate-no-any-unknown/executor.js.map +0 -1
  64. package/architecture/executors/validate-no-any-unknown/executor.ts +0 -540
  65. package/architecture/executors/validate-no-any-unknown/schema.json +0 -24
  66. package/architecture/executors/validate-no-architecture-cycles/executor.d.ts +0 -16
  67. package/architecture/executors/validate-no-architecture-cycles/executor.js +0 -48
  68. package/architecture/executors/validate-no-architecture-cycles/executor.js.map +0 -1
  69. package/architecture/executors/validate-no-architecture-cycles/executor.ts +0 -60
  70. package/architecture/executors/validate-no-architecture-cycles/schema.json +0 -8
  71. package/architecture/executors/validate-no-destructure/executor.d.ts +0 -52
  72. package/architecture/executors/validate-no-destructure/executor.js +0 -491
  73. package/architecture/executors/validate-no-destructure/executor.js.map +0 -1
  74. package/architecture/executors/validate-no-destructure/executor.ts +0 -578
  75. package/architecture/executors/validate-no-destructure/schema.json +0 -24
  76. package/architecture/executors/validate-no-direct-api-resolver/executor.d.ts +0 -47
  77. package/architecture/executors/validate-no-direct-api-resolver/executor.js +0 -566
  78. package/architecture/executors/validate-no-direct-api-resolver/executor.js.map +0 -1
  79. package/architecture/executors/validate-no-direct-api-resolver/executor.ts +0 -666
  80. package/architecture/executors/validate-no-direct-api-resolver/schema.json +0 -29
  81. package/architecture/executors/validate-no-inline-types/executor.d.ts +0 -91
  82. package/architecture/executors/validate-no-inline-types/executor.js +0 -669
  83. package/architecture/executors/validate-no-inline-types/executor.js.map +0 -1
  84. package/architecture/executors/validate-no-inline-types/executor.ts +0 -775
  85. package/architecture/executors/validate-no-inline-types/schema.json +0 -24
  86. package/architecture/executors/validate-no-skiplevel-deps/executor.d.ts +0 -19
  87. package/architecture/executors/validate-no-skiplevel-deps/executor.js +0 -227
  88. package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +0 -1
  89. package/architecture/executors/validate-no-skiplevel-deps/executor.ts +0 -267
  90. package/architecture/executors/validate-no-skiplevel-deps/schema.json +0 -8
  91. package/architecture/executors/validate-packagejson/executor.d.ts +0 -16
  92. package/architecture/executors/validate-packagejson/executor.js +0 -57
  93. package/architecture/executors/validate-packagejson/executor.js.map +0 -1
  94. package/architecture/executors/validate-packagejson/executor.ts +0 -74
  95. package/architecture/executors/validate-packagejson/schema.json +0 -8
  96. package/architecture/executors/validate-prisma-converters/executor.d.ts +0 -60
  97. package/architecture/executors/validate-prisma-converters/executor.js +0 -634
  98. package/architecture/executors/validate-prisma-converters/executor.js.map +0 -1
  99. package/architecture/executors/validate-prisma-converters/executor.ts +0 -822
  100. package/architecture/executors/validate-prisma-converters/schema.json +0 -38
  101. package/architecture/executors/validate-return-types/executor.d.ts +0 -29
  102. package/architecture/executors/validate-return-types/executor.js +0 -439
  103. package/architecture/executors/validate-return-types/executor.js.map +0 -1
  104. package/architecture/executors/validate-return-types/executor.ts +0 -524
  105. package/architecture/executors/validate-return-types/schema.json +0 -24
  106. package/architecture/executors/visualize/executor.d.ts +0 -17
  107. package/architecture/executors/visualize/executor.js +0 -49
  108. package/architecture/executors/visualize/executor.js.map +0 -1
  109. package/architecture/executors/visualize/executor.ts +0 -63
  110. package/architecture/executors/visualize/schema.json +0 -14
  111. package/architecture/index.d.ts +0 -19
  112. package/architecture/index.js +0 -23
  113. package/architecture/index.js.map +0 -1
  114. package/architecture/index.ts +0 -20
  115. package/architecture/lib/graph-comparator.d.ts +0 -39
  116. package/architecture/lib/graph-comparator.js +0 -100
  117. package/architecture/lib/graph-comparator.js.map +0 -1
  118. package/architecture/lib/graph-comparator.ts +0 -141
  119. package/architecture/lib/graph-generator.d.ts +0 -19
  120. package/architecture/lib/graph-generator.js +0 -84
  121. package/architecture/lib/graph-generator.js.map +0 -1
  122. package/architecture/lib/graph-generator.ts +0 -97
  123. package/architecture/lib/graph-loader.d.ts +0 -31
  124. package/architecture/lib/graph-loader.js +0 -98
  125. package/architecture/lib/graph-loader.js.map +0 -1
  126. package/architecture/lib/graph-loader.ts +0 -116
  127. package/architecture/lib/graph-sorter.d.ts +0 -37
  128. package/architecture/lib/graph-sorter.js +0 -110
  129. package/architecture/lib/graph-sorter.js.map +0 -1
  130. package/architecture/lib/graph-sorter.ts +0 -137
  131. package/architecture/lib/graph-visualizer.d.ts +0 -29
  132. package/architecture/lib/graph-visualizer.js +0 -217
  133. package/architecture/lib/graph-visualizer.js.map +0 -1
  134. package/architecture/lib/graph-visualizer.ts +0 -231
  135. package/architecture/lib/package-validator.d.ts +0 -38
  136. package/architecture/lib/package-validator.js +0 -126
  137. package/architecture/lib/package-validator.js.map +0 -1
  138. package/architecture/lib/package-validator.ts +0 -170
  139. package/eslint-plugin/__tests__/catch-error-pattern.test.ts +0 -359
  140. package/eslint-plugin/__tests__/max-file-lines.test.ts +0 -207
  141. package/eslint-plugin/__tests__/max-method-lines.test.ts +0 -258
  142. package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +0 -359
  143. package/eslint-plugin/index.d.ts +0 -23
  144. package/eslint-plugin/index.js +0 -30
  145. package/eslint-plugin/index.js.map +0 -1
  146. package/eslint-plugin/index.ts +0 -29
  147. package/eslint-plugin/rules/catch-error-pattern.d.ts +0 -11
  148. package/eslint-plugin/rules/catch-error-pattern.js +0 -196
  149. package/eslint-plugin/rules/catch-error-pattern.js.map +0 -1
  150. package/eslint-plugin/rules/catch-error-pattern.ts +0 -281
  151. package/eslint-plugin/rules/enforce-architecture.d.ts +0 -15
  152. package/eslint-plugin/rules/enforce-architecture.js +0 -476
  153. package/eslint-plugin/rules/enforce-architecture.js.map +0 -1
  154. package/eslint-plugin/rules/enforce-architecture.ts +0 -543
  155. package/eslint-plugin/rules/max-file-lines.d.ts +0 -12
  156. package/eslint-plugin/rules/max-file-lines.js +0 -257
  157. package/eslint-plugin/rules/max-file-lines.js.map +0 -1
  158. package/eslint-plugin/rules/max-file-lines.ts +0 -272
  159. package/eslint-plugin/rules/max-method-lines.d.ts +0 -12
  160. package/eslint-plugin/rules/max-method-lines.js +0 -240
  161. package/eslint-plugin/rules/max-method-lines.js.map +0 -1
  162. package/eslint-plugin/rules/max-method-lines.ts +0 -287
  163. package/eslint-plugin/rules/no-unmanaged-exceptions.d.ts +0 -22
  164. package/eslint-plugin/rules/no-unmanaged-exceptions.js +0 -160
  165. package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +0 -1
  166. package/eslint-plugin/rules/no-unmanaged-exceptions.ts +0 -179
  167. package/executors/help/executor.js.map +0 -1
  168. package/executors/help/executor.ts +0 -61
  169. package/executors/validate-eslint-sync/executor.js.map +0 -1
  170. package/executors/validate-eslint-sync/executor.ts +0 -83
  171. package/executors/validate-versions-locked/executor.js.map +0 -1
  172. package/executors/validate-versions-locked/executor.ts +0 -367
  173. package/plugin/README.md +0 -243
  174. package/plugin/index.d.ts +0 -4
  175. package/plugin/index.js +0 -8
  176. package/plugin/index.js.map +0 -1
  177. package/plugin/index.ts +0 -4
  178. /package/{executors → src/executors}/help/executor.js +0 -0
  179. /package/{executors → src/executors}/help/schema.json +0 -0
  180. /package/{executors → src/executors}/validate-eslint-sync/executor.js +0 -0
  181. /package/{executors → src/executors}/validate-eslint-sync/schema.json +0 -0
  182. /package/{executors → src/executors}/validate-versions-locked/executor.d.ts +0 -0
  183. /package/{executors → src/executors}/validate-versions-locked/schema.json +0 -0
@@ -1,797 +0,0 @@
1
- /**
2
- * Validate Modified Methods Executor
3
- *
4
- * Validates that modified methods don't exceed a maximum line count (default 80).
5
- * This encourages gradual cleanup of legacy long methods - when you touch a method,
6
- * you must bring it under the limit.
7
- *
8
- * Combined with validate-new-methods, this creates a gradual
9
- * transition to cleaner code:
10
- * - New methods: must be under limit
11
- * - Modified methods: must be under limit (cleanup when touched)
12
- * - Untouched methods: no limit (legacy allowed)
13
- *
14
- * Usage:
15
- * nx affected --target=validate-modified-methods --base=origin/main
16
- *
17
- * Escape hatch: Add webpieces-disable max-lines-modified comment with date and justification
18
- * Format: // webpieces-disable max-lines-modified 2025/01/15 -- [reason]
19
- * The disable expires after 1 month from the date specified.
20
- */
21
-
22
- import type { ExecutorContext } from '@nx/devkit';
23
- import { execSync } from 'child_process';
24
- import * as fs from 'fs';
25
- import * as path from 'path';
26
- import * as ts from 'typescript';
27
-
28
- export type MethodMaxLimitMode = 'OFF' | 'NEW_METHODS' | 'NEW_AND_MODIFIED_METHODS' | 'MODIFIED_FILES';
29
-
30
- export interface ValidateModifiedMethodsOptions {
31
- limit?: number;
32
- mode?: MethodMaxLimitMode;
33
- disableAllowed?: boolean;
34
- }
35
-
36
- export interface ExecutorResult {
37
- success: boolean;
38
- }
39
-
40
- interface MethodViolation {
41
- file: string;
42
- methodName: string;
43
- line: number;
44
- lines: number;
45
- expiredDisable?: boolean;
46
- expiredDate?: string;
47
- }
48
-
49
- const TMP_DIR = 'tmp/webpieces';
50
- const TMP_MD_FILE = 'webpieces.methodsize.md';
51
-
52
- const METHODSIZE_DOC_CONTENT = `# Instructions: Method Too Long
53
-
54
- ## Requirement
55
-
56
- **~99% of the time**, you can stay under the \`limit\` from nx.json
57
- by extracting logical units into well-named methods.
58
- Nearly all software can be written with methods under this size.
59
- Take the extra time to refactor - it's worth it for long-term maintainability.
60
-
61
- ## The "Table of Contents" Principle
62
-
63
- Good code reads like a book's table of contents:
64
- - Chapter titles (method names) tell you WHAT happens
65
- - Reading chapter titles gives you the full story
66
- - You can dive into chapters (implementations) for details
67
-
68
- ## Why Limit Method Sizes?
69
-
70
- Methods under reasonable limits are:
71
- - Easy to review in a single screen
72
- - Simple to understand without scrolling
73
- - Quick for AI to analyze and suggest improvements
74
- - More testable in isolation
75
- - Self-documenting through well-named extracted methods
76
-
77
- ## Gradual Cleanup Strategy
78
-
79
- This codebase uses a gradual cleanup approach:
80
- - **New methods**: Must be under \`limit\` from nx.json
81
- - **Modified methods**: Must be under \`limit\` from nx.json
82
- - **Untouched methods**: No limit (legacy code is allowed until touched)
83
-
84
- ## How to Refactor
85
-
86
- Instead of:
87
- \`\`\`typescript
88
- async processOrder(order: Order): Promise<Result> {
89
- // 100 lines of validation, transformation, saving, notifications...
90
- }
91
- \`\`\`
92
-
93
- Write:
94
- \`\`\`typescript
95
- async processOrder(order: Order): Promise<Result> {
96
- const validated = this.validateOrder(order);
97
- const transformed = this.applyBusinessRules(validated);
98
- const saved = await this.saveToDatabase(transformed);
99
- await this.notifyStakeholders(saved);
100
- return this.buildResult(saved);
101
- }
102
- \`\`\`
103
-
104
- Now the main method is a "table of contents" - each line tells part of the story!
105
-
106
- ## Patterns for Extraction
107
-
108
- ### Pattern 1: Extract Loop Bodies
109
- \`\`\`typescript
110
- // BEFORE
111
- for (const item of items) {
112
- // 20 lines of processing
113
- }
114
-
115
- // AFTER
116
- for (const item of items) {
117
- this.processItem(item);
118
- }
119
- \`\`\`
120
-
121
- ### Pattern 2: Extract Conditional Blocks
122
- \`\`\`typescript
123
- // BEFORE
124
- if (isAdmin(user)) {
125
- // 15 lines of admin logic
126
- }
127
-
128
- // AFTER
129
- if (isAdmin(user)) {
130
- this.handleAdminUser(user);
131
- }
132
- \`\`\`
133
-
134
- ### Pattern 3: Extract Data Transformations
135
- \`\`\`typescript
136
- // BEFORE
137
- const result = {
138
- // 10+ lines of object construction
139
- };
140
-
141
- // AFTER
142
- const result = this.buildResultObject(data);
143
- \`\`\`
144
-
145
- ## If Refactoring Is Not Feasible
146
-
147
- Sometimes methods genuinely need to be longer (complex algorithms, state machines, etc.).
148
-
149
- **Escape hatch**: Add a webpieces-disable comment with DATE and justification:
150
-
151
- \`\`\`typescript
152
- // webpieces-disable max-lines-modified 2025/01/15 -- Complex state machine, splitting reduces clarity
153
- async complexStateMachine(): Promise<void> {
154
- // ... longer method with justification
155
- }
156
- \`\`\`
157
-
158
- **IMPORTANT**: The date format is yyyy/mm/dd. The disable will EXPIRE after 1 month from this date.
159
- After expiration, you must either fix the method or update the date to get another month.
160
- This ensures that disable comments are reviewed periodically.
161
-
162
- ## AI Agent Action Steps
163
-
164
- 1. **READ** the method to understand its logical sections
165
- 2. **IDENTIFY** logical units that can be extracted
166
- 3. **EXTRACT** into well-named private methods
167
- 4. **VERIFY** the main method now reads like a table of contents
168
- 5. **IF NOT FEASIBLE**: Add webpieces-disable max-lines-modified comment with clear justification
169
-
170
- ## Remember
171
-
172
- - Every method you write today will be read many times tomorrow
173
- - The best code explains itself through structure
174
- - When in doubt, extract and name it
175
- `;
176
-
177
- /**
178
- * Write the instructions documentation to tmp directory
179
- */
180
- function writeTmpInstructions(workspaceRoot: string): string {
181
- const tmpDir = path.join(workspaceRoot, TMP_DIR);
182
- const mdPath = path.join(tmpDir, TMP_MD_FILE);
183
-
184
- fs.mkdirSync(tmpDir, { recursive: true });
185
- fs.writeFileSync(mdPath, METHODSIZE_DOC_CONTENT);
186
-
187
- return mdPath;
188
- }
189
-
190
- /**
191
- * Get changed TypeScript files between base and head (or working tree if head not specified).
192
- * Uses `git diff base [head]` to match what `nx affected` does.
193
- * When head is NOT specified, also includes untracked files (matching nx affected behavior).
194
- */
195
- function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head?: string): string[] {
196
- try {
197
- // If head is specified, diff base to head; otherwise diff base to working tree
198
- const diffTarget = head ? `${base} ${head}` : base;
199
- const output = execSync(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
200
- cwd: workspaceRoot,
201
- encoding: 'utf-8',
202
- });
203
- const changedFiles = output
204
- .trim()
205
- .split('\n')
206
- .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
207
-
208
- // When comparing to working tree (no head specified), also include untracked files
209
- // This matches what nx affected does: "All modified files not yet committed or tracked will also be added"
210
- if (!head) {
211
- try {
212
- const untrackedOutput = execSync(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
213
- cwd: workspaceRoot,
214
- encoding: 'utf-8',
215
- });
216
- const untrackedFiles = untrackedOutput
217
- .trim()
218
- .split('\n')
219
- .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
220
- // Merge and dedupe
221
- const allFiles = new Set([...changedFiles, ...untrackedFiles]);
222
- return Array.from(allFiles);
223
- } catch {
224
- // If ls-files fails, just return the changed files
225
- return changedFiles;
226
- }
227
- }
228
-
229
- return changedFiles;
230
- } catch {
231
- return [];
232
- }
233
- }
234
-
235
- /**
236
- * Get the diff content for a specific file between base and head (or working tree if head not specified).
237
- * Uses `git diff base [head]` to match what `nx affected` does.
238
- * For untracked files, returns the entire file content as additions.
239
- */
240
- function getFileDiff(workspaceRoot: string, file: string, base: string, head?: string): string {
241
- try {
242
- // If head is specified, diff base to head; otherwise diff base to working tree
243
- const diffTarget = head ? `${base} ${head}` : base;
244
- const diff = execSync(`git diff ${diffTarget} -- "${file}"`, {
245
- cwd: workspaceRoot,
246
- encoding: 'utf-8',
247
- });
248
-
249
- // If diff is empty and we're comparing to working tree, check if it's an untracked file
250
- if (!diff && !head) {
251
- const fullPath = path.join(workspaceRoot, file);
252
- if (fs.existsSync(fullPath)) {
253
- // Check if file is untracked
254
- const isUntracked = execSync(`git ls-files --others --exclude-standard "${file}"`, {
255
- cwd: workspaceRoot,
256
- encoding: 'utf-8',
257
- }).trim();
258
-
259
- if (isUntracked) {
260
- // For untracked files, treat entire content as additions
261
- const content = fs.readFileSync(fullPath, 'utf-8');
262
- const lines = content.split('\n');
263
- // Create a pseudo-diff where all lines are additions
264
- return lines.map((line) => `+${line}`).join('\n');
265
- }
266
- }
267
- }
268
-
269
- return diff;
270
- } catch {
271
- return '';
272
- }
273
- }
274
-
275
- /**
276
- * Parse diff to find NEW method signatures.
277
- * Must handle: export function, async function, const/let arrow functions, class methods
278
- */
279
- // webpieces-disable max-lines-new-methods -- Regex patterns require inline documentation
280
- function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
281
- const newMethods = new Set<string>();
282
- const lines = diffContent.split('\n');
283
-
284
- // Patterns to match method definitions (same as validate-new-methods)
285
- const patterns = [
286
- // [export] [async] function methodName( - most explicit, check first
287
- /^\+\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
288
- // [export] const/let methodName = [async] (
289
- /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/,
290
- // [export] const/let methodName = [async] function
291
- /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?function/,
292
- // class method: [public/private/protected] [static] [async] methodName( - but NOT constructor, if, for, while, etc.
293
- /^\+\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*\(/,
294
- ];
295
-
296
- for (const line of lines) {
297
- if (line.startsWith('+') && !line.startsWith('+++')) {
298
- for (const pattern of patterns) {
299
- const match = line.match(pattern);
300
- if (match) {
301
- // Extract method name - now always in capture group 1
302
- const methodName = match[1];
303
- if (methodName && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName)) {
304
- newMethods.add(methodName);
305
- }
306
- break;
307
- }
308
- }
309
- }
310
- }
311
-
312
- return newMethods;
313
- }
314
-
315
- /**
316
- * Parse diff to find line numbers that have changes in the new file
317
- */
318
- function getChangedLineNumbers(diffContent: string): Set<number> {
319
- const changedLines = new Set<number>();
320
- const lines = diffContent.split('\n');
321
-
322
- let currentNewLine = 0;
323
-
324
- for (const line of lines) {
325
- // Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@
326
- const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
327
- if (hunkMatch) {
328
- currentNewLine = parseInt(hunkMatch[1], 10);
329
- continue;
330
- }
331
-
332
- if (currentNewLine === 0) continue;
333
-
334
- if (line.startsWith('+') && !line.startsWith('+++')) {
335
- // Added line
336
- changedLines.add(currentNewLine);
337
- currentNewLine++;
338
- } else if (line.startsWith('-') && !line.startsWith('---')) {
339
- // Removed line - doesn't increment new line counter
340
- } else if (!line.startsWith('\\')) {
341
- // Context line (unchanged)
342
- currentNewLine++;
343
- }
344
- }
345
-
346
- return changedLines;
347
- }
348
-
349
- /**
350
- * Parse a date string in yyyy/mm/dd format and return a Date object.
351
- * Returns null if the format is invalid.
352
- */
353
- function parseDisableDate(dateStr: string): Date | null {
354
- // Match yyyy/mm/dd format
355
- const match = dateStr.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
356
- if (!match) return null;
357
-
358
- const year = parseInt(match[1], 10);
359
- const month = parseInt(match[2], 10) - 1; // JS months are 0-indexed
360
- const day = parseInt(match[3], 10);
361
-
362
- const date = new Date(year, month, day);
363
-
364
- // Validate the date is valid (e.g., not Feb 30)
365
- if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
366
- return null;
367
- }
368
-
369
- return date;
370
- }
371
-
372
- /**
373
- * Check if a date is within the last month (not expired).
374
- */
375
- function isDateWithinMonth(date: Date): boolean {
376
- const now = new Date();
377
- const oneMonthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
378
- return date >= oneMonthAgo;
379
- }
380
-
381
- /**
382
- * Get today's date in yyyy/mm/dd format for error messages
383
- */
384
- function getTodayDateString(): string {
385
- const now = new Date();
386
- const year = now.getFullYear();
387
- const month = String(now.getMonth() + 1).padStart(2, '0');
388
- const day = String(now.getDate()).padStart(2, '0');
389
- return `${year}/${month}/${day}`;
390
- }
391
-
392
- interface DisableInfo {
393
- type: 'full' | 'new-only' | 'none';
394
- isExpired: boolean;
395
- date?: string;
396
- }
397
-
398
- /**
399
- * Check what kind of webpieces-disable comment is present for a method.
400
- * Returns: DisableInfo with type, expiration status, and date
401
- * - 'full': max-lines-modified (ultimate escape, skips both validators)
402
- * - 'new-only': max-lines-new-methods (escaped 30-line check, still needs 80-line check)
403
- * - 'none': no escape hatch
404
- */
405
- // webpieces-disable max-lines-new-methods -- Complex validation logic with multiple escape hatch types
406
- function getDisableInfo(lines: string[], lineNumber: number): DisableInfo {
407
- const startCheck = Math.max(0, lineNumber - 5);
408
- for (let i = lineNumber - 2; i >= startCheck; i--) {
409
- const line = lines[i]?.trim() ?? '';
410
- if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {
411
- break;
412
- }
413
- if (line.includes('webpieces-disable')) {
414
- if (line.includes('max-lines-modified')) {
415
- // Check for date in format: max-lines-modified yyyy/mm/dd
416
- const dateMatch = line.match(/max-lines-modified\s+(\d{4}\/\d{2}\/\d{2}|XXXX\/XX\/XX)/);
417
-
418
- if (!dateMatch) {
419
- // No date found - treat as expired (invalid)
420
- return { type: 'full', isExpired: true, date: undefined };
421
- }
422
-
423
- const dateStr = dateMatch[1];
424
-
425
- // Secret permanent disable
426
- if (dateStr === 'XXXX/XX/XX') {
427
- return { type: 'full', isExpired: false, date: dateStr };
428
- }
429
-
430
- const date = parseDisableDate(dateStr);
431
- if (!date) {
432
- // Invalid date format - treat as expired
433
- return { type: 'full', isExpired: true, date: dateStr };
434
- }
435
-
436
- if (!isDateWithinMonth(date)) {
437
- // Date is expired (older than 1 month)
438
- return { type: 'full', isExpired: true, date: dateStr };
439
- }
440
-
441
- // Valid and not expired
442
- return { type: 'full', isExpired: false, date: dateStr };
443
- }
444
- if (line.includes('max-lines-new-methods')) {
445
- // Check for date in format: max-lines-new-methods yyyy/mm/dd
446
- const dateMatch = line.match(/max-lines-new-methods\s+(\d{4}\/\d{2}\/\d{2}|XXXX\/XX\/XX)/);
447
-
448
- if (!dateMatch) {
449
- // No date found - treat as expired (invalid)
450
- return { type: 'new-only', isExpired: true, date: undefined };
451
- }
452
-
453
- const dateStr = dateMatch[1];
454
-
455
- // Secret permanent disable
456
- if (dateStr === 'XXXX/XX/XX') {
457
- return { type: 'new-only', isExpired: false, date: dateStr };
458
- }
459
-
460
- const date = parseDisableDate(dateStr);
461
- if (!date) {
462
- // Invalid date format - treat as expired
463
- return { type: 'new-only', isExpired: true, date: dateStr };
464
- }
465
-
466
- if (!isDateWithinMonth(date)) {
467
- // Date is expired (older than 1 month)
468
- return { type: 'new-only', isExpired: true, date: dateStr };
469
- }
470
-
471
- // Valid and not expired
472
- return { type: 'new-only', isExpired: false, date: dateStr };
473
- }
474
- }
475
- }
476
- return { type: 'none', isExpired: false };
477
- }
478
-
479
- interface MethodInfo {
480
- name: string;
481
- line: number;
482
- endLine: number;
483
- lines: number;
484
- disableInfo: DisableInfo;
485
- }
486
-
487
- /**
488
- * Parse a TypeScript file and find methods with their line counts
489
- */
490
- // webpieces-disable max-lines-new-methods -- AST traversal requires inline visitor function
491
- function findMethodsInFile(filePath: string, workspaceRoot: string): MethodInfo[] {
492
- const fullPath = path.join(workspaceRoot, filePath);
493
- if (!fs.existsSync(fullPath)) return [];
494
-
495
- const content = fs.readFileSync(fullPath, 'utf-8');
496
- const fileLines = content.split('\n');
497
- const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
498
-
499
- const methods: MethodInfo[] = [];
500
-
501
- // webpieces-disable max-lines-new-methods -- AST visitor pattern requires handling multiple node types
502
- function visit(node: ts.Node): void {
503
- let methodName: string | undefined;
504
- let startLine: number | undefined;
505
- let endLine: number | undefined;
506
-
507
- if (ts.isMethodDeclaration(node) && node.name) {
508
- methodName = node.name.getText(sourceFile);
509
- const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
510
- const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
511
- startLine = start.line + 1;
512
- endLine = end.line + 1;
513
- } else if (ts.isFunctionDeclaration(node) && node.name) {
514
- methodName = node.name.getText(sourceFile);
515
- const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
516
- const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
517
- startLine = start.line + 1;
518
- endLine = end.line + 1;
519
- } else if (ts.isArrowFunction(node)) {
520
- if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
521
- methodName = node.parent.name.getText(sourceFile);
522
- const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
523
- const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
524
- startLine = start.line + 1;
525
- endLine = end.line + 1;
526
- }
527
- }
528
-
529
- if (methodName && startLine !== undefined && endLine !== undefined) {
530
- methods.push({
531
- name: methodName,
532
- line: startLine,
533
- endLine: endLine,
534
- lines: endLine - startLine + 1,
535
- disableInfo: getDisableInfo(fileLines, startLine),
536
- });
537
- }
538
-
539
- ts.forEachChild(node, visit);
540
- }
541
-
542
- visit(sourceFile);
543
- return methods;
544
- }
545
-
546
- /**
547
- * Check if a method has any changes within its line range
548
- */
549
- function methodHasChanges(method: MethodInfo, changedLineNumbers: Set<number>): boolean {
550
- for (let line = method.line; line <= method.endLine; line++) {
551
- if (changedLineNumbers.has(line)) return true;
552
- }
553
- return false;
554
- }
555
-
556
- /**
557
- * Check a NEW method and return violation if applicable
558
- */
559
- function checkNewMethodViolation(file: string, method: MethodInfo, disableAllowed: boolean): MethodViolation | null {
560
- const { type: disableType, isExpired, date: disableDate } = method.disableInfo;
561
-
562
- if (!disableAllowed) {
563
- // When disableAllowed is false, skip NEW methods without escape (let validate-new-methods handle)
564
- if (disableType === 'none') return null;
565
- return { file, methodName: method.name, line: method.line, lines: method.lines };
566
- }
567
-
568
- if (disableType === 'full' && isExpired) {
569
- return { file, methodName: method.name, line: method.line, lines: method.lines, expiredDisable: true, expiredDate: disableDate };
570
- }
571
- if (disableType !== 'new-only') return null;
572
-
573
- if (isExpired) {
574
- return { file, methodName: method.name, line: method.line, lines: method.lines, expiredDisable: true, expiredDate: disableDate };
575
- }
576
- return { file, methodName: method.name, line: method.line, lines: method.lines };
577
- }
578
-
579
- /**
580
- * Check a MODIFIED method and return violation if applicable
581
- */
582
- function checkModifiedMethodViolation(file: string, method: MethodInfo, disableAllowed: boolean): MethodViolation | null {
583
- const { type: disableType, isExpired, date: disableDate } = method.disableInfo;
584
-
585
- if (!disableAllowed) {
586
- return { file, methodName: method.name, line: method.line, lines: method.lines };
587
- }
588
- if (disableType === 'full' && !isExpired) {
589
- // Valid escape, no violation
590
- return null;
591
- }
592
- if (disableType === 'full' && isExpired) {
593
- return { file, methodName: method.name, line: method.line, lines: method.lines, expiredDisable: true, expiredDate: disableDate };
594
- }
595
- return { file, methodName: method.name, line: method.line, lines: method.lines };
596
- }
597
-
598
- /**
599
- * Find methods that exceed the limit.
600
- * Checks NEW methods with escape hatches and MODIFIED methods.
601
- */
602
- function findViolations(
603
- workspaceRoot: string,
604
- changedFiles: string[],
605
- base: string,
606
- limit: number,
607
- disableAllowed: boolean,
608
- head?: string
609
- ): MethodViolation[] {
610
- const violations: MethodViolation[] = [];
611
-
612
- for (const file of changedFiles) {
613
- const diff = getFileDiff(workspaceRoot, file, base, head);
614
- if (!diff) continue;
615
-
616
- const newMethodNames = findNewMethodSignaturesInDiff(diff);
617
- const changedLineNumbers = getChangedLineNumbers(diff);
618
- if (changedLineNumbers.size === 0) continue;
619
-
620
- const methods = findMethodsInFile(file, workspaceRoot);
621
-
622
- for (const method of methods) {
623
- const { type: disableType, isExpired } = method.disableInfo;
624
-
625
- // Skip methods with valid, non-expired full escape - unless disableAllowed is false
626
- if (disableAllowed && disableType === 'full' && !isExpired) continue;
627
- if (method.lines <= limit) continue;
628
-
629
- const isNewMethod = newMethodNames.has(method.name);
630
-
631
- if (isNewMethod) {
632
- const violation = checkNewMethodViolation(file, method, disableAllowed);
633
- if (violation) violations.push(violation);
634
- } else if (methodHasChanges(method, changedLineNumbers)) {
635
- const violation = checkModifiedMethodViolation(file, method, disableAllowed);
636
- if (violation) violations.push(violation);
637
- }
638
- }
639
- }
640
-
641
- return violations;
642
- }
643
-
644
- /**
645
- * Auto-detect the base branch by finding the merge-base with origin/main.
646
- */
647
- function detectBase(workspaceRoot: string): string | null {
648
- try {
649
- const mergeBase = execSync('git merge-base HEAD origin/main', {
650
- cwd: workspaceRoot,
651
- encoding: 'utf-8',
652
- stdio: ['pipe', 'pipe', 'pipe'],
653
- }).trim();
654
-
655
- if (mergeBase) {
656
- return mergeBase;
657
- }
658
- } catch {
659
- try {
660
- const mergeBase = execSync('git merge-base HEAD main', {
661
- cwd: workspaceRoot,
662
- encoding: 'utf-8',
663
- stdio: ['pipe', 'pipe', 'pipe'],
664
- }).trim();
665
-
666
- if (mergeBase) {
667
- return mergeBase;
668
- }
669
- } catch {
670
- // Ignore
671
- }
672
- }
673
- return null;
674
- }
675
-
676
- /**
677
- * Report violations to console
678
- */
679
- // webpieces-disable max-lines-new-methods -- Error output formatting with multiple message sections
680
- function reportViolations(violations: MethodViolation[], limit: number, disableAllowed: boolean): void {
681
- console.error('');
682
- console.error('\u274c Modified methods exceed ' + limit + ' lines!');
683
- console.error('');
684
- console.error('\ud83d\udcda When you modify a method, you must bring it under ' + limit + ' lines.');
685
- console.error(' This rule encourages GRADUAL cleanup so even though you did not cause it,');
686
- console.error(' you touched it, so you should fix now as part of your PR');
687
- console.error(' (this is for vibe coding and AI to fix as it touches things).');
688
- console.error(' You can refactor to stay under the limit 50% of the time. If not feasible, use the escape hatch.');
689
- console.error('');
690
- console.error(
691
- '\u26a0\ufe0f *** READ tmp/webpieces/webpieces.methodsize.md for detailed guidance on how to fix this easily *** \u26a0\ufe0f'
692
- );
693
- console.error('');
694
-
695
- for (const v of violations) {
696
- if (v.expiredDisable) {
697
- console.error(` \u274c ${v.file}:${v.line}`);
698
- console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${limit})`);
699
- console.error(` \u23f0 EXPIRED DISABLE: Your disable comment dated ${v.expiredDate ?? 'unknown'} has expired (>1 month old).`);
700
- console.error(` You must either FIX the method or UPDATE the date to get another month.`);
701
- } else {
702
- console.error(` \u274c ${v.file}:${v.line}`);
703
- console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${limit})`);
704
- }
705
- }
706
- console.error('');
707
-
708
- // Only show escape hatch instructions when disableAllowed is true
709
- if (disableAllowed) {
710
- console.error(' You can disable this error, but you will be forced to fix again in 1 month');
711
- console.error(' since 99% of methods can be less than ' + limit + ' lines of code.');
712
- console.error('');
713
- console.error(' Use escape with DATE (expires in 1 month):');
714
- console.error(` // webpieces-disable max-lines-modified ${getTodayDateString()} -- [your reason]`);
715
- console.error('');
716
- } else {
717
- console.error(' \u26a0\ufe0f disableAllowed is false - disable comments are NOT allowed.');
718
- console.error(' This rule must be met and cannot be disabled since nx.json disableAllowed is set to false.');
719
- console.error(' You MUST refactor to reduce method size.');
720
- console.error('');
721
- console.error(' For a major refactor, a human can add "ignoreModifiedUntilEpoch" to nx.json validate-code options.');
722
- console.error(' This is an expiry timestamp (epoch ms) for when we start forcing everyone to meet size rules again.');
723
- console.error(' Sometimes for speed, we allow methods to expand during a refactor and over time,');
724
- console.error(' each PR reduces methods as they get touched.');
725
- console.error(' AI agents should NOT add ignoreModifiedUntilEpoch - ask a human to do it.');
726
- console.error('');
727
- }
728
- }
729
-
730
- export default async function runExecutor(
731
- options: ValidateModifiedMethodsOptions,
732
- context: ExecutorContext
733
- ): Promise<ExecutorResult> {
734
- const workspaceRoot = context.root;
735
- const limit = options.limit ?? 80;
736
- const mode: MethodMaxLimitMode = options.mode ?? 'NEW_AND_MODIFIED_METHODS';
737
- const disableAllowed = options.disableAllowed ?? true;
738
-
739
- // Skip validation entirely if mode is OFF
740
- if (mode === 'OFF') {
741
- console.log('\n\u23ed\ufe0f Skipping modified method validation (mode: OFF)');
742
- console.log('');
743
- return { success: true };
744
- }
745
-
746
- // If NX_HEAD is set (via nx affected --head=X), use it; otherwise compare to working tree
747
- let base = process.env['NX_BASE'];
748
- const head = process.env['NX_HEAD'];
749
-
750
- if (!base) {
751
- base = detectBase(workspaceRoot) ?? undefined;
752
-
753
- if (!base) {
754
- console.log('\n\u23ed\ufe0f Skipping modified method validation (could not detect base branch)');
755
- console.log(' To run explicitly: nx affected --target=validate-modified-methods --base=origin/main');
756
- console.log('');
757
- return { success: true };
758
- }
759
-
760
- console.log('\n\ud83d\udccf Validating Modified Method Sizes (auto-detected base)\n');
761
- } else {
762
- console.log('\n\ud83d\udccf Validating Modified Method Sizes\n');
763
- }
764
-
765
- console.log(` Base: ${base}`);
766
- console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
767
- console.log(` Mode: ${mode}`);
768
- console.log(` Limit for modified methods: ${limit}`);
769
- console.log(` Disable allowed: ${disableAllowed}${!disableAllowed ? ' (no escape hatch)' : ''}`);
770
- console.log('');
771
-
772
- try {
773
- const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
774
-
775
- if (changedFiles.length === 0) {
776
- console.log('\u2705 No TypeScript files changed');
777
- return { success: true };
778
- }
779
-
780
- console.log(`\ud83d\udcc2 Checking ${changedFiles.length} changed file(s)...`);
781
-
782
- const violations = findViolations(workspaceRoot, changedFiles, base, limit, disableAllowed, head);
783
-
784
- if (violations.length === 0) {
785
- console.log('\u2705 All modified methods are under ' + limit + ' lines');
786
- return { success: true };
787
- }
788
-
789
- writeTmpInstructions(workspaceRoot);
790
- reportViolations(violations, limit, disableAllowed);
791
- return { success: false };
792
- } catch (err: unknown) {
793
- const error = err instanceof Error ? err : new Error(String(err));
794
- console.error('\u274c Modified method validation failed:', error.message);
795
- return { success: false };
796
- }
797
- }