@webpieces/dev-config 0.2.45 → 0.2.47

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.
@@ -2,15 +2,18 @@
2
2
  * Validate New Methods Executor
3
3
  *
4
4
  * Validates that newly added methods don't exceed a maximum line count.
5
- * Only runs when NX_BASE environment variable is set (affected mode).
5
+ * Runs in affected mode when:
6
+ * 1. NX_BASE environment variable is set (via nx affected), OR
7
+ * 2. Auto-detects base by finding merge-base with origin/main
6
8
  *
7
9
  * This validator encourages writing methods that read like a "table of contents"
8
10
  * where each method call describes a larger piece of work.
9
11
  *
10
12
  * Usage:
11
13
  * nx affected --target=validate-new-methods --base=origin/main
14
+ * OR: runs automatically via build's architecture:validate-complete dependency
12
15
  *
13
- * Escape hatch: Add eslint-disable comment with justification
16
+ * Escape hatch: Add webpieces-disable max-lines-new-methods comment with justification
14
17
  */
15
18
 
16
19
  import type { ExecutorContext } from '@nx/devkit';
@@ -40,6 +43,14 @@ const TMP_MD_FILE = 'webpieces.methodsize.md';
40
43
 
41
44
  const METHODSIZE_DOC_CONTENT = `# Instructions: New Method Too Long
42
45
 
46
+ ## Requirement
47
+
48
+ **~50% of the time**, you can stay under the \`newMethodsMaxLines\` limit from nx.json
49
+ by extracting logical units into well-named methods.
50
+
51
+ **~99% of the time**, you can stay under the \`modifiedAndNewMethodsMaxLines\` limit from nx.json.
52
+ Nearly all software can be written with methods under this size.
53
+
43
54
  ## The "Table of Contents" Principle
44
55
 
45
56
  Good code reads like a book's table of contents:
@@ -47,17 +58,16 @@ Good code reads like a book's table of contents:
47
58
  - Reading chapter titles gives you the full story
48
59
  - You can dive into chapters (implementations) for details
49
60
 
50
- ## Why Limit New Methods to 20-30 Lines?
61
+ ## Why Limit New Methods?
51
62
 
52
- Methods under 20-30 lines are:
63
+ Methods under the limit are:
53
64
  - Easy to review in a single screen
54
65
  - Simple to understand without scrolling
55
66
  - Quick for AI to analyze and suggest improvements
56
67
  - More testable in isolation
57
68
  - Self-documenting through well-named extracted methods
58
69
 
59
- **~50% of the time**, you can stay under 20-30 lines in new code by extracting
60
- logical units into well-named methods. This makes code more readable for both
70
+ Extracting logical units into well-named methods makes code more readable for both
61
71
  AI and humans.
62
72
 
63
73
  ## How to Refactor
@@ -125,10 +135,10 @@ const result = this.buildResultObject(data);
125
135
 
126
136
  Sometimes methods genuinely need to be longer (complex algorithms, state machines, etc.).
127
137
 
128
- **Escape hatch**: Add an eslint-disable comment with justification:
138
+ **Escape hatch**: Add a webpieces-disable comment with justification:
129
139
 
130
140
  \`\`\`typescript
131
- // eslint-disable-next-line @webpieces/max-method-lines -- Complex state machine, splitting reduces clarity
141
+ // webpieces-disable max-lines-new-methods -- Complex state machine, splitting reduces clarity
132
142
  async complexStateMachine(): Promise<void> {
133
143
  // ... longer method with justification
134
144
  }
@@ -140,7 +150,7 @@ async complexStateMachine(): Promise<void> {
140
150
  2. **IDENTIFY** logical units that can be extracted
141
151
  3. **EXTRACT** into well-named private methods
142
152
  4. **VERIFY** the main method now reads like a table of contents
143
- 5. **IF NOT FEASIBLE**: Add eslint-disable with clear justification
153
+ 5. **IF NOT FEASIBLE**: Add webpieces-disable max-lines-new-methods comment with clear justification
144
154
 
145
155
  ## Remember
146
156
 
@@ -163,11 +173,14 @@ function writeTmpInstructions(workspaceRoot: string): string {
163
173
  }
164
174
 
165
175
  /**
166
- * Get changed TypeScript files between base and head
176
+ * Get changed TypeScript files between base and working tree.
177
+ * Uses `git diff base` (no three-dots) to match what `nx affected` does -
178
+ * this includes both committed and uncommitted changes in one diff.
167
179
  */
168
- function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head: string): string[] {
180
+ function getChangedTypeScriptFiles(workspaceRoot: string, base: string): string[] {
169
181
  try {
170
- const output = execSync(`git diff --name-only ${base}...${head} -- '*.ts' '*.tsx'`, {
182
+ // Use two-dot diff (base to working tree) - same as nx affected
183
+ const output = execSync(`git diff --name-only ${base} -- '*.ts' '*.tsx'`, {
171
184
  cwd: workspaceRoot,
172
185
  encoding: 'utf-8',
173
186
  });
@@ -181,11 +194,14 @@ function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head: st
181
194
  }
182
195
 
183
196
  /**
184
- * Get the diff content for a specific file
197
+ * Get the diff content for a specific file between base and working tree.
198
+ * Uses `git diff base` (no three-dots) to match what `nx affected` does -
199
+ * this includes both committed and uncommitted changes in one diff.
185
200
  */
186
- function getFileDiff(workspaceRoot: string, file: string, base: string, head: string): string {
201
+ function getFileDiff(workspaceRoot: string, file: string, base: string): string {
187
202
  try {
188
- return execSync(`git diff ${base}...${head} -- "${file}"`, {
203
+ // Use two-dot diff (base to working tree) - same as nx affected
204
+ return execSync(`git diff ${base} -- "${file}"`, {
189
205
  cwd: workspaceRoot,
190
206
  encoding: 'utf-8',
191
207
  });
@@ -203,14 +219,14 @@ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
203
219
 
204
220
  // Patterns to match method definitions
205
221
  const patterns = [
206
- // async methodName( or methodName(
207
- /^\+\s*(async\s+)?(\w+)\s*\(/,
208
- // function methodName(
209
- /^\+\s*(async\s+)?function\s+(\w+)\s*\(/,
210
- // const/let methodName = (async)? (
211
- /^\+\s*(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/,
212
- // const/let methodName = (async)? function
213
- /^\+\s*(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?function/,
222
+ // [export] [async] function methodName( - most explicit, check first
223
+ /^\+\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
224
+ // [export] const/let methodName = [async] (
225
+ /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/,
226
+ // [export] const/let methodName = [async] function
227
+ /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?function/,
228
+ // class method: [async] methodName( - but NOT constructor, if, for, while, etc.
229
+ /^\+\s*(?:async\s+)?(\w+)\s*\(/,
214
230
  ];
215
231
 
216
232
  for (const line of lines) {
@@ -218,8 +234,8 @@ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
218
234
  for (const pattern of patterns) {
219
235
  const match = line.match(pattern);
220
236
  if (match) {
221
- // Extract method name from different capture groups
222
- const methodName = match[2] || match[1];
237
+ // Extract method name - now always in capture group 1
238
+ const methodName = match[1];
223
239
  if (methodName && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName)) {
224
240
  newMethods.add(methodName);
225
241
  }
@@ -232,20 +248,47 @@ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
232
248
  return newMethods;
233
249
  }
234
250
 
251
+ /**
252
+ * Check if a line contains a webpieces-disable comment that exempts from new method validation.
253
+ * Both max-lines-new-methods AND max-lines-new-and-modified are accepted here.
254
+ * - max-lines-new-methods: Exempts from 30-line check, still checked by 80-line validator
255
+ * - max-lines-new-and-modified: Exempts from both validators (ultimate escape hatch)
256
+ */
257
+ function hasDisableComment(lines: string[], lineNumber: number): boolean {
258
+ // Check the line before the method (lineNumber is 1-indexed, array is 0-indexed)
259
+ // We need to check a few lines before in case there's JSDoc or decorators
260
+ const startCheck = Math.max(0, lineNumber - 5);
261
+ for (let i = lineNumber - 2; i >= startCheck; i--) {
262
+ const line = lines[i]?.trim() ?? '';
263
+ // Stop if we hit another function/class/etc
264
+ if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {
265
+ break;
266
+ }
267
+ if (line.includes('webpieces-disable')) {
268
+ // Either escape hatch exempts from the 30-line new method check
269
+ if (line.includes('max-lines-new-methods') || line.includes('max-lines-new-and-modified')) {
270
+ return true;
271
+ }
272
+ }
273
+ }
274
+ return false;
275
+ }
276
+
235
277
  /**
236
278
  * Parse a TypeScript file and find methods with their line counts
237
279
  */
238
280
  function findMethodsInFile(
239
281
  filePath: string,
240
282
  workspaceRoot: string
241
- ): Array<{ name: string; line: number; lines: number }> {
283
+ ): Array<{ name: string; line: number; lines: number; hasDisableComment: boolean }> {
242
284
  const fullPath = path.join(workspaceRoot, filePath);
243
285
  if (!fs.existsSync(fullPath)) return [];
244
286
 
245
287
  const content = fs.readFileSync(fullPath, 'utf-8');
288
+ const fileLines = content.split('\n');
246
289
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
247
290
 
248
- const methods: Array<{ name: string; line: number; lines: number }> = [];
291
+ const methods: Array<{ name: string; line: number; lines: number; hasDisableComment: boolean }> = [];
249
292
 
250
293
  function visit(node: ts.Node): void {
251
294
  let methodName: string | undefined;
@@ -280,6 +323,7 @@ function findMethodsInFile(
280
323
  name: methodName,
281
324
  line: startLine,
282
325
  lines: endLine - startLine + 1,
326
+ hasDisableComment: hasDisableComment(fileLines, startLine),
283
327
  });
284
328
  }
285
329
 
@@ -297,14 +341,13 @@ function findViolations(
297
341
  workspaceRoot: string,
298
342
  changedFiles: string[],
299
343
  base: string,
300
- head: string,
301
344
  maxLines: number
302
345
  ): MethodViolation[] {
303
346
  const violations: MethodViolation[] = [];
304
347
 
305
348
  for (const file of changedFiles) {
306
349
  // Get the diff to find which methods are NEW (not just modified)
307
- const diff = getFileDiff(workspaceRoot, file, base, head);
350
+ const diff = getFileDiff(workspaceRoot, file, base);
308
351
  const newMethodNames = findNewMethodSignaturesInDiff(diff);
309
352
 
310
353
  if (newMethodNames.size === 0) continue;
@@ -313,8 +356,8 @@ function findViolations(
313
356
  const methods = findMethodsInFile(file, workspaceRoot);
314
357
 
315
358
  for (const method of methods) {
316
- // Only check NEW methods
317
- if (newMethodNames.has(method.name) && method.lines > maxLines) {
359
+ // Only check NEW methods that don't have webpieces-disable comment
360
+ if (newMethodNames.has(method.name) && method.lines > maxLines && !method.hasDisableComment) {
318
361
  violations.push({
319
362
  file,
320
363
  methodName: method.name,
@@ -329,6 +372,41 @@ function findViolations(
329
372
  return violations;
330
373
  }
331
374
 
375
+ /**
376
+ * Auto-detect the base branch by finding the merge-base with origin/main.
377
+ * This allows the executor to run even when NX_BASE isn't set (e.g., via dependsOn).
378
+ */
379
+ function detectBase(workspaceRoot: string): string | null {
380
+ try {
381
+ // First, try to get merge-base with origin/main
382
+ const mergeBase = execSync('git merge-base HEAD origin/main', {
383
+ cwd: workspaceRoot,
384
+ encoding: 'utf-8',
385
+ stdio: ['pipe', 'pipe', 'pipe'],
386
+ }).trim();
387
+
388
+ if (mergeBase) {
389
+ return mergeBase;
390
+ }
391
+ } catch {
392
+ // origin/main might not exist, try main
393
+ try {
394
+ const mergeBase = execSync('git merge-base HEAD main', {
395
+ cwd: workspaceRoot,
396
+ encoding: 'utf-8',
397
+ stdio: ['pipe', 'pipe', 'pipe'],
398
+ }).trim();
399
+
400
+ if (mergeBase) {
401
+ return mergeBase;
402
+ }
403
+ } catch {
404
+ // Ignore - will return null
405
+ }
406
+ }
407
+ return null;
408
+ }
409
+
332
410
  export default async function runExecutor(
333
411
  options: ValidateNewMethodsOptions,
334
412
  context: ExecutorContext
@@ -336,26 +414,35 @@ export default async function runExecutor(
336
414
  const workspaceRoot = context.root;
337
415
  const maxLines = options.max ?? 30;
338
416
 
339
- // Check if running in affected mode
340
- const base = process.env['NX_BASE'];
341
- const head = process.env['NX_HEAD'] || 'HEAD';
417
+ // Check if running in affected mode via NX_BASE, or auto-detect
418
+ // We use NX_BASE as the base, and compare to WORKING TREE (not NX_HEAD)
419
+ // This matches what `nx affected` does - it compares base to working tree
420
+ let base = process.env['NX_BASE'];
342
421
 
343
422
  if (!base) {
344
- console.log('\n⏭️ Skipping new method validation (not in affected mode)');
345
- console.log(' To run: nx affected --target=validate-new-methods --base=origin/main');
346
- console.log('');
347
- return { success: true };
423
+ // Try to auto-detect base from git merge-base
424
+ base = detectBase(workspaceRoot) ?? undefined;
425
+
426
+ if (!base) {
427
+ console.log('\n⏭️ Skipping new method validation (could not detect base branch)');
428
+ console.log(' To run explicitly: nx affected --target=validate-new-methods --base=origin/main');
429
+ console.log('');
430
+ return { success: true };
431
+ }
432
+
433
+ console.log('\n📏 Validating New Method Sizes (auto-detected base)\n');
434
+ } else {
435
+ console.log('\n📏 Validating New Method Sizes\n');
348
436
  }
349
437
 
350
- console.log('\n📏 Validating New Method Sizes\n');
351
438
  console.log(` Base: ${base}`);
352
- console.log(` Head: ${head}`);
439
+ console.log(` Comparing to: working tree (includes uncommitted changes)`);
353
440
  console.log(` Max lines for new methods: ${maxLines}`);
354
441
  console.log('');
355
442
 
356
443
  try {
357
- // Get changed TypeScript files
358
- const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
444
+ // Get changed TypeScript files (base to working tree, like nx affected)
445
+ const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base);
359
446
 
360
447
  if (changedFiles.length === 0) {
361
448
  console.log('✅ No TypeScript files changed');
@@ -365,7 +452,7 @@ export default async function runExecutor(
365
452
  console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);
366
453
 
367
454
  // Find violations
368
- const violations = findViolations(workspaceRoot, changedFiles, base, head, maxLines);
455
+ const violations = findViolations(workspaceRoot, changedFiles, base, maxLines);
369
456
 
370
457
  if (violations.length === 0) {
371
458
  console.log('✅ All new methods are under ' + maxLines + ' lines');
@@ -373,30 +460,23 @@ export default async function runExecutor(
373
460
  }
374
461
 
375
462
  // Write instructions file
376
- const mdPath = writeTmpInstructions(workspaceRoot);
463
+ writeTmpInstructions(workspaceRoot);
377
464
 
378
465
  // Report violations
379
466
  console.error('');
380
467
  console.error('❌ New methods exceed ' + maxLines + ' lines!');
381
468
  console.error('');
382
469
  console.error('📚 Methods should read like a "table of contents" - each method call');
383
- console.error(' describes a larger piece of work. ~50% of the time, you can refactor');
384
- console.error(' to stay under ' + maxLines + ' lines. If not feasible, use the escape hatch.');
470
+ console.error(' describes a larger piece of work. You can refactor');
471
+ console.error(' to stay under ' + maxLines + ' lines 50% of the time. If not feasible, use the escape hatch.');
472
+ console.error('');
473
+ console.error('⚠️ *** READ tmp/webpieces/webpieces.methodsize.md for detailed guidance on how to fix this easily *** ⚠️');
385
474
  console.error('');
386
475
 
387
476
  for (const v of violations) {
388
477
  console.error(` ❌ ${v.file}:${v.line}`);
389
478
  console.error(` Method: ${v.methodName} (${v.lines} lines, max: ${maxLines})`);
390
- console.error(` READ ${mdPath} to fix this error properly`);
391
- console.error('');
392
479
  }
393
-
394
- console.error('💡 To fix:');
395
- console.error(' 1. Refactor the method to read like a table of contents (preferred)');
396
- console.error(' 2. OR add eslint-disable comment with justification:');
397
- console.error(' // eslint-disable-next-line @webpieces/max-method-lines -- [reason]');
398
- console.error('');
399
- console.error(`⚠️ *** READ ${mdPath} for detailed guidance *** ⚠️`);
400
480
  console.error('');
401
481
 
402
482
  return { success: false };
@@ -37,6 +37,34 @@ function loadBlessedGraph(workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
37
37
  throw new Error(`Failed to load graph from ${fullPath}: ${err}`);
38
38
  }
39
39
  }
40
+ /**
41
+ * Format a graph as JSON with multi-line arrays for readability
42
+ */
43
+ function formatGraphJson(graph) {
44
+ const lines = ['{'];
45
+ const keys = Object.keys(graph).sort();
46
+ keys.forEach((key, index) => {
47
+ const entry = graph[key];
48
+ const isLast = index === keys.length - 1;
49
+ const comma = isLast ? '' : ',';
50
+ lines.push(` "${key}": {`);
51
+ lines.push(` "level": ${entry.level},`);
52
+ if (entry.dependsOn.length === 0) {
53
+ lines.push(` "dependsOn": []`);
54
+ }
55
+ else {
56
+ lines.push(` "dependsOn": [`);
57
+ entry.dependsOn.forEach((dep, depIndex) => {
58
+ const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';
59
+ lines.push(` "${dep}"${depComma}`);
60
+ });
61
+ lines.push(` ]`);
62
+ }
63
+ lines.push(` }${comma}`);
64
+ });
65
+ lines.push('}');
66
+ return lines.join('\n') + '\n';
67
+ }
40
68
  /**
41
69
  * Save the graph to disk
42
70
  *
@@ -57,7 +85,7 @@ function saveGraph(graph, workspaceRoot, graphPath = exports.DEFAULT_GRAPH_PATH)
57
85
  for (const key of sortedKeys) {
58
86
  sortedGraph[key] = graph[key];
59
87
  }
60
- const content = JSON.stringify(sortedGraph, null, 4) + '\n';
88
+ const content = formatGraphJson(sortedGraph);
61
89
  fs.writeFileSync(fullPath, content, 'utf-8');
62
90
  }
63
91
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-loader.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAkBH,4CAgBC;AASD,8BAsBC;AAKD,0CAMC;;AA1ED,+CAAyB;AACzB,mDAA6B;AAG7B;;GAEG;AACU,QAAA,kBAAkB,GAAG,gCAAgC,CAAC;AAEnE;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAC5B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IAErD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAC;IAChD,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,KAAK,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,SAAS,CACrB,KAAoB,EACpB,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEnC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,qCAAqC;IACrC,MAAM,WAAW,GAAkB,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7C,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IAC5D,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC;AAED;;GAEG;AACH,SAAgB,eAAe,CAC3B,aAAqB,EACrB,YAAoB,0BAAkB;IAEtC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrD,OAAO,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph } from './graph-sorter';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Load the blessed graph from disk\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n * @returns The blessed graph, or null if file doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): EnhancedGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n return JSON.parse(content) as EnhancedGraph;\n } catch (err: unknown) {\n throw new Error(`Failed to load graph from ${fullPath}: ${err}`);\n }\n}\n\n/**\n * Save the graph to disk\n *\n * @param graph - The graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = JSON.stringify(sortedGraph, null, 4) + '\\n';\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
1
+ {"version":3,"file":"graph-loader.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Loader\n *\n * Handles loading and saving the blessed dependency graph file.\n * The graph is stored at architecture/dependencies.json in the workspace root.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph } from './graph-sorter';\n\n/**\n * Default path for the dependencies file (relative to workspace root)\n */\nexport const DEFAULT_GRAPH_PATH = 'architecture/dependencies.json';\n\n/**\n * Load the blessed graph from disk\n *\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n * @returns The blessed graph, or null if file doesn't exist\n */\nexport function loadBlessedGraph(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): EnhancedGraph | null {\n const fullPath = path.join(workspaceRoot, graphPath);\n\n if (!fs.existsSync(fullPath)) {\n return null;\n }\n\n try {\n const content = fs.readFileSync(fullPath, 'utf-8');\n return JSON.parse(content) as EnhancedGraph;\n } catch (err: unknown) {\n throw new Error(`Failed to load graph from ${fullPath}: ${err}`);\n }\n}\n\n/**\n * Format a graph as JSON with multi-line arrays for readability\n */\nfunction formatGraphJson(graph: EnhancedGraph): string {\n const lines: string[] = ['{'];\n const keys = Object.keys(graph).sort();\n\n keys.forEach((key, index) => {\n const entry = graph[key];\n const isLast = index === keys.length - 1;\n const comma = isLast ? '' : ',';\n\n lines.push(` \"${key}\": {`);\n lines.push(` \"level\": ${entry.level},`);\n\n if (entry.dependsOn.length === 0) {\n lines.push(` \"dependsOn\": []`);\n } else {\n lines.push(` \"dependsOn\": [`);\n entry.dependsOn.forEach((dep, depIndex) => {\n const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';\n lines.push(` \"${dep}\"${depComma}`);\n });\n lines.push(` ]`);\n }\n\n lines.push(` }${comma}`);\n });\n\n lines.push('}');\n return lines.join('\\n') + '\\n';\n}\n\n/**\n * Save the graph to disk\n *\n * @param graph - The graph to save\n * @param workspaceRoot - Absolute path to workspace root\n * @param graphPath - Relative path to graph file (default: .graphs/dependencies.json)\n */\nexport function saveGraph(\n graph: EnhancedGraph,\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): void {\n const fullPath = path.join(workspaceRoot, graphPath);\n const dir = path.dirname(fullPath);\n\n // Ensure directory exists\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Sort keys for deterministic output\n const sortedGraph: EnhancedGraph = {};\n const sortedKeys = Object.keys(graph).sort();\n for (const key of sortedKeys) {\n sortedGraph[key] = graph[key];\n }\n\n const content = formatGraphJson(sortedGraph);\n fs.writeFileSync(fullPath, content, 'utf-8');\n}\n\n/**\n * Check if the graph file exists\n */\nexport function graphFileExists(\n workspaceRoot: string,\n graphPath: string = DEFAULT_GRAPH_PATH\n): boolean {\n const fullPath = path.join(workspaceRoot, graphPath);\n return fs.existsSync(fullPath);\n}\n"]}
@@ -39,6 +39,39 @@ export function loadBlessedGraph(
39
39
  }
40
40
  }
41
41
 
42
+ /**
43
+ * Format a graph as JSON with multi-line arrays for readability
44
+ */
45
+ function formatGraphJson(graph: EnhancedGraph): string {
46
+ const lines: string[] = ['{'];
47
+ const keys = Object.keys(graph).sort();
48
+
49
+ keys.forEach((key, index) => {
50
+ const entry = graph[key];
51
+ const isLast = index === keys.length - 1;
52
+ const comma = isLast ? '' : ',';
53
+
54
+ lines.push(` "${key}": {`);
55
+ lines.push(` "level": ${entry.level},`);
56
+
57
+ if (entry.dependsOn.length === 0) {
58
+ lines.push(` "dependsOn": []`);
59
+ } else {
60
+ lines.push(` "dependsOn": [`);
61
+ entry.dependsOn.forEach((dep, depIndex) => {
62
+ const depComma = depIndex === entry.dependsOn.length - 1 ? '' : ',';
63
+ lines.push(` "${dep}"${depComma}`);
64
+ });
65
+ lines.push(` ]`);
66
+ }
67
+
68
+ lines.push(` }${comma}`);
69
+ });
70
+
71
+ lines.push('}');
72
+ return lines.join('\n') + '\n';
73
+ }
74
+
42
75
  /**
43
76
  * Save the graph to disk
44
77
  *
@@ -66,7 +99,7 @@ export function saveGraph(
66
99
  sortedGraph[key] = graph[key];
67
100
  }
68
101
 
69
- const content = JSON.stringify(sortedGraph, null, 4) + '\n';
102
+ const content = formatGraphJson(sortedGraph);
70
103
  fs.writeFileSync(fullPath, content, 'utf-8');
71
104
  }
72
105
 
package/executors.json CHANGED
@@ -35,6 +35,11 @@
35
35
  "schema": "./architecture/executors/validate-new-methods/schema.json",
36
36
  "description": "Validate new methods don't exceed max line count (only runs in affected mode)"
37
37
  },
38
+ "validate-modified-methods": {
39
+ "implementation": "./architecture/executors/validate-modified-methods/executor",
40
+ "schema": "./architecture/executors/validate-modified-methods/schema.json",
41
+ "description": "Validate modified methods don't exceed max line count (encourages gradual cleanup)"
42
+ },
38
43
  "help": {
39
44
  "implementation": "./executors/help/executor",
40
45
  "schema": "./executors/help/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/dev-config",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "Development configuration, scripts, and patterns for WebPieces projects",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/plugin.js CHANGED
@@ -36,6 +36,9 @@ const DEFAULT_OPTIONS = {
36
36
  architectureUnchanged: true,
37
37
  validatePackageJson: true,
38
38
  validateNewMethods: true,
39
+ validateModifiedMethods: true,
40
+ newMethodsMaxLines: 30,
41
+ modifiedAndNewMethodsMaxLines: 80,
39
42
  },
40
43
  features: {
41
44
  generate: true,
@@ -165,7 +168,10 @@ function createWorkspaceTargetsWithoutPrefix(opts) {
165
168
  targets['validate-packagejson'] = createValidatePackageJsonTarget();
166
169
  }
167
170
  if (opts.workspace.validations.validateNewMethods) {
168
- targets['validate-new-methods'] = createValidateNewMethodsTarget();
171
+ targets['validate-new-methods'] = createValidateNewMethodsTarget(opts.workspace.validations.newMethodsMaxLines);
172
+ }
173
+ if (opts.workspace.validations.validateModifiedMethods) {
174
+ targets['validate-modified-methods'] = createValidateModifiedMethodsTarget(opts.workspace.validations.modifiedAndNewMethodsMaxLines);
169
175
  }
170
176
  // Add validate-complete target that runs all validations
171
177
  const validationTargets = [];
@@ -184,6 +190,9 @@ function createWorkspaceTargetsWithoutPrefix(opts) {
184
190
  if (opts.workspace.validations.validateNewMethods) {
185
191
  validationTargets.push('validate-new-methods');
186
192
  }
193
+ if (opts.workspace.validations.validateModifiedMethods) {
194
+ validationTargets.push('validate-modified-methods');
195
+ }
187
196
  if (validationTargets.length > 0) {
188
197
  targets['validate-complete'] = createValidateCompleteTarget(validationTargets);
189
198
  }
@@ -218,7 +227,10 @@ function createWorkspaceTargets(opts) {
218
227
  targets[`${prefix}validate-packagejson`] = createValidatePackageJsonTarget();
219
228
  }
220
229
  if (opts.workspace.validations.validateNewMethods) {
221
- targets[`${prefix}validate-new-methods`] = createValidateNewMethodsTarget();
230
+ targets[`${prefix}validate-new-methods`] = createValidateNewMethodsTarget(opts.workspace.validations.newMethodsMaxLines);
231
+ }
232
+ if (opts.workspace.validations.validateModifiedMethods) {
233
+ targets[`${prefix}validate-modified-methods`] = createValidateModifiedMethodsTarget(opts.workspace.validations.modifiedAndNewMethodsMaxLines);
222
234
  }
223
235
  return targets;
224
236
  }
@@ -302,15 +314,27 @@ function createValidatePackageJsonTarget() {
302
314
  },
303
315
  };
304
316
  }
305
- function createValidateNewMethodsTarget() {
317
+ function createValidateNewMethodsTarget(maxLines) {
306
318
  return {
307
319
  executor: '@webpieces/dev-config:validate-new-methods',
308
320
  cache: false, // Don't cache - depends on git state
309
321
  inputs: ['default'],
310
- options: { max: 30 },
322
+ options: { max: maxLines },
323
+ metadata: {
324
+ technologies: ['nx'],
325
+ description: `Validate new methods do not exceed ${maxLines} lines (only runs in affected mode)`,
326
+ },
327
+ };
328
+ }
329
+ function createValidateModifiedMethodsTarget(maxLines) {
330
+ return {
331
+ executor: '@webpieces/dev-config:validate-modified-methods',
332
+ cache: false, // Don't cache - depends on git state
333
+ inputs: ['default'],
334
+ options: { max: maxLines },
311
335
  metadata: {
312
336
  technologies: ['nx'],
313
- description: 'Validate new methods do not exceed max line count (only runs in affected mode)',
337
+ description: `Validate new and modified methods do not exceed ${maxLines} lines (encourages gradual cleanup)`,
314
338
  },
315
339
  };
316
340
  }
@@ -53,9 +53,20 @@ function registerPlugin(tree) {
53
53
  const pluginName = '@webpieces/dev-config';
54
54
  const alreadyRegistered = nxJson.plugins.some((p) => typeof p === 'string' ? p === pluginName : p.plugin === pluginName);
55
55
  if (!alreadyRegistered) {
56
- nxJson.plugins.push(pluginName);
56
+ // Register plugin with default options for method size validation
57
+ nxJson.plugins.push({
58
+ plugin: pluginName,
59
+ options: {
60
+ workspace: {
61
+ validations: {
62
+ newMethodsMaxLines: 30,
63
+ modifiedAndNewMethodsMaxLines: 80,
64
+ },
65
+ },
66
+ },
67
+ });
57
68
  (0, devkit_1.updateNxJson)(tree, nxJson);
58
- console.log(`✅ Registered ${pluginName} plugin in nx.json`);
69
+ console.log(`✅ Registered ${pluginName} plugin in nx.json with default options`);
59
70
  }
60
71
  else {
61
72
  console.log(`ℹ️ ${pluginName} plugin is already registered`);