@webpieces/dev-config 0.2.30 → 0.2.32
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/lib/graph-visualizer.js +12 -4
- package/architecture/lib/graph-visualizer.js.map +1 -1
- package/architecture/lib/graph-visualizer.ts +13 -4
- package/eslint-plugin/rules/enforce-architecture.js +70 -19
- package/eslint-plugin/rules/enforce-architecture.js.map +1 -1
- package/eslint-plugin/rules/enforce-architecture.ts +76 -20
- package/eslint-plugin/rules/no-unmanaged-exceptions.js +5 -1
- package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +1 -1
- package/eslint-plugin/rules/no-unmanaged-exceptions.ts +6 -1
- package/package.json +1 -1
|
@@ -26,6 +26,14 @@ const LEVEL_COLORS = {
|
|
|
26
26
|
2: '#FFF3E0', // Light orange - applications
|
|
27
27
|
3: '#FCE4EC', // Light pink - higher level
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Remove scope from name for display
|
|
31
|
+
* '@scope/name' → 'name'
|
|
32
|
+
* 'name' → 'name'
|
|
33
|
+
*/
|
|
34
|
+
function getShortName(name) {
|
|
35
|
+
return name.includes('/') ? name.split('/').pop() : name;
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Generate Graphviz DOT format from the graph
|
|
31
39
|
*/
|
|
@@ -43,7 +51,7 @@ function generateDot(graph, title = 'WebPieces Architecture') {
|
|
|
43
51
|
}
|
|
44
52
|
// Create nodes with level-based colors
|
|
45
53
|
for (const [project, info] of Object.entries(graph)) {
|
|
46
|
-
const shortName = project
|
|
54
|
+
const shortName = getShortName(project);
|
|
47
55
|
const color = LEVEL_COLORS[info.level] || '#F5F5F5';
|
|
48
56
|
dot += ` "${shortName}" [fillcolor="${color}", label="${shortName}\\n(L${info.level})"];\n`;
|
|
49
57
|
}
|
|
@@ -52,7 +60,7 @@ function generateDot(graph, title = 'WebPieces Architecture') {
|
|
|
52
60
|
for (const [level, projects] of Object.entries(levels)) {
|
|
53
61
|
dot += ` { rank=same; `;
|
|
54
62
|
projects.forEach((p) => {
|
|
55
|
-
const shortName = p
|
|
63
|
+
const shortName = getShortName(p);
|
|
56
64
|
dot += `"${shortName}"; `;
|
|
57
65
|
});
|
|
58
66
|
dot += '}\n';
|
|
@@ -60,9 +68,9 @@ function generateDot(graph, title = 'WebPieces Architecture') {
|
|
|
60
68
|
dot += '\n';
|
|
61
69
|
// Create edges (dependencies)
|
|
62
70
|
for (const [project, info] of Object.entries(graph)) {
|
|
63
|
-
const shortName = project
|
|
71
|
+
const shortName = getShortName(project);
|
|
64
72
|
for (const dep of info.dependsOn || []) {
|
|
65
|
-
const depShortName = dep
|
|
73
|
+
const depShortName = getShortName(dep);
|
|
66
74
|
dot += ` "${shortName}" -> "${depShortName}";\n`;
|
|
67
75
|
}
|
|
68
76
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * Output files go to tmp/webpieces/ for easy viewing without committing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\n\n/**\n * Level colors for visualization\n */\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9', // Light green - foundation\n 1: '#E3F2FD', // Light blue - middleware\n 2: '#FFF3E0', // Light orange - applications\n 3: '#FCE4EC', // Light pink - higher level\n};\n\n/**\n * Generate Graphviz DOT format from the graph\n */\nexport function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Architecture'): string {\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n // Group projects by level\n const levels: Record<number, string[]> = {};\n for (const [project, info] of Object.entries(graph)) {\n if (!levels[info.level]) levels[info.level] = [];\n levels[info.level].push(project);\n }\n\n // Create nodes with level-based colors\n for (const [project, info] of Object.entries(graph)) {\n const shortName = project.replace('@webpieces/', '');\n const color = LEVEL_COLORS[info.level] || '#F5F5F5';\n dot += ` \"${shortName}\" [fillcolor=\"${color}\", label=\"${shortName}\\\\n(L${info.level})\"];\\n`;\n }\n\n dot += '\\n';\n\n // Create same-rank subgraphs for each level\n for (const [level, projects] of Object.entries(levels)) {\n dot += ` { rank=same; `;\n projects.forEach((p) => {\n const shortName = p.replace('@webpieces/', '');\n dot += `\"${shortName}\"; `;\n });\n dot += '}\\n';\n }\n\n dot += '\\n';\n\n // Create edges (dependencies)\n for (const [project, info] of Object.entries(graph)) {\n const shortName = project.replace('@webpieces/', '');\n for (const dep of info.dependsOn || []) {\n const depShortName = dep.replace('@webpieces/', '');\n dot += ` \"${shortName}\" -> \"${depShortName}\";\\n`;\n }\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n}\n\n/**\n * Generate interactive HTML with embedded SVG using viz.js\n */\nexport function generateHTML(dot: string, title: string = 'WebPieces Architecture'): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body {\n margin: 0;\n padding: 20px;\n font-family: Arial, sans-serif;\n background: #f5f5f5;\n }\n h1 {\n text-align: center;\n color: #333;\n }\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 600px;\n padding: 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 {\n margin-top: 0;\n }\n .legend-item {\n margin: 8px 0;\n }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n\n <div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>Level 0:</strong> Foundation libraries (no dependencies)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Level 1:</strong> Middleware libraries (depend on Level 0)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Level 2:</strong> Applications (depend on Level 1)\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Note: Transitive dependencies are allowed but not shown in the graph.</em>\n </div>\n </div>\n\n <div id=\"graph\"></div>\n\n <script>\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n\n viz.renderSVGElement(dot)\n .then(element => {\n document.getElementById('graph').appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';\n });\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Write visualization files to tmp/webpieces/\n */\nexport function writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Architecture'\n): { dotPath: string; htmlPath: string } {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n\n // Ensure directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n // Generate DOT\n const dot = generateDot(graph, title);\n const dotPath = path.join(outputDir, 'architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n // Generate HTML\n const html = generateHTML(dot, title);\n const htmlPath = path.join(outputDir, 'architecture.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return { dotPath, htmlPath };\n}\n\n/**\n * Open the HTML visualization in the default browser\n */\nexport function openVisualization(htmlPath: string): boolean {\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n return false;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/architecture/lib/graph-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Visualizer\n *\n * Generates visual representations of the architecture graph:\n * - DOT format (for Graphviz)\n * - Interactive HTML (using viz.js)\n *\n * Output files go to tmp/webpieces/ for easy viewing without committing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\nimport type { EnhancedGraph } from './graph-sorter';\n\n/**\n * Level colors for visualization\n */\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9', // Light green - foundation\n 1: '#E3F2FD', // Light blue - middleware\n 2: '#FFF3E0', // Light orange - applications\n 3: '#FCE4EC', // Light pink - higher level\n};\n\n/**\n * Remove scope from name for display\n * '@scope/name' → 'name'\n * 'name' → 'name'\n */\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/**\n * Generate Graphviz DOT format from the graph\n */\nexport function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Architecture'): string {\n let dot = 'digraph Architecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=filled, fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\"];\\n\\n';\n\n // Group projects by level\n const levels: Record<number, string[]> = {};\n for (const [project, info] of Object.entries(graph)) {\n if (!levels[info.level]) levels[info.level] = [];\n levels[info.level].push(project);\n }\n\n // Create nodes with level-based colors\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n const color = LEVEL_COLORS[info.level] || '#F5F5F5';\n dot += ` \"${shortName}\" [fillcolor=\"${color}\", label=\"${shortName}\\\\n(L${info.level})\"];\\n`;\n }\n\n dot += '\\n';\n\n // Create same-rank subgraphs for each level\n for (const [level, projects] of Object.entries(levels)) {\n dot += ` { rank=same; `;\n projects.forEach((p) => {\n const shortName = getShortName(p);\n dot += `\"${shortName}\"; `;\n });\n dot += '}\\n';\n }\n\n dot += '\\n';\n\n // Create edges (dependencies)\n for (const [project, info] of Object.entries(graph)) {\n const shortName = getShortName(project);\n for (const dep of info.dependsOn || []) {\n const depShortName = getShortName(dep);\n dot += ` \"${shortName}\" -> \"${depShortName}\";\\n`;\n }\n }\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n\n return dot;\n}\n\n/**\n * Generate interactive HTML with embedded SVG using viz.js\n */\nexport function generateHTML(dot: string, title: string = 'WebPieces Architecture'): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body {\n margin: 0;\n padding: 20px;\n font-family: Arial, sans-serif;\n background: #f5f5f5;\n }\n h1 {\n text-align: center;\n color: #333;\n }\n #graph {\n text-align: center;\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend {\n margin: 20px auto;\n max-width: 600px;\n padding: 15px;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n .legend h2 {\n margin-top: 0;\n }\n .legend-item {\n margin: 8px 0;\n }\n .legend-box {\n display: inline-block;\n width: 20px;\n height: 20px;\n border: 1px solid #ccc;\n margin-right: 10px;\n vertical-align: middle;\n }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n\n <div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E8F5E9;\"></span>\n <strong>Level 0:</strong> Foundation libraries (no dependencies)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Level 1:</strong> Middleware libraries (depend on Level 0)\n </div>\n <div class=\"legend-item\">\n <span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Level 2:</strong> Applications (depend on Level 1)\n </div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>Note: Transitive dependencies are allowed but not shown in the graph.</em>\n </div>\n </div>\n\n <div id=\"graph\"></div>\n\n <script>\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n\n viz.renderSVGElement(dot)\n .then(element => {\n document.getElementById('graph').appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>';\n });\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Write visualization files to tmp/webpieces/\n */\nexport function writeVisualization(\n graph: EnhancedGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Architecture'\n): { dotPath: string; htmlPath: string } {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n\n // Ensure directory exists\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n // Generate DOT\n const dot = generateDot(graph, title);\n const dotPath = path.join(outputDir, 'architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n // Generate HTML\n const html = generateHTML(dot, title);\n const htmlPath = path.join(outputDir, 'architecture.html');\n fs.writeFileSync(htmlPath, html, 'utf-8');\n\n return { dotPath, htmlPath };\n}\n\n/**\n * Open the HTML visualization in the default browser\n */\nexport function openVisualization(htmlPath: string): boolean {\n try {\n const platform = process.platform;\n let openCommand: string;\n\n if (platform === 'darwin') {\n openCommand = `open \"${htmlPath}\"`;\n } else if (platform === 'win32') {\n openCommand = `start \"\" \"${htmlPath}\"`;\n } else {\n openCommand = `xdg-open \"${htmlPath}\"`;\n }\n\n execSync(openCommand, { stdio: 'ignore' });\n return true;\n } catch (err: unknown) {\n return false;\n }\n}\n"]}
|
|
@@ -23,6 +23,15 @@ const LEVEL_COLORS: Record<number, string> = {
|
|
|
23
23
|
3: '#FCE4EC', // Light pink - higher level
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Remove scope from name for display
|
|
28
|
+
* '@scope/name' → 'name'
|
|
29
|
+
* 'name' → 'name'
|
|
30
|
+
*/
|
|
31
|
+
function getShortName(name: string): string {
|
|
32
|
+
return name.includes('/') ? name.split('/').pop()! : name;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Generate Graphviz DOT format from the graph
|
|
28
37
|
*/
|
|
@@ -41,7 +50,7 @@ export function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Arc
|
|
|
41
50
|
|
|
42
51
|
// Create nodes with level-based colors
|
|
43
52
|
for (const [project, info] of Object.entries(graph)) {
|
|
44
|
-
const shortName = project
|
|
53
|
+
const shortName = getShortName(project);
|
|
45
54
|
const color = LEVEL_COLORS[info.level] || '#F5F5F5';
|
|
46
55
|
dot += ` "${shortName}" [fillcolor="${color}", label="${shortName}\\n(L${info.level})"];\n`;
|
|
47
56
|
}
|
|
@@ -52,7 +61,7 @@ export function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Arc
|
|
|
52
61
|
for (const [level, projects] of Object.entries(levels)) {
|
|
53
62
|
dot += ` { rank=same; `;
|
|
54
63
|
projects.forEach((p) => {
|
|
55
|
-
const shortName = p
|
|
64
|
+
const shortName = getShortName(p);
|
|
56
65
|
dot += `"${shortName}"; `;
|
|
57
66
|
});
|
|
58
67
|
dot += '}\n';
|
|
@@ -62,9 +71,9 @@ export function generateDot(graph: EnhancedGraph, title: string = 'WebPieces Arc
|
|
|
62
71
|
|
|
63
72
|
// Create edges (dependencies)
|
|
64
73
|
for (const [project, info] of Object.entries(graph)) {
|
|
65
|
-
const shortName = project
|
|
74
|
+
const shortName = getShortName(project);
|
|
66
75
|
for (const dep of info.dependsOn || []) {
|
|
67
|
-
const depShortName = dep
|
|
76
|
+
const depShortName = getShortName(dep);
|
|
68
77
|
dot += ` "${shortName}" -> "${depShortName}";\n`;
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -227,6 +227,61 @@ function loadBlessedGraph(workspaceRoot) {
|
|
|
227
227
|
return null;
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Build set of all workspace package names (from package.json files)
|
|
232
|
+
* Used to detect workspace imports (works for any scope or unscoped)
|
|
233
|
+
*/
|
|
234
|
+
function buildWorkspacePackageNames(workspaceRoot) {
|
|
235
|
+
const packageNames = new Set();
|
|
236
|
+
const mappings = buildProjectMappings(workspaceRoot);
|
|
237
|
+
for (const mapping of mappings) {
|
|
238
|
+
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
239
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
240
|
+
try {
|
|
241
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
242
|
+
if (pkgJson.name) {
|
|
243
|
+
packageNames.add(pkgJson.name);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Ignore parse errors
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return packageNames;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Check if an import path is a workspace project
|
|
255
|
+
* Works for scoped (@scope/name) or unscoped (name) packages
|
|
256
|
+
*/
|
|
257
|
+
function isWorkspaceImport(importPath, workspaceRoot) {
|
|
258
|
+
const workspacePackages = buildWorkspacePackageNames(workspaceRoot);
|
|
259
|
+
return workspacePackages.has(importPath);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Get project name from package name
|
|
263
|
+
* e.g., '@webpieces/client' → 'client', 'apis' → 'apis'
|
|
264
|
+
*/
|
|
265
|
+
function getProjectNameFromPackageName(packageName, workspaceRoot) {
|
|
266
|
+
const mappings = buildProjectMappings(workspaceRoot);
|
|
267
|
+
// Try to find by reading package.json files
|
|
268
|
+
for (const mapping of mappings) {
|
|
269
|
+
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
270
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
271
|
+
try {
|
|
272
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
273
|
+
if (pkgJson.name === packageName) {
|
|
274
|
+
return mapping.name; // Return project name
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Ignore parse errors
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Fallback: return package name as-is (might be unscoped project name)
|
|
283
|
+
return packageName;
|
|
284
|
+
}
|
|
230
285
|
/**
|
|
231
286
|
* Build project mappings from project.json files in workspace
|
|
232
287
|
*/
|
|
@@ -263,12 +318,8 @@ function scanForProjects(dir, workspaceRoot, mappings) {
|
|
|
263
318
|
try {
|
|
264
319
|
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
|
265
320
|
const projectRoot = path.relative(workspaceRoot, fullPath);
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
// Add @webpieces/ prefix if not present
|
|
269
|
-
if (!projectName.startsWith('@webpieces/')) {
|
|
270
|
-
projectName = `@webpieces/${projectName}`;
|
|
271
|
-
}
|
|
321
|
+
// Use project name from project.json as-is (no scope forcing)
|
|
322
|
+
const projectName = projectJson.name || entry.name;
|
|
272
323
|
mappings.push({
|
|
273
324
|
root: projectRoot,
|
|
274
325
|
name: projectName,
|
|
@@ -348,38 +399,38 @@ const rule = {
|
|
|
348
399
|
return {
|
|
349
400
|
ImportDeclaration(node) {
|
|
350
401
|
const importPath = node.source.value;
|
|
351
|
-
//
|
|
352
|
-
if (!importPath
|
|
353
|
-
return;
|
|
402
|
+
// Check if this is a workspace import (works for any scope or unscoped)
|
|
403
|
+
if (!isWorkspaceImport(importPath, workspaceRoot)) {
|
|
404
|
+
return; // Not a workspace import, skip validation
|
|
354
405
|
}
|
|
355
406
|
// Determine which project this file belongs to
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
407
|
+
const sourceProject = getProjectFromFile(filename, workspaceRoot);
|
|
408
|
+
if (!sourceProject) {
|
|
358
409
|
// File not in any known project (e.g., tools/, scripts/)
|
|
359
410
|
return;
|
|
360
411
|
}
|
|
412
|
+
// Convert import (package name) to project name
|
|
413
|
+
const targetProject = getProjectNameFromPackageName(importPath, workspaceRoot);
|
|
361
414
|
// Self-import is always allowed
|
|
362
|
-
if (
|
|
415
|
+
if (targetProject === sourceProject) {
|
|
363
416
|
return;
|
|
364
417
|
}
|
|
365
418
|
// Load blessed graph
|
|
366
419
|
const graph = loadBlessedGraph(workspaceRoot);
|
|
367
420
|
if (!graph) {
|
|
368
421
|
// No graph file - warn but don't fail (allows gradual adoption)
|
|
369
|
-
// Uncomment below to enforce graph existence:
|
|
370
|
-
// context.report({ node: node.source, messageId: 'noGraph' });
|
|
371
422
|
return;
|
|
372
423
|
}
|
|
373
424
|
// Get project entry
|
|
374
|
-
const projectEntry = graph[
|
|
425
|
+
const projectEntry = graph[sourceProject];
|
|
375
426
|
if (!projectEntry) {
|
|
376
427
|
// Project not in graph (new project?) - allow
|
|
377
428
|
return;
|
|
378
429
|
}
|
|
379
430
|
// Compute allowed dependencies (direct + transitive)
|
|
380
|
-
const allowedDeps = computeTransitiveDependencies(
|
|
381
|
-
// Check if import is allowed
|
|
382
|
-
if (!allowedDeps.has(
|
|
431
|
+
const allowedDeps = computeTransitiveDependencies(sourceProject, graph);
|
|
432
|
+
// Check if import is allowed (use project name, not package name)
|
|
433
|
+
if (!allowedDeps.has(targetProject)) {
|
|
383
434
|
// Write documentation file for AI/developer to read
|
|
384
435
|
ensureDependenciesDoc(workspaceRoot);
|
|
385
436
|
const directDeps = projectEntry.dependsOn || [];
|
|
@@ -392,7 +443,7 @@ const rule = {
|
|
|
392
443
|
messageId: 'illegalImport',
|
|
393
444
|
data: {
|
|
394
445
|
imported: importPath,
|
|
395
|
-
project:
|
|
446
|
+
project: sourceProject,
|
|
396
447
|
level: String(projectEntry.level),
|
|
397
448
|
allowedList: allowedList,
|
|
398
449
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enforce-architecture.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/enforce-architecture.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce architecture boundaries\n *\n * Validates that imports from @webpieces/* packages comply with the\n * blessed dependency graph in .graphs/dependencies.json\n *\n * Supports transitive dependencies: if A depends on B and B depends on C,\n * then A can import from C.\n *\n * Configuration:\n * '@webpieces/enforce-architecture': 'error'\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nconst DEPENDENCIES_DOC_CONTENT = `# AI Agent Instructions: Architecture Dependency Violation\n\n**READ THIS FILE FIRST before making any changes!**\n\n## ⚠️ CRITICAL WARNING ⚠️\n\n**This is a VERY IMPORTANT change that has LARGE REPERCUSSIONS later!**\n\nAdding new dependencies creates technical debt that compounds over time:\n- Creates coupling between packages that may be hard to undo\n- Can create circular dependency tangles\n- Makes packages harder to test in isolation\n- Increases build times and bundle sizes\n- May force unnecessary upgrades across the codebase\n\n**DO NOT add dependencies without senior developer approval!**\n\n## Understanding the Error\n\nYou've attempted to import from a package that is not in your project's allowed dependencies.\nThe architecture enforces a layered dependency structure where:\n- Level 0 packages are foundation packages with NO dependencies on other @webpieces packages\n- Higher level packages can only depend on lower level packages\n- All dependencies must be explicitly declared\n\n## Steps to Resolve\n\n### Step 1: Generate Current Dependency Graph\nRun this command to see the current architecture:\n\\`\\`\\`bash\nnpx nx run architecture:generate\n\\`\\`\\`\nThis creates/updates \\`architecture/dependencies.json\\` showing all packages and their levels.\n\n### Step 2: Analyze the Proposed Change\nAsk yourself:\n1. **Is this import truly necessary?** Can you refactor to avoid it?\n2. **Should the code move instead?** Maybe the code belongs in a different package.\n3. **Will this create a cycle?** Use \\`npx nx graph\\` to visualize dependencies.\n4. **Can you use an interface/abstraction?** Define interface in lower-level package, implement in higher-level.\n\n### Step 3: Get Senior Developer Approval\n\n## 🛑 AI AGENTS: STOP HERE AND ASK FOR HUMAN APPROVAL! 🛑\n\n**YOU MUST NOT PROCEED TO STEP 4 WITHOUT EXPLICIT HUMAN APPROVAL!**\n\n**REQUIRED**: Discuss this architectural change with a senior developer before proceeding.\n- Explain why the dependency is needed\n- Show you've considered alternatives (Step 2)\n- **WAIT for explicit approval before making ANY changes to project.json or package.json**\n\n**AI Agent Instructions:**\n1. Present your analysis from Step 2 to the human\n2. Explain which package needs which dependency and why\n3. ASK: \"Do you approve adding this dependency?\"\n4. **DO NOT modify project.json or package.json until you receive explicit \"yes\" or approval**\n\n### Step 4: If Approved, Add the Dependency\n\n## ⛔ NEVER MODIFY THESE FILES WITHOUT HUMAN APPROVAL FROM STEP 3! ⛔\n\nOnly after receiving explicit human approval in Step 3, make these changes:\n\n1. **Update project.json** - Add to \\`build.dependsOn\\`:\n \\`\\`\\`json\n {\n \"targets\": {\n \"build\": {\n \"dependsOn\": [\"^build\", \"dep1:build\", \"NEW_PACKAGE:build\"]\n }\n }\n }\n \\`\\`\\`\n\n2. **Update package.json** - Add to \\`dependencies\\`:\n \\`\\`\\`json\n {\n \"dependencies\": {\n \"@webpieces/NEW_PACKAGE\": \"*\"\n }\n }\n \\`\\`\\`\n\n### Step 5: Update Architecture Definition\nRun this command to validate and update the architecture:\n\\`\\`\\`bash\nnpx nx run architecture:validate --mode=update\n\\`\\`\\`\n\nThis will:\n- Detect any cycles (which MUST be fixed before proceeding)\n- Update \\`architecture/dependencies.json\\` with the new dependency\n- Recalculate package levels\n\n### Step 6: Verify No Cycles\n\\`\\`\\`bash\nnpx nx run architecture:validate\n\\`\\`\\`\n\nIf cycles are detected, you MUST refactor to break the cycle. Common strategies:\n- Move shared code to a lower-level package\n- Use dependency inversion (interfaces in low-level, implementations in high-level)\n- Restructure package boundaries\n\n## Alternative Solutions (Preferred over adding dependencies)\n\n### Option A: Move the Code\nIf you need functionality from another package, consider moving that code to a shared lower-level package.\n\n### Option B: Dependency Inversion\nDefine an interface in the lower-level package, implement it in the higher-level package:\n\\`\\`\\`typescript\n// In foundation package (level 0)\nexport interface Logger { log(msg: string): void; }\n\n// In higher-level package\nexport class ConsoleLogger implements Logger { ... }\n\\`\\`\\`\n\n### Option C: Pass Dependencies as Parameters\nInstead of importing, receive the dependency as a constructor or method parameter.\n\n## Remember\n- Every dependency you add today is technical debt for tomorrow\n- The best dependency is the one you don't need\n- When in doubt, refactor rather than add dependencies\n`;\n\n// Module-level flag to prevent redundant file creation\nlet dependenciesDocCreated = false;\n\n/**\n * Ensure a documentation file exists at the given path.\n */\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n void err;\n console.warn(`[webpieces] Could not create doc file: ${docPath}`);\n return false;\n }\n}\n\n/**\n * Ensure the dependencies documentation file exists.\n * Called when an architecture violation is detected.\n */\nfunction ensureDependenciesDoc(workspaceRoot: string): void {\n if (dependenciesDocCreated) return;\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.dependencies.md');\n if (ensureDocFile(docPath, DEPENDENCIES_DOC_CONTENT)) {\n dependenciesDocCreated = true;\n }\n}\n\n/**\n * Graph entry format from .graphs/dependencies.json\n */\ninterface GraphEntry {\n level: number;\n dependsOn: string[];\n}\n\ntype EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Project mapping entry\n */\ninterface ProjectMapping {\n root: string;\n name: string;\n}\n\n// Cache for blessed graph (loaded once per lint run)\nlet cachedGraph: EnhancedGraph | null = null;\nlet cachedGraphPath: string | null = null;\n\n// Cache for project mappings\nlet cachedProjectMappings: ProjectMapping[] | null = null;\n\n/**\n * Find workspace root by walking up from file location\n */\nfunction findWorkspaceRoot(startPath: string): string {\n let currentDir = path.dirname(startPath);\n\n for (let i = 0; i < 20; i++) {\n const packagePath = path.join(currentDir, 'package.json');\n if (fs.existsSync(packagePath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return currentDir;\n }\n } catch (err: any) {\n //const error = toError(err);\n void err;\n }\n }\n\n const parent = path.dirname(currentDir);\n if (parent === currentDir) break;\n currentDir = parent;\n }\n\n return process.cwd();\n}\n\n/**\n * Load blessed graph from architecture/dependencies.json\n */\nfunction loadBlessedGraph(workspaceRoot: string): EnhancedGraph | null {\n const graphPath = path.join(workspaceRoot, 'architecture', 'dependencies.json');\n\n // Return cached if same path\n if (cachedGraphPath === graphPath && cachedGraph !== null) {\n return cachedGraph;\n }\n\n if (!fs.existsSync(graphPath)) {\n return null;\n }\n\n try {\n const content = fs.readFileSync(graphPath, 'utf-8');\n cachedGraph = JSON.parse(content) as EnhancedGraph;\n cachedGraphPath = graphPath;\n return cachedGraph;\n } catch (err: any) {\n //const error = toError(err);\n // err is used below\n console.error(`[ESLint @webpieces/enforce-architecture] Could not load graph: ${err}`);\n return null;\n }\n}\n\n/**\n * Build project mappings from project.json files in workspace\n */\nfunction buildProjectMappings(workspaceRoot: string): ProjectMapping[] {\n if (cachedProjectMappings !== null) {\n return cachedProjectMappings;\n }\n\n const mappings: ProjectMapping[] = [];\n\n // Scan common locations for project.json files\n const searchDirs = ['packages', 'apps', 'libs', 'libraries', 'services'];\n\n for (const searchDir of searchDirs) {\n const searchPath = path.join(workspaceRoot, searchDir);\n if (!fs.existsSync(searchPath)) continue;\n\n scanForProjects(searchPath, workspaceRoot, mappings);\n }\n\n // Sort by path length (longest first) for more specific matching\n mappings.sort((a, b) => b.root.length - a.root.length);\n\n cachedProjectMappings = mappings;\n return mappings;\n}\n\n/**\n * Recursively scan for project.json files\n */\nfunction scanForProjects(\n dir: string,\n workspaceRoot: string,\n mappings: ProjectMapping[]\n): void {\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {\n // Check for project.json in this directory\n const projectJsonPath = path.join(fullPath, 'project.json');\n if (fs.existsSync(projectJsonPath)) {\n try {\n const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));\n const projectRoot = path.relative(workspaceRoot, fullPath);\n\n // Determine project name\n let projectName = projectJson.name || entry.name;\n\n // Add @webpieces/ prefix if not present\n if (!projectName.startsWith('@webpieces/')) {\n projectName = `@webpieces/${projectName}`;\n }\n\n mappings.push({\n root: projectRoot,\n name: projectName,\n });\n } catch (err: any) {\n //const error = toError(err);\n void err;\n }\n }\n\n // Continue scanning subdirectories\n scanForProjects(fullPath, workspaceRoot, mappings);\n }\n }\n } catch (err: any) {\n //const error = toError(err);\n void err;\n }\n}\n\n/**\n * Get project name from file path\n */\nfunction getProjectFromFile(filePath: string, workspaceRoot: string): string | null {\n const relativePath = path.relative(workspaceRoot, filePath).replace(/\\\\/g, '/');\n const mappings = buildProjectMappings(workspaceRoot);\n\n for (const mapping of mappings) {\n if (relativePath.startsWith(mapping.root + '/') || relativePath.startsWith(mapping.root)) {\n return mapping.name;\n }\n }\n\n return null;\n}\n\n/**\n * Compute all transitive dependencies for a project\n */\nfunction computeTransitiveDependencies(project: string, graph: EnhancedGraph): Set<string> {\n const result = new Set<string>();\n const visited = new Set<string>();\n\n function visit(currentProject: string): void {\n if (visited.has(currentProject)) return;\n visited.add(currentProject);\n\n const entry = graph[currentProject];\n if (!entry || !entry.dependsOn) return;\n\n for (const dep of entry.dependsOn) {\n result.add(dep);\n visit(dep);\n }\n }\n\n visit(project);\n return result;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce blessed architecture dependencies',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n illegalImport:\n \"⚠️ AI Agent: READ tmp/webpieces/webpieces.dependencies.md for resolution steps!\\n\\n\" +\n \"Import '{{imported}}' violates architecture boundaries.\\n\\n\" +\n \"Project '{{project}}' (level {{level}}) can only import from:\\n\" +\n '{{allowedList}}',\n noGraph:\n 'No architecture graph found at architecture/dependencies.json\\n' +\n 'Run: nx run architecture:validate --mode=update',\n },\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const filename = context.filename || context.getFilename();\n const workspaceRoot = findWorkspaceRoot(filename);\n\n return {\n ImportDeclaration(node: any): void {\n const importPath = node.source.value as string;\n\n // Only check @webpieces/* imports\n if (!importPath.startsWith('@webpieces/')) {\n return;\n }\n\n // Determine which project this file belongs to\n const project = getProjectFromFile(filename, workspaceRoot);\n if (!project) {\n // File not in any known project (e.g., tools/, scripts/)\n return;\n }\n\n // Self-import is always allowed\n if (importPath === project) {\n return;\n }\n\n // Load blessed graph\n const graph = loadBlessedGraph(workspaceRoot);\n if (!graph) {\n // No graph file - warn but don't fail (allows gradual adoption)\n // Uncomment below to enforce graph existence:\n // context.report({ node: node.source, messageId: 'noGraph' });\n return;\n }\n\n // Get project entry\n const projectEntry = graph[project];\n if (!projectEntry) {\n // Project not in graph (new project?) - allow\n return;\n }\n\n // Compute allowed dependencies (direct + transitive)\n const allowedDeps = computeTransitiveDependencies(project, graph);\n\n // Check if import is allowed\n if (!allowedDeps.has(importPath)) {\n // Write documentation file for AI/developer to read\n ensureDependenciesDoc(workspaceRoot);\n\n const directDeps = projectEntry.dependsOn || [];\n const allowedList =\n directDeps.length > 0\n ? directDeps.map((dep) => ` - ${dep}`).join('\\n') +\n '\\n (and their transitive dependencies)'\n : ' (none - this is a foundation project)';\n\n context.report({\n node: node.source,\n messageId: 'illegalImport',\n data: {\n imported: importPath,\n project: project,\n level: String(projectEntry.level),\n allowedList: allowedList,\n },\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
1
|
+
{"version":3,"file":"enforce-architecture.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/enforce-architecture.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce architecture boundaries\n *\n * Validates that imports from @webpieces/* packages comply with the\n * blessed dependency graph in .graphs/dependencies.json\n *\n * Supports transitive dependencies: if A depends on B and B depends on C,\n * then A can import from C.\n *\n * Configuration:\n * '@webpieces/enforce-architecture': 'error'\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nconst DEPENDENCIES_DOC_CONTENT = `# AI Agent Instructions: Architecture Dependency Violation\n\n**READ THIS FILE FIRST before making any changes!**\n\n## ⚠️ CRITICAL WARNING ⚠️\n\n**This is a VERY IMPORTANT change that has LARGE REPERCUSSIONS later!**\n\nAdding new dependencies creates technical debt that compounds over time:\n- Creates coupling between packages that may be hard to undo\n- Can create circular dependency tangles\n- Makes packages harder to test in isolation\n- Increases build times and bundle sizes\n- May force unnecessary upgrades across the codebase\n\n**DO NOT add dependencies without senior developer approval!**\n\n## Understanding the Error\n\nYou've attempted to import from a package that is not in your project's allowed dependencies.\nThe architecture enforces a layered dependency structure where:\n- Level 0 packages are foundation packages with NO dependencies on other @webpieces packages\n- Higher level packages can only depend on lower level packages\n- All dependencies must be explicitly declared\n\n## Steps to Resolve\n\n### Step 1: Generate Current Dependency Graph\nRun this command to see the current architecture:\n\\`\\`\\`bash\nnpx nx run architecture:generate\n\\`\\`\\`\nThis creates/updates \\`architecture/dependencies.json\\` showing all packages and their levels.\n\n### Step 2: Analyze the Proposed Change\nAsk yourself:\n1. **Is this import truly necessary?** Can you refactor to avoid it?\n2. **Should the code move instead?** Maybe the code belongs in a different package.\n3. **Will this create a cycle?** Use \\`npx nx graph\\` to visualize dependencies.\n4. **Can you use an interface/abstraction?** Define interface in lower-level package, implement in higher-level.\n\n### Step 3: Get Senior Developer Approval\n\n## 🛑 AI AGENTS: STOP HERE AND ASK FOR HUMAN APPROVAL! 🛑\n\n**YOU MUST NOT PROCEED TO STEP 4 WITHOUT EXPLICIT HUMAN APPROVAL!**\n\n**REQUIRED**: Discuss this architectural change with a senior developer before proceeding.\n- Explain why the dependency is needed\n- Show you've considered alternatives (Step 2)\n- **WAIT for explicit approval before making ANY changes to project.json or package.json**\n\n**AI Agent Instructions:**\n1. Present your analysis from Step 2 to the human\n2. Explain which package needs which dependency and why\n3. ASK: \"Do you approve adding this dependency?\"\n4. **DO NOT modify project.json or package.json until you receive explicit \"yes\" or approval**\n\n### Step 4: If Approved, Add the Dependency\n\n## ⛔ NEVER MODIFY THESE FILES WITHOUT HUMAN APPROVAL FROM STEP 3! ⛔\n\nOnly after receiving explicit human approval in Step 3, make these changes:\n\n1. **Update project.json** - Add to \\`build.dependsOn\\`:\n \\`\\`\\`json\n {\n \"targets\": {\n \"build\": {\n \"dependsOn\": [\"^build\", \"dep1:build\", \"NEW_PACKAGE:build\"]\n }\n }\n }\n \\`\\`\\`\n\n2. **Update package.json** - Add to \\`dependencies\\`:\n \\`\\`\\`json\n {\n \"dependencies\": {\n \"@webpieces/NEW_PACKAGE\": \"*\"\n }\n }\n \\`\\`\\`\n\n### Step 5: Update Architecture Definition\nRun this command to validate and update the architecture:\n\\`\\`\\`bash\nnpx nx run architecture:validate --mode=update\n\\`\\`\\`\n\nThis will:\n- Detect any cycles (which MUST be fixed before proceeding)\n- Update \\`architecture/dependencies.json\\` with the new dependency\n- Recalculate package levels\n\n### Step 6: Verify No Cycles\n\\`\\`\\`bash\nnpx nx run architecture:validate\n\\`\\`\\`\n\nIf cycles are detected, you MUST refactor to break the cycle. Common strategies:\n- Move shared code to a lower-level package\n- Use dependency inversion (interfaces in low-level, implementations in high-level)\n- Restructure package boundaries\n\n## Alternative Solutions (Preferred over adding dependencies)\n\n### Option A: Move the Code\nIf you need functionality from another package, consider moving that code to a shared lower-level package.\n\n### Option B: Dependency Inversion\nDefine an interface in the lower-level package, implement it in the higher-level package:\n\\`\\`\\`typescript\n// In foundation package (level 0)\nexport interface Logger { log(msg: string): void; }\n\n// In higher-level package\nexport class ConsoleLogger implements Logger { ... }\n\\`\\`\\`\n\n### Option C: Pass Dependencies as Parameters\nInstead of importing, receive the dependency as a constructor or method parameter.\n\n## Remember\n- Every dependency you add today is technical debt for tomorrow\n- The best dependency is the one you don't need\n- When in doubt, refactor rather than add dependencies\n`;\n\n// Module-level flag to prevent redundant file creation\nlet dependenciesDocCreated = false;\n\n/**\n * Ensure a documentation file exists at the given path.\n */\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n void err;\n console.warn(`[webpieces] Could not create doc file: ${docPath}`);\n return false;\n }\n}\n\n/**\n * Ensure the dependencies documentation file exists.\n * Called when an architecture violation is detected.\n */\nfunction ensureDependenciesDoc(workspaceRoot: string): void {\n if (dependenciesDocCreated) return;\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.dependencies.md');\n if (ensureDocFile(docPath, DEPENDENCIES_DOC_CONTENT)) {\n dependenciesDocCreated = true;\n }\n}\n\n/**\n * Graph entry format from .graphs/dependencies.json\n */\ninterface GraphEntry {\n level: number;\n dependsOn: string[];\n}\n\ntype EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Project mapping entry\n */\ninterface ProjectMapping {\n root: string;\n name: string;\n}\n\n// Cache for blessed graph (loaded once per lint run)\nlet cachedGraph: EnhancedGraph | null = null;\nlet cachedGraphPath: string | null = null;\n\n// Cache for project mappings\nlet cachedProjectMappings: ProjectMapping[] | null = null;\n\n/**\n * Find workspace root by walking up from file location\n */\nfunction findWorkspaceRoot(startPath: string): string {\n let currentDir = path.dirname(startPath);\n\n for (let i = 0; i < 20; i++) {\n const packagePath = path.join(currentDir, 'package.json');\n if (fs.existsSync(packagePath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return currentDir;\n }\n } catch (err: any) {\n //const error = toError(err);\n void err;\n }\n }\n\n const parent = path.dirname(currentDir);\n if (parent === currentDir) break;\n currentDir = parent;\n }\n\n return process.cwd();\n}\n\n/**\n * Load blessed graph from architecture/dependencies.json\n */\nfunction loadBlessedGraph(workspaceRoot: string): EnhancedGraph | null {\n const graphPath = path.join(workspaceRoot, 'architecture', 'dependencies.json');\n\n // Return cached if same path\n if (cachedGraphPath === graphPath && cachedGraph !== null) {\n return cachedGraph;\n }\n\n if (!fs.existsSync(graphPath)) {\n return null;\n }\n\n try {\n const content = fs.readFileSync(graphPath, 'utf-8');\n cachedGraph = JSON.parse(content) as EnhancedGraph;\n cachedGraphPath = graphPath;\n return cachedGraph;\n } catch (err: any) {\n //const error = toError(err);\n // err is used below\n console.error(`[ESLint @webpieces/enforce-architecture] Could not load graph: ${err}`);\n return null;\n }\n}\n\n/**\n * Build set of all workspace package names (from package.json files)\n * Used to detect workspace imports (works for any scope or unscoped)\n */\nfunction buildWorkspacePackageNames(workspaceRoot: string): Set<string> {\n const packageNames = new Set<string>();\n const mappings = buildProjectMappings(workspaceRoot);\n\n for (const mapping of mappings) {\n const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');\n if (fs.existsSync(pkgJsonPath)) {\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n if (pkgJson.name) {\n packageNames.add(pkgJson.name);\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n return packageNames;\n}\n\n/**\n * Check if an import path is a workspace project\n * Works for scoped (@scope/name) or unscoped (name) packages\n */\nfunction isWorkspaceImport(importPath: string, workspaceRoot: string): boolean {\n const workspacePackages = buildWorkspacePackageNames(workspaceRoot);\n return workspacePackages.has(importPath);\n}\n\n/**\n * Get project name from package name\n * e.g., '@webpieces/client' → 'client', 'apis' → 'apis'\n */\nfunction getProjectNameFromPackageName(packageName: string, workspaceRoot: string): string {\n const mappings = buildProjectMappings(workspaceRoot);\n\n // Try to find by reading package.json files\n for (const mapping of mappings) {\n const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');\n if (fs.existsSync(pkgJsonPath)) {\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n if (pkgJson.name === packageName) {\n return mapping.name; // Return project name\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n // Fallback: return package name as-is (might be unscoped project name)\n return packageName;\n}\n\n/**\n * Build project mappings from project.json files in workspace\n */\nfunction buildProjectMappings(workspaceRoot: string): ProjectMapping[] {\n if (cachedProjectMappings !== null) {\n return cachedProjectMappings;\n }\n\n const mappings: ProjectMapping[] = [];\n\n // Scan common locations for project.json files\n const searchDirs = ['packages', 'apps', 'libs', 'libraries', 'services'];\n\n for (const searchDir of searchDirs) {\n const searchPath = path.join(workspaceRoot, searchDir);\n if (!fs.existsSync(searchPath)) continue;\n\n scanForProjects(searchPath, workspaceRoot, mappings);\n }\n\n // Sort by path length (longest first) for more specific matching\n mappings.sort((a, b) => b.root.length - a.root.length);\n\n cachedProjectMappings = mappings;\n return mappings;\n}\n\n/**\n * Recursively scan for project.json files\n */\nfunction scanForProjects(\n dir: string,\n workspaceRoot: string,\n mappings: ProjectMapping[]\n): void {\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {\n // Check for project.json in this directory\n const projectJsonPath = path.join(fullPath, 'project.json');\n if (fs.existsSync(projectJsonPath)) {\n try {\n const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));\n const projectRoot = path.relative(workspaceRoot, fullPath);\n\n // Use project name from project.json as-is (no scope forcing)\n const projectName = projectJson.name || entry.name;\n\n mappings.push({\n root: projectRoot,\n name: projectName,\n });\n } catch (err: any) {\n //const error = toError(err);\n void err;\n }\n }\n\n // Continue scanning subdirectories\n scanForProjects(fullPath, workspaceRoot, mappings);\n }\n }\n } catch (err: any) {\n //const error = toError(err);\n void err;\n }\n}\n\n/**\n * Get project name from file path\n */\nfunction getProjectFromFile(filePath: string, workspaceRoot: string): string | null {\n const relativePath = path.relative(workspaceRoot, filePath).replace(/\\\\/g, '/');\n const mappings = buildProjectMappings(workspaceRoot);\n\n for (const mapping of mappings) {\n if (relativePath.startsWith(mapping.root + '/') || relativePath.startsWith(mapping.root)) {\n return mapping.name;\n }\n }\n\n return null;\n}\n\n/**\n * Compute all transitive dependencies for a project\n */\nfunction computeTransitiveDependencies(project: string, graph: EnhancedGraph): Set<string> {\n const result = new Set<string>();\n const visited = new Set<string>();\n\n function visit(currentProject: string): void {\n if (visited.has(currentProject)) return;\n visited.add(currentProject);\n\n const entry = graph[currentProject];\n if (!entry || !entry.dependsOn) return;\n\n for (const dep of entry.dependsOn) {\n result.add(dep);\n visit(dep);\n }\n }\n\n visit(project);\n return result;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce blessed architecture dependencies',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n illegalImport:\n \"⚠️ AI Agent: READ tmp/webpieces/webpieces.dependencies.md for resolution steps!\\n\\n\" +\n \"Import '{{imported}}' violates architecture boundaries.\\n\\n\" +\n \"Project '{{project}}' (level {{level}}) can only import from:\\n\" +\n '{{allowedList}}',\n noGraph:\n 'No architecture graph found at architecture/dependencies.json\\n' +\n 'Run: nx run architecture:validate --mode=update',\n },\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const filename = context.filename || context.getFilename();\n const workspaceRoot = findWorkspaceRoot(filename);\n\n return {\n ImportDeclaration(node: any): void {\n const importPath = node.source.value as string;\n\n // Check if this is a workspace import (works for any scope or unscoped)\n if (!isWorkspaceImport(importPath, workspaceRoot)) {\n return; // Not a workspace import, skip validation\n }\n\n // Determine which project this file belongs to\n const sourceProject = getProjectFromFile(filename, workspaceRoot);\n if (!sourceProject) {\n // File not in any known project (e.g., tools/, scripts/)\n return;\n }\n\n // Convert import (package name) to project name\n const targetProject = getProjectNameFromPackageName(importPath, workspaceRoot);\n\n // Self-import is always allowed\n if (targetProject === sourceProject) {\n return;\n }\n\n // Load blessed graph\n const graph = loadBlessedGraph(workspaceRoot);\n if (!graph) {\n // No graph file - warn but don't fail (allows gradual adoption)\n return;\n }\n\n // Get project entry\n const projectEntry = graph[sourceProject];\n if (!projectEntry) {\n // Project not in graph (new project?) - allow\n return;\n }\n\n // Compute allowed dependencies (direct + transitive)\n const allowedDeps = computeTransitiveDependencies(sourceProject, graph);\n\n // Check if import is allowed (use project name, not package name)\n if (!allowedDeps.has(targetProject)) {\n // Write documentation file for AI/developer to read\n ensureDependenciesDoc(workspaceRoot);\n\n const directDeps = projectEntry.dependsOn || [];\n const allowedList =\n directDeps.length > 0\n ? directDeps.map((dep) => ` - ${dep}`).join('\\n') +\n '\\n (and their transitive dependencies)'\n : ' (none - this is a foundation project)';\n\n context.report({\n node: node.source,\n messageId: 'illegalImport',\n data: {\n imported: importPath,\n project: sourceProject,\n level: String(projectEntry.level),\n allowedList: allowedList,\n },\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -255,6 +255,66 @@ function loadBlessedGraph(workspaceRoot: string): EnhancedGraph | null {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Build set of all workspace package names (from package.json files)
|
|
260
|
+
* Used to detect workspace imports (works for any scope or unscoped)
|
|
261
|
+
*/
|
|
262
|
+
function buildWorkspacePackageNames(workspaceRoot: string): Set<string> {
|
|
263
|
+
const packageNames = new Set<string>();
|
|
264
|
+
const mappings = buildProjectMappings(workspaceRoot);
|
|
265
|
+
|
|
266
|
+
for (const mapping of mappings) {
|
|
267
|
+
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
268
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
269
|
+
try {
|
|
270
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
271
|
+
if (pkgJson.name) {
|
|
272
|
+
packageNames.add(pkgJson.name);
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
// Ignore parse errors
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return packageNames;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if an import path is a workspace project
|
|
285
|
+
* Works for scoped (@scope/name) or unscoped (name) packages
|
|
286
|
+
*/
|
|
287
|
+
function isWorkspaceImport(importPath: string, workspaceRoot: string): boolean {
|
|
288
|
+
const workspacePackages = buildWorkspacePackageNames(workspaceRoot);
|
|
289
|
+
return workspacePackages.has(importPath);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get project name from package name
|
|
294
|
+
* e.g., '@webpieces/client' → 'client', 'apis' → 'apis'
|
|
295
|
+
*/
|
|
296
|
+
function getProjectNameFromPackageName(packageName: string, workspaceRoot: string): string {
|
|
297
|
+
const mappings = buildProjectMappings(workspaceRoot);
|
|
298
|
+
|
|
299
|
+
// Try to find by reading package.json files
|
|
300
|
+
for (const mapping of mappings) {
|
|
301
|
+
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
302
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
303
|
+
try {
|
|
304
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
305
|
+
if (pkgJson.name === packageName) {
|
|
306
|
+
return mapping.name; // Return project name
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// Ignore parse errors
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Fallback: return package name as-is (might be unscoped project name)
|
|
315
|
+
return packageName;
|
|
316
|
+
}
|
|
317
|
+
|
|
258
318
|
/**
|
|
259
319
|
* Build project mappings from project.json files in workspace
|
|
260
320
|
*/
|
|
@@ -304,13 +364,8 @@ function scanForProjects(
|
|
|
304
364
|
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
|
305
365
|
const projectRoot = path.relative(workspaceRoot, fullPath);
|
|
306
366
|
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// Add @webpieces/ prefix if not present
|
|
311
|
-
if (!projectName.startsWith('@webpieces/')) {
|
|
312
|
-
projectName = `@webpieces/${projectName}`;
|
|
313
|
-
}
|
|
367
|
+
// Use project name from project.json as-is (no scope forcing)
|
|
368
|
+
const projectName = projectJson.name || entry.name;
|
|
314
369
|
|
|
315
370
|
mappings.push({
|
|
316
371
|
root: projectRoot,
|
|
@@ -402,20 +457,23 @@ const rule: Rule.RuleModule = {
|
|
|
402
457
|
ImportDeclaration(node: any): void {
|
|
403
458
|
const importPath = node.source.value as string;
|
|
404
459
|
|
|
405
|
-
//
|
|
406
|
-
if (!importPath
|
|
407
|
-
return;
|
|
460
|
+
// Check if this is a workspace import (works for any scope or unscoped)
|
|
461
|
+
if (!isWorkspaceImport(importPath, workspaceRoot)) {
|
|
462
|
+
return; // Not a workspace import, skip validation
|
|
408
463
|
}
|
|
409
464
|
|
|
410
465
|
// Determine which project this file belongs to
|
|
411
|
-
const
|
|
412
|
-
if (!
|
|
466
|
+
const sourceProject = getProjectFromFile(filename, workspaceRoot);
|
|
467
|
+
if (!sourceProject) {
|
|
413
468
|
// File not in any known project (e.g., tools/, scripts/)
|
|
414
469
|
return;
|
|
415
470
|
}
|
|
416
471
|
|
|
472
|
+
// Convert import (package name) to project name
|
|
473
|
+
const targetProject = getProjectNameFromPackageName(importPath, workspaceRoot);
|
|
474
|
+
|
|
417
475
|
// Self-import is always allowed
|
|
418
|
-
if (
|
|
476
|
+
if (targetProject === sourceProject) {
|
|
419
477
|
return;
|
|
420
478
|
}
|
|
421
479
|
|
|
@@ -423,23 +481,21 @@ const rule: Rule.RuleModule = {
|
|
|
423
481
|
const graph = loadBlessedGraph(workspaceRoot);
|
|
424
482
|
if (!graph) {
|
|
425
483
|
// No graph file - warn but don't fail (allows gradual adoption)
|
|
426
|
-
// Uncomment below to enforce graph existence:
|
|
427
|
-
// context.report({ node: node.source, messageId: 'noGraph' });
|
|
428
484
|
return;
|
|
429
485
|
}
|
|
430
486
|
|
|
431
487
|
// Get project entry
|
|
432
|
-
const projectEntry = graph[
|
|
488
|
+
const projectEntry = graph[sourceProject];
|
|
433
489
|
if (!projectEntry) {
|
|
434
490
|
// Project not in graph (new project?) - allow
|
|
435
491
|
return;
|
|
436
492
|
}
|
|
437
493
|
|
|
438
494
|
// Compute allowed dependencies (direct + transitive)
|
|
439
|
-
const allowedDeps = computeTransitiveDependencies(
|
|
495
|
+
const allowedDeps = computeTransitiveDependencies(sourceProject, graph);
|
|
440
496
|
|
|
441
|
-
// Check if import is allowed
|
|
442
|
-
if (!allowedDeps.has(
|
|
497
|
+
// Check if import is allowed (use project name, not package name)
|
|
498
|
+
if (!allowedDeps.has(targetProject)) {
|
|
443
499
|
// Write documentation file for AI/developer to read
|
|
444
500
|
ensureDependenciesDoc(workspaceRoot);
|
|
445
501
|
|
|
@@ -455,7 +511,7 @@ const rule: Rule.RuleModule = {
|
|
|
455
511
|
messageId: 'illegalImport',
|
|
456
512
|
data: {
|
|
457
513
|
imported: importPath,
|
|
458
|
-
project:
|
|
514
|
+
project: sourceProject,
|
|
459
515
|
level: String(projectEntry.level),
|
|
460
516
|
allowedList: allowedList,
|
|
461
517
|
},
|
|
@@ -586,12 +586,16 @@ const rule = {
|
|
|
586
586
|
create(context) {
|
|
587
587
|
return {
|
|
588
588
|
TryStatement(node) {
|
|
589
|
+
// Skip try..finally blocks (no catch handler, no exception handling)
|
|
590
|
+
if (!node.handler) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
589
593
|
// Auto-allow in test files
|
|
590
594
|
const filename = context.filename || context.getFilename();
|
|
591
595
|
if (isTestFile(filename)) {
|
|
592
596
|
return;
|
|
593
597
|
}
|
|
594
|
-
//
|
|
598
|
+
// Has catch block outside test file - report violation
|
|
595
599
|
ensureExceptionDoc(context);
|
|
596
600
|
context.report({
|
|
597
601
|
node,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to discourage try-catch blocks outside test files\n *\n * Works alongside catch-error-pattern rule:\n * - catch-error-pattern: Enforces HOW to handle exceptions (with toError())\n * - no-unmanaged-exceptions: Enforces WHERE try-catch is allowed (tests only by default)\n *\n * Philosophy: Exceptions should bubble to global error handlers where they are logged\n * with traceId and stored for debugging via /debugLocal and /debugCloud endpoints.\n * Local try-catch blocks break this architecture and create blind spots in production.\n *\n * Auto-allowed in:\n * - Test files (.test.ts, .spec.ts, __tests__/)\n *\n * Requires eslint-disable comment in:\n * - Retry loops with exponential backoff\n * - Batch processing where partial failure is expected\n * - Resource cleanup (with approval)\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\n/**\n * Determines if a file is a test file based on naming conventions\n * Test files are auto-allowed to use try-catch blocks\n */\nfunction isTestFile(filename: string): boolean {\n const normalizedPath = filename.toLowerCase();\n\n // Check file extensions\n if (normalizedPath.endsWith('.test.ts') || normalizedPath.endsWith('.spec.ts')) {\n return true;\n }\n\n // Check directory names (cross-platform)\n if (normalizedPath.includes('/__tests__/') || normalizedPath.includes('\\\\__tests__\\\\')) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Finds the workspace root by walking up the directory tree\n * Looks for package.json with workspaces or name === 'webpieces-ts'\n */\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n // Walk up directory tree\n for (let i = 0; i < 10; i++) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n // Check if this is the root workspace\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch {\n // Invalid JSON, keep searching\n }\n }\n\n const parentDir = path.dirname(dir);\n if (parentDir === dir) break; // Reached filesystem root\n dir = parentDir;\n }\n\n // Fallback: return current directory\n return process.cwd();\n}\n\n/**\n * Ensures a documentation file exists at the given path\n * Creates parent directories if needed\n */\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n const dir = path.dirname(docPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Only write if file doesn't exist or is empty\n if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {\n fs.writeFileSync(docPath, content, 'utf-8');\n }\n\n return true;\n } catch (error) {\n // Silently fail - don't break linting if file creation fails\n return false;\n }\n}\n\n/**\n * Ensures the exception documentation markdown file exists\n * Only creates file once per lint run using module-level flag\n */\nfunction ensureExceptionDoc(context: Rule.RuleContext): void {\n if (exceptionDocCreated) return;\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.exceptions.md');\n\n if (ensureDocFile(docPath, EXCEPTION_DOC_CONTENT)) {\n exceptionDocCreated = true;\n }\n}\n\n// Module-level flag to prevent redundant markdown file creation\nlet exceptionDocCreated = false;\n\n// Comprehensive markdown documentation content\nconst EXCEPTION_DOC_CONTENT = `# AI Agent Instructions: Try-Catch Blocks Detected\n\n**READ THIS FILE to understand why try-catch blocks are restricted and how to fix violations**\n\n## Core Principle\n\n**EXCEPTIONS MUST BUBBLE TO GLOBAL HANDLER WITH TRACEID FOR DEBUGGABILITY.**\n\nThe webpieces framework uses a global error handling architecture where:\n- Every request gets a unique traceId stored in RequestContext\n- All errors bubble to the global handler (WebpiecesMiddleware.globalErrorHandler)\n- Error IDs enable lookup via \\`/debugLocal/{id}\\` and \\`/debugCloud/{id}\\` endpoints\n- Local try-catch blocks break this pattern by losing error IDs and context\n\nThis is not a performance concern - it's an architecture decision for distributed tracing and debugging in production.\n\n## Why This Rule Exists\n\n### Problem 1: AI Over-Adds Try-Catch (Especially Frontend)\nAI agents tend to add defensive try-catch blocks everywhere, which:\n- Swallows errors and loses traceId\n- Shows custom error messages without debugging context\n- Makes production issues impossible to trace\n- Creates \"blind spots\" where errors disappear\n\n### Problem 2: Lost TraceId = Lost Debugging Capability\nWithout traceId in errors:\n- \\`/debugLocal/{id}\\` endpoint cannot retrieve error details\n- \\`/debugCloud/{id}\\` endpoint cannot correlate logs\n- DevOps cannot trace request flow through distributed systems\n- Users report \"an error occurred\" with no way to investigate\n\n### Problem 3: Try-Catch-Rethrow Is Code Smell\n\\`\\`\\`typescript\n// BAD: Why catch if you're just rethrowing?\ntry {\n await operation();\n} catch (err: any) {\n const error = toError(err);\n console.error('Failed:', error);\n throw error; // Why catch at all???\n}\n\\`\\`\\`\n99% of the time, there's a better pattern (logging filter, global handler, etc.).\n\n### Problem 4: Swallowing Exceptions = Lazy Programming\n\\`\\`\\`typescript\n// BAD: \"I don't want to deal with this error\"\ntry {\n await riskyOperation();\n} catch (err: any) {\n // Silence...\n}\n\\`\\`\\`\nThis is the #1 shortcut developers take that creates production nightmares.\n\n## Industry Best Practices (2025)\n\n### Distributed Tracing: The Three Pillars\nModern observability requires correlation across:\n1. **Traces** - Request flow through services\n2. **Logs** - Contextual debugging information\n3. **Metrics** - Aggregated system health\n\nTraceId (also called correlation ID, request ID) ties these together.\n\n### Research Findings\n- **Performance**: Try-catch is an expensive operation in V8 engine (source: Node.js performance docs)\n- **Error Handling**: Global handlers at highest level reduce blind spots by 40% (source: Google SRE practices)\n- **Middleware Pattern**: Express/Koa middleware with async error boundaries is industry standard (source: Express.js error handling docs)\n- **Only Catch What You Can Handle**: If you can't recover, let it bubble (source: \"Effective Error Handling\" - JavaScript design patterns)\n\n### 2025 Trends\n- Correlation IDs are standard in microservices (OpenTelemetry, Datadog, New Relic)\n- Structured logging with context (Winston, Pino)\n- Middleware-based error boundaries reduce boilerplate\n- Frontend: React Error Boundaries, not scattered try-catch\n\n## Command: Remove Try-Catch and Use Global Handler\n\n## AI Agent Action Steps\n\n1. **IDENTIFY** the try-catch block flagged in the error message\n\n2. **ANALYZE** the purpose:\n - Is it catching errors just to log them? → Remove (use LogApiFilter)\n - Is it catching to show custom message? → Remove (use global handler)\n - Is it catching to retry? → Requires approval (see Acceptable Patterns)\n - Is it catching in a batch loop? → Requires approval (see Acceptable Patterns)\n - Is it catching for cleanup? → Usually wrong pattern\n\n3. **REMOVE** the try-catch block:\n - Delete the \\`try {\\` and \\`} catch (err: any) { ... }\\` wrapper\n - Let the code execute normally\n - Errors will bubble to global handler automatically\n\n4. **VERIFY** global handler exists:\n - Check that WebpiecesMiddleware.globalErrorHandler is registered\n - Check that ContextFilter is setting up RequestContext\n - Check that traceId is being added to RequestContext\n\n5. **ADD** traceId to RequestContext (if not already present):\n - In ContextFilter or similar high-priority filter\n - Use \\`RequestContext.put('TRACE_ID', generateTraceId())\\`\n\n6. **TEST** error flow:\n - Trigger an error in the code\n - Verify error is logged with traceId\n - Verify \\`/debugLocal/{traceId}\\` endpoint works\n\n## Pattern 1: Global Error Handler (GOOD)\n\n### Server-Side: WebpiecesMiddleware\n\n\\`\\`\\`typescript\n// packages/http/http-server/src/WebpiecesMiddleware.ts\n@provideSingleton()\n@injectable()\nexport class WebpiecesMiddleware {\n async globalErrorHandler(\n req: Request,\n res: Response,\n next: NextFunction\n ): Promise<void> {\n console.log('[GlobalErrorHandler] Request START:', req.method, req.path);\n\n try {\n // Await catches BOTH sync throws AND rejected promises\n await next();\n console.log('[GlobalErrorHandler] Request END (success)');\n } catch (err: any) {\n const error = toError(err);\n const traceId = RequestContext.get<string>('TRACE_ID');\n\n // Log with traceId for /debugLocal lookup\n console.error('[GlobalErrorHandler] ERROR:', {\n traceId,\n message: error.message,\n stack: error.stack,\n path: req.path,\n method: req.method,\n });\n\n // Store error for /debugLocal/{id} endpoint\n ErrorStore.save(traceId, error);\n\n if (!res.headersSent) {\n res.status(500).send(\\`\n <!DOCTYPE html>\n <html>\n <head><title>Server Error</title></head>\n <body>\n <h1>Server Error</h1>\n <p>An error occurred. Reference ID: \\${traceId}</p>\n <p>Contact support with this ID to investigate.</p>\n </body>\n </html>\n \\`);\n }\n }\n }\n}\n\\`\\`\\`\n\n### Adding TraceId: ContextFilter\n\n\\`\\`\\`typescript\n// packages/http/http-server/src/filters/ContextFilter.ts\nimport { v4 as uuidv4 } from 'uuid';\n\n@provideSingleton()\n@injectable()\nexport class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>\n ): Promise<WpResponse<unknown>> {\n return RequestContext.run(async () => {\n // Generate unique traceId for this request\n const traceId = uuidv4();\n RequestContext.put('TRACE_ID', traceId);\n RequestContext.put('METHOD_META', meta);\n RequestContext.put('REQUEST_PATH', meta.path);\n\n return await nextFilter.invoke(meta);\n // RequestContext auto-cleared when done\n });\n }\n}\n\\`\\`\\`\n\n## Pattern 2: Debug Endpoints (GOOD)\n\n\\`\\`\\`typescript\n// Example debug endpoint for local development\n@provideSingleton()\n@Controller()\nexport class DebugController implements DebugApi {\n @Get()\n @Path('/debugLocal/:id')\n async getErrorById(@PathParam('id') id: string): Promise<DebugErrorResponse> {\n const error = ErrorStore.get(id);\n if (!error) {\n throw new HttpNotFoundError(\\`Error \\${id} not found\\`);\n }\n\n return {\n traceId: id,\n message: error.message,\n stack: error.stack,\n timestamp: error.timestamp,\n requestPath: error.requestPath,\n requestMethod: error.requestMethod,\n };\n }\n}\n\n// ErrorStore singleton (in-memory for local, Redis for production)\nclass ErrorStoreImpl {\n private errors = new Map<string, ErrorRecord>();\n\n save(traceId: string, error: Error): void {\n this.errors.set(traceId, {\n traceId,\n message: error.message,\n stack: error.stack,\n timestamp: new Date(),\n requestPath: RequestContext.get('REQUEST_PATH'),\n requestMethod: RequestContext.get('HTTP_METHOD'),\n });\n }\n\n get(traceId: string): ErrorRecord | undefined {\n return this.errors.get(traceId);\n }\n}\n\nexport const ErrorStore = new ErrorStoreImpl();\n\\`\\`\\`\n\n## Examples\n\n### BAD Example 1: Local Try-Catch That Swallows Error\n\n\\`\\`\\`typescript\n// BAD: Error is swallowed, no traceId in logs\nasync function processOrder(order: Order): Promise<void> {\n try {\n await validateOrder(order);\n await saveToDatabase(order);\n } catch (err: any) {\n // Error disappears into void - debugging nightmare!\n console.log('Order processing failed');\n }\n}\n\\`\\`\\`\n\n**Problem**: When this fails in production, you have:\n- No traceId to look up the error\n- No stack trace\n- No request context\n- No way to investigate\n\n### BAD Example 2: Try-Catch With Custom Error (No TraceId)\n\n\\`\\`\\`typescript\n// BAD: Shows custom message but loses traceId\nasync function fetchUserData(userId: string): Promise<User> {\n try {\n const response = await fetch(\\`/api/users/\\${userId}\\`);\n return await response.json();\n } catch (err: any) {\n const error = toError(err);\n // Custom message without traceId\n throw new Error(\\`Failed to fetch user \\${userId}: \\${error.message}\\`);\n }\n}\n\\`\\`\\`\n\n**Problem**:\n- Original error context is lost\n- No traceId attached to new error\n- Global handler receives generic error, can't trace root cause\n\n### GOOD Example 1: Let Error Bubble\n\n\\`\\`\\`typescript\n// GOOD: Error bubbles to global handler with traceId\nasync function processOrder(order: Order): Promise<void> {\n // No try-catch needed!\n await validateOrder(order);\n await saveToDatabase(order);\n // If error occurs, it bubbles with traceId intact\n}\n\\`\\`\\`\n\n**Why GOOD**:\n- Global handler catches error\n- TraceId from RequestContext is preserved\n- Full stack trace available\n- \\`/debugLocal/{traceId}\\` endpoint works\n\n### GOOD Example 2: Global Handler Logs With TraceId\n\n\\`\\`\\`typescript\n// GOOD: Global handler has full context\n// In WebpiecesMiddleware.globalErrorHandler (see Pattern 1 above)\ncatch (err: any) {\n const error = toError(err);\n const traceId = RequestContext.get<string>('TRACE_ID');\n\n console.error('[GlobalErrorHandler] ERROR:', {\n traceId, // Unique ID for this request\n message: error.message,\n stack: error.stack,\n path: req.path, // Request context preserved\n });\n}\n\\`\\`\\`\n\n**Why GOOD**:\n- TraceId logged with every error\n- Full request context available\n- Error stored for \\`/debugLocal/{id}\\` lookup\n- DevOps can trace distributed requests\n\n### ACCEPTABLE Example 1: Retry Loop (With eslint-disable)\n\n\\`\\`\\`typescript\n// ACCEPTABLE: Retry pattern requires try-catch\n// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Retry loop with exponential backoff\nasync function callVendorApiWithRetry(request: VendorRequest): Promise<VendorResponse> {\n const maxRetries = 3;\n let lastError: Error | undefined;\n\n for (let i = 0; i < maxRetries; i++) {\n try {\n return await vendorApi.call(request);\n } catch (err: any) {\n const error = toError(err);\n lastError = error;\n console.warn(\\`Retry \\${i + 1}/\\${maxRetries} failed:\\`, error.message);\n await sleep(1000 * Math.pow(2, i)); // Exponential backoff\n }\n }\n\n // After retries exhausted, throw with traceId\n const traceId = RequestContext.get<string>('TRACE_ID');\n throw new HttpVendorError(\n \\`Vendor API failed after \\${maxRetries} retries. TraceId: \\${traceId}\\`,\n lastError\n );\n}\n\\`\\`\\`\n\n**Why ACCEPTABLE**:\n- Legitimate use case: retry logic\n- Final error still includes traceId\n- Error still bubbles to global handler\n- Requires senior developer approval (enforced by PR review)\n\n### ACCEPTABLE Example 2: Batching Pattern (With eslint-disable)\n\n\\`\\`\\`typescript\n// ACCEPTABLE: Batching requires try-catch to continue processing\n// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Batch processing continues on individual failures\nasync function processBatch(items: Item[]): Promise<BatchResult> {\n const results: ItemResult[] = [];\n const errors: ItemError[] = [];\n const traceId = RequestContext.get<string>('TRACE_ID');\n\n for (const item of items) {\n try {\n const result = await processItem(item);\n results.push(result);\n } catch (err: any) {\n const error = toError(err);\n // Log individual error with traceId\n console.error(\\`[Batch] Item \\${item.id} failed (traceId: \\${traceId}):\\`, error);\n errors.push({ itemId: item.id, error: error.message, traceId });\n }\n }\n\n // Return both successes and failures\n return {\n traceId,\n successCount: results.length,\n failureCount: errors.length,\n results,\n errors,\n };\n}\n\\`\\`\\`\n\n**Why ACCEPTABLE**:\n- Legitimate use case: partial failure handling\n- Each error logged with traceId\n- Batch traceId included in response\n- Requires senior developer approval (enforced by PR review)\n\n### UNACCEPTABLE Example: Try-Catch-Rethrow\n\n\\`\\`\\`typescript\n// UNACCEPTABLE: Pointless try-catch that just rethrows\nasync function saveUser(user: User): Promise<void> {\n try {\n await database.save(user);\n } catch (err: any) {\n const error = toError(err);\n console.error('Save failed:', error);\n throw error; // Why catch at all???\n }\n}\n\\`\\`\\`\n\n**Why UNACCEPTABLE**:\n- Adds no value - logging should be in LogApiFilter\n- Global handler already logs errors\n- Just adds noise and confusion\n- Remove the try-catch entirely!\n\n## When eslint-disable IS Acceptable\n\nYou may use \\`// eslint-disable-next-line @webpieces/no-unmanaged-exceptions\\` ONLY for:\n\n1. **Retry loops** with exponential backoff (vendor API calls)\n2. **Batching patterns** where partial failure is expected\n3. **Resource cleanup** with explicit approval\n\nAll three require:\n- Senior developer approval in PR review\n- Comment explaining WHY try-catch is needed\n- TraceId must still be logged/included in final error\n\n## How to Request Approval\n\nIf you believe you have a legitimate use case for try-catch:\n\n1. **Add a comment** explaining why it's needed:\n \\`\\`\\`typescript\n // JUSTIFICATION: Vendor API requires retry loop with exponential backoff\n // to handle rate limiting. Final error includes traceId for debugging.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n \\`\\`\\`\n\n2. **Ensure traceId is preserved** in final error or logged\n\n3. **Request PR review** from senior developer\n\n4. **Be prepared to justify** - 99% of try-catch can be removed\n\n## Summary\n\n**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.\n\n**Key takeaways**:\n- Global error handler with traceId = debuggable production issues\n- Local try-catch = lost context and debugging nightmares\n- 99% of try-catch blocks can be removed safely\n- Only use try-catch for: retries, batching (with approval)\n- TraceId enables \\`/debugLocal/{id}\\` and \\`/debugCloud/{id}\\` endpoints\n\n**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.\n`;\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Discourage try-catch blocks outside test files - use global error handlers',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts/blob/main/CLAUDE.md#exception-handling-philosophy',\n },\n messages: {\n noUnmanagedExceptions:\n 'AI Agent: READ tmp/webpieces/webpieces.exceptions.md for context. Try-catch blocks are discouraged - use global error handlers instead. Only allowed in test files or with eslint-disable comment.',\n },\n fixable: undefined,\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n return {\n TryStatement(node: any): void {\n // Auto-allow in test files\n const filename = context.filename || context.getFilename();\n if (isTestFile(filename)) {\n return;\n }\n\n // Not in test file - report violation\n ensureExceptionDoc(context);\n context.report({\n node,\n messageId: 'noUnmanagedExceptions',\n });\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
1
|
+
{"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to discourage try-catch blocks outside test files\n *\n * Works alongside catch-error-pattern rule:\n * - catch-error-pattern: Enforces HOW to handle exceptions (with toError())\n * - no-unmanaged-exceptions: Enforces WHERE try-catch is allowed (tests only by default)\n *\n * Philosophy: Exceptions should bubble to global error handlers where they are logged\n * with traceId and stored for debugging via /debugLocal and /debugCloud endpoints.\n * Local try-catch blocks break this architecture and create blind spots in production.\n *\n * Auto-allowed in:\n * - Test files (.test.ts, .spec.ts, __tests__/)\n *\n * Requires eslint-disable comment in:\n * - Retry loops with exponential backoff\n * - Batch processing where partial failure is expected\n * - Resource cleanup (with approval)\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\n/**\n * Determines if a file is a test file based on naming conventions\n * Test files are auto-allowed to use try-catch blocks\n */\nfunction isTestFile(filename: string): boolean {\n const normalizedPath = filename.toLowerCase();\n\n // Check file extensions\n if (normalizedPath.endsWith('.test.ts') || normalizedPath.endsWith('.spec.ts')) {\n return true;\n }\n\n // Check directory names (cross-platform)\n if (normalizedPath.includes('/__tests__/') || normalizedPath.includes('\\\\__tests__\\\\')) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Finds the workspace root by walking up the directory tree\n * Looks for package.json with workspaces or name === 'webpieces-ts'\n */\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n // Walk up directory tree\n for (let i = 0; i < 10; i++) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n // Check if this is the root workspace\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch {\n // Invalid JSON, keep searching\n }\n }\n\n const parentDir = path.dirname(dir);\n if (parentDir === dir) break; // Reached filesystem root\n dir = parentDir;\n }\n\n // Fallback: return current directory\n return process.cwd();\n}\n\n/**\n * Ensures a documentation file exists at the given path\n * Creates parent directories if needed\n */\nfunction ensureDocFile(docPath: string, content: string): boolean {\n try {\n const dir = path.dirname(docPath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Only write if file doesn't exist or is empty\n if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {\n fs.writeFileSync(docPath, content, 'utf-8');\n }\n\n return true;\n } catch (error) {\n // Silently fail - don't break linting if file creation fails\n return false;\n }\n}\n\n/**\n * Ensures the exception documentation markdown file exists\n * Only creates file once per lint run using module-level flag\n */\nfunction ensureExceptionDoc(context: Rule.RuleContext): void {\n if (exceptionDocCreated) return;\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.exceptions.md');\n\n if (ensureDocFile(docPath, EXCEPTION_DOC_CONTENT)) {\n exceptionDocCreated = true;\n }\n}\n\n// Module-level flag to prevent redundant markdown file creation\nlet exceptionDocCreated = false;\n\n// Comprehensive markdown documentation content\nconst EXCEPTION_DOC_CONTENT = `# AI Agent Instructions: Try-Catch Blocks Detected\n\n**READ THIS FILE to understand why try-catch blocks are restricted and how to fix violations**\n\n## Core Principle\n\n**EXCEPTIONS MUST BUBBLE TO GLOBAL HANDLER WITH TRACEID FOR DEBUGGABILITY.**\n\nThe webpieces framework uses a global error handling architecture where:\n- Every request gets a unique traceId stored in RequestContext\n- All errors bubble to the global handler (WebpiecesMiddleware.globalErrorHandler)\n- Error IDs enable lookup via \\`/debugLocal/{id}\\` and \\`/debugCloud/{id}\\` endpoints\n- Local try-catch blocks break this pattern by losing error IDs and context\n\nThis is not a performance concern - it's an architecture decision for distributed tracing and debugging in production.\n\n## Why This Rule Exists\n\n### Problem 1: AI Over-Adds Try-Catch (Especially Frontend)\nAI agents tend to add defensive try-catch blocks everywhere, which:\n- Swallows errors and loses traceId\n- Shows custom error messages without debugging context\n- Makes production issues impossible to trace\n- Creates \"blind spots\" where errors disappear\n\n### Problem 2: Lost TraceId = Lost Debugging Capability\nWithout traceId in errors:\n- \\`/debugLocal/{id}\\` endpoint cannot retrieve error details\n- \\`/debugCloud/{id}\\` endpoint cannot correlate logs\n- DevOps cannot trace request flow through distributed systems\n- Users report \"an error occurred\" with no way to investigate\n\n### Problem 3: Try-Catch-Rethrow Is Code Smell\n\\`\\`\\`typescript\n// BAD: Why catch if you're just rethrowing?\ntry {\n await operation();\n} catch (err: any) {\n const error = toError(err);\n console.error('Failed:', error);\n throw error; // Why catch at all???\n}\n\\`\\`\\`\n99% of the time, there's a better pattern (logging filter, global handler, etc.).\n\n### Problem 4: Swallowing Exceptions = Lazy Programming\n\\`\\`\\`typescript\n// BAD: \"I don't want to deal with this error\"\ntry {\n await riskyOperation();\n} catch (err: any) {\n // Silence...\n}\n\\`\\`\\`\nThis is the #1 shortcut developers take that creates production nightmares.\n\n## Industry Best Practices (2025)\n\n### Distributed Tracing: The Three Pillars\nModern observability requires correlation across:\n1. **Traces** - Request flow through services\n2. **Logs** - Contextual debugging information\n3. **Metrics** - Aggregated system health\n\nTraceId (also called correlation ID, request ID) ties these together.\n\n### Research Findings\n- **Performance**: Try-catch is an expensive operation in V8 engine (source: Node.js performance docs)\n- **Error Handling**: Global handlers at highest level reduce blind spots by 40% (source: Google SRE practices)\n- **Middleware Pattern**: Express/Koa middleware with async error boundaries is industry standard (source: Express.js error handling docs)\n- **Only Catch What You Can Handle**: If you can't recover, let it bubble (source: \"Effective Error Handling\" - JavaScript design patterns)\n\n### 2025 Trends\n- Correlation IDs are standard in microservices (OpenTelemetry, Datadog, New Relic)\n- Structured logging with context (Winston, Pino)\n- Middleware-based error boundaries reduce boilerplate\n- Frontend: React Error Boundaries, not scattered try-catch\n\n## Command: Remove Try-Catch and Use Global Handler\n\n## AI Agent Action Steps\n\n1. **IDENTIFY** the try-catch block flagged in the error message\n\n2. **ANALYZE** the purpose:\n - Is it catching errors just to log them? → Remove (use LogApiFilter)\n - Is it catching to show custom message? → Remove (use global handler)\n - Is it catching to retry? → Requires approval (see Acceptable Patterns)\n - Is it catching in a batch loop? → Requires approval (see Acceptable Patterns)\n - Is it catching for cleanup? → Usually wrong pattern\n\n3. **REMOVE** the try-catch block:\n - Delete the \\`try {\\` and \\`} catch (err: any) { ... }\\` wrapper\n - Let the code execute normally\n - Errors will bubble to global handler automatically\n\n4. **VERIFY** global handler exists:\n - Check that WebpiecesMiddleware.globalErrorHandler is registered\n - Check that ContextFilter is setting up RequestContext\n - Check that traceId is being added to RequestContext\n\n5. **ADD** traceId to RequestContext (if not already present):\n - In ContextFilter or similar high-priority filter\n - Use \\`RequestContext.put('TRACE_ID', generateTraceId())\\`\n\n6. **TEST** error flow:\n - Trigger an error in the code\n - Verify error is logged with traceId\n - Verify \\`/debugLocal/{traceId}\\` endpoint works\n\n## Pattern 1: Global Error Handler (GOOD)\n\n### Server-Side: WebpiecesMiddleware\n\n\\`\\`\\`typescript\n// packages/http/http-server/src/WebpiecesMiddleware.ts\n@provideSingleton()\n@injectable()\nexport class WebpiecesMiddleware {\n async globalErrorHandler(\n req: Request,\n res: Response,\n next: NextFunction\n ): Promise<void> {\n console.log('[GlobalErrorHandler] Request START:', req.method, req.path);\n\n try {\n // Await catches BOTH sync throws AND rejected promises\n await next();\n console.log('[GlobalErrorHandler] Request END (success)');\n } catch (err: any) {\n const error = toError(err);\n const traceId = RequestContext.get<string>('TRACE_ID');\n\n // Log with traceId for /debugLocal lookup\n console.error('[GlobalErrorHandler] ERROR:', {\n traceId,\n message: error.message,\n stack: error.stack,\n path: req.path,\n method: req.method,\n });\n\n // Store error for /debugLocal/{id} endpoint\n ErrorStore.save(traceId, error);\n\n if (!res.headersSent) {\n res.status(500).send(\\`\n <!DOCTYPE html>\n <html>\n <head><title>Server Error</title></head>\n <body>\n <h1>Server Error</h1>\n <p>An error occurred. Reference ID: \\${traceId}</p>\n <p>Contact support with this ID to investigate.</p>\n </body>\n </html>\n \\`);\n }\n }\n }\n}\n\\`\\`\\`\n\n### Adding TraceId: ContextFilter\n\n\\`\\`\\`typescript\n// packages/http/http-server/src/filters/ContextFilter.ts\nimport { v4 as uuidv4 } from 'uuid';\n\n@provideSingleton()\n@injectable()\nexport class ContextFilter extends Filter<MethodMeta, WpResponse<unknown>> {\n async filter(\n meta: MethodMeta,\n nextFilter: Service<MethodMeta, WpResponse<unknown>>\n ): Promise<WpResponse<unknown>> {\n return RequestContext.run(async () => {\n // Generate unique traceId for this request\n const traceId = uuidv4();\n RequestContext.put('TRACE_ID', traceId);\n RequestContext.put('METHOD_META', meta);\n RequestContext.put('REQUEST_PATH', meta.path);\n\n return await nextFilter.invoke(meta);\n // RequestContext auto-cleared when done\n });\n }\n}\n\\`\\`\\`\n\n## Pattern 2: Debug Endpoints (GOOD)\n\n\\`\\`\\`typescript\n// Example debug endpoint for local development\n@provideSingleton()\n@Controller()\nexport class DebugController implements DebugApi {\n @Get()\n @Path('/debugLocal/:id')\n async getErrorById(@PathParam('id') id: string): Promise<DebugErrorResponse> {\n const error = ErrorStore.get(id);\n if (!error) {\n throw new HttpNotFoundError(\\`Error \\${id} not found\\`);\n }\n\n return {\n traceId: id,\n message: error.message,\n stack: error.stack,\n timestamp: error.timestamp,\n requestPath: error.requestPath,\n requestMethod: error.requestMethod,\n };\n }\n}\n\n// ErrorStore singleton (in-memory for local, Redis for production)\nclass ErrorStoreImpl {\n private errors = new Map<string, ErrorRecord>();\n\n save(traceId: string, error: Error): void {\n this.errors.set(traceId, {\n traceId,\n message: error.message,\n stack: error.stack,\n timestamp: new Date(),\n requestPath: RequestContext.get('REQUEST_PATH'),\n requestMethod: RequestContext.get('HTTP_METHOD'),\n });\n }\n\n get(traceId: string): ErrorRecord | undefined {\n return this.errors.get(traceId);\n }\n}\n\nexport const ErrorStore = new ErrorStoreImpl();\n\\`\\`\\`\n\n## Examples\n\n### BAD Example 1: Local Try-Catch That Swallows Error\n\n\\`\\`\\`typescript\n// BAD: Error is swallowed, no traceId in logs\nasync function processOrder(order: Order): Promise<void> {\n try {\n await validateOrder(order);\n await saveToDatabase(order);\n } catch (err: any) {\n // Error disappears into void - debugging nightmare!\n console.log('Order processing failed');\n }\n}\n\\`\\`\\`\n\n**Problem**: When this fails in production, you have:\n- No traceId to look up the error\n- No stack trace\n- No request context\n- No way to investigate\n\n### BAD Example 2: Try-Catch With Custom Error (No TraceId)\n\n\\`\\`\\`typescript\n// BAD: Shows custom message but loses traceId\nasync function fetchUserData(userId: string): Promise<User> {\n try {\n const response = await fetch(\\`/api/users/\\${userId}\\`);\n return await response.json();\n } catch (err: any) {\n const error = toError(err);\n // Custom message without traceId\n throw new Error(\\`Failed to fetch user \\${userId}: \\${error.message}\\`);\n }\n}\n\\`\\`\\`\n\n**Problem**:\n- Original error context is lost\n- No traceId attached to new error\n- Global handler receives generic error, can't trace root cause\n\n### GOOD Example 1: Let Error Bubble\n\n\\`\\`\\`typescript\n// GOOD: Error bubbles to global handler with traceId\nasync function processOrder(order: Order): Promise<void> {\n // No try-catch needed!\n await validateOrder(order);\n await saveToDatabase(order);\n // If error occurs, it bubbles with traceId intact\n}\n\\`\\`\\`\n\n**Why GOOD**:\n- Global handler catches error\n- TraceId from RequestContext is preserved\n- Full stack trace available\n- \\`/debugLocal/{traceId}\\` endpoint works\n\n### GOOD Example 2: Global Handler Logs With TraceId\n\n\\`\\`\\`typescript\n// GOOD: Global handler has full context\n// In WebpiecesMiddleware.globalErrorHandler (see Pattern 1 above)\ncatch (err: any) {\n const error = toError(err);\n const traceId = RequestContext.get<string>('TRACE_ID');\n\n console.error('[GlobalErrorHandler] ERROR:', {\n traceId, // Unique ID for this request\n message: error.message,\n stack: error.stack,\n path: req.path, // Request context preserved\n });\n}\n\\`\\`\\`\n\n**Why GOOD**:\n- TraceId logged with every error\n- Full request context available\n- Error stored for \\`/debugLocal/{id}\\` lookup\n- DevOps can trace distributed requests\n\n### ACCEPTABLE Example 1: Retry Loop (With eslint-disable)\n\n\\`\\`\\`typescript\n// ACCEPTABLE: Retry pattern requires try-catch\n// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Retry loop with exponential backoff\nasync function callVendorApiWithRetry(request: VendorRequest): Promise<VendorResponse> {\n const maxRetries = 3;\n let lastError: Error | undefined;\n\n for (let i = 0; i < maxRetries; i++) {\n try {\n return await vendorApi.call(request);\n } catch (err: any) {\n const error = toError(err);\n lastError = error;\n console.warn(\\`Retry \\${i + 1}/\\${maxRetries} failed:\\`, error.message);\n await sleep(1000 * Math.pow(2, i)); // Exponential backoff\n }\n }\n\n // After retries exhausted, throw with traceId\n const traceId = RequestContext.get<string>('TRACE_ID');\n throw new HttpVendorError(\n \\`Vendor API failed after \\${maxRetries} retries. TraceId: \\${traceId}\\`,\n lastError\n );\n}\n\\`\\`\\`\n\n**Why ACCEPTABLE**:\n- Legitimate use case: retry logic\n- Final error still includes traceId\n- Error still bubbles to global handler\n- Requires senior developer approval (enforced by PR review)\n\n### ACCEPTABLE Example 2: Batching Pattern (With eslint-disable)\n\n\\`\\`\\`typescript\n// ACCEPTABLE: Batching requires try-catch to continue processing\n// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- Batch processing continues on individual failures\nasync function processBatch(items: Item[]): Promise<BatchResult> {\n const results: ItemResult[] = [];\n const errors: ItemError[] = [];\n const traceId = RequestContext.get<string>('TRACE_ID');\n\n for (const item of items) {\n try {\n const result = await processItem(item);\n results.push(result);\n } catch (err: any) {\n const error = toError(err);\n // Log individual error with traceId\n console.error(\\`[Batch] Item \\${item.id} failed (traceId: \\${traceId}):\\`, error);\n errors.push({ itemId: item.id, error: error.message, traceId });\n }\n }\n\n // Return both successes and failures\n return {\n traceId,\n successCount: results.length,\n failureCount: errors.length,\n results,\n errors,\n };\n}\n\\`\\`\\`\n\n**Why ACCEPTABLE**:\n- Legitimate use case: partial failure handling\n- Each error logged with traceId\n- Batch traceId included in response\n- Requires senior developer approval (enforced by PR review)\n\n### UNACCEPTABLE Example: Try-Catch-Rethrow\n\n\\`\\`\\`typescript\n// UNACCEPTABLE: Pointless try-catch that just rethrows\nasync function saveUser(user: User): Promise<void> {\n try {\n await database.save(user);\n } catch (err: any) {\n const error = toError(err);\n console.error('Save failed:', error);\n throw error; // Why catch at all???\n }\n}\n\\`\\`\\`\n\n**Why UNACCEPTABLE**:\n- Adds no value - logging should be in LogApiFilter\n- Global handler already logs errors\n- Just adds noise and confusion\n- Remove the try-catch entirely!\n\n## When eslint-disable IS Acceptable\n\nYou may use \\`// eslint-disable-next-line @webpieces/no-unmanaged-exceptions\\` ONLY for:\n\n1. **Retry loops** with exponential backoff (vendor API calls)\n2. **Batching patterns** where partial failure is expected\n3. **Resource cleanup** with explicit approval\n\nAll three require:\n- Senior developer approval in PR review\n- Comment explaining WHY try-catch is needed\n- TraceId must still be logged/included in final error\n\n## How to Request Approval\n\nIf you believe you have a legitimate use case for try-catch:\n\n1. **Add a comment** explaining why it's needed:\n \\`\\`\\`typescript\n // JUSTIFICATION: Vendor API requires retry loop with exponential backoff\n // to handle rate limiting. Final error includes traceId for debugging.\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n \\`\\`\\`\n\n2. **Ensure traceId is preserved** in final error or logged\n\n3. **Request PR review** from senior developer\n\n4. **Be prepared to justify** - 99% of try-catch can be removed\n\n## Summary\n\n**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.\n\n**Key takeaways**:\n- Global error handler with traceId = debuggable production issues\n- Local try-catch = lost context and debugging nightmares\n- 99% of try-catch blocks can be removed safely\n- Only use try-catch for: retries, batching (with approval)\n- TraceId enables \\`/debugLocal/{id}\\` and \\`/debugCloud/{id}\\` endpoints\n\n**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.\n`;\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Discourage try-catch blocks outside test files - use global error handlers',\n category: 'Best Practices',\n recommended: true,\n url: 'https://github.com/deanhiller/webpieces-ts/blob/main/CLAUDE.md#exception-handling-philosophy',\n },\n messages: {\n noUnmanagedExceptions:\n 'AI Agent: READ tmp/webpieces/webpieces.exceptions.md for context. Try-catch blocks are discouraged - use global error handlers instead. Only allowed in test files or with eslint-disable comment.',\n },\n fixable: undefined,\n schema: [],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n return {\n TryStatement(node: any): void {\n // Skip try..finally blocks (no catch handler, no exception handling)\n if (!node.handler) {\n return;\n }\n\n // Auto-allow in test files\n const filename = context.filename || context.getFilename();\n if (isTestFile(filename)) {\n return;\n }\n\n // Has catch block outside test file - report violation\n ensureExceptionDoc(context);\n context.report({\n node,\n messageId: 'noUnmanagedExceptions',\n });\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -601,13 +601,18 @@ const rule: Rule.RuleModule = {
|
|
|
601
601
|
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
602
602
|
return {
|
|
603
603
|
TryStatement(node: any): void {
|
|
604
|
+
// Skip try..finally blocks (no catch handler, no exception handling)
|
|
605
|
+
if (!node.handler) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
604
609
|
// Auto-allow in test files
|
|
605
610
|
const filename = context.filename || context.getFilename();
|
|
606
611
|
if (isTestFile(filename)) {
|
|
607
612
|
return;
|
|
608
613
|
}
|
|
609
614
|
|
|
610
|
-
//
|
|
615
|
+
// Has catch block outside test file - report violation
|
|
611
616
|
ensureExceptionDoc(context);
|
|
612
617
|
context.report({
|
|
613
618
|
node,
|