@webpieces/eslint-rules 0.2.113 → 0.2.115
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/package.json +14 -1
- package/recommended.mjs +88 -0
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/rules/enforce-architecture.js +9 -157
- package/src/rules/enforce-architecture.js.map +1 -1
- package/src/rules/max-file-lines.js +8 -162
- package/src/rules/max-file-lines.js.map +1 -1
- package/src/rules/max-method-lines.js +8 -114
- package/src/rules/max-method-lines.js.map +1 -1
- package/src/rules/no-unmanaged-exceptions.js +5 -40
- package/src/rules/no-unmanaged-exceptions.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/eslint-rules",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.115",
|
|
4
4
|
"description": "ESLint rules for WebPieces code patterns and architecture enforcement",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./recommended": "./recommended.mjs",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src/**/*",
|
|
14
|
+
"recommended.mjs",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
7
17
|
"author": "Dean Hiller",
|
|
8
18
|
"license": "Apache-2.0",
|
|
9
19
|
"repository": {
|
|
@@ -11,6 +21,9 @@
|
|
|
11
21
|
"url": "https://github.com/deanhiller/webpieces-ts.git",
|
|
12
22
|
"directory": "packages/tooling/eslint-rules"
|
|
13
23
|
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@webpieces/rules-config": "0.2.115"
|
|
26
|
+
},
|
|
14
27
|
"peerDependencies": {
|
|
15
28
|
"eslint": ">=8.0.0"
|
|
16
29
|
},
|
package/recommended.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// @webpieces/eslint-rules recommended ESLint configuration
|
|
2
|
+
// Consumer projects can extend this configuration
|
|
3
|
+
|
|
4
|
+
import webpiecesPlugin from '@webpieces/eslint-rules';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WebPieces recommended ESLint configuration using flat config format.
|
|
8
|
+
*
|
|
9
|
+
* Includes custom WebPieces rules:
|
|
10
|
+
* - catch-error-pattern: Enforces toError() usage in catch blocks
|
|
11
|
+
* - no-unmanaged-exceptions: Discourages try-catch outside test files
|
|
12
|
+
* - max-method-lines: Enforces maximum method length (70 lines)
|
|
13
|
+
* - max-file-lines: Enforces maximum file length (700 lines)
|
|
14
|
+
* - enforce-architecture: Enforces architecture dependency boundaries
|
|
15
|
+
* - no-json-property-primitive-type: Enforces DTO field typing conventions
|
|
16
|
+
*
|
|
17
|
+
* Usage in consumer projects:
|
|
18
|
+
*
|
|
19
|
+
* ```javascript
|
|
20
|
+
* // eslint.config.mjs
|
|
21
|
+
* import webpiecesConfig from '@webpieces/eslint-rules/recommended';
|
|
22
|
+
*
|
|
23
|
+
* export default [
|
|
24
|
+
* ...webpiecesConfig,
|
|
25
|
+
* {
|
|
26
|
+
* // Project-specific overrides
|
|
27
|
+
* rules: {}
|
|
28
|
+
* }
|
|
29
|
+
* ];
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export default [
|
|
33
|
+
{
|
|
34
|
+
ignores: [
|
|
35
|
+
'**/dist',
|
|
36
|
+
'**/out-tsc',
|
|
37
|
+
'**/tmp',
|
|
38
|
+
'**/coverage',
|
|
39
|
+
'**/node_modules',
|
|
40
|
+
'**/.nx',
|
|
41
|
+
'**/.vscode',
|
|
42
|
+
'**/.idea',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
47
|
+
plugins: {
|
|
48
|
+
'@webpieces': webpiecesPlugin,
|
|
49
|
+
},
|
|
50
|
+
languageOptions: {
|
|
51
|
+
ecmaVersion: 2021,
|
|
52
|
+
sourceType: 'module',
|
|
53
|
+
},
|
|
54
|
+
rules: {
|
|
55
|
+
'@webpieces/catch-error-pattern': 'error',
|
|
56
|
+
'@webpieces/no-unmanaged-exceptions': 'error',
|
|
57
|
+
'@webpieces/max-method-lines': ['error', { max: 70 }],
|
|
58
|
+
'@webpieces/max-file-lines': ['error', { max: 700 }],
|
|
59
|
+
'@webpieces/enforce-architecture': 'error',
|
|
60
|
+
'@webpieces/no-json-property-primitive-type': 'error',
|
|
61
|
+
'no-console': 'off',
|
|
62
|
+
'no-debugger': 'warn',
|
|
63
|
+
'no-alert': 'warn',
|
|
64
|
+
'no-var': 'error',
|
|
65
|
+
'prefer-const': 'warn',
|
|
66
|
+
'prefer-arrow-callback': 'warn',
|
|
67
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
68
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
69
|
+
'@typescript-eslint/no-unused-vars': [
|
|
70
|
+
'warn',
|
|
71
|
+
{
|
|
72
|
+
argsIgnorePattern: '^_',
|
|
73
|
+
varsIgnorePattern: '^_',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
'@typescript-eslint/no-empty-interface': 'off',
|
|
77
|
+
'@typescript-eslint/no-empty-function': 'off',
|
|
78
|
+
'sort-imports': 'off',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
files: ['**/*.spec.ts', '**/*.test.ts'],
|
|
83
|
+
rules: {
|
|
84
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
85
|
+
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
package/src/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ESLint plugin for WebPieces
|
|
3
3
|
* Provides rules for enforcing WebPieces code patterns
|
|
4
4
|
*
|
|
5
|
-
* This plugin is automatically included in @webpieces/webpieces-rules
|
|
5
|
+
* This plugin is automatically included in @webpieces/nx-webpieces-rules
|
|
6
6
|
*
|
|
7
7
|
* Available rules:
|
|
8
8
|
* - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ESLint plugin for WebPieces
|
|
4
4
|
* Provides rules for enforcing WebPieces code patterns
|
|
5
5
|
*
|
|
6
|
-
* This plugin is automatically included in @webpieces/webpieces-rules
|
|
6
|
+
* This plugin is automatically included in @webpieces/nx-webpieces-rules
|
|
7
7
|
*
|
|
8
8
|
* Available rules:
|
|
9
9
|
* - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/eslint-rules/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;AAEH,8FAA4D;AAC5D,sGAAoE;AACpE,wFAAsD;AACtD,oFAAkD;AAClD,gGAA+D;AAC/D,sHAAkF;AAClF,oGAAkE;AAClE,sFAAmD;AAEnD,iBAAS;IACL,KAAK,EAAE;QACH,qBAAqB,EAAE,6BAAiB;QACxC,yBAAyB,EAAE,iCAAqB;QAChD,kBAAkB,EAAE,0BAAc;QAClC,gBAAgB,EAAE,wBAAY;QAC9B,sBAAsB,EAAE,8BAAmB;QAC3C,iCAAiC,EAAE,yCAA2B;QAC9D,wBAAwB,EAAE,gCAAoB;QAC9C,iBAAiB,EAAE,yBAAY;KAClC;CACJ,CAAC","sourcesContent":["/**\n * ESLint plugin for WebPieces\n * Provides rules for enforcing WebPieces code patterns\n *\n * This plugin is automatically included in @webpieces/webpieces-rules\n *\n * Available rules:\n * - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)\n * - no-unmanaged-exceptions: Discourage try-catch outside tests (WHERE to handle)\n * - max-method-lines: Enforce maximum method length (default: 70 lines)\n * - max-file-lines: Enforce maximum file length (default: 700 lines)\n * - enforce-architecture: Enforce architecture dependency boundaries\n * - no-json-property-primitive-type: Ban @JsonProperty({ type: String/Number/Boolean })\n * - require-typed-template: Require [templateClassType] on ng-template with let- variables (Angular)\n * - no-mat-cell-def: Ban *matCellDef/*matHeaderCellDef — use div-grid tables (Angular)\n */\n\nimport catchErrorPattern from './rules/catch-error-pattern';\nimport noUnmanagedExceptions from './rules/no-unmanaged-exceptions';\nimport maxMethodLines from './rules/max-method-lines';\nimport maxFileLines from './rules/max-file-lines';\nimport enforceArchitecture from './rules/enforce-architecture';\nimport noJsonPropertyPrimitiveType from './rules/no-json-property-primitive-type';\nimport requireTypedTemplate from './rules/require-typed-template';\nimport noMatCellDef from './rules/no-mat-cell-def';\n\nexport = {\n rules: {\n 'catch-error-pattern': catchErrorPattern,\n 'no-unmanaged-exceptions': noUnmanagedExceptions,\n 'max-method-lines': maxMethodLines,\n 'max-file-lines': maxFileLines,\n 'enforce-architecture': enforceArchitecture,\n 'no-json-property-primitive-type': noJsonPropertyPrimitiveType,\n 'require-typed-template': requireTypedTemplate,\n 'no-mat-cell-def': noMatCellDef,\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/tooling/eslint-rules/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;AAEH,8FAA4D;AAC5D,sGAAoE;AACpE,wFAAsD;AACtD,oFAAkD;AAClD,gGAA+D;AAC/D,sHAAkF;AAClF,oGAAkE;AAClE,sFAAmD;AAEnD,iBAAS;IACL,KAAK,EAAE;QACH,qBAAqB,EAAE,6BAAiB;QACxC,yBAAyB,EAAE,iCAAqB;QAChD,kBAAkB,EAAE,0BAAc;QAClC,gBAAgB,EAAE,wBAAY;QAC9B,sBAAsB,EAAE,8BAAmB;QAC3C,iCAAiC,EAAE,yCAA2B;QAC9D,wBAAwB,EAAE,gCAAoB;QAC9C,iBAAiB,EAAE,yBAAY;KAClC;CACJ,CAAC","sourcesContent":["/**\n * ESLint plugin for WebPieces\n * Provides rules for enforcing WebPieces code patterns\n *\n * This plugin is automatically included in @webpieces/nx-webpieces-rules\n *\n * Available rules:\n * - catch-error-pattern: Enforce toError() usage in catch blocks (HOW to handle)\n * - no-unmanaged-exceptions: Discourage try-catch outside tests (WHERE to handle)\n * - max-method-lines: Enforce maximum method length (default: 70 lines)\n * - max-file-lines: Enforce maximum file length (default: 700 lines)\n * - enforce-architecture: Enforce architecture dependency boundaries\n * - no-json-property-primitive-type: Ban @JsonProperty({ type: String/Number/Boolean })\n * - require-typed-template: Require [templateClassType] on ng-template with let- variables (Angular)\n * - no-mat-cell-def: Ban *matCellDef/*matHeaderCellDef — use div-grid tables (Angular)\n */\n\nimport catchErrorPattern from './rules/catch-error-pattern';\nimport noUnmanagedExceptions from './rules/no-unmanaged-exceptions';\nimport maxMethodLines from './rules/max-method-lines';\nimport maxFileLines from './rules/max-file-lines';\nimport enforceArchitecture from './rules/enforce-architecture';\nimport noJsonPropertyPrimitiveType from './rules/no-json-property-primitive-type';\nimport requireTypedTemplate from './rules/require-typed-template';\nimport noMatCellDef from './rules/no-mat-cell-def';\n\nexport = {\n rules: {\n 'catch-error-pattern': catchErrorPattern,\n 'no-unmanaged-exceptions': noUnmanagedExceptions,\n 'max-method-lines': maxMethodLines,\n 'max-file-lines': maxFileLines,\n 'enforce-architecture': enforceArchitecture,\n 'no-json-property-primitive-type': noJsonPropertyPrimitiveType,\n 'require-typed-template': requireTypedTemplate,\n 'no-mat-cell-def': noMatCellDef,\n },\n};\n"]}
|
|
@@ -14,173 +14,25 @@
|
|
|
14
14
|
const tslib_1 = require("tslib");
|
|
15
15
|
const fs = tslib_1.__importStar(require("fs"));
|
|
16
16
|
const path = tslib_1.__importStar(require("path"));
|
|
17
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
17
18
|
const toError_1 = require("../toError");
|
|
18
|
-
const DEPENDENCIES_DOC_CONTENT = `# Instructions: Architecture Dependency Violation
|
|
19
|
-
|
|
20
|
-
IN GENERAL, it is better to avoid these changes and find a different way by moving classes
|
|
21
|
-
around to existing packages you already depend on. It is not always avoidable though.
|
|
22
|
-
A clean dependency graph keeps you out of huge trouble later.
|
|
23
|
-
|
|
24
|
-
If you are a human, simply run these commands:
|
|
25
|
-
* nx run architecture:visualize - to see the new dependencies and validate that change is desired
|
|
26
|
-
* nx run architecture:generate - updates the dep graph
|
|
27
|
-
* git diff architecture/dependencies.json - to see the deps changes you made
|
|
28
|
-
|
|
29
|
-
**READ THIS FILE FIRST before making any changes!**
|
|
30
|
-
|
|
31
|
-
## ⚠️ CRITICAL WARNING ⚠️
|
|
32
|
-
|
|
33
|
-
**This is a VERY IMPORTANT change that has LARGE REPERCUSSIONS later!**
|
|
34
|
-
|
|
35
|
-
Adding new dependencies creates technical debt that compounds over time:
|
|
36
|
-
- Creates coupling between packages that may be hard to undo
|
|
37
|
-
- Can create circular dependency tangles
|
|
38
|
-
- Makes packages harder to test in isolation
|
|
39
|
-
- Increases build times and bundle sizes
|
|
40
|
-
- May force unnecessary upgrades across the codebase
|
|
41
|
-
|
|
42
|
-
**DO NOT add dependencies without senior developer approval!**
|
|
43
|
-
|
|
44
|
-
## Understanding the Error
|
|
45
|
-
|
|
46
|
-
You've attempted to import from a package that is not in your project's allowed dependencies.
|
|
47
|
-
The architecture enforces a layered dependency structure where:
|
|
48
|
-
- Level 0 packages are foundation packages with NO dependencies on other @webpieces packages
|
|
49
|
-
- Higher level packages can only depend on lower level packages
|
|
50
|
-
- All dependencies must be explicitly declared
|
|
51
|
-
|
|
52
|
-
## Steps to Resolve
|
|
53
|
-
|
|
54
|
-
### Step 1: Generate Current Dependency Graph
|
|
55
|
-
Run this command to see the current architecture:
|
|
56
|
-
\`\`\`bash
|
|
57
|
-
npx nx run architecture:generate
|
|
58
|
-
\`\`\`
|
|
59
|
-
This creates/updates \`architecture/dependencies.json\` showing all packages and their levels.
|
|
60
|
-
|
|
61
|
-
### Step 2: Analyze the Proposed Change
|
|
62
|
-
Ask yourself:
|
|
63
|
-
1. **Is this import truly necessary?** Can you refactor to avoid it?
|
|
64
|
-
2. **Should the code move instead?** Maybe the code belongs in a different package.
|
|
65
|
-
3. **Will this create a cycle?** Use \`npx nx graph\` to visualize dependencies.
|
|
66
|
-
4. **Can you use an interface/abstraction?** Define interface in lower-level package, implement in higher-level.
|
|
67
|
-
|
|
68
|
-
### Step 3: Get Senior Developer Approval
|
|
69
|
-
|
|
70
|
-
## 🛑 AI AGENTS: STOP HERE AND ASK FOR HUMAN APPROVAL! 🛑
|
|
71
|
-
|
|
72
|
-
**YOU MUST NOT PROCEED TO STEP 4 WITHOUT EXPLICIT HUMAN APPROVAL!**
|
|
73
|
-
|
|
74
|
-
**REQUIRED**: Discuss this architectural change with a senior developer before proceeding.
|
|
75
|
-
- Explain why the dependency is needed
|
|
76
|
-
- Show you've considered alternatives (Step 2)
|
|
77
|
-
- **WAIT for explicit approval before making ANY changes to project.json or package.json**
|
|
78
|
-
|
|
79
|
-
**AI Agent Instructions:**
|
|
80
|
-
1. Present your analysis from Step 2 to the human
|
|
81
|
-
2. Explain which package needs which dependency and why
|
|
82
|
-
3. ASK: "Do you approve adding this dependency?"
|
|
83
|
-
4. **DO NOT modify project.json or package.json until you receive explicit "yes" or approval**
|
|
84
|
-
|
|
85
|
-
### Step 4: If Approved, Add the Dependency
|
|
86
|
-
|
|
87
|
-
## ⛔ NEVER MODIFY THESE FILES WITHOUT HUMAN APPROVAL FROM STEP 3! ⛔
|
|
88
|
-
|
|
89
|
-
Only after receiving explicit human approval in Step 3, make these changes:
|
|
90
|
-
|
|
91
|
-
1. **Update project.json** - Add to \`build.dependsOn\`:
|
|
92
|
-
\`\`\`json
|
|
93
|
-
{
|
|
94
|
-
"targets": {
|
|
95
|
-
"build": {
|
|
96
|
-
"dependsOn": ["^build", "dep1:build", "NEW_PACKAGE:build"]
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
\`\`\`
|
|
101
|
-
|
|
102
|
-
2. **Update package.json** - Add to \`dependencies\`:
|
|
103
|
-
\`\`\`json
|
|
104
|
-
{
|
|
105
|
-
"dependencies": {
|
|
106
|
-
"@webpieces/NEW_PACKAGE": "*"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
\`\`\`
|
|
110
|
-
|
|
111
|
-
### Step 5: Update Architecture Definition
|
|
112
|
-
Run this command to validate and update the architecture:
|
|
113
|
-
\`\`\`bash
|
|
114
|
-
npx nx run architecture:generate
|
|
115
|
-
\`\`\`
|
|
116
|
-
|
|
117
|
-
This will:
|
|
118
|
-
- Detect any cycles (which MUST be fixed before proceeding)
|
|
119
|
-
- Update \`architecture/dependencies.json\` with the new dependency
|
|
120
|
-
- Recalculate package levels
|
|
121
|
-
|
|
122
|
-
### Step 6: Verify No Cycles
|
|
123
|
-
\`\`\`bash
|
|
124
|
-
npx nx run architecture:validate-no-architecture-cycles
|
|
125
|
-
\`\`\`
|
|
126
|
-
|
|
127
|
-
If cycles are detected, you MUST refactor to break the cycle. Common strategies:
|
|
128
|
-
- Move shared code to a lower-level package
|
|
129
|
-
- Use dependency inversion (interfaces in low-level, implementations in high-level)
|
|
130
|
-
- Restructure package boundaries
|
|
131
|
-
|
|
132
|
-
## Alternative Solutions (Preferred over adding dependencies)
|
|
133
|
-
|
|
134
|
-
### Option A: Move the Code
|
|
135
|
-
If you need functionality from another package, consider moving that code to a shared lower-level package.
|
|
136
|
-
|
|
137
|
-
### Option B: Dependency Inversion
|
|
138
|
-
Define an interface in the lower-level package, implement it in the higher-level package:
|
|
139
|
-
\`\`\`typescript
|
|
140
|
-
// In foundation package (level 0)
|
|
141
|
-
export interface Logger { log(msg: string): void; }
|
|
142
|
-
|
|
143
|
-
// In higher-level package
|
|
144
|
-
export class ConsoleLogger implements Logger { ... }
|
|
145
|
-
\`\`\`
|
|
146
|
-
|
|
147
|
-
### Option C: Pass Dependencies as Parameters
|
|
148
|
-
Instead of importing, receive the dependency as a constructor or method parameter.
|
|
149
|
-
|
|
150
|
-
## Remember
|
|
151
|
-
- Every dependency you add today is technical debt for tomorrow
|
|
152
|
-
- The best dependency is the one you don't need
|
|
153
|
-
- When in doubt, refactor rather than add dependencies
|
|
154
|
-
`;
|
|
155
19
|
// Module-level flag to prevent redundant file creation
|
|
156
20
|
let dependenciesDocCreated = false;
|
|
157
21
|
/**
|
|
158
|
-
* Ensure
|
|
22
|
+
* Ensure the dependencies documentation file exists at
|
|
23
|
+
* .webpieces/instruct-ai/webpieces.dependencies.md. Sourced from @webpieces/rules-config.
|
|
159
24
|
*/
|
|
160
|
-
function
|
|
25
|
+
function ensureDependenciesDoc(workspaceRoot) {
|
|
26
|
+
if (dependenciesDocCreated)
|
|
27
|
+
return;
|
|
161
28
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
162
29
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return true;
|
|
30
|
+
(0, rules_config_1.writeTemplateIfMissing)(workspaceRoot, 'webpieces.dependencies.md');
|
|
31
|
+
dependenciesDocCreated = true;
|
|
166
32
|
}
|
|
167
33
|
catch (err) {
|
|
168
|
-
//const error = toError(err);
|
|
169
34
|
void err;
|
|
170
|
-
console.warn(
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Ensure the dependencies documentation file exists.
|
|
176
|
-
* Called when an architecture violation is detected.
|
|
177
|
-
*/
|
|
178
|
-
function ensureDependenciesDoc(workspaceRoot) {
|
|
179
|
-
if (dependenciesDocCreated)
|
|
180
|
-
return;
|
|
181
|
-
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.dependencies.md');
|
|
182
|
-
if (ensureDocFile(docPath, DEPENDENCIES_DOC_CONTENT)) {
|
|
183
|
-
dependenciesDocCreated = true;
|
|
35
|
+
console.warn('[webpieces] Could not write webpieces.dependencies.md');
|
|
184
36
|
}
|
|
185
37
|
}
|
|
186
38
|
// Cache for blessed graph (loaded once per lint run)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enforce-architecture.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/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';\nimport { toError } from '../toError';\n\nconst DEPENDENCIES_DOC_CONTENT = `# Instructions: Architecture Dependency Violation\n\nIN GENERAL, it is better to avoid these changes and find a different way by moving classes\naround to existing packages you already depend on. It is not always avoidable though.\nA clean dependency graph keeps you out of huge trouble later.\n\nIf you are a human, simply run these commands:\n* nx run architecture:visualize - to see the new dependencies and validate that change is desired\n* nx run architecture:generate - updates the dep graph\n* git diff architecture/dependencies.json - to see the deps changes you made\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:generate\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-no-architecture-cycles\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: unknown) {\n //const error = toError(err);\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\n const error = toError(err);\n console.error(`[ESLint @webpieces/enforce-architecture] Could not load graph: ${error.message}`);\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n if (pkgJson.name) {\n packageNames.add(pkgJson.name);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err; // 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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 (err: unknown) {\n //const error = toError(err);\n void err; // 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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\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: unknown) {\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\nfunction buildAllowedDepsList(allowedDeps: Set<string>, graph: EnhancedGraph): string {\n const sorted = Array.from(allowedDeps).sort((a, b) => {\n const diff = (graph[b]?.level ?? 0) - (graph[a]?.level ?? 0);\n return diff !== 0 ? diff : a.localeCompare(b);\n });\n if (sorted.length === 0) return ' (none - this is a foundation project)';\n return sorted.map((dep) => ` - ${dep} (level ${graph[dep]?.level ?? '?'})`).join('\\n');\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 .webpieces/instruct-ai/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 // webpieces-disable max-lines-new-methods -- ESLint rule create method with AST validation\n create(context: Rule.RuleContext): Rule.RuleListener {\n const filename = context.filename || context.getFilename();\n const workspaceRoot = findWorkspaceRoot(filename);\n\n return {\n // webpieces-disable no-any-unknown -- ESLint visitor callback receives untyped AST node\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 allowedList = buildAllowedDepsList(allowedDeps, graph);\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"]}
|
|
1
|
+
{"version":3,"file":"enforce-architecture.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/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';\nimport { writeTemplateIfMissing } from '@webpieces/rules-config';\nimport { toError } from '../toError';\n\n// Module-level flag to prevent redundant file creation\nlet dependenciesDocCreated = false;\n\n/**\n * Ensure the dependencies documentation file exists at\n * .webpieces/instruct-ai/webpieces.dependencies.md. Sourced from @webpieces/rules-config.\n */\nfunction ensureDependenciesDoc(workspaceRoot: string): void {\n if (dependenciesDocCreated) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n writeTemplateIfMissing(workspaceRoot, 'webpieces.dependencies.md');\n dependenciesDocCreated = true;\n } catch (err: unknown) {\n void err;\n console.warn('[webpieces] Could not write webpieces.dependencies.md');\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\n const error = toError(err);\n console.error(`[ESLint @webpieces/enforce-architecture] Could not load graph: ${error.message}`);\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n if (pkgJson.name) {\n packageNames.add(pkgJson.name);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err; // 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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 (err: unknown) {\n //const error = toError(err);\n void err; // 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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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: unknown) {\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: unknown) {\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\nfunction buildAllowedDepsList(allowedDeps: Set<string>, graph: EnhancedGraph): string {\n const sorted = Array.from(allowedDeps).sort((a, b) => {\n const diff = (graph[b]?.level ?? 0) - (graph[a]?.level ?? 0);\n return diff !== 0 ? diff : a.localeCompare(b);\n });\n if (sorted.length === 0) return ' (none - this is a foundation project)';\n return sorted.map((dep) => ` - ${dep} (level ${graph[dep]?.level ?? '?'})`).join('\\n');\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 .webpieces/instruct-ai/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 // webpieces-disable max-lines-new-methods -- ESLint rule create method with AST validation\n create(context: Rule.RuleContext): Rule.RuleListener {\n const filename = context.filename || context.getFilename();\n const workspaceRoot = findWorkspaceRoot(filename);\n\n return {\n // webpieces-disable no-any-unknown -- ESLint visitor callback receives untyped AST node\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 allowedList = buildAllowedDepsList(allowedDeps, graph);\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"]}
|
|
@@ -11,154 +11,8 @@
|
|
|
11
11
|
const tslib_1 = require("tslib");
|
|
12
12
|
const fs = tslib_1.__importStar(require("fs"));
|
|
13
13
|
const path = tslib_1.__importStar(require("path"));
|
|
14
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
14
15
|
const toError_1 = require("../toError");
|
|
15
|
-
const FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
|
|
16
|
-
|
|
17
|
-
**READ THIS FILE to fix files that are too long**
|
|
18
|
-
|
|
19
|
-
## Core Principle
|
|
20
|
-
Files should contain a SINGLE COHESIVE UNIT.
|
|
21
|
-
- One class per file (Java convention)
|
|
22
|
-
- If class is too large, extract child responsibilities
|
|
23
|
-
- Use dependency injection to compose functionality
|
|
24
|
-
|
|
25
|
-
## Command: Reduce File Size
|
|
26
|
-
|
|
27
|
-
### Step 1: Check for Multiple Classes
|
|
28
|
-
If the file contains multiple classes, **SEPARATE each class into its own file**.
|
|
29
|
-
|
|
30
|
-
\`\`\`typescript
|
|
31
|
-
// BAD: UserController.ts (multiple classes)
|
|
32
|
-
export class UserController { /* ... */ }
|
|
33
|
-
export class UserValidator { /* ... */ }
|
|
34
|
-
export class UserNotifier { /* ... */ }
|
|
35
|
-
|
|
36
|
-
// GOOD: Three separate files
|
|
37
|
-
// UserController.ts
|
|
38
|
-
export class UserController { /* ... */ }
|
|
39
|
-
|
|
40
|
-
// UserValidator.ts
|
|
41
|
-
export class UserValidator { /* ... */ }
|
|
42
|
-
|
|
43
|
-
// UserNotifier.ts
|
|
44
|
-
export class UserNotifier { /* ... */ }
|
|
45
|
-
\`\`\`
|
|
46
|
-
|
|
47
|
-
### Step 2: Extract Child Responsibilities (if single class is too large)
|
|
48
|
-
|
|
49
|
-
#### Pattern: Create New Service Class with Dependency Injection
|
|
50
|
-
|
|
51
|
-
\`\`\`typescript
|
|
52
|
-
// BAD: UserController.ts (800 lines, single class)
|
|
53
|
-
@provideSingleton()
|
|
54
|
-
@Controller()
|
|
55
|
-
export class UserController {
|
|
56
|
-
// 200 lines: CRUD operations
|
|
57
|
-
// 300 lines: validation logic
|
|
58
|
-
// 200 lines: notification logic
|
|
59
|
-
// 100 lines: analytics logic
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// GOOD: Extract validation service
|
|
63
|
-
// 1. Create UserValidationService.ts
|
|
64
|
-
@provideSingleton()
|
|
65
|
-
export class UserValidationService {
|
|
66
|
-
validateUserData(data: UserData): ValidationResult {
|
|
67
|
-
// 300 lines of validation logic moved here
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
validateEmail(email: string): boolean { /* ... */ }
|
|
71
|
-
validatePassword(password: string): boolean { /* ... */ }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 2. Inject into UserController.ts
|
|
75
|
-
@provideSingleton()
|
|
76
|
-
@Controller()
|
|
77
|
-
export class UserController {
|
|
78
|
-
constructor(
|
|
79
|
-
@inject(TYPES.UserValidationService)
|
|
80
|
-
private validator: UserValidationService
|
|
81
|
-
) {}
|
|
82
|
-
|
|
83
|
-
async createUser(data: UserData): Promise<User> {
|
|
84
|
-
const validation = this.validator.validateUserData(data);
|
|
85
|
-
if (!validation.isValid) {
|
|
86
|
-
throw new ValidationError(validation.errors);
|
|
87
|
-
}
|
|
88
|
-
// ... rest of logic
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
\`\`\`
|
|
92
|
-
|
|
93
|
-
## AI Agent Action Steps
|
|
94
|
-
|
|
95
|
-
1. **ANALYZE** the file structure:
|
|
96
|
-
- Count classes (if >1, separate immediately)
|
|
97
|
-
- Identify logical responsibilities within single class
|
|
98
|
-
|
|
99
|
-
2. **IDENTIFY** "child code" to extract:
|
|
100
|
-
- Validation logic -> ValidationService
|
|
101
|
-
- Notification logic -> NotificationService
|
|
102
|
-
- Data transformation -> TransformerService
|
|
103
|
-
- External API calls -> ApiService
|
|
104
|
-
- Business rules -> RulesEngine
|
|
105
|
-
|
|
106
|
-
3. **CREATE** new service file(s):
|
|
107
|
-
- Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
|
|
108
|
-
- Add \`@provideSingleton()\` decorator
|
|
109
|
-
- Move child methods to new class
|
|
110
|
-
|
|
111
|
-
4. **UPDATE** dependency injection:
|
|
112
|
-
- Add to \`TYPES\` constants (if using symbol-based DI)
|
|
113
|
-
- Inject new service into original class constructor
|
|
114
|
-
- Replace direct method calls with \`this.serviceName.method()\`
|
|
115
|
-
|
|
116
|
-
5. **RENAME** extracted file:
|
|
117
|
-
- Read the extracted code to understand its purpose
|
|
118
|
-
- Rename \`XXXX.ts\` to logical name (e.g., \`UserValidationService.ts\`)
|
|
119
|
-
|
|
120
|
-
6. **VERIFY** file sizes:
|
|
121
|
-
- Original file should now be <700 lines
|
|
122
|
-
- Each extracted file should be <700 lines
|
|
123
|
-
- If still too large, extract more services
|
|
124
|
-
|
|
125
|
-
## Examples of Child Responsibilities to Extract
|
|
126
|
-
|
|
127
|
-
| If File Contains | Extract To | Pattern |
|
|
128
|
-
|-----------------|------------|---------|
|
|
129
|
-
| Validation logic (200+ lines) | \`XValidator.ts\` or \`XValidationService.ts\` | Singleton service |
|
|
130
|
-
| Notification logic (150+ lines) | \`XNotifier.ts\` or \`XNotificationService.ts\` | Singleton service |
|
|
131
|
-
| Data transformation (200+ lines) | \`XTransformer.ts\` | Singleton service |
|
|
132
|
-
| External API calls (200+ lines) | \`XApiClient.ts\` | Singleton service |
|
|
133
|
-
| Complex business rules (300+ lines) | \`XRulesEngine.ts\` | Singleton service |
|
|
134
|
-
| Database queries (200+ lines) | \`XRepository.ts\` | Singleton service |
|
|
135
|
-
|
|
136
|
-
## WebPieces Dependency Injection Pattern
|
|
137
|
-
|
|
138
|
-
\`\`\`typescript
|
|
139
|
-
// 1. Define service with @provideSingleton
|
|
140
|
-
import { provideSingleton } from '@webpieces/http-routing';
|
|
141
|
-
|
|
142
|
-
@provideSingleton()
|
|
143
|
-
export class MyService {
|
|
144
|
-
doSomething(): void { /* ... */ }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 2. Inject into consumer
|
|
148
|
-
import { inject } from 'inversify';
|
|
149
|
-
import { TYPES } from './types';
|
|
150
|
-
|
|
151
|
-
@provideSingleton()
|
|
152
|
-
@Controller()
|
|
153
|
-
export class MyController {
|
|
154
|
-
constructor(
|
|
155
|
-
@inject(TYPES.MyService) private service: MyService
|
|
156
|
-
) {}
|
|
157
|
-
}
|
|
158
|
-
\`\`\`
|
|
159
|
-
|
|
160
|
-
Remember: Find the "child code" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.
|
|
161
|
-
`;
|
|
162
16
|
// Module-level flag to prevent redundant file creation
|
|
163
17
|
let fileDocCreated = false;
|
|
164
18
|
function getWorkspaceRoot(context) {
|
|
@@ -184,26 +38,18 @@ function getWorkspaceRoot(context) {
|
|
|
184
38
|
}
|
|
185
39
|
return process.cwd(); // Fallback
|
|
186
40
|
}
|
|
187
|
-
function
|
|
41
|
+
function ensureFileDoc(context) {
|
|
42
|
+
if (fileDocCreated)
|
|
43
|
+
return;
|
|
44
|
+
const workspaceRoot = getWorkspaceRoot(context);
|
|
188
45
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
189
46
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return true;
|
|
47
|
+
(0, rules_config_1.writeTemplateIfMissing)(workspaceRoot, 'webpieces.filesize.md');
|
|
48
|
+
fileDocCreated = true;
|
|
193
49
|
}
|
|
194
50
|
catch (err) {
|
|
195
51
|
const error = (0, toError_1.toError)(err);
|
|
196
|
-
console.warn(
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function ensureFileDoc(context) {
|
|
201
|
-
if (fileDocCreated)
|
|
202
|
-
return; // Performance: only create once per lint run
|
|
203
|
-
const workspaceRoot = getWorkspaceRoot(context);
|
|
204
|
-
const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.filesize.md');
|
|
205
|
-
if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {
|
|
206
|
-
fileDocCreated = true;
|
|
52
|
+
console.warn('[webpieces] Could not write webpieces.filesize.md', error);
|
|
207
53
|
}
|
|
208
54
|
}
|
|
209
55
|
const rule = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/max-file-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum file length\n *\n * Enforces a configurable maximum line count for files.\n * Default: 700 lines\n *\n * Configuration:\n * '@webpieces/max-file-lines': ['error', { max: 700 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../toError';\n\ninterface FileLinesOptions {\n max: number;\n}\n\nconst FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long\n\n**READ THIS FILE to fix files that are too long**\n\n## Core Principle\nFiles should contain a SINGLE COHESIVE UNIT.\n- One class per file (Java convention)\n- If class is too large, extract child responsibilities\n- Use dependency injection to compose functionality\n\n## Command: Reduce File Size\n\n### Step 1: Check for Multiple Classes\nIf the file contains multiple classes, **SEPARATE each class into its own file**.\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (multiple classes)\nexport class UserController { /* ... */ }\nexport class UserValidator { /* ... */ }\nexport class UserNotifier { /* ... */ }\n\n// GOOD: Three separate files\n// UserController.ts\nexport class UserController { /* ... */ }\n\n// UserValidator.ts\nexport class UserValidator { /* ... */ }\n\n// UserNotifier.ts\nexport class UserNotifier { /* ... */ }\n\\`\\`\\`\n\n### Step 2: Extract Child Responsibilities (if single class is too large)\n\n#### Pattern: Create New Service Class with Dependency Injection\n\n\\`\\`\\`typescript\n// BAD: UserController.ts (800 lines, single class)\n@provideSingleton()\n@Controller()\nexport class UserController {\n // 200 lines: CRUD operations\n // 300 lines: validation logic\n // 200 lines: notification logic\n // 100 lines: analytics logic\n}\n\n// GOOD: Extract validation service\n// 1. Create UserValidationService.ts\n@provideSingleton()\nexport class UserValidationService {\n validateUserData(data: UserData): ValidationResult {\n // 300 lines of validation logic moved here\n }\n\n validateEmail(email: string): boolean { /* ... */ }\n validatePassword(password: string): boolean { /* ... */ }\n}\n\n// 2. Inject into UserController.ts\n@provideSingleton()\n@Controller()\nexport class UserController {\n constructor(\n @inject(TYPES.UserValidationService)\n private validator: UserValidationService\n ) {}\n\n async createUser(data: UserData): Promise<User> {\n const validation = this.validator.validateUserData(data);\n if (!validation.isValid) {\n throw new ValidationError(validation.errors);\n }\n // ... rest of logic\n }\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **ANALYZE** the file structure:\n - Count classes (if >1, separate immediately)\n - Identify logical responsibilities within single class\n\n2. **IDENTIFY** \"child code\" to extract:\n - Validation logic -> ValidationService\n - Notification logic -> NotificationService\n - Data transformation -> TransformerService\n - External API calls -> ApiService\n - Business rules -> RulesEngine\n\n3. **CREATE** new service file(s):\n - Start with temporary name: \\`XXXX.ts\\` or \\`ChildService.ts\\`\n - Add \\`@provideSingleton()\\` decorator\n - Move child methods to new class\n\n4. **UPDATE** dependency injection:\n - Add to \\`TYPES\\` constants (if using symbol-based DI)\n - Inject new service into original class constructor\n - Replace direct method calls with \\`this.serviceName.method()\\`\n\n5. **RENAME** extracted file:\n - Read the extracted code to understand its purpose\n - Rename \\`XXXX.ts\\` to logical name (e.g., \\`UserValidationService.ts\\`)\n\n6. **VERIFY** file sizes:\n - Original file should now be <700 lines\n - Each extracted file should be <700 lines\n - If still too large, extract more services\n\n## Examples of Child Responsibilities to Extract\n\n| If File Contains | Extract To | Pattern |\n|-----------------|------------|---------|\n| Validation logic (200+ lines) | \\`XValidator.ts\\` or \\`XValidationService.ts\\` | Singleton service |\n| Notification logic (150+ lines) | \\`XNotifier.ts\\` or \\`XNotificationService.ts\\` | Singleton service |\n| Data transformation (200+ lines) | \\`XTransformer.ts\\` | Singleton service |\n| External API calls (200+ lines) | \\`XApiClient.ts\\` | Singleton service |\n| Complex business rules (300+ lines) | \\`XRulesEngine.ts\\` | Singleton service |\n| Database queries (200+ lines) | \\`XRepository.ts\\` | Singleton service |\n\n## WebPieces Dependency Injection Pattern\n\n\\`\\`\\`typescript\n// 1. Define service with @provideSingleton\nimport { provideSingleton } from '@webpieces/http-routing';\n\n@provideSingleton()\nexport class MyService {\n doSomething(): void { /* ... */ }\n}\n\n// 2. Inject into consumer\nimport { inject } from 'inversify';\nimport { TYPES } from './types';\n\n@provideSingleton()\n@Controller()\nexport class MyController {\n constructor(\n @inject(TYPES.MyService) private service: MyService\n ) {}\n}\n\\`\\`\\`\n\nRemember: Find the \"child code\" and pull it down into a new class. Once moved, the code's purpose becomes clear, making it easy to rename to a logical name.\n`;\n\n// Module-level flag to prevent redundant file creation\nlet fileDocCreated = false;\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 to find workspace root\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err; // Continue searching if JSON parse fails\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd(); // Fallback\n}\n\nfunction ensureDocFile(docPath: string, content: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, error);\n return false;\n }\n}\n\nfunction ensureFileDoc(context: Rule.RuleContext): void {\n if (fileDocCreated) return; // Performance: only create once per lint run\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.filesize.md');\n\n if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {\n fileDocCreated = true;\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum file length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ .webpieces/instruct-ai/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as FileLinesOptions | undefined;\n const maxLines = options?.max ?? 700;\n\n return {\n // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\n Program(node: any): void {\n ensureFileDoc(context);\n\n const sourceCode = context.sourceCode || context.getSourceCode();\n const lines = sourceCode.lines;\n const lineCount = lines.length;\n\n if (lineCount > maxLines) {\n context.report({\n node,\n messageId: 'tooLong',\n data: {\n actual: String(lineCount),\n max: String(maxLines),\n },\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
1
|
+
{"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/max-file-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum file length\n *\n * Enforces a configurable maximum line count for files.\n * Default: 700 lines\n *\n * Configuration:\n * '@webpieces/max-file-lines': ['error', { max: 700 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { writeTemplateIfMissing } from '@webpieces/rules-config';\nimport { toError } from '../toError';\n\ninterface FileLinesOptions {\n max: number;\n}\n\n// Module-level flag to prevent redundant file creation\nlet fileDocCreated = false;\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 to find workspace root\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err; // Continue searching if JSON parse fails\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd(); // Fallback\n}\n\nfunction ensureFileDoc(context: Rule.RuleContext): void {\n if (fileDocCreated) return;\n const workspaceRoot = getWorkspaceRoot(context);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n writeTemplateIfMissing(workspaceRoot, 'webpieces.filesize.md');\n fileDocCreated = true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn('[webpieces] Could not write webpieces.filesize.md', error);\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum file length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ .webpieces/instruct-ai/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as FileLinesOptions | undefined;\n const maxLines = options?.max ?? 700;\n\n return {\n // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\n Program(node: any): void {\n ensureFileDoc(context);\n\n const sourceCode = context.sourceCode || context.getSourceCode();\n const lines = sourceCode.lines;\n const lineCount = lines.length;\n\n if (lineCount > maxLines) {\n context.report({\n node,\n messageId: 'tooLong',\n data: {\n actual: String(lineCount),\n max: String(maxLines),\n },\n });\n }\n },\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -11,105 +11,8 @@
|
|
|
11
11
|
const tslib_1 = require("tslib");
|
|
12
12
|
const fs = tslib_1.__importStar(require("fs"));
|
|
13
13
|
const path = tslib_1.__importStar(require("path"));
|
|
14
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
14
15
|
const toError_1 = require("../toError");
|
|
15
|
-
const METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long
|
|
16
|
-
|
|
17
|
-
**READ THIS FILE to fix methods that are too long**
|
|
18
|
-
|
|
19
|
-
## Core Principle
|
|
20
|
-
Every method should read like a TABLE OF CONTENTS of a book.
|
|
21
|
-
- Each method call is a "chapter"
|
|
22
|
-
- When you dive into a method, you find another table of contents
|
|
23
|
-
- Keeping methods under 70 lines is achievable with proper extraction
|
|
24
|
-
|
|
25
|
-
## Command: Extract Code into Named Methods
|
|
26
|
-
|
|
27
|
-
### Pattern 1: Extract Loop Bodies
|
|
28
|
-
\`\`\`typescript
|
|
29
|
-
// BAD: 50 lines embedded in loop
|
|
30
|
-
for (const order of orders) {
|
|
31
|
-
// 20 lines of validation logic
|
|
32
|
-
// 15 lines of processing logic
|
|
33
|
-
// 10 lines of notification logic
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// GOOD: Extracted to named methods
|
|
37
|
-
for (const order of orders) {
|
|
38
|
-
validateOrder(order);
|
|
39
|
-
processOrderItems(order);
|
|
40
|
-
sendNotifications(order);
|
|
41
|
-
}
|
|
42
|
-
\`\`\`
|
|
43
|
-
|
|
44
|
-
### Pattern 2: Try-Catch Wrapper for Exception Handling
|
|
45
|
-
\`\`\`typescript
|
|
46
|
-
// GOOD: Separates success path from error handling
|
|
47
|
-
async function handleRequest(req: Request): Promise<Response> {
|
|
48
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
49
|
-
try {
|
|
50
|
-
return await executeRequest(req);
|
|
51
|
-
} catch (err: unknown) {
|
|
52
|
-
const error = toError(err);
|
|
53
|
-
return createErrorResponse(error);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
\`\`\`
|
|
57
|
-
|
|
58
|
-
### Pattern 3: Sequential Method Calls (Table of Contents)
|
|
59
|
-
\`\`\`typescript
|
|
60
|
-
// GOOD: Self-documenting steps
|
|
61
|
-
function processOrder(order: Order): void {
|
|
62
|
-
validateOrderData(order);
|
|
63
|
-
calculateTotals(order);
|
|
64
|
-
applyDiscounts(order);
|
|
65
|
-
processPayment(order);
|
|
66
|
-
updateInventory(order);
|
|
67
|
-
sendConfirmation(order);
|
|
68
|
-
}
|
|
69
|
-
\`\`\`
|
|
70
|
-
|
|
71
|
-
### Pattern 4: Separate Data Object Creation
|
|
72
|
-
\`\`\`typescript
|
|
73
|
-
// BAD: 15 lines of inline object creation
|
|
74
|
-
doSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });
|
|
75
|
-
|
|
76
|
-
// GOOD: Extract to factory method
|
|
77
|
-
const request = createRequestObject(data);
|
|
78
|
-
doSomething(request);
|
|
79
|
-
\`\`\`
|
|
80
|
-
|
|
81
|
-
### Pattern 5: Extract Inline Logic to Named Functions
|
|
82
|
-
\`\`\`typescript
|
|
83
|
-
// BAD: Complex inline logic
|
|
84
|
-
if (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {
|
|
85
|
-
// 30 lines of admin logic
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// GOOD: Extract to named methods
|
|
89
|
-
if (isAdminWithWriteAccess(user)) {
|
|
90
|
-
performAdminOperation(user);
|
|
91
|
-
}
|
|
92
|
-
\`\`\`
|
|
93
|
-
|
|
94
|
-
## AI Agent Action Steps
|
|
95
|
-
|
|
96
|
-
1. **IDENTIFY** the long method in the error message
|
|
97
|
-
2. **READ** the method to understand its logical sections
|
|
98
|
-
3. **EXTRACT** logical units into separate methods with descriptive names
|
|
99
|
-
4. **REPLACE** inline code with method calls
|
|
100
|
-
5. **VERIFY** each extracted method is <70 lines
|
|
101
|
-
6. **TEST** that functionality remains unchanged
|
|
102
|
-
|
|
103
|
-
## Examples of "Logical Units" to Extract
|
|
104
|
-
- Validation logic -> \`validateX()\`
|
|
105
|
-
- Data transformation -> \`transformXToY()\`
|
|
106
|
-
- API calls -> \`fetchXFromApi()\`
|
|
107
|
-
- Object creation -> \`createX()\`
|
|
108
|
-
- Loop bodies -> \`processItem()\`
|
|
109
|
-
- Error handling -> \`handleXError()\`
|
|
110
|
-
|
|
111
|
-
Remember: Methods should read like a table of contents. Each line should be a "chapter title" (method call) that describes what happens, not how it happens.
|
|
112
|
-
`;
|
|
113
16
|
// Module-level flag to prevent redundant file creation
|
|
114
17
|
let methodDocCreated = false;
|
|
115
18
|
function getWorkspaceRoot(context) {
|
|
@@ -134,27 +37,18 @@ function getWorkspaceRoot(context) {
|
|
|
134
37
|
}
|
|
135
38
|
return process.cwd();
|
|
136
39
|
}
|
|
137
|
-
function
|
|
40
|
+
function ensureMethodDoc(context) {
|
|
41
|
+
if (methodDocCreated)
|
|
42
|
+
return;
|
|
43
|
+
const workspaceRoot = getWorkspaceRoot(context);
|
|
138
44
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
139
45
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return true;
|
|
46
|
+
(0, rules_config_1.writeTemplateIfMissing)(workspaceRoot, 'webpieces.methods.md');
|
|
47
|
+
methodDocCreated = true;
|
|
143
48
|
}
|
|
144
49
|
catch (err) {
|
|
145
50
|
const error = (0, toError_1.toError)(err);
|
|
146
|
-
console.warn(
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function ensureMethodDoc(context) {
|
|
151
|
-
const workspaceRoot = getWorkspaceRoot(context);
|
|
152
|
-
const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.methods.md');
|
|
153
|
-
// Check if file exists AND flag is true - if both, skip
|
|
154
|
-
if (methodDocCreated && fs.existsSync(docPath))
|
|
155
|
-
return;
|
|
156
|
-
if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {
|
|
157
|
-
methodDocCreated = true;
|
|
51
|
+
console.warn('[webpieces] Could not write webpieces.methods.md', error);
|
|
158
52
|
}
|
|
159
53
|
}
|
|
160
54
|
function getFunctionName(funcNode) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"max-method-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/max-method-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum method length\n *\n * Enforces a configurable maximum line count for methods, functions, and arrow functions.\n * Default: 70 lines\n *\n * Configuration:\n * '@webpieces/max-method-lines': ['error', { max: 70 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../toError';\n\ninterface MethodLinesOptions {\n max: number;\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\ninterface FunctionNode {\n type:\n | 'FunctionDeclaration'\n | 'FunctionExpression'\n | 'ArrowFunctionExpression'\n | 'MethodDefinition';\n // webpieces-disable no-any-unknown -- ESTree AST dynamic body\n body?: any;\n loc?: {\n start: { line: number };\n end: { line: number };\n };\n key?: {\n name?: string;\n };\n id?: {\n name?: string;\n };\n // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\n [key: string]: any;\n}\n\ninterface CheckerContext {\n context: Rule.RuleContext;\n maxLines: number;\n}\n\nconst METHOD_DOC_CONTENT = `# AI Agent Instructions: Method Too Long\n\n**READ THIS FILE to fix methods that are too long**\n\n## Core Principle\nEvery method should read like a TABLE OF CONTENTS of a book.\n- Each method call is a \"chapter\"\n- When you dive into a method, you find another table of contents\n- Keeping methods under 70 lines is achievable with proper extraction\n\n## Command: Extract Code into Named Methods\n\n### Pattern 1: Extract Loop Bodies\n\\`\\`\\`typescript\n// BAD: 50 lines embedded in loop\nfor (const order of orders) {\n // 20 lines of validation logic\n // 15 lines of processing logic\n // 10 lines of notification logic\n}\n\n// GOOD: Extracted to named methods\nfor (const order of orders) {\n validateOrder(order);\n processOrderItems(order);\n sendNotifications(order);\n}\n\\`\\`\\`\n\n### Pattern 2: Try-Catch Wrapper for Exception Handling\n\\`\\`\\`typescript\n// GOOD: Separates success path from error handling\nasync function handleRequest(req: Request): Promise<Response> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await executeRequest(req);\n } catch (err: unknown) {\n const error = toError(err);\n return createErrorResponse(error);\n }\n}\n\\`\\`\\`\n\n### Pattern 3: Sequential Method Calls (Table of Contents)\n\\`\\`\\`typescript\n// GOOD: Self-documenting steps\nfunction processOrder(order: Order): void {\n validateOrderData(order);\n calculateTotals(order);\n applyDiscounts(order);\n processPayment(order);\n updateInventory(order);\n sendConfirmation(order);\n}\n\\`\\`\\`\n\n### Pattern 4: Separate Data Object Creation\n\\`\\`\\`typescript\n// BAD: 15 lines of inline object creation\ndoSomething({ field1: ..., field2: ..., field3: ..., /* 15 more fields */ });\n\n// GOOD: Extract to factory method\nconst request = createRequestObject(data);\ndoSomething(request);\n\\`\\`\\`\n\n### Pattern 5: Extract Inline Logic to Named Functions\n\\`\\`\\`typescript\n// BAD: Complex inline logic\nif (user.role === 'admin' && user.permissions.includes('write') && !user.suspended) {\n // 30 lines of admin logic\n}\n\n// GOOD: Extract to named methods\nif (isAdminWithWriteAccess(user)) {\n performAdminOperation(user);\n}\n\\`\\`\\`\n\n## AI Agent Action Steps\n\n1. **IDENTIFY** the long method in the error message\n2. **READ** the method to understand its logical sections\n3. **EXTRACT** logical units into separate methods with descriptive names\n4. **REPLACE** inline code with method calls\n5. **VERIFY** each extracted method is <70 lines\n6. **TEST** that functionality remains unchanged\n\n## Examples of \"Logical Units\" to Extract\n- Validation logic -> \\`validateX()\\`\n- Data transformation -> \\`transformXToY()\\`\n- API calls -> \\`fetchXFromApi()\\`\n- Object creation -> \\`createX()\\`\n- Loop bodies -> \\`processItem()\\`\n- Error handling -> \\`handleXError()\\`\n\nRemember: Methods should read like a table of contents. Each line should be a \"chapter title\" (method call) that describes what happens, not how it happens.\n`;\n\n// Module-level flag to prevent redundant file creation\nlet methodDocCreated = false;\n\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd();\n}\n\nfunction ensureDocFile(docPath: string, content: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, error);\n return false;\n }\n}\n\nfunction ensureMethodDoc(context: Rule.RuleContext): void {\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.methods.md');\n\n // Check if file exists AND flag is true - if both, skip\n if (methodDocCreated && fs.existsSync(docPath)) return;\n\n if (ensureDocFile(docPath, METHOD_DOC_CONTENT)) {\n methodDocCreated = true;\n }\n}\n\nfunction getFunctionName(funcNode: FunctionNode): string {\n if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {\n return funcNode.id.name;\n }\n if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {\n return funcNode.id.name;\n }\n return 'anonymous';\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\nfunction reportTooLong(ctx: CheckerContext, node: any, name: string, lineCount: number): void {\n ctx.context.report({\n node,\n messageId: 'tooLong',\n data: {\n name,\n actual: String(lineCount),\n max: String(ctx.maxLines),\n },\n });\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\nfunction checkFunctionNode(ctx: CheckerContext, node: any): void {\n ensureMethodDoc(ctx.context);\n const funcNode = node as FunctionNode;\n\n // Skip function expressions inside method definitions\n if (funcNode.type === 'FunctionExpression' && funcNode['parent']?.type === 'MethodDefinition') {\n return;\n }\n\n if (!funcNode.loc || !funcNode.body) return;\n\n const name = getFunctionName(funcNode);\n const lineCount = funcNode.loc.end.line - funcNode.loc.start.line + 1;\n\n if (lineCount > ctx.maxLines) {\n reportTooLong(ctx, funcNode, name, lineCount);\n }\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\nfunction checkMethodNode(ctx: CheckerContext, node: any): void {\n ensureMethodDoc(ctx.context);\n\n if (!node.loc || !node.value) return;\n\n const name = node.key?.name || 'anonymous';\n const lineCount = node.loc.end.line - node.loc.start.line + 1;\n\n if (lineCount > ctx.maxLines) {\n reportTooLong(ctx, node, name, lineCount);\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum method length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ .webpieces/instruct-ai/webpieces.methods.md for fix instructions. Method \"{{name}}\" has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as MethodLinesOptions | undefined;\n const ctx: CheckerContext = { context, maxLines: options?.max ?? 70 };\n\n return {\n FunctionDeclaration: (node) => checkFunctionNode(ctx, node),\n FunctionExpression: (node) => checkFunctionNode(ctx, node),\n ArrowFunctionExpression: (node) => checkFunctionNode(ctx, node),\n MethodDefinition: (node) => checkMethodNode(ctx, node),\n };\n },\n};\n\nexport = rule;\n"]}
|
|
1
|
+
{"version":3,"file":"max-method-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/max-method-lines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESLint rule to enforce maximum method length\n *\n * Enforces a configurable maximum line count for methods, functions, and arrow functions.\n * Default: 70 lines\n *\n * Configuration:\n * '@webpieces/max-method-lines': ['error', { max: 70 }]\n */\n\nimport type { Rule } from 'eslint';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { writeTemplateIfMissing } from '@webpieces/rules-config';\nimport { toError } from '../toError';\n\ninterface MethodLinesOptions {\n max: number;\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\ninterface FunctionNode {\n type:\n | 'FunctionDeclaration'\n | 'FunctionExpression'\n | 'ArrowFunctionExpression'\n | 'MethodDefinition';\n // webpieces-disable no-any-unknown -- ESTree AST dynamic body\n body?: any;\n loc?: {\n start: { line: number };\n end: { line: number };\n };\n key?: {\n name?: string;\n };\n id?: {\n name?: string;\n };\n // webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\n [key: string]: any;\n}\n\ninterface CheckerContext {\n context: Rule.RuleContext;\n maxLines: number;\n}\n\n// Module-level flag to prevent redundant file creation\nlet methodDocCreated = false;\n\nfunction getWorkspaceRoot(context: Rule.RuleContext): string {\n const filename = context.filename || context.getFilename();\n let dir = path.dirname(filename);\n\n while (dir !== path.dirname(dir)) {\n const pkgPath = path.join(dir, 'package.json');\n if (fs.existsSync(pkgPath)) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));\n if (pkg.workspaces || pkg.name === 'webpieces-ts') {\n return dir;\n }\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n }\n dir = path.dirname(dir);\n }\n return process.cwd();\n}\n\nfunction ensureMethodDoc(context: Rule.RuleContext): void {\n if (methodDocCreated) return;\n const workspaceRoot = getWorkspaceRoot(context);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n writeTemplateIfMissing(workspaceRoot, 'webpieces.methods.md');\n methodDocCreated = true;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn('[webpieces] Could not write webpieces.methods.md', error);\n }\n}\n\nfunction getFunctionName(funcNode: FunctionNode): string {\n if (funcNode.type === 'FunctionDeclaration' && funcNode.id?.name) {\n return funcNode.id.name;\n }\n if (funcNode.type === 'FunctionExpression' && funcNode.id?.name) {\n return funcNode.id.name;\n }\n return 'anonymous';\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\nfunction reportTooLong(ctx: CheckerContext, node: any, name: string, lineCount: number): void {\n ctx.context.report({\n node,\n messageId: 'tooLong',\n data: {\n name,\n actual: String(lineCount),\n max: String(ctx.maxLines),\n },\n });\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\nfunction checkFunctionNode(ctx: CheckerContext, node: any): void {\n ensureMethodDoc(ctx.context);\n const funcNode = node as FunctionNode;\n\n // Skip function expressions inside method definitions\n if (funcNode.type === 'FunctionExpression' && funcNode['parent']?.type === 'MethodDefinition') {\n return;\n }\n\n if (!funcNode.loc || !funcNode.body) return;\n\n const name = getFunctionName(funcNode);\n const lineCount = funcNode.loc.end.line - funcNode.loc.start.line + 1;\n\n if (lineCount > ctx.maxLines) {\n reportTooLong(ctx, funcNode, name, lineCount);\n }\n}\n\n// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties\nfunction checkMethodNode(ctx: CheckerContext, node: any): void {\n ensureMethodDoc(ctx.context);\n\n if (!node.loc || !node.value) return;\n\n const name = node.key?.name || 'anonymous';\n const lineCount = node.loc.end.line - node.loc.start.line + 1;\n\n if (lineCount > ctx.maxLines) {\n reportTooLong(ctx, node, name, lineCount);\n }\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Enforce maximum method length',\n category: 'Best Practices',\n recommended: false,\n url: 'https://github.com/deanhiller/webpieces-ts',\n },\n messages: {\n tooLong:\n 'AI Agent: READ .webpieces/instruct-ai/webpieces.methods.md for fix instructions. Method \"{{name}}\" has {{actual}} lines (max: {{max}})',\n },\n fixable: undefined,\n schema: [\n {\n type: 'object',\n properties: {\n max: {\n type: 'integer',\n minimum: 1,\n },\n },\n additionalProperties: false,\n },\n ],\n },\n\n create(context: Rule.RuleContext): Rule.RuleListener {\n const options = context.options[0] as MethodLinesOptions | undefined;\n const ctx: CheckerContext = { context, maxLines: options?.max ?? 70 };\n\n return {\n FunctionDeclaration: (node) => checkFunctionNode(ctx, node),\n FunctionExpression: (node) => checkFunctionNode(ctx, node),\n ArrowFunctionExpression: (node) => checkFunctionNode(ctx, node),\n MethodDefinition: (node) => checkMethodNode(ctx, node),\n };\n },\n};\n\nexport = rule;\n"]}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
const tslib_1 = require("tslib");
|
|
22
22
|
const fs = tslib_1.__importStar(require("fs"));
|
|
23
23
|
const path = tslib_1.__importStar(require("path"));
|
|
24
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
24
25
|
/**
|
|
25
26
|
* Determines if a file is a test file based on naming conventions
|
|
26
27
|
* Test files are auto-allowed to use try-catch blocks
|
|
@@ -70,56 +71,20 @@ function getWorkspaceRoot(context) {
|
|
|
70
71
|
return process.cwd();
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
|
-
* Ensures
|
|
74
|
-
*
|
|
75
|
-
*/
|
|
76
|
-
function ensureDocFile(docPath, content) {
|
|
77
|
-
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
78
|
-
try {
|
|
79
|
-
const dir = path.dirname(docPath);
|
|
80
|
-
if (!fs.existsSync(dir)) {
|
|
81
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
-
}
|
|
83
|
-
// Only write if file doesn't exist or is empty
|
|
84
|
-
if (!fs.existsSync(docPath) || fs.readFileSync(docPath, 'utf-8').trim() === '') {
|
|
85
|
-
fs.writeFileSync(docPath, content, 'utf-8');
|
|
86
|
-
}
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
//const error = toError(err);
|
|
91
|
-
void err; // Silently fail - don't break linting if file creation fails
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Ensures the exception documentation markdown file exists
|
|
97
|
-
* Only creates file once per lint run using module-level flag
|
|
98
|
-
*
|
|
99
|
-
* Reads from the template file packaged with @webpieces/webpieces-rules
|
|
100
|
-
* and copies it to .webpieces/instruct-ai/ for AI agents to read.
|
|
74
|
+
* Ensures the exception documentation markdown file exists at
|
|
75
|
+
* .webpieces/instruct-ai/webpieces.exceptions.md. Sourced from @webpieces/rules-config.
|
|
101
76
|
*/
|
|
102
77
|
function ensureExceptionDoc(context) {
|
|
103
78
|
if (exceptionDocCreated)
|
|
104
79
|
return;
|
|
105
80
|
const workspaceRoot = getWorkspaceRoot(context);
|
|
106
|
-
const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.exceptions.md');
|
|
107
|
-
// Read from the template file packaged with the npm module
|
|
108
|
-
// Path: from eslint-plugin/rules/ -> ../../templates/
|
|
109
|
-
const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.exceptions.md');
|
|
110
|
-
let content;
|
|
111
81
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
112
82
|
try {
|
|
113
|
-
|
|
83
|
+
(0, rules_config_1.writeTemplateIfMissing)(workspaceRoot, 'webpieces.exceptions.md');
|
|
84
|
+
exceptionDocCreated = true;
|
|
114
85
|
}
|
|
115
86
|
catch (err) {
|
|
116
|
-
//const error = toError(err);
|
|
117
87
|
void err;
|
|
118
|
-
// Fallback message if template not found (shouldn't happen in published package)
|
|
119
|
-
content = `# Exception Documentation Not Found\n\nTemplate file not found at: ${templatePath}\n\nPlease ensure @webpieces/webpieces-rules is properly installed.`;
|
|
120
|
-
}
|
|
121
|
-
if (ensureDocFile(docPath, content)) {
|
|
122
|
-
exceptionDocCreated = true;
|
|
123
88
|
}
|
|
124
89
|
}
|
|
125
90
|
// Module-level flag to prevent redundant markdown file creation
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/no-unmanaged-exceptions.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;AAGH,+CAAyB;AACzB,mDAA6B;AAQ7B;;;GAGG;AACH,SAAS,UAAU,CAAC,QAAgB;IAChC,MAAM,cAAc,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IAE9C,wBAAwB;IACxB,IAAI,cAAc,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7E,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,yCAAyC;IACzC,IAAI,cAAc,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;QACrF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,OAAyB;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAC3D,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEjC,yBAAyB;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QAC/C,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,8DAA8D;YAC9D,IAAI,CAAC;gBACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC1D,sCAAsC;gBACtC,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;oBAChD,OAAO,GAAG,CAAC;gBACf,CAAC;YACL,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,6BAA6B;gBAC7B,KAAK,GAAG,CAAC,CAAC,+BAA+B;YAC7C,CAAC;QACL,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,SAAS,KAAK,GAAG;YAAE,MAAM,CAAC,0BAA0B;QACxD,GAAG,GAAG,SAAS,CAAC;IACpB,CAAC;IAED,qCAAqC;IACrC,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;AACzB,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa,CAAC,OAAe,EAAE,OAAe;IACnD,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,+CAA+C;QAC/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7E,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAChD,CAAC;QAED,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC,CAAC,6DAA6D;QACvE,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,SAAS,kBAAkB,CAAC,OAAyB;IACjD,IAAI,mBAAmB;QAAE,OAAO;IAEhC,MAAM,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,yBAAyB,CAAC,CAAC;IAEjG,2DAA2D;IAC3D,sDAAsD;IACtD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,yBAAyB,CAAC,CAAC;IAE9F,IAAI,OAAe,CAAC;IACpB,8DAA8D;IAC9D,IAAI,CAAC;QACD,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC;QACT,iFAAiF;QACjF,OAAO,GAAG,sEAAsE,YAAY,qEAAqE,CAAC;IACtK,CAAC;IAED,IAAI,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;QAClC,mBAAmB,GAAG,IAAI,CAAC;IAC/B,CAAC;AACL,CAAC;AAED,gEAAgE;AAChE,IAAI,mBAAmB,GAAG,KAAK,CAAC;AAEhC,yEAAyE;AACzE,mEAAmE;AAEnE,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACF,WAAW,EAAE,4EAA4E;YACzF,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,8FAA8F;SACtG;QACD,QAAQ,EAAE;YACN,qBAAqB,EACjB,6MAA6M;SACpN;QACD,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE,EAAE;KACb;IAED,MAAM,CAAC,OAAyB;QAC5B,OAAO;YACH,6EAA6E;YAC7E,YAAY,CAAC,IAAa;gBACtB,qEAAqE;gBACrE,qEAAqE;gBACrE,IAAI,CAAE,IAAyB,CAAC,OAAO,EAAE,CAAC;oBACtC,OAAO;gBACX,CAAC;gBAED,2BAA2B;gBAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;gBAC3D,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;oBACvB,OAAO;gBACX,CAAC;gBAED,uDAAuD;gBACvD,kBAAkB,CAAC,OAAO,CAAC,CAAC;gBAC5B,OAAO,CAAC,MAAM,CAAC;oBACX,IAAI,EAAE,IAAiB;oBACvB,SAAS,EAAE,uBAAuB;iBACrC,CAAC,CAAC;YACP,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","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';\nimport { toError } from '../toError';\n\n// webpieces-disable no-any-unknown -- ESTree AST node interface\ninterface TryStatementNode {\n handler?: unknown;\n}\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 (err: unknown) {\n //const error = toError(err);\n void err; // 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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 (err: unknown) {\n //const error = toError(err);\n void err; // 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 *\n * Reads from the template file packaged with @webpieces/webpieces-rules\n * and copies it to .webpieces/instruct-ai/ for AI agents to read.\n */\nfunction ensureExceptionDoc(context: Rule.RuleContext): void {\n if (exceptionDocCreated) return;\n\n const workspaceRoot = getWorkspaceRoot(context);\n const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.exceptions.md');\n\n // Read from the template file packaged with the npm module\n // Path: from eslint-plugin/rules/ -> ../../templates/\n const templatePath = path.join(__dirname, '..', '..', 'templates', 'webpieces.exceptions.md');\n\n let content: string;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n content = fs.readFileSync(templatePath, 'utf-8');\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n // Fallback message if template not found (shouldn't happen in published package)\n content = `# Exception Documentation Not Found\\n\\nTemplate file not found at: ${templatePath}\\n\\nPlease ensure @webpieces/webpieces-rules is properly installed.`;\n }\n\n if (ensureDocFile(docPath, content)) {\n exceptionDocCreated = true;\n }\n}\n\n// Module-level flag to prevent redundant markdown file creation\nlet exceptionDocCreated = false;\n\n// NOTE: Documentation content moved to templates/webpieces.exceptions.md\n// The ensureExceptionDoc function reads from that file at runtime.\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 .webpieces/instruct-ai/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 // webpieces-disable no-any-unknown -- ESLint visitor callback parameter type\n TryStatement(node: unknown): void {\n // Skip try..finally blocks (no catch handler, no exception handling)\n // webpieces-disable no-any-unknown -- ESTree AST node type assertion\n if (!(node as TryStatementNode).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: node as Rule.Node,\n messageId: 'noUnmanagedExceptions',\n });\n },\n };\n },\n};\n\nexport = rule;\n\n"]}
|
|
1
|
+
{"version":3,"file":"no-unmanaged-exceptions.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/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';\nimport { writeTemplateIfMissing } from '@webpieces/rules-config';\nimport { toError } from '../toError';\n\n// webpieces-disable no-any-unknown -- ESTree AST node interface\ninterface TryStatementNode {\n handler?: unknown;\n}\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 // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\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 (err: unknown) {\n //const error = toError(err);\n void err; // 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 the exception documentation markdown file exists at\n * .webpieces/instruct-ai/webpieces.exceptions.md. Sourced from @webpieces/rules-config.\n */\nfunction ensureExceptionDoc(context: Rule.RuleContext): void {\n if (exceptionDocCreated) return;\n const workspaceRoot = getWorkspaceRoot(context);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n writeTemplateIfMissing(workspaceRoot, 'webpieces.exceptions.md');\n exceptionDocCreated = true;\n } catch (err: unknown) {\n void err;\n }\n}\n\n// Module-level flag to prevent redundant markdown file creation\nlet exceptionDocCreated = false;\n\n// NOTE: Documentation content moved to templates/webpieces.exceptions.md\n// The ensureExceptionDoc function reads from that file at runtime.\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 .webpieces/instruct-ai/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 // webpieces-disable no-any-unknown -- ESLint visitor callback parameter type\n TryStatement(node: unknown): void {\n // Skip try..finally blocks (no catch handler, no exception handling)\n // webpieces-disable no-any-unknown -- ESTree AST node type assertion\n if (!(node as TryStatementNode).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: node as Rule.Node,\n messageId: 'noUnmanagedExceptions',\n });\n },\n };\n },\n};\n\nexport = rule;\n\n"]}
|