@webpieces/dev-config 0.0.0-dev → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/architecture/executors/generate/executor.d.ts +17 -0
- package/architecture/executors/generate/executor.js +67 -0
- package/architecture/executors/generate/executor.js.map +1 -0
- package/architecture/executors/generate/executor.ts +83 -0
- package/architecture/executors/generate/schema.json +14 -0
- package/architecture/executors/validate-architecture-unchanged/executor.d.ts +17 -0
- package/architecture/executors/validate-architecture-unchanged/executor.js +65 -0
- package/architecture/executors/validate-architecture-unchanged/executor.js.map +1 -0
- package/architecture/executors/validate-architecture-unchanged/executor.ts +81 -0
- package/architecture/executors/validate-architecture-unchanged/schema.json +14 -0
- package/architecture/executors/validate-no-cycles/executor.d.ts +16 -0
- package/architecture/executors/validate-no-cycles/executor.js +48 -0
- package/architecture/executors/validate-no-cycles/executor.js.map +1 -0
- package/architecture/executors/validate-no-cycles/executor.ts +60 -0
- package/architecture/executors/validate-no-cycles/schema.json +8 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.d.ts +19 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.js +227 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.js.map +1 -0
- package/architecture/executors/validate-no-skiplevel-deps/executor.ts +267 -0
- package/architecture/executors/validate-no-skiplevel-deps/schema.json +8 -0
- package/architecture/executors/visualize/executor.d.ts +17 -0
- package/architecture/executors/visualize/executor.js +49 -0
- package/architecture/executors/visualize/executor.js.map +1 -0
- package/architecture/executors/visualize/executor.ts +63 -0
- package/architecture/executors/visualize/schema.json +14 -0
- package/architecture/index.d.ts +19 -0
- package/architecture/index.js +23 -0
- package/architecture/index.js.map +1 -0
- package/architecture/index.ts +20 -0
- package/architecture/lib/graph-comparator.d.ts +39 -0
- package/architecture/lib/graph-comparator.js +100 -0
- package/architecture/lib/graph-comparator.js.map +1 -0
- package/architecture/lib/graph-comparator.ts +141 -0
- package/architecture/lib/graph-generator.d.ts +19 -0
- package/architecture/lib/graph-generator.js +88 -0
- package/architecture/lib/graph-generator.js.map +1 -0
- package/architecture/lib/graph-generator.ts +102 -0
- package/architecture/lib/graph-loader.d.ts +31 -0
- package/architecture/lib/graph-loader.js +70 -0
- package/architecture/lib/graph-loader.js.map +1 -0
- package/architecture/lib/graph-loader.ts +82 -0
- package/architecture/lib/graph-sorter.d.ts +37 -0
- package/architecture/lib/graph-sorter.js +110 -0
- package/architecture/lib/graph-sorter.js.map +1 -0
- package/architecture/lib/graph-sorter.ts +137 -0
- package/architecture/lib/graph-visualizer.d.ts +29 -0
- package/architecture/lib/graph-visualizer.js +209 -0
- package/architecture/lib/graph-visualizer.js.map +1 -0
- package/architecture/lib/graph-visualizer.ts +222 -0
- package/architecture/lib/package-validator.d.ts +38 -0
- package/architecture/lib/package-validator.js +105 -0
- package/architecture/lib/package-validator.js.map +1 -0
- package/architecture/lib/package-validator.ts +144 -0
- package/config/eslint/base.mjs +6 -0
- package/eslint-plugin/__tests__/catch-error-pattern.test.ts +0 -1
- package/eslint-plugin/__tests__/max-file-lines.test.ts +29 -17
- package/eslint-plugin/__tests__/max-method-lines.test.ts +27 -15
- package/eslint-plugin/__tests__/no-unmanaged-exceptions.test.ts +359 -0
- package/eslint-plugin/index.d.ts +9 -0
- package/eslint-plugin/index.js +11 -0
- package/eslint-plugin/index.js.map +1 -1
- package/eslint-plugin/index.ts +11 -0
- package/eslint-plugin/rules/enforce-architecture.d.ts +15 -0
- package/eslint-plugin/rules/enforce-architecture.js +406 -0
- package/eslint-plugin/rules/enforce-architecture.js.map +1 -0
- package/eslint-plugin/rules/enforce-architecture.ts +469 -0
- package/eslint-plugin/rules/max-file-lines.js +11 -11
- package/eslint-plugin/rules/max-file-lines.js.map +1 -1
- package/eslint-plugin/rules/max-file-lines.ts +11 -11
- package/eslint-plugin/rules/max-method-lines.js +71 -88
- package/eslint-plugin/rules/max-method-lines.js.map +1 -1
- package/eslint-plugin/rules/max-method-lines.ts +85 -102
- package/eslint-plugin/rules/no-unmanaged-exceptions.d.ts +22 -0
- package/eslint-plugin/rules/no-unmanaged-exceptions.js +605 -0
- package/eslint-plugin/rules/no-unmanaged-exceptions.js.map +1 -0
- package/eslint-plugin/rules/no-unmanaged-exceptions.ts +621 -0
- package/executors.json +29 -0
- package/package.json +13 -7
- package/plugins/circular-deps/index.d.ts +8 -0
- package/plugins/circular-deps/index.js +14 -0
- package/plugins/circular-deps/index.js.map +1 -0
- package/plugins/circular-deps/index.ts +9 -0
- package/plugins/circular-deps/plugin.d.ts +32 -0
- package/plugins/circular-deps/plugin.js +73 -0
- package/plugins/circular-deps/plugin.js.map +1 -0
- package/plugins/circular-deps/plugin.ts +83 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce architecture boundaries
|
|
3
|
+
*
|
|
4
|
+
* Validates that imports from @webpieces/* packages comply with the
|
|
5
|
+
* blessed dependency graph in .graphs/dependencies.json
|
|
6
|
+
*
|
|
7
|
+
* Supports transitive dependencies: if A depends on B and B depends on C,
|
|
8
|
+
* then A can import from C.
|
|
9
|
+
*
|
|
10
|
+
* Configuration:
|
|
11
|
+
* '@webpieces/enforce-architecture': 'error'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Rule } from 'eslint';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
|
|
18
|
+
const DEPENDENCIES_DOC_CONTENT = `# AI Agent Instructions: Architecture Dependency Violation
|
|
19
|
+
|
|
20
|
+
**READ THIS FILE FIRST before making any changes!**
|
|
21
|
+
|
|
22
|
+
## ⚠️ CRITICAL WARNING ⚠️
|
|
23
|
+
|
|
24
|
+
**This is a VERY IMPORTANT change that has LARGE REPERCUSSIONS later!**
|
|
25
|
+
|
|
26
|
+
Adding new dependencies creates technical debt that compounds over time:
|
|
27
|
+
- Creates coupling between packages that may be hard to undo
|
|
28
|
+
- Can create circular dependency tangles
|
|
29
|
+
- Makes packages harder to test in isolation
|
|
30
|
+
- Increases build times and bundle sizes
|
|
31
|
+
- May force unnecessary upgrades across the codebase
|
|
32
|
+
|
|
33
|
+
**DO NOT add dependencies without senior developer approval!**
|
|
34
|
+
|
|
35
|
+
## Understanding the Error
|
|
36
|
+
|
|
37
|
+
You've attempted to import from a package that is not in your project's allowed dependencies.
|
|
38
|
+
The architecture enforces a layered dependency structure where:
|
|
39
|
+
- Level 0 packages are foundation packages with NO dependencies on other @webpieces packages
|
|
40
|
+
- Higher level packages can only depend on lower level packages
|
|
41
|
+
- All dependencies must be explicitly declared
|
|
42
|
+
|
|
43
|
+
## Steps to Resolve
|
|
44
|
+
|
|
45
|
+
### Step 1: Generate Current Dependency Graph
|
|
46
|
+
Run this command to see the current architecture:
|
|
47
|
+
\`\`\`bash
|
|
48
|
+
npx nx run architecture:generate
|
|
49
|
+
\`\`\`
|
|
50
|
+
This creates/updates \`architecture/dependencies.json\` showing all packages and their levels.
|
|
51
|
+
|
|
52
|
+
### Step 2: Analyze the Proposed Change
|
|
53
|
+
Ask yourself:
|
|
54
|
+
1. **Is this import truly necessary?** Can you refactor to avoid it?
|
|
55
|
+
2. **Should the code move instead?** Maybe the code belongs in a different package.
|
|
56
|
+
3. **Will this create a cycle?** Use \`npx nx graph\` to visualize dependencies.
|
|
57
|
+
4. **Can you use an interface/abstraction?** Define interface in lower-level package, implement in higher-level.
|
|
58
|
+
|
|
59
|
+
### Step 3: Get Senior Developer Approval
|
|
60
|
+
|
|
61
|
+
## 🛑 AI AGENTS: STOP HERE AND ASK FOR HUMAN APPROVAL! 🛑
|
|
62
|
+
|
|
63
|
+
**YOU MUST NOT PROCEED TO STEP 4 WITHOUT EXPLICIT HUMAN APPROVAL!**
|
|
64
|
+
|
|
65
|
+
**REQUIRED**: Discuss this architectural change with a senior developer before proceeding.
|
|
66
|
+
- Explain why the dependency is needed
|
|
67
|
+
- Show you've considered alternatives (Step 2)
|
|
68
|
+
- **WAIT for explicit approval before making ANY changes to project.json or package.json**
|
|
69
|
+
|
|
70
|
+
**AI Agent Instructions:**
|
|
71
|
+
1. Present your analysis from Step 2 to the human
|
|
72
|
+
2. Explain which package needs which dependency and why
|
|
73
|
+
3. ASK: "Do you approve adding this dependency?"
|
|
74
|
+
4. **DO NOT modify project.json or package.json until you receive explicit "yes" or approval**
|
|
75
|
+
|
|
76
|
+
### Step 4: If Approved, Add the Dependency
|
|
77
|
+
|
|
78
|
+
## ⛔ NEVER MODIFY THESE FILES WITHOUT HUMAN APPROVAL FROM STEP 3! ⛔
|
|
79
|
+
|
|
80
|
+
Only after receiving explicit human approval in Step 3, make these changes:
|
|
81
|
+
|
|
82
|
+
1. **Update project.json** - Add to \`build.dependsOn\`:
|
|
83
|
+
\`\`\`json
|
|
84
|
+
{
|
|
85
|
+
"targets": {
|
|
86
|
+
"build": {
|
|
87
|
+
"dependsOn": ["^build", "dep1:build", "NEW_PACKAGE:build"]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
2. **Update package.json** - Add to \`dependencies\`:
|
|
94
|
+
\`\`\`json
|
|
95
|
+
{
|
|
96
|
+
"dependencies": {
|
|
97
|
+
"@webpieces/NEW_PACKAGE": "*"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
\`\`\`
|
|
101
|
+
|
|
102
|
+
### Step 5: Update Architecture Definition
|
|
103
|
+
Run this command to validate and update the architecture:
|
|
104
|
+
\`\`\`bash
|
|
105
|
+
npx nx run architecture:validate --mode=update
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
This will:
|
|
109
|
+
- Detect any cycles (which MUST be fixed before proceeding)
|
|
110
|
+
- Update \`architecture/dependencies.json\` with the new dependency
|
|
111
|
+
- Recalculate package levels
|
|
112
|
+
|
|
113
|
+
### Step 6: Verify No Cycles
|
|
114
|
+
\`\`\`bash
|
|
115
|
+
npx nx run architecture:validate
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
If cycles are detected, you MUST refactor to break the cycle. Common strategies:
|
|
119
|
+
- Move shared code to a lower-level package
|
|
120
|
+
- Use dependency inversion (interfaces in low-level, implementations in high-level)
|
|
121
|
+
- Restructure package boundaries
|
|
122
|
+
|
|
123
|
+
## Alternative Solutions (Preferred over adding dependencies)
|
|
124
|
+
|
|
125
|
+
### Option A: Move the Code
|
|
126
|
+
If you need functionality from another package, consider moving that code to a shared lower-level package.
|
|
127
|
+
|
|
128
|
+
### Option B: Dependency Inversion
|
|
129
|
+
Define an interface in the lower-level package, implement it in the higher-level package:
|
|
130
|
+
\`\`\`typescript
|
|
131
|
+
// In foundation package (level 0)
|
|
132
|
+
export interface Logger { log(msg: string): void; }
|
|
133
|
+
|
|
134
|
+
// In higher-level package
|
|
135
|
+
export class ConsoleLogger implements Logger { ... }
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
### Option C: Pass Dependencies as Parameters
|
|
139
|
+
Instead of importing, receive the dependency as a constructor or method parameter.
|
|
140
|
+
|
|
141
|
+
## Remember
|
|
142
|
+
- Every dependency you add today is technical debt for tomorrow
|
|
143
|
+
- The best dependency is the one you don't need
|
|
144
|
+
- When in doubt, refactor rather than add dependencies
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
// Module-level flag to prevent redundant file creation
|
|
148
|
+
let dependenciesDocCreated = false;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Ensure a documentation file exists at the given path.
|
|
152
|
+
*/
|
|
153
|
+
function ensureDocFile(docPath: string, content: string): boolean {
|
|
154
|
+
try {
|
|
155
|
+
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
156
|
+
fs.writeFileSync(docPath, content, 'utf-8');
|
|
157
|
+
return true;
|
|
158
|
+
} catch (err: any) {
|
|
159
|
+
void err;
|
|
160
|
+
console.warn(`[webpieces] Could not create doc file: ${docPath}`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Ensure the dependencies documentation file exists.
|
|
167
|
+
* Called when an architecture violation is detected.
|
|
168
|
+
*/
|
|
169
|
+
function ensureDependenciesDoc(workspaceRoot: string): void {
|
|
170
|
+
if (dependenciesDocCreated) return;
|
|
171
|
+
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.dependencies.md');
|
|
172
|
+
if (ensureDocFile(docPath, DEPENDENCIES_DOC_CONTENT)) {
|
|
173
|
+
dependenciesDocCreated = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Graph entry format from .graphs/dependencies.json
|
|
179
|
+
*/
|
|
180
|
+
interface GraphEntry {
|
|
181
|
+
level: number;
|
|
182
|
+
dependsOn: string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type EnhancedGraph = Record<string, GraphEntry>;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Project mapping entry
|
|
189
|
+
*/
|
|
190
|
+
interface ProjectMapping {
|
|
191
|
+
root: string;
|
|
192
|
+
name: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Cache for blessed graph (loaded once per lint run)
|
|
196
|
+
let cachedGraph: EnhancedGraph | null = null;
|
|
197
|
+
let cachedGraphPath: string | null = null;
|
|
198
|
+
|
|
199
|
+
// Cache for project mappings
|
|
200
|
+
let cachedProjectMappings: ProjectMapping[] | null = null;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Find workspace root by walking up from file location
|
|
204
|
+
*/
|
|
205
|
+
function findWorkspaceRoot(startPath: string): string {
|
|
206
|
+
let currentDir = path.dirname(startPath);
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < 20; i++) {
|
|
209
|
+
const packagePath = path.join(currentDir, 'package.json');
|
|
210
|
+
if (fs.existsSync(packagePath)) {
|
|
211
|
+
try {
|
|
212
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
|
213
|
+
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
214
|
+
return currentDir;
|
|
215
|
+
}
|
|
216
|
+
} catch (err: any) {
|
|
217
|
+
//const error = toError(err);
|
|
218
|
+
void err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const parent = path.dirname(currentDir);
|
|
223
|
+
if (parent === currentDir) break;
|
|
224
|
+
currentDir = parent;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return process.cwd();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Load blessed graph from architecture/dependencies.json
|
|
232
|
+
*/
|
|
233
|
+
function loadBlessedGraph(workspaceRoot: string): EnhancedGraph | null {
|
|
234
|
+
const graphPath = path.join(workspaceRoot, 'architecture', 'dependencies.json');
|
|
235
|
+
|
|
236
|
+
// Return cached if same path
|
|
237
|
+
if (cachedGraphPath === graphPath && cachedGraph !== null) {
|
|
238
|
+
return cachedGraph;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!fs.existsSync(graphPath)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const content = fs.readFileSync(graphPath, 'utf-8');
|
|
247
|
+
cachedGraph = JSON.parse(content) as EnhancedGraph;
|
|
248
|
+
cachedGraphPath = graphPath;
|
|
249
|
+
return cachedGraph;
|
|
250
|
+
} catch (err: any) {
|
|
251
|
+
//const error = toError(err);
|
|
252
|
+
// err is used below
|
|
253
|
+
console.error(`[ESLint @webpieces/enforce-architecture] Could not load graph: ${err}`);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build project mappings from project.json files in workspace
|
|
260
|
+
*/
|
|
261
|
+
function buildProjectMappings(workspaceRoot: string): ProjectMapping[] {
|
|
262
|
+
if (cachedProjectMappings !== null) {
|
|
263
|
+
return cachedProjectMappings;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const mappings: ProjectMapping[] = [];
|
|
267
|
+
|
|
268
|
+
// Scan common locations for project.json files
|
|
269
|
+
const searchDirs = ['packages', 'apps', 'libs', 'libraries', 'services'];
|
|
270
|
+
|
|
271
|
+
for (const searchDir of searchDirs) {
|
|
272
|
+
const searchPath = path.join(workspaceRoot, searchDir);
|
|
273
|
+
if (!fs.existsSync(searchPath)) continue;
|
|
274
|
+
|
|
275
|
+
scanForProjects(searchPath, workspaceRoot, mappings);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Sort by path length (longest first) for more specific matching
|
|
279
|
+
mappings.sort((a, b) => b.root.length - a.root.length);
|
|
280
|
+
|
|
281
|
+
cachedProjectMappings = mappings;
|
|
282
|
+
return mappings;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Recursively scan for project.json files
|
|
287
|
+
*/
|
|
288
|
+
function scanForProjects(
|
|
289
|
+
dir: string,
|
|
290
|
+
workspaceRoot: string,
|
|
291
|
+
mappings: ProjectMapping[]
|
|
292
|
+
): void {
|
|
293
|
+
try {
|
|
294
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
295
|
+
|
|
296
|
+
for (const entry of entries) {
|
|
297
|
+
const fullPath = path.join(dir, entry.name);
|
|
298
|
+
|
|
299
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
300
|
+
// Check for project.json in this directory
|
|
301
|
+
const projectJsonPath = path.join(fullPath, 'project.json');
|
|
302
|
+
if (fs.existsSync(projectJsonPath)) {
|
|
303
|
+
try {
|
|
304
|
+
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
|
305
|
+
const projectRoot = path.relative(workspaceRoot, fullPath);
|
|
306
|
+
|
|
307
|
+
// Determine project name
|
|
308
|
+
let projectName = projectJson.name || entry.name;
|
|
309
|
+
|
|
310
|
+
// Add @webpieces/ prefix if not present
|
|
311
|
+
if (!projectName.startsWith('@webpieces/')) {
|
|
312
|
+
projectName = `@webpieces/${projectName}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
mappings.push({
|
|
316
|
+
root: projectRoot,
|
|
317
|
+
name: projectName,
|
|
318
|
+
});
|
|
319
|
+
} catch (err: any) {
|
|
320
|
+
//const error = toError(err);
|
|
321
|
+
void err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Continue scanning subdirectories
|
|
326
|
+
scanForProjects(fullPath, workspaceRoot, mappings);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (err: any) {
|
|
330
|
+
//const error = toError(err);
|
|
331
|
+
void err;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get project name from file path
|
|
337
|
+
*/
|
|
338
|
+
function getProjectFromFile(filePath: string, workspaceRoot: string): string | null {
|
|
339
|
+
const relativePath = path.relative(workspaceRoot, filePath).replace(/\\/g, '/');
|
|
340
|
+
const mappings = buildProjectMappings(workspaceRoot);
|
|
341
|
+
|
|
342
|
+
for (const mapping of mappings) {
|
|
343
|
+
if (relativePath.startsWith(mapping.root + '/') || relativePath.startsWith(mapping.root)) {
|
|
344
|
+
return mapping.name;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Compute all transitive dependencies for a project
|
|
353
|
+
*/
|
|
354
|
+
function computeTransitiveDependencies(project: string, graph: EnhancedGraph): Set<string> {
|
|
355
|
+
const result = new Set<string>();
|
|
356
|
+
const visited = new Set<string>();
|
|
357
|
+
|
|
358
|
+
function visit(currentProject: string): void {
|
|
359
|
+
if (visited.has(currentProject)) return;
|
|
360
|
+
visited.add(currentProject);
|
|
361
|
+
|
|
362
|
+
const entry = graph[currentProject];
|
|
363
|
+
if (!entry || !entry.dependsOn) return;
|
|
364
|
+
|
|
365
|
+
for (const dep of entry.dependsOn) {
|
|
366
|
+
result.add(dep);
|
|
367
|
+
visit(dep);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
visit(project);
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const rule: Rule.RuleModule = {
|
|
376
|
+
meta: {
|
|
377
|
+
type: 'problem',
|
|
378
|
+
docs: {
|
|
379
|
+
description: 'Enforce blessed architecture dependencies',
|
|
380
|
+
category: 'Best Practices',
|
|
381
|
+
recommended: true,
|
|
382
|
+
url: 'https://github.com/deanhiller/webpieces-ts',
|
|
383
|
+
},
|
|
384
|
+
messages: {
|
|
385
|
+
illegalImport:
|
|
386
|
+
"⚠️ AI Agent: READ tmp/webpieces/webpieces.dependencies.md for resolution steps!\n\n" +
|
|
387
|
+
"Import '{{imported}}' violates architecture boundaries.\n\n" +
|
|
388
|
+
"Project '{{project}}' (level {{level}}) can only import from:\n" +
|
|
389
|
+
'{{allowedList}}',
|
|
390
|
+
noGraph:
|
|
391
|
+
'No architecture graph found at architecture/dependencies.json\n' +
|
|
392
|
+
'Run: nx run architecture:validate --mode=update',
|
|
393
|
+
},
|
|
394
|
+
schema: [],
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
create(context: Rule.RuleContext): Rule.RuleListener {
|
|
398
|
+
const filename = context.filename || context.getFilename();
|
|
399
|
+
const workspaceRoot = findWorkspaceRoot(filename);
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
ImportDeclaration(node: any): void {
|
|
403
|
+
const importPath = node.source.value as string;
|
|
404
|
+
|
|
405
|
+
// Only check @webpieces/* imports
|
|
406
|
+
if (!importPath.startsWith('@webpieces/')) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Determine which project this file belongs to
|
|
411
|
+
const project = getProjectFromFile(filename, workspaceRoot);
|
|
412
|
+
if (!project) {
|
|
413
|
+
// File not in any known project (e.g., tools/, scripts/)
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Self-import is always allowed
|
|
418
|
+
if (importPath === project) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Load blessed graph
|
|
423
|
+
const graph = loadBlessedGraph(workspaceRoot);
|
|
424
|
+
if (!graph) {
|
|
425
|
+
// No graph file - warn but don't fail (allows gradual adoption)
|
|
426
|
+
// Uncomment below to enforce graph existence:
|
|
427
|
+
// context.report({ node: node.source, messageId: 'noGraph' });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Get project entry
|
|
432
|
+
const projectEntry = graph[project];
|
|
433
|
+
if (!projectEntry) {
|
|
434
|
+
// Project not in graph (new project?) - allow
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Compute allowed dependencies (direct + transitive)
|
|
439
|
+
const allowedDeps = computeTransitiveDependencies(project, graph);
|
|
440
|
+
|
|
441
|
+
// Check if import is allowed
|
|
442
|
+
if (!allowedDeps.has(importPath)) {
|
|
443
|
+
// Write documentation file for AI/developer to read
|
|
444
|
+
ensureDependenciesDoc(workspaceRoot);
|
|
445
|
+
|
|
446
|
+
const directDeps = projectEntry.dependsOn || [];
|
|
447
|
+
const allowedList =
|
|
448
|
+
directDeps.length > 0
|
|
449
|
+
? directDeps.map((dep) => ` - ${dep}`).join('\n') +
|
|
450
|
+
'\n (and their transitive dependencies)'
|
|
451
|
+
: ' (none - this is a foundation project)';
|
|
452
|
+
|
|
453
|
+
context.report({
|
|
454
|
+
node: node.source,
|
|
455
|
+
messageId: 'illegalImport',
|
|
456
|
+
data: {
|
|
457
|
+
imported: importPath,
|
|
458
|
+
project: project,
|
|
459
|
+
level: String(projectEntry.level),
|
|
460
|
+
allowedList: allowedList,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
export = rule;
|
|
@@ -27,12 +27,12 @@ Files should contain a SINGLE COHESIVE UNIT.
|
|
|
27
27
|
If the file contains multiple classes, **SEPARATE each class into its own file**.
|
|
28
28
|
|
|
29
29
|
\`\`\`typescript
|
|
30
|
-
//
|
|
30
|
+
// BAD: UserController.ts (multiple classes)
|
|
31
31
|
export class UserController { /* ... */ }
|
|
32
32
|
export class UserValidator { /* ... */ }
|
|
33
33
|
export class UserNotifier { /* ... */ }
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// GOOD: Three separate files
|
|
36
36
|
// UserController.ts
|
|
37
37
|
export class UserController { /* ... */ }
|
|
38
38
|
|
|
@@ -48,7 +48,7 @@ export class UserNotifier { /* ... */ }
|
|
|
48
48
|
#### Pattern: Create New Service Class with Dependency Injection
|
|
49
49
|
|
|
50
50
|
\`\`\`typescript
|
|
51
|
-
//
|
|
51
|
+
// BAD: UserController.ts (800 lines, single class)
|
|
52
52
|
@provideSingleton()
|
|
53
53
|
@Controller()
|
|
54
54
|
export class UserController {
|
|
@@ -58,7 +58,7 @@ export class UserController {
|
|
|
58
58
|
// 100 lines: analytics logic
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// GOOD: Extract validation service
|
|
62
62
|
// 1. Create UserValidationService.ts
|
|
63
63
|
@provideSingleton()
|
|
64
64
|
export class UserValidationService {
|
|
@@ -96,11 +96,11 @@ export class UserController {
|
|
|
96
96
|
- Identify logical responsibilities within single class
|
|
97
97
|
|
|
98
98
|
2. **IDENTIFY** "child code" to extract:
|
|
99
|
-
- Validation logic
|
|
100
|
-
- Notification logic
|
|
101
|
-
- Data transformation
|
|
102
|
-
- External API calls
|
|
103
|
-
- Business rules
|
|
99
|
+
- Validation logic -> ValidationService
|
|
100
|
+
- Notification logic -> NotificationService
|
|
101
|
+
- Data transformation -> TransformerService
|
|
102
|
+
- External API calls -> ApiService
|
|
103
|
+
- Business rules -> RulesEngine
|
|
104
104
|
|
|
105
105
|
3. **CREATE** new service file(s):
|
|
106
106
|
- Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
|
|
@@ -175,7 +175,7 @@ function getWorkspaceRoot(context) {
|
|
|
175
175
|
}
|
|
176
176
|
catch (err) {
|
|
177
177
|
//const error = toError(err);
|
|
178
|
-
// Continue searching if JSON parse fails
|
|
178
|
+
void err; // Continue searching if JSON parse fails
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
dir = path.dirname(dir);
|
|
@@ -190,7 +190,7 @@ function ensureDocFile(docPath, content) {
|
|
|
190
190
|
}
|
|
191
191
|
catch (err) {
|
|
192
192
|
//const error = toError(err);
|
|
193
|
-
//
|
|
193
|
+
// err is used in console.warn below
|
|
194
194
|
console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
|
|
195
195
|
return false;
|
|
196
196
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"max-file-lines.js","sourceRoot":"","sources":["../../../../../../packages/tooling/dev-config/eslint-plugin/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';\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 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: any) {\n //const error = toError(err);\n // 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 try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n //const error = toError(err);\n // Graceful degradation: log warning but don't break lint\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);\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, 'tmp', 'webpieces', '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 tmp/webpieces/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 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/dev-config/eslint-plugin/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';\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 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: any) {\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 try {\n fs.mkdirSync(path.dirname(docPath), { recursive: true });\n fs.writeFileSync(docPath, content, 'utf-8');\n return true;\n } catch (err: any) {\n //const error = toError(err);\n // err is used in console.warn below\n console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);\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, 'tmp', 'webpieces', '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 tmp/webpieces/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 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"]}
|
|
@@ -32,12 +32,12 @@ Files should contain a SINGLE COHESIVE UNIT.
|
|
|
32
32
|
If the file contains multiple classes, **SEPARATE each class into its own file**.
|
|
33
33
|
|
|
34
34
|
\`\`\`typescript
|
|
35
|
-
//
|
|
35
|
+
// BAD: UserController.ts (multiple classes)
|
|
36
36
|
export class UserController { /* ... */ }
|
|
37
37
|
export class UserValidator { /* ... */ }
|
|
38
38
|
export class UserNotifier { /* ... */ }
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// GOOD: Three separate files
|
|
41
41
|
// UserController.ts
|
|
42
42
|
export class UserController { /* ... */ }
|
|
43
43
|
|
|
@@ -53,7 +53,7 @@ export class UserNotifier { /* ... */ }
|
|
|
53
53
|
#### Pattern: Create New Service Class with Dependency Injection
|
|
54
54
|
|
|
55
55
|
\`\`\`typescript
|
|
56
|
-
//
|
|
56
|
+
// BAD: UserController.ts (800 lines, single class)
|
|
57
57
|
@provideSingleton()
|
|
58
58
|
@Controller()
|
|
59
59
|
export class UserController {
|
|
@@ -63,7 +63,7 @@ export class UserController {
|
|
|
63
63
|
// 100 lines: analytics logic
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// GOOD: Extract validation service
|
|
67
67
|
// 1. Create UserValidationService.ts
|
|
68
68
|
@provideSingleton()
|
|
69
69
|
export class UserValidationService {
|
|
@@ -101,11 +101,11 @@ export class UserController {
|
|
|
101
101
|
- Identify logical responsibilities within single class
|
|
102
102
|
|
|
103
103
|
2. **IDENTIFY** "child code" to extract:
|
|
104
|
-
- Validation logic
|
|
105
|
-
- Notification logic
|
|
106
|
-
- Data transformation
|
|
107
|
-
- External API calls
|
|
108
|
-
- Business rules
|
|
104
|
+
- Validation logic -> ValidationService
|
|
105
|
+
- Notification logic -> NotificationService
|
|
106
|
+
- Data transformation -> TransformerService
|
|
107
|
+
- External API calls -> ApiService
|
|
108
|
+
- Business rules -> RulesEngine
|
|
109
109
|
|
|
110
110
|
3. **CREATE** new service file(s):
|
|
111
111
|
- Start with temporary name: \`XXXX.ts\` or \`ChildService.ts\`
|
|
@@ -182,7 +182,7 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
182
182
|
}
|
|
183
183
|
} catch (err: any) {
|
|
184
184
|
//const error = toError(err);
|
|
185
|
-
// Continue searching if JSON parse fails
|
|
185
|
+
void err; // Continue searching if JSON parse fails
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
dir = path.dirname(dir);
|
|
@@ -197,7 +197,7 @@ function ensureDocFile(docPath: string, content: string): boolean {
|
|
|
197
197
|
return true;
|
|
198
198
|
} catch (err: any) {
|
|
199
199
|
//const error = toError(err);
|
|
200
|
-
//
|
|
200
|
+
// err is used in console.warn below
|
|
201
201
|
console.warn(`[webpieces] Could not create doc file: ${docPath}`, err);
|
|
202
202
|
return false;
|
|
203
203
|
}
|