@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.
- package/architecture/executors/validate-modified-methods/executor.d.ts +26 -0
- package/architecture/executors/validate-modified-methods/executor.js +505 -0
- package/architecture/executors/validate-modified-methods/executor.js.map +1 -0
- package/architecture/executors/validate-modified-methods/executor.ts +569 -0
- package/architecture/executors/validate-modified-methods/schema.json +14 -0
- package/architecture/executors/validate-new-methods/executor.d.ts +5 -2
- package/architecture/executors/validate-new-methods/executor.js +131 -52
- package/architecture/executors/validate-new-methods/executor.js.map +1 -1
- package/architecture/executors/validate-new-methods/executor.ts +135 -55
- package/architecture/lib/graph-loader.js +29 -1
- package/architecture/lib/graph-loader.js.map +1 -1
- package/architecture/lib/graph-loader.ts +34 -1
- package/executors.json +5 -0
- package/package.json +1 -1
- package/plugin.js +29 -5
- package/src/generators/init/generator.js +13 -2
- package/src/generators/init/generator.js.map +1 -1
- package/templates/eslint.webpieces.config.mjs +2 -2
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
61
|
+
## Why Limit New Methods?
|
|
51
62
|
|
|
52
|
-
Methods under
|
|
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
|
-
|
|
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
|
|
138
|
+
**Escape hatch**: Add a webpieces-disable comment with justification:
|
|
129
139
|
|
|
130
140
|
\`\`\`typescript
|
|
131
|
-
//
|
|
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
|
|
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
|
|
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
|
|
180
|
+
function getChangedTypeScriptFiles(workspaceRoot: string, base: string): string[] {
|
|
169
181
|
try {
|
|
170
|
-
|
|
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
|
|
201
|
+
function getFileDiff(workspaceRoot: string, file: string, base: string): string {
|
|
187
202
|
try {
|
|
188
|
-
|
|
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(
|
|
207
|
-
/^\+\s*(async\s+)?(\w+)\s*\(/,
|
|
208
|
-
//
|
|
209
|
-
/^\+\s*(
|
|
210
|
-
// const/let methodName =
|
|
211
|
-
/^\+\s*(?:const|let)\s+(\w+)\s*=\s*(?:async\s
|
|
212
|
-
//
|
|
213
|
-
/^\+\s*(?:
|
|
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
|
|
222
|
-
const methodName = match[
|
|
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
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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(`
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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;
|
|
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 =
|
|
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
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:
|
|
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:
|
|
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
|
-
|
|
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`);
|