@webpieces/dev-config 0.2.69 → 0.2.71

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.
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Validate Return Types Executor
3
+ *
4
+ * Validates that methods have explicit return type annotations for better code readability.
5
+ * Instead of relying on TypeScript's type inference, explicit return types make code clearer:
6
+ *
7
+ * BAD: method() { return new MyClass(); }
8
+ * GOOD: method(): MyClass { return new MyClass(); }
9
+ * GOOD: async method(): Promise<MyType> { ... }
10
+ *
11
+ * Modes:
12
+ * - OFF: Skip validation entirely
13
+ * - MODIFIED_NEW: Only validate new methods (detected via git diff)
14
+ * - MODIFIED: Validate all methods in modified files
15
+ * - ALL: Validate all methods in all TypeScript files
16
+ *
17
+ * Escape hatch: Add webpieces-disable require-return-type comment with justification
18
+ */
19
+
20
+ import type { ExecutorContext } from '@nx/devkit';
21
+ import { execSync } from 'child_process';
22
+ import * as fs from 'fs';
23
+ import * as path from 'path';
24
+ import * as ts from 'typescript';
25
+
26
+ export type ReturnTypeMode = 'OFF' | 'MODIFIED_NEW' | 'MODIFIED' | 'ALL';
27
+
28
+ export interface ValidateReturnTypesOptions {
29
+ mode?: ReturnTypeMode;
30
+ }
31
+
32
+ export interface ExecutorResult {
33
+ success: boolean;
34
+ }
35
+
36
+ interface MethodViolation {
37
+ file: string;
38
+ methodName: string;
39
+ line: number;
40
+ }
41
+
42
+ /**
43
+ * Get changed TypeScript files between base and head (or working tree if head not specified).
44
+ */
45
+ // webpieces-disable max-lines-new-methods -- Git command handling with untracked files requires multiple code paths
46
+ function getChangedTypeScriptFiles(workspaceRoot: string, base: string, head?: string): string[] {
47
+ try {
48
+ const diffTarget = head ? `${base} ${head}` : base;
49
+ const output = execSync(`git diff --name-only ${diffTarget} -- '*.ts' '*.tsx'`, {
50
+ cwd: workspaceRoot,
51
+ encoding: 'utf-8',
52
+ });
53
+ const changedFiles = output
54
+ .trim()
55
+ .split('\n')
56
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
57
+
58
+ if (!head) {
59
+ try {
60
+ const untrackedOutput = execSync(`git ls-files --others --exclude-standard '*.ts' '*.tsx'`, {
61
+ cwd: workspaceRoot,
62
+ encoding: 'utf-8',
63
+ });
64
+ const untrackedFiles = untrackedOutput
65
+ .trim()
66
+ .split('\n')
67
+ .filter((f) => f && !f.includes('.spec.ts') && !f.includes('.test.ts'));
68
+ const allFiles = new Set([...changedFiles, ...untrackedFiles]);
69
+ return Array.from(allFiles);
70
+ } catch {
71
+ return changedFiles;
72
+ }
73
+ }
74
+
75
+ return changedFiles;
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get all TypeScript files in the workspace (excluding node_modules, dist, tests).
83
+ */
84
+ function getAllTypeScriptFiles(workspaceRoot: string): string[] {
85
+ try {
86
+ const output = execSync(
87
+ `find packages apps -type f \\( -name "*.ts" -o -name "*.tsx" \\) | grep -v node_modules | grep -v dist | grep -v ".spec.ts" | grep -v ".test.ts"`,
88
+ {
89
+ cwd: workspaceRoot,
90
+ encoding: 'utf-8',
91
+ }
92
+ );
93
+ return output
94
+ .trim()
95
+ .split('\n')
96
+ .filter((f) => f);
97
+ } catch {
98
+ return [];
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Get the diff content for a specific file.
104
+ */
105
+ function getFileDiff(workspaceRoot: string, file: string, base: string, head?: string): string {
106
+ try {
107
+ const diffTarget = head ? `${base} ${head}` : base;
108
+ const diff = execSync(`git diff ${diffTarget} -- "${file}"`, {
109
+ cwd: workspaceRoot,
110
+ encoding: 'utf-8',
111
+ });
112
+
113
+ if (!diff && !head) {
114
+ const fullPath = path.join(workspaceRoot, file);
115
+ if (fs.existsSync(fullPath)) {
116
+ const isUntracked = execSync(`git ls-files --others --exclude-standard "${file}"`, {
117
+ cwd: workspaceRoot,
118
+ encoding: 'utf-8',
119
+ }).trim();
120
+
121
+ if (isUntracked) {
122
+ const content = fs.readFileSync(fullPath, 'utf-8');
123
+ const lines = content.split('\n');
124
+ return lines.map((line) => `+${line}`).join('\n');
125
+ }
126
+ }
127
+ }
128
+
129
+ return diff;
130
+ } catch {
131
+ return '';
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Parse diff to find newly added method signatures.
137
+ */
138
+ function findNewMethodSignaturesInDiff(diffContent: string): Set<string> {
139
+ const newMethods = new Set<string>();
140
+ const lines = diffContent.split('\n');
141
+
142
+ const patterns = [
143
+ /^\+\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/,
144
+ /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\(/,
145
+ /^\+\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?function/,
146
+ /^\+\s*(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:async\s+)?(\w+)\s*\(/,
147
+ ];
148
+
149
+ for (const line of lines) {
150
+ if (line.startsWith('+') && !line.startsWith('+++')) {
151
+ for (const pattern of patterns) {
152
+ const match = line.match(pattern);
153
+ if (match) {
154
+ const methodName = match[1];
155
+ if (methodName && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodName)) {
156
+ newMethods.add(methodName);
157
+ }
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ return newMethods;
165
+ }
166
+
167
+ /**
168
+ * Check if a line contains a webpieces-disable comment for return type.
169
+ */
170
+ function hasDisableComment(lines: string[], lineNumber: number): boolean {
171
+ const startCheck = Math.max(0, lineNumber - 5);
172
+ for (let i = lineNumber - 2; i >= startCheck; i--) {
173
+ const line = lines[i]?.trim() ?? '';
174
+ if (line.startsWith('function ') || line.startsWith('class ') || line.endsWith('}')) {
175
+ break;
176
+ }
177
+ if (line.includes('webpieces-disable') && line.includes('require-return-type')) {
178
+ return true;
179
+ }
180
+ }
181
+ return false;
182
+ }
183
+
184
+ /**
185
+ * Check if a method has an explicit return type annotation.
186
+ */
187
+ function hasExplicitReturnType(node: ts.MethodDeclaration | ts.FunctionDeclaration | ts.ArrowFunction): boolean {
188
+ return node.type !== undefined;
189
+ }
190
+
191
+ interface MethodInfo {
192
+ name: string;
193
+ line: number;
194
+ hasReturnType: boolean;
195
+ hasDisableComment: boolean;
196
+ }
197
+
198
+ /**
199
+ * Parse a TypeScript file and find methods with their return type status.
200
+ */
201
+ // webpieces-disable max-lines-new-methods -- AST traversal requires inline visitor function
202
+ function findMethodsInFile(filePath: string, workspaceRoot: string): MethodInfo[] {
203
+ const fullPath = path.join(workspaceRoot, filePath);
204
+ if (!fs.existsSync(fullPath)) return [];
205
+
206
+ const content = fs.readFileSync(fullPath, 'utf-8');
207
+ const fileLines = content.split('\n');
208
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
209
+
210
+ const methods: MethodInfo[] = [];
211
+
212
+ // webpieces-disable max-lines-new-methods -- AST visitor pattern requires handling multiple node types
213
+ function visit(node: ts.Node): void {
214
+ let methodName: string | undefined;
215
+ let startLine: number | undefined;
216
+ let hasReturnType = false;
217
+
218
+ if (ts.isMethodDeclaration(node) && node.name) {
219
+ methodName = node.name.getText(sourceFile);
220
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
221
+ startLine = start.line + 1;
222
+ hasReturnType = hasExplicitReturnType(node);
223
+ } else if (ts.isFunctionDeclaration(node) && node.name) {
224
+ methodName = node.name.getText(sourceFile);
225
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
226
+ startLine = start.line + 1;
227
+ hasReturnType = hasExplicitReturnType(node);
228
+ } else if (ts.isArrowFunction(node)) {
229
+ if (ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
230
+ methodName = node.parent.name.getText(sourceFile);
231
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
232
+ startLine = start.line + 1;
233
+ hasReturnType = hasExplicitReturnType(node);
234
+ }
235
+ }
236
+
237
+ if (methodName && startLine !== undefined) {
238
+ methods.push({
239
+ name: methodName,
240
+ line: startLine,
241
+ hasReturnType,
242
+ hasDisableComment: hasDisableComment(fileLines, startLine),
243
+ });
244
+ }
245
+
246
+ ts.forEachChild(node, visit);
247
+ }
248
+
249
+ visit(sourceFile);
250
+ return methods;
251
+ }
252
+
253
+ /**
254
+ * Find methods without explicit return types based on mode.
255
+ */
256
+ // webpieces-disable max-lines-new-methods -- File iteration with diff parsing and method matching
257
+ function findViolationsForModifiedNew(
258
+ workspaceRoot: string,
259
+ changedFiles: string[],
260
+ base: string,
261
+ head?: string
262
+ ): MethodViolation[] {
263
+ const violations: MethodViolation[] = [];
264
+
265
+ for (const file of changedFiles) {
266
+ const diff = getFileDiff(workspaceRoot, file, base, head);
267
+ const newMethodNames = findNewMethodSignaturesInDiff(diff);
268
+
269
+ if (newMethodNames.size === 0) continue;
270
+
271
+ const methods = findMethodsInFile(file, workspaceRoot);
272
+
273
+ for (const method of methods) {
274
+ if (!newMethodNames.has(method.name)) continue;
275
+ if (method.hasReturnType) continue;
276
+ if (method.hasDisableComment) continue;
277
+
278
+ violations.push({
279
+ file,
280
+ methodName: method.name,
281
+ line: method.line,
282
+ });
283
+ }
284
+ }
285
+
286
+ return violations;
287
+ }
288
+
289
+ /**
290
+ * Find all methods without explicit return types in modified files.
291
+ */
292
+ function findViolationsForModified(workspaceRoot: string, changedFiles: string[]): MethodViolation[] {
293
+ const violations: MethodViolation[] = [];
294
+
295
+ for (const file of changedFiles) {
296
+ const methods = findMethodsInFile(file, workspaceRoot);
297
+
298
+ for (const method of methods) {
299
+ if (method.hasReturnType) continue;
300
+ if (method.hasDisableComment) continue;
301
+
302
+ violations.push({
303
+ file,
304
+ methodName: method.name,
305
+ line: method.line,
306
+ });
307
+ }
308
+ }
309
+
310
+ return violations;
311
+ }
312
+
313
+ /**
314
+ * Find all methods without explicit return types in all files.
315
+ */
316
+ function findViolationsForAll(workspaceRoot: string): MethodViolation[] {
317
+ const allFiles = getAllTypeScriptFiles(workspaceRoot);
318
+ return findViolationsForModified(workspaceRoot, allFiles);
319
+ }
320
+
321
+ /**
322
+ * Auto-detect the base branch by finding the merge-base with origin/main.
323
+ */
324
+ function detectBase(workspaceRoot: string): string | null {
325
+ try {
326
+ const mergeBase = execSync('git merge-base HEAD origin/main', {
327
+ cwd: workspaceRoot,
328
+ encoding: 'utf-8',
329
+ stdio: ['pipe', 'pipe', 'pipe'],
330
+ }).trim();
331
+
332
+ if (mergeBase) {
333
+ return mergeBase;
334
+ }
335
+ } catch {
336
+ try {
337
+ const mergeBase = execSync('git merge-base HEAD main', {
338
+ cwd: workspaceRoot,
339
+ encoding: 'utf-8',
340
+ stdio: ['pipe', 'pipe', 'pipe'],
341
+ }).trim();
342
+
343
+ if (mergeBase) {
344
+ return mergeBase;
345
+ }
346
+ } catch {
347
+ // Ignore
348
+ }
349
+ }
350
+ return null;
351
+ }
352
+
353
+ /**
354
+ * Report violations to console.
355
+ */
356
+ function reportViolations(violations: MethodViolation[], mode: ReturnTypeMode): void {
357
+ console.error('');
358
+ console.error('āŒ Methods missing explicit return types!');
359
+ console.error('');
360
+ console.error('šŸ“š Explicit return types improve code readability:');
361
+ console.error('');
362
+ console.error(' BAD: method() { return new MyClass(); }');
363
+ console.error(' GOOD: method(): MyClass { return new MyClass(); }');
364
+ console.error(' GOOD: async method(): Promise<MyType> { ... }');
365
+ console.error('');
366
+
367
+ for (const v of violations) {
368
+ console.error(` āŒ ${v.file}:${v.line}`);
369
+ console.error(` Method: ${v.methodName} - missing return type annotation`);
370
+ }
371
+ console.error('');
372
+
373
+ console.error(' To fix: Add explicit return type after the parameter list');
374
+ console.error('');
375
+ console.error(' Escape hatch (use sparingly):');
376
+ console.error(' // webpieces-disable require-return-type -- [your reason]');
377
+ console.error('');
378
+ console.error(` Current mode: ${mode}`);
379
+ console.error('');
380
+ }
381
+
382
+ export default async function runExecutor(
383
+ options: ValidateReturnTypesOptions,
384
+ context: ExecutorContext
385
+ ): Promise<ExecutorResult> {
386
+ const workspaceRoot = context.root;
387
+ const mode: ReturnTypeMode = options.mode ?? 'MODIFIED_NEW';
388
+
389
+ if (mode === 'OFF') {
390
+ console.log('\nā­ļø Skipping return type validation (mode: OFF)');
391
+ console.log('');
392
+ return { success: true };
393
+ }
394
+
395
+ console.log('\nšŸ“ Validating Return Types\n');
396
+ console.log(` Mode: ${mode}`);
397
+
398
+ let violations: MethodViolation[] = [];
399
+
400
+ if (mode === 'ALL') {
401
+ console.log(' Scope: All TypeScript files');
402
+ console.log('');
403
+ violations = findViolationsForAll(workspaceRoot);
404
+ } else {
405
+ let base = process.env['NX_BASE'];
406
+ const head = process.env['NX_HEAD'];
407
+
408
+ if (!base) {
409
+ base = detectBase(workspaceRoot) ?? undefined;
410
+
411
+ if (!base) {
412
+ console.log('\nā­ļø Skipping return type validation (could not detect base branch)');
413
+ console.log('');
414
+ return { success: true };
415
+ }
416
+ }
417
+
418
+ console.log(` Base: ${base}`);
419
+ console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}`);
420
+ console.log('');
421
+
422
+ const changedFiles = getChangedTypeScriptFiles(workspaceRoot, base, head);
423
+
424
+ if (changedFiles.length === 0) {
425
+ console.log('āœ… No TypeScript files changed');
426
+ return { success: true };
427
+ }
428
+
429
+ console.log(`šŸ“‚ Checking ${changedFiles.length} changed file(s)...`);
430
+
431
+ if (mode === 'MODIFIED_NEW') {
432
+ violations = findViolationsForModifiedNew(workspaceRoot, changedFiles, base, head);
433
+ } else if (mode === 'MODIFIED') {
434
+ violations = findViolationsForModified(workspaceRoot, changedFiles);
435
+ }
436
+ }
437
+
438
+ if (violations.length === 0) {
439
+ console.log('āœ… All methods have explicit return types');
440
+ return { success: true };
441
+ }
442
+
443
+ reportViolations(violations, mode);
444
+
445
+ return { success: false };
446
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "title": "Validate Return Types Executor",
4
+ "description": "Validates that methods have explicit return type annotations for better code readability.",
5
+ "type": "object",
6
+ "properties": {
7
+ "mode": {
8
+ "type": "string",
9
+ "enum": ["OFF", "MODIFIED_NEW", "MODIFIED", "ALL"],
10
+ "description": "OFF: skip validation. MODIFIED_NEW: only validate new methods. MODIFIED: validate all methods in modified files. ALL: validate all methods in all files.",
11
+ "default": "MODIFIED_NEW"
12
+ }
13
+ },
14
+ "required": []
15
+ }
package/executors.json CHANGED
@@ -64,6 +64,11 @@
64
64
  "implementation": "./architecture/executors/validate-code/executor",
65
65
  "schema": "./architecture/executors/validate-code/schema.json",
66
66
  "description": "Combined validation for new methods, modified methods, and file sizes"
67
+ },
68
+ "validate-return-types": {
69
+ "implementation": "./architecture/executors/validate-return-types/executor",
70
+ "schema": "./architecture/executors/validate-return-types/schema.json",
71
+ "description": "Validate methods have explicit return type annotations"
67
72
  }
68
73
  }
69
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/dev-config",
3
- "version": "0.2.69",
3
+ "version": "0.2.71",
4
4
  "description": "Development configuration, scripts, and patterns for WebPieces projects",
5
5
  "type": "commonjs",
6
6
  "bin": {