@webpieces/dev-config 0.2.31 → 0.2.33

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.
@@ -100,6 +100,9 @@ function ensureDocFile(docPath: string, content: string): boolean {
100
100
  /**
101
101
  * Ensures the exception documentation markdown file exists
102
102
  * Only creates file once per lint run using module-level flag
103
+ *
104
+ * Reads from the template file packaged with @webpieces/dev-config
105
+ * and copies it to tmp/webpieces/ for AI agents to read.
103
106
  */
104
107
  function ensureExceptionDoc(context: Rule.RuleContext): void {
105
108
  if (exceptionDocCreated) return;
@@ -107,479 +110,28 @@ function ensureExceptionDoc(context: Rule.RuleContext): void {
107
110
  const workspaceRoot = getWorkspaceRoot(context);
108
111
  const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.exceptions.md');
109
112
 
110
- if (ensureDocFile(docPath, EXCEPTION_DOC_CONTENT)) {
111
- exceptionDocCreated = true;
112
- }
113
- }
114
-
115
- // Module-level flag to prevent redundant markdown file creation
116
- let exceptionDocCreated = false;
117
-
118
- // Comprehensive markdown documentation content
119
- const EXCEPTION_DOC_CONTENT = `# AI Agent Instructions: Try-Catch Blocks Detected
120
-
121
- **READ THIS FILE to understand why try-catch blocks are restricted and how to fix violations**
122
-
123
- ## Core Principle
124
-
125
- **EXCEPTIONS MUST BUBBLE TO GLOBAL HANDLER WITH TRACEID FOR DEBUGGABILITY.**
126
-
127
- The webpieces framework uses a global error handling architecture where:
128
- - Every request gets a unique traceId stored in RequestContext
129
- - All errors bubble to the global handler (WebpiecesMiddleware.globalErrorHandler)
130
- - Error IDs enable lookup via \`/debugLocal/{id}\` and \`/debugCloud/{id}\` endpoints
131
- - Local try-catch blocks break this pattern by losing error IDs and context
132
-
133
- This is not a performance concern - it's an architecture decision for distributed tracing and debugging in production.
134
-
135
- ## Why This Rule Exists
136
-
137
- ### Problem 1: AI Over-Adds Try-Catch (Especially Frontend)
138
- AI agents tend to add defensive try-catch blocks everywhere, which:
139
- - Swallows errors and loses traceId
140
- - Shows custom error messages without debugging context
141
- - Makes production issues impossible to trace
142
- - Creates "blind spots" where errors disappear
143
-
144
- ### Problem 2: Lost TraceId = Lost Debugging Capability
145
- Without traceId in errors:
146
- - \`/debugLocal/{id}\` endpoint cannot retrieve error details
147
- - \`/debugCloud/{id}\` endpoint cannot correlate logs
148
- - DevOps cannot trace request flow through distributed systems
149
- - Users report "an error occurred" with no way to investigate
150
-
151
- ### Problem 3: Try-Catch-Rethrow Is Code Smell
152
- \`\`\`typescript
153
- // BAD: Why catch if you're just rethrowing?
154
- try {
155
- await operation();
156
- } catch (err: any) {
157
- const error = toError(err);
158
- console.error('Failed:', error);
159
- throw error; // Why catch at all???
160
- }
161
- \`\`\`
162
- 99% of the time, there's a better pattern (logging filter, global handler, etc.).
163
-
164
- ### Problem 4: Swallowing Exceptions = Lazy Programming
165
- \`\`\`typescript
166
- // BAD: "I don't want to deal with this error"
167
- try {
168
- await riskyOperation();
169
- } catch (err: any) {
170
- // Silence...
171
- }
172
- \`\`\`
173
- This is the #1 shortcut developers take that creates production nightmares.
174
-
175
- ## Industry Best Practices (2025)
176
-
177
- ### Distributed Tracing: The Three Pillars
178
- Modern observability requires correlation across:
179
- 1. **Traces** - Request flow through services
180
- 2. **Logs** - Contextual debugging information
181
- 3. **Metrics** - Aggregated system health
182
-
183
- TraceId (also called correlation ID, request ID) ties these together.
184
-
185
- ### Research Findings
186
- - **Performance**: Try-catch is an expensive operation in V8 engine (source: Node.js performance docs)
187
- - **Error Handling**: Global handlers at highest level reduce blind spots by 40% (source: Google SRE practices)
188
- - **Middleware Pattern**: Express/Koa middleware with async error boundaries is industry standard (source: Express.js error handling docs)
189
- - **Only Catch What You Can Handle**: If you can't recover, let it bubble (source: "Effective Error Handling" - JavaScript design patterns)
190
-
191
- ### 2025 Trends
192
- - Correlation IDs are standard in microservices (OpenTelemetry, Datadog, New Relic)
193
- - Structured logging with context (Winston, Pino)
194
- - Middleware-based error boundaries reduce boilerplate
195
- - Frontend: React Error Boundaries, not scattered try-catch
196
-
197
- ## Command: Remove Try-Catch and Use Global Handler
198
-
199
- ## AI Agent Action Steps
200
-
201
- 1. **IDENTIFY** the try-catch block flagged in the error message
202
-
203
- 2. **ANALYZE** the purpose:
204
- - Is it catching errors just to log them? → Remove (use LogApiFilter)
205
- - Is it catching to show custom message? → Remove (use global handler)
206
- - Is it catching to retry? → Requires approval (see Acceptable Patterns)
207
- - Is it catching in a batch loop? → Requires approval (see Acceptable Patterns)
208
- - Is it catching for cleanup? → Usually wrong pattern
209
-
210
- 3. **REMOVE** the try-catch block:
211
- - Delete the \`try {\` and \`} catch (err: any) { ... }\` wrapper
212
- - Let the code execute normally
213
- - Errors will bubble to global handler automatically
214
-
215
- 4. **VERIFY** global handler exists:
216
- - Check that WebpiecesMiddleware.globalErrorHandler is registered
217
- - Check that ContextFilter is setting up RequestContext
218
- - Check that traceId is being added to RequestContext
219
-
220
- 5. **ADD** traceId to RequestContext (if not already present):
221
- - In ContextFilter or similar high-priority filter
222
- - Use \`RequestContext.put('TRACE_ID', generateTraceId())\`
223
-
224
- 6. **TEST** error flow:
225
- - Trigger an error in the code
226
- - Verify error is logged with traceId
227
- - Verify \`/debugLocal/{traceId}\` endpoint works
228
-
229
- ## Pattern 1: Global Error Handler (GOOD)
230
-
231
- ### Server-Side: WebpiecesMiddleware
232
-
233
- \`\`\`typescript
234
- // packages/http/http-server/src/WebpiecesMiddleware.ts
235
- @provideSingleton()
236
- @injectable()
237
- export class WebpiecesMiddleware {
238
- async globalErrorHandler(
239
- req: Request,
240
- res: Response,
241
- next: NextFunction
242
- ): Promise<void> {
243
- console.log('[GlobalErrorHandler] Request START:', req.method, req.path);
113
+ // Read from the template file packaged with the npm module
114
+ // Path: from eslint-plugin/rules/ -> ../../templates/
115
+ const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.exceptions.md');
244
116
 
117
+ let content: string;
245
118
  try {
246
- // Await catches BOTH sync throws AND rejected promises
247
- await next();
248
- console.log('[GlobalErrorHandler] Request END (success)');
249
- } catch (err: any) {
250
- const error = toError(err);
251
- const traceId = RequestContext.get<string>('TRACE_ID');
252
-
253
- // Log with traceId for /debugLocal lookup
254
- console.error('[GlobalErrorHandler] ERROR:', {
255
- traceId,
256
- message: error.message,
257
- stack: error.stack,
258
- path: req.path,
259
- method: req.method,
260
- });
261
-
262
- // Store error for /debugLocal/{id} endpoint
263
- ErrorStore.save(traceId, error);
264
-
265
- if (!res.headersSent) {
266
- res.status(500).send(\`
267
- <!DOCTYPE html>
268
- <html>
269
- <head><title>Server Error</title></head>
270
- <body>
271
- <h1>Server Error</h1>
272
- <p>An error occurred. Reference ID: \${traceId}</p>
273
- <p>Contact support with this ID to investigate.</p>
274
- </body>
275
- </html>
276
- \`);
277
- }
119
+ content = fs.readFileSync(templatePath, 'utf-8');
120
+ } catch {
121
+ // Fallback message if template not found (shouldn't happen in published package)
122
+ content = `# Exception Documentation Not Found\n\nTemplate file not found at: ${templatePath}\n\nPlease ensure @webpieces/dev-config is properly installed.`;
278
123
  }
279
- }
280
- }
281
- \`\`\`
282
-
283
- ### Adding TraceId: ContextFilter
284
-
285
- \`\`\`typescript
286
- // packages/http/http-server/src/filters/ContextFilter.ts
287
- import { v4 as uuidv4 } from 'uuid';
288
-
289
- @provideSingleton()
290
- @injectable()
291
- export class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {
292
- async filter(
293
- meta: MethodMeta,
294
- nextFilter: Service<MethodMeta, WpResponse<unknown>>
295
- ): Promise<WpResponse<unknown>> {
296
- return RequestContext.run(async () => {
297
- // Generate unique traceId for this request
298
- const traceId = uuidv4();
299
- RequestContext.put('TRACE_ID', traceId);
300
- RequestContext.put('METHOD_META', meta);
301
- RequestContext.put('REQUEST_PATH', meta.path);
302
-
303
- return await nextFilter.invoke(meta);
304
- // RequestContext auto-cleared when done
305
- });
306
- }
307
- }
308
- \`\`\`
309
-
310
- ## Pattern 2: Debug Endpoints (GOOD)
311
-
312
- \`\`\`typescript
313
- // Example debug endpoint for local development
314
- @provideSingleton()
315
- @Controller()
316
- export class DebugController implements DebugApi {
317
- @Get()
318
- @Path('/debugLocal/:id')
319
- async getErrorById(@PathParam('id') id: string): Promise<DebugErrorResponse> {
320
- const error = ErrorStore.get(id);
321
- if (!error) {
322
- throw new HttpNotFoundError(\`Error \${id} not found\`);
323
- }
324
-
325
- return {
326
- traceId: id,
327
- message: error.message,
328
- stack: error.stack,
329
- timestamp: error.timestamp,
330
- requestPath: error.requestPath,
331
- requestMethod: error.requestMethod,
332
- };
333
- }
334
- }
335
-
336
- // ErrorStore singleton (in-memory for local, Redis for production)
337
- class ErrorStoreImpl {
338
- private errors = new Map<string, ErrorRecord>();
339
-
340
- save(traceId: string, error: Error): void {
341
- this.errors.set(traceId, {
342
- traceId,
343
- message: error.message,
344
- stack: error.stack,
345
- timestamp: new Date(),
346
- requestPath: RequestContext.get('REQUEST_PATH'),
347
- requestMethod: RequestContext.get('HTTP_METHOD'),
348
- });
349
- }
350
-
351
- get(traceId: string): ErrorRecord | undefined {
352
- return this.errors.get(traceId);
353
- }
354
- }
355
-
356
- export const ErrorStore = new ErrorStoreImpl();
357
- \`\`\`
358
124
 
359
- ## Examples
360
-
361
- ### BAD Example 1: Local Try-Catch That Swallows Error
362
-
363
- \`\`\`typescript
364
- // BAD: Error is swallowed, no traceId in logs
365
- async function processOrder(order: Order): Promise<void> {
366
- try {
367
- await validateOrder(order);
368
- await saveToDatabase(order);
369
- } catch (err: any) {
370
- // Error disappears into void - debugging nightmare!
371
- console.log('Order processing failed');
372
- }
373
- }
374
- \`\`\`
375
-
376
- **Problem**: When this fails in production, you have:
377
- - No traceId to look up the error
378
- - No stack trace
379
- - No request context
380
- - No way to investigate
381
-
382
- ### BAD Example 2: Try-Catch With Custom Error (No TraceId)
383
-
384
- \`\`\`typescript
385
- // BAD: Shows custom message but loses traceId
386
- async function fetchUserData(userId: string): Promise<User> {
387
- try {
388
- const response = await fetch(\`/api/users/\${userId}\`);
389
- return await response.json();
390
- } catch (err: any) {
391
- const error = toError(err);
392
- // Custom message without traceId
393
- throw new Error(\`Failed to fetch user \${userId}: \${error.message}\`);
394
- }
395
- }
396
- \`\`\`
397
-
398
- **Problem**:
399
- - Original error context is lost
400
- - No traceId attached to new error
401
- - Global handler receives generic error, can't trace root cause
402
-
403
- ### GOOD Example 1: Let Error Bubble
404
-
405
- \`\`\`typescript
406
- // GOOD: Error bubbles to global handler with traceId
407
- async function processOrder(order: Order): Promise<void> {
408
- // No try-catch needed!
409
- await validateOrder(order);
410
- await saveToDatabase(order);
411
- // If error occurs, it bubbles with traceId intact
412
- }
413
- \`\`\`
414
-
415
- **Why GOOD**:
416
- - Global handler catches error
417
- - TraceId from RequestContext is preserved
418
- - Full stack trace available
419
- - \`/debugLocal/{traceId}\` endpoint works
420
-
421
- ### GOOD Example 2: Global Handler Logs With TraceId
422
-
423
- \`\`\`typescript
424
- // GOOD: Global handler has full context
425
- // In WebpiecesMiddleware.globalErrorHandler (see Pattern 1 above)
426
- catch (err: any) {
427
- const error = toError(err);
428
- const traceId = RequestContext.get<string>('TRACE_ID');
429
-
430
- console.error('[GlobalErrorHandler] ERROR:', {
431
- traceId, // Unique ID for this request
432
- message: error.message,
433
- stack: error.stack,
434
- path: req.path, // Request context preserved
435
- });
436
- }
437
- \`\`\`
438
-
439
- **Why GOOD**:
440
- - TraceId logged with every error
441
- - Full request context available
442
- - Error stored for \`/debugLocal/{id}\` lookup
443
- - DevOps can trace distributed requests
444
-
445
- ### ACCEPTABLE Example 1: Retry Loop (With eslint-disable)
446
-
447
- \`\`\`typescript
448
- // ACCEPTABLE: Retry pattern requires try-catch
449
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Retry loop with exponential backoff
450
- async function callVendorApiWithRetry(request: VendorRequest): Promise<VendorResponse> {
451
- const maxRetries = 3;
452
- let lastError: Error | undefined;
453
-
454
- for (let i = 0; i < maxRetries; i++) {
455
- try {
456
- return await vendorApi.call(request);
457
- } catch (err: any) {
458
- const error = toError(err);
459
- lastError = error;
460
- console.warn(\`Retry \${i + 1}/\${maxRetries} failed:\`, error.message);
461
- await sleep(1000 * Math.pow(2, i)); // Exponential backoff
462
- }
463
- }
464
-
465
- // After retries exhausted, throw with traceId
466
- const traceId = RequestContext.get<string>('TRACE_ID');
467
- throw new HttpVendorError(
468
- \`Vendor API failed after \${maxRetries} retries. TraceId: \${traceId}\`,
469
- lastError
470
- );
471
- }
472
- \`\`\`
473
-
474
- **Why ACCEPTABLE**:
475
- - Legitimate use case: retry logic
476
- - Final error still includes traceId
477
- - Error still bubbles to global handler
478
- - Requires senior developer approval (enforced by PR review)
479
-
480
- ### ACCEPTABLE Example 2: Batching Pattern (With eslint-disable)
481
-
482
- \`\`\`typescript
483
- // ACCEPTABLE: Batching requires try-catch to continue processing
484
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Batch processing continues on individual failures
485
- async function processBatch(items: Item[]): Promise<BatchResult> {
486
- const results: ItemResult[] = [];
487
- const errors: ItemError[] = [];
488
- const traceId = RequestContext.get<string>('TRACE_ID');
489
-
490
- for (const item of items) {
491
- try {
492
- const result = await processItem(item);
493
- results.push(result);
494
- } catch (err: any) {
495
- const error = toError(err);
496
- // Log individual error with traceId
497
- console.error(\`[Batch] Item \${item.id} failed (traceId: \${traceId}):\`, error);
498
- errors.push({ itemId: item.id, error: error.message, traceId });
125
+ if (ensureDocFile(docPath, content)) {
126
+ exceptionDocCreated = true;
499
127
  }
500
- }
501
-
502
- // Return both successes and failures
503
- return {
504
- traceId,
505
- successCount: results.length,
506
- failureCount: errors.length,
507
- results,
508
- errors,
509
- };
510
- }
511
- \`\`\`
512
-
513
- **Why ACCEPTABLE**:
514
- - Legitimate use case: partial failure handling
515
- - Each error logged with traceId
516
- - Batch traceId included in response
517
- - Requires senior developer approval (enforced by PR review)
518
-
519
- ### UNACCEPTABLE Example: Try-Catch-Rethrow
520
-
521
- \`\`\`typescript
522
- // UNACCEPTABLE: Pointless try-catch that just rethrows
523
- async function saveUser(user: User): Promise<void> {
524
- try {
525
- await database.save(user);
526
- } catch (err: any) {
527
- const error = toError(err);
528
- console.error('Save failed:', error);
529
- throw error; // Why catch at all???
530
- }
531
128
  }
532
- \`\`\`
533
-
534
- **Why UNACCEPTABLE**:
535
- - Adds no value - logging should be in LogApiFilter
536
- - Global handler already logs errors
537
- - Just adds noise and confusion
538
- - Remove the try-catch entirely!
539
-
540
- ## When eslint-disable IS Acceptable
541
-
542
- You may use \`// eslint-disable-next-line @webpieces/no-unmanaged-exceptions\` ONLY for:
543
-
544
- 1. **Retry loops** with exponential backoff (vendor API calls)
545
- 2. **Batching patterns** where partial failure is expected
546
- 3. **Resource cleanup** with explicit approval
547
-
548
- All three require:
549
- - Senior developer approval in PR review
550
- - Comment explaining WHY try-catch is needed
551
- - TraceId must still be logged/included in final error
552
-
553
- ## How to Request Approval
554
129
 
555
- If you believe you have a legitimate use case for try-catch:
556
-
557
- 1. **Add a comment** explaining why it's needed:
558
- \`\`\`typescript
559
- // JUSTIFICATION: Vendor API requires retry loop with exponential backoff
560
- // to handle rate limiting. Final error includes traceId for debugging.
561
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
562
- \`\`\`
563
-
564
- 2. **Ensure traceId is preserved** in final error or logged
565
-
566
- 3. **Request PR review** from senior developer
567
-
568
- 4. **Be prepared to justify** - 99% of try-catch can be removed
569
-
570
- ## Summary
571
-
572
- **The webpieces philosophy**: Errors should bubble to the global handler where they are logged with traceId and stored for debugging. Local try-catch blocks break this architecture and create blind spots in production.
573
-
574
- **Key takeaways**:
575
- - Global error handler with traceId = debuggable production issues
576
- - Local try-catch = lost context and debugging nightmares
577
- - 99% of try-catch blocks can be removed safely
578
- - Only use try-catch for: retries, batching (with approval)
579
- - TraceId enables \`/debugLocal/{id}\` and \`/debugCloud/{id}\` endpoints
130
+ // Module-level flag to prevent redundant markdown file creation
131
+ let exceptionDocCreated = false;
580
132
 
581
- **Remember**: If you can't handle the error meaningfully, don't catch it. Let it bubble to the global handler where it will be logged with full context and traceId.
582
- `;
133
+ // NOTE: Documentation content moved to templates/webpieces.exceptions.md
134
+ // The ensureExceptionDoc function reads from that file at runtime.
583
135
 
584
136
  const rule: Rule.RuleModule = {
585
137
  meta: {
@@ -600,17 +152,22 @@ const rule: Rule.RuleModule = {
600
152
 
601
153
  create(context: Rule.RuleContext): Rule.RuleListener {
602
154
  return {
603
- TryStatement(node: any): void {
155
+ TryStatement(node: unknown): void {
156
+ // Skip try..finally blocks (no catch handler, no exception handling)
157
+ if (!(node as { handler?: unknown }).handler) {
158
+ return;
159
+ }
160
+
604
161
  // Auto-allow in test files
605
162
  const filename = context.filename || context.getFilename();
606
163
  if (isTestFile(filename)) {
607
164
  return;
608
165
  }
609
166
 
610
- // Not in test file - report violation
167
+ // Has catch block outside test file - report violation
611
168
  ensureExceptionDoc(context);
612
169
  context.report({
613
- node,
170
+ node: node as Rule.Node,
614
171
  messageId: 'noUnmanagedExceptions',
615
172
  });
616
173
  },
@@ -619,3 +176,4 @@ const rule: Rule.RuleModule = {
619
176
  };
620
177
 
621
178
  export = rule;
179
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/dev-config",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "Development configuration, scripts, and patterns for WebPieces projects",
5
5
  "type": "commonjs",
6
6
  "bin": {