@webpieces/eslint-rules 0.0.1 → 0.2.114
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 +3 -2
- package/src/index.d.ts +29 -0
- package/src/index.js +39 -0
- package/src/index.js.map +1 -0
- package/src/rules/catch-error-pattern.d.ts +11 -0
- package/src/rules/{catch-error-pattern.ts → catch-error-pattern.js} +30 -142
- package/src/rules/catch-error-pattern.js.map +1 -0
- package/src/rules/enforce-architecture.d.ts +15 -0
- package/src/rules/{enforce-architecture.ts → enforce-architecture.js} +61 -128
- package/src/rules/enforce-architecture.js.map +1 -0
- package/src/rules/max-file-lines.d.ts +12 -0
- package/src/rules/{max-file-lines.ts → max-file-lines.js} +22 -37
- package/src/rules/max-file-lines.js.map +1 -0
- package/src/rules/max-method-lines.d.ts +12 -0
- package/src/rules/{max-method-lines.ts → max-method-lines.js} +31 -81
- package/src/rules/max-method-lines.js.map +1 -0
- package/src/rules/no-json-property-primitive-type.d.ts +17 -0
- package/src/rules/no-json-property-primitive-type.js +57 -0
- package/src/rules/no-json-property-primitive-type.js.map +1 -0
- package/src/rules/no-mat-cell-def.d.ts +15 -0
- package/src/rules/{no-mat-cell-def.ts → no-mat-cell-def.js} +8 -21
- package/src/rules/no-mat-cell-def.js.map +1 -0
- package/src/rules/no-unmanaged-exceptions.d.ts +22 -0
- package/src/rules/{no-unmanaged-exceptions.ts → no-unmanaged-exceptions.js} +27 -52
- package/src/rules/no-unmanaged-exceptions.js.map +1 -0
- package/src/rules/require-typed-template.d.ts +17 -0
- package/src/rules/{require-typed-template.ts → require-typed-template.js} +11 -31
- package/src/rules/require-typed-template.js.map +1 -0
- package/src/toError.d.ts +5 -0
- package/src/{toError.ts → toError.js} +7 -6
- package/src/toError.js.map +1 -0
- package/.webpieces/instruct-ai/webpieces.exceptions.md +0 -5
- package/.webpieces/instruct-ai/webpieces.filesize.md +0 -146
- package/.webpieces/instruct-ai/webpieces.methods.md +0 -97
- package/LICENSE +0 -373
- package/jest.config.ts +0 -16
- package/project.json +0 -22
- package/src/__tests__/catch-error-pattern.test.ts +0 -374
- package/src/__tests__/max-file-lines.test.ts +0 -207
- package/src/__tests__/max-method-lines.test.ts +0 -258
- package/src/__tests__/no-unmanaged-exceptions.test.ts +0 -359
- package/src/index.ts +0 -38
- package/src/rules/no-json-property-primitive-type.ts +0 -85
- package/tmp/webpieces/webpieces.exceptions.md +0 -5
- package/tsconfig.json +0 -22
- package/tsconfig.lib.json +0 -10
- package/tsconfig.spec.json +0 -14
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* ESLint rule to enforce architecture boundaries
|
|
3
4
|
*
|
|
@@ -10,12 +11,10 @@
|
|
|
10
11
|
* Configuration:
|
|
11
12
|
* '@webpieces/enforce-architecture': 'error'
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import { toError } from '../toError';
|
|
18
|
-
|
|
14
|
+
const tslib_1 = require("tslib");
|
|
15
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
16
|
+
const path = tslib_1.__importStar(require("path"));
|
|
17
|
+
const toError_1 = require("../toError");
|
|
19
18
|
const DEPENDENCIES_DOC_CONTENT = `# Instructions: Architecture Dependency Violation
|
|
20
19
|
|
|
21
20
|
IN GENERAL, it is better to avoid these changes and find a different way by moving classes
|
|
@@ -153,70 +152,47 @@ Instead of importing, receive the dependency as a constructor or method paramete
|
|
|
153
152
|
- The best dependency is the one you don't need
|
|
154
153
|
- When in doubt, refactor rather than add dependencies
|
|
155
154
|
`;
|
|
156
|
-
|
|
157
155
|
// Module-level flag to prevent redundant file creation
|
|
158
156
|
let dependenciesDocCreated = false;
|
|
159
|
-
|
|
160
157
|
/**
|
|
161
158
|
* Ensure a documentation file exists at the given path.
|
|
162
159
|
*/
|
|
163
|
-
function ensureDocFile(docPath
|
|
160
|
+
function ensureDocFile(docPath, content) {
|
|
164
161
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
165
162
|
try {
|
|
166
163
|
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
167
164
|
fs.writeFileSync(docPath, content, 'utf-8');
|
|
168
165
|
return true;
|
|
169
|
-
}
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
170
168
|
//const error = toError(err);
|
|
171
169
|
void err;
|
|
172
170
|
console.warn(`[webpieces] Could not create doc file: ${docPath}`);
|
|
173
171
|
return false;
|
|
174
172
|
}
|
|
175
173
|
}
|
|
176
|
-
|
|
177
174
|
/**
|
|
178
175
|
* Ensure the dependencies documentation file exists.
|
|
179
176
|
* Called when an architecture violation is detected.
|
|
180
177
|
*/
|
|
181
|
-
function ensureDependenciesDoc(workspaceRoot
|
|
182
|
-
if (dependenciesDocCreated)
|
|
178
|
+
function ensureDependenciesDoc(workspaceRoot) {
|
|
179
|
+
if (dependenciesDocCreated)
|
|
180
|
+
return;
|
|
183
181
|
const docPath = path.join(workspaceRoot, 'tmp', 'webpieces', 'webpieces.dependencies.md');
|
|
184
182
|
if (ensureDocFile(docPath, DEPENDENCIES_DOC_CONTENT)) {
|
|
185
183
|
dependenciesDocCreated = true;
|
|
186
184
|
}
|
|
187
185
|
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Graph entry format from .graphs/dependencies.json
|
|
191
|
-
*/
|
|
192
|
-
interface GraphEntry {
|
|
193
|
-
level: number;
|
|
194
|
-
dependsOn: string[];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
type EnhancedGraph = Record<string, GraphEntry>;
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Project mapping entry
|
|
201
|
-
*/
|
|
202
|
-
interface ProjectMapping {
|
|
203
|
-
root: string;
|
|
204
|
-
name: string;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
186
|
// Cache for blessed graph (loaded once per lint run)
|
|
208
|
-
let cachedGraph
|
|
209
|
-
let cachedGraphPath
|
|
210
|
-
|
|
187
|
+
let cachedGraph = null;
|
|
188
|
+
let cachedGraphPath = null;
|
|
211
189
|
// Cache for project mappings
|
|
212
|
-
let cachedProjectMappings
|
|
213
|
-
|
|
190
|
+
let cachedProjectMappings = null;
|
|
214
191
|
/**
|
|
215
192
|
* Find workspace root by walking up from file location
|
|
216
193
|
*/
|
|
217
|
-
function findWorkspaceRoot(startPath
|
|
194
|
+
function findWorkspaceRoot(startPath) {
|
|
218
195
|
let currentDir = path.dirname(startPath);
|
|
219
|
-
|
|
220
196
|
for (let i = 0; i < 20; i++) {
|
|
221
197
|
const packagePath = path.join(currentDir, 'package.json');
|
|
222
198
|
if (fs.existsSync(packagePath)) {
|
|
@@ -226,56 +202,51 @@ function findWorkspaceRoot(startPath: string): string {
|
|
|
226
202
|
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
227
203
|
return currentDir;
|
|
228
204
|
}
|
|
229
|
-
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
230
207
|
//const error = toError(err);
|
|
231
208
|
void err;
|
|
232
209
|
}
|
|
233
210
|
}
|
|
234
|
-
|
|
235
211
|
const parent = path.dirname(currentDir);
|
|
236
|
-
if (parent === currentDir)
|
|
212
|
+
if (parent === currentDir)
|
|
213
|
+
break;
|
|
237
214
|
currentDir = parent;
|
|
238
215
|
}
|
|
239
|
-
|
|
240
216
|
return process.cwd();
|
|
241
217
|
}
|
|
242
|
-
|
|
243
218
|
/**
|
|
244
219
|
* Load blessed graph from architecture/dependencies.json
|
|
245
220
|
*/
|
|
246
|
-
function loadBlessedGraph(workspaceRoot
|
|
221
|
+
function loadBlessedGraph(workspaceRoot) {
|
|
247
222
|
const graphPath = path.join(workspaceRoot, 'architecture', 'dependencies.json');
|
|
248
|
-
|
|
249
223
|
// Return cached if same path
|
|
250
224
|
if (cachedGraphPath === graphPath && cachedGraph !== null) {
|
|
251
225
|
return cachedGraph;
|
|
252
226
|
}
|
|
253
|
-
|
|
254
227
|
if (!fs.existsSync(graphPath)) {
|
|
255
228
|
return null;
|
|
256
229
|
}
|
|
257
|
-
|
|
258
230
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
259
231
|
try {
|
|
260
232
|
const content = fs.readFileSync(graphPath, 'utf-8');
|
|
261
|
-
cachedGraph = JSON.parse(content)
|
|
233
|
+
cachedGraph = JSON.parse(content);
|
|
262
234
|
cachedGraphPath = graphPath;
|
|
263
235
|
return cachedGraph;
|
|
264
|
-
}
|
|
265
|
-
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
const error = (0, toError_1.toError)(err);
|
|
266
239
|
console.error(`[ESLint @webpieces/enforce-architecture] Could not load graph: ${error.message}`);
|
|
267
240
|
return null;
|
|
268
241
|
}
|
|
269
242
|
}
|
|
270
|
-
|
|
271
243
|
/**
|
|
272
244
|
* Build set of all workspace package names (from package.json files)
|
|
273
245
|
* Used to detect workspace imports (works for any scope or unscoped)
|
|
274
246
|
*/
|
|
275
|
-
function buildWorkspacePackageNames(workspaceRoot
|
|
276
|
-
const packageNames = new Set
|
|
247
|
+
function buildWorkspacePackageNames(workspaceRoot) {
|
|
248
|
+
const packageNames = new Set();
|
|
277
249
|
const mappings = buildProjectMappings(workspaceRoot);
|
|
278
|
-
|
|
279
250
|
for (const mapping of mappings) {
|
|
280
251
|
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
281
252
|
if (fs.existsSync(pkgJsonPath)) {
|
|
@@ -285,32 +256,29 @@ function buildWorkspacePackageNames(workspaceRoot: string): Set<string> {
|
|
|
285
256
|
if (pkgJson.name) {
|
|
286
257
|
packageNames.add(pkgJson.name);
|
|
287
258
|
}
|
|
288
|
-
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
289
261
|
//const error = toError(err);
|
|
290
262
|
void err; // Ignore parse errors
|
|
291
263
|
}
|
|
292
264
|
}
|
|
293
265
|
}
|
|
294
|
-
|
|
295
266
|
return packageNames;
|
|
296
267
|
}
|
|
297
|
-
|
|
298
268
|
/**
|
|
299
269
|
* Check if an import path is a workspace project
|
|
300
270
|
* Works for scoped (@scope/name) or unscoped (name) packages
|
|
301
271
|
*/
|
|
302
|
-
function isWorkspaceImport(importPath
|
|
272
|
+
function isWorkspaceImport(importPath, workspaceRoot) {
|
|
303
273
|
const workspacePackages = buildWorkspacePackageNames(workspaceRoot);
|
|
304
274
|
return workspacePackages.has(importPath);
|
|
305
275
|
}
|
|
306
|
-
|
|
307
276
|
/**
|
|
308
277
|
* Get project name from package name
|
|
309
278
|
* e.g., '@webpieces/client' → 'client', 'apis' → 'apis'
|
|
310
279
|
*/
|
|
311
|
-
function getProjectNameFromPackageName(packageName
|
|
280
|
+
function getProjectNameFromPackageName(packageName, workspaceRoot) {
|
|
312
281
|
const mappings = buildProjectMappings(workspaceRoot);
|
|
313
|
-
|
|
314
282
|
// Try to find by reading package.json files
|
|
315
283
|
for (const mapping of mappings) {
|
|
316
284
|
const pkgJsonPath = path.join(workspaceRoot, mapping.root, 'package.json');
|
|
@@ -321,59 +289,46 @@ function getProjectNameFromPackageName(packageName: string, workspaceRoot: strin
|
|
|
321
289
|
if (pkgJson.name === packageName) {
|
|
322
290
|
return mapping.name; // Return project name
|
|
323
291
|
}
|
|
324
|
-
}
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
325
294
|
//const error = toError(err);
|
|
326
295
|
void err; // Ignore parse errors
|
|
327
296
|
}
|
|
328
297
|
}
|
|
329
298
|
}
|
|
330
|
-
|
|
331
299
|
// Fallback: return package name as-is (might be unscoped project name)
|
|
332
300
|
return packageName;
|
|
333
301
|
}
|
|
334
|
-
|
|
335
302
|
/**
|
|
336
303
|
* Build project mappings from project.json files in workspace
|
|
337
304
|
*/
|
|
338
|
-
function buildProjectMappings(workspaceRoot
|
|
305
|
+
function buildProjectMappings(workspaceRoot) {
|
|
339
306
|
if (cachedProjectMappings !== null) {
|
|
340
307
|
return cachedProjectMappings;
|
|
341
308
|
}
|
|
342
|
-
|
|
343
|
-
const mappings: ProjectMapping[] = [];
|
|
344
|
-
|
|
309
|
+
const mappings = [];
|
|
345
310
|
// Scan common locations for project.json files
|
|
346
311
|
const searchDirs = ['packages', 'apps', 'libs', 'libraries', 'services'];
|
|
347
|
-
|
|
348
312
|
for (const searchDir of searchDirs) {
|
|
349
313
|
const searchPath = path.join(workspaceRoot, searchDir);
|
|
350
|
-
if (!fs.existsSync(searchPath))
|
|
351
|
-
|
|
314
|
+
if (!fs.existsSync(searchPath))
|
|
315
|
+
continue;
|
|
352
316
|
scanForProjects(searchPath, workspaceRoot, mappings);
|
|
353
317
|
}
|
|
354
|
-
|
|
355
318
|
// Sort by path length (longest first) for more specific matching
|
|
356
319
|
mappings.sort((a, b) => b.root.length - a.root.length);
|
|
357
|
-
|
|
358
320
|
cachedProjectMappings = mappings;
|
|
359
321
|
return mappings;
|
|
360
322
|
}
|
|
361
|
-
|
|
362
323
|
/**
|
|
363
324
|
* Recursively scan for project.json files
|
|
364
325
|
*/
|
|
365
|
-
function scanForProjects(
|
|
366
|
-
dir: string,
|
|
367
|
-
workspaceRoot: string,
|
|
368
|
-
mappings: ProjectMapping[]
|
|
369
|
-
): void {
|
|
326
|
+
function scanForProjects(dir, workspaceRoot, mappings) {
|
|
370
327
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
371
328
|
try {
|
|
372
329
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
373
|
-
|
|
374
330
|
for (const entry of entries) {
|
|
375
331
|
const fullPath = path.join(dir, entry.name);
|
|
376
|
-
|
|
377
332
|
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
378
333
|
// Check for project.json in this directory
|
|
379
334
|
const projectJsonPath = path.join(fullPath, 'project.json');
|
|
@@ -382,80 +337,72 @@ function scanForProjects(
|
|
|
382
337
|
try {
|
|
383
338
|
const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
|
|
384
339
|
const projectRoot = path.relative(workspaceRoot, fullPath);
|
|
385
|
-
|
|
386
340
|
// Use project name from project.json as-is (no scope forcing)
|
|
387
341
|
const projectName = projectJson.name || entry.name;
|
|
388
|
-
|
|
389
342
|
mappings.push({
|
|
390
343
|
root: projectRoot,
|
|
391
344
|
name: projectName,
|
|
392
345
|
});
|
|
393
|
-
}
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
394
348
|
//const error = toError(err);
|
|
395
349
|
void err;
|
|
396
350
|
}
|
|
397
351
|
}
|
|
398
|
-
|
|
399
352
|
// Continue scanning subdirectories
|
|
400
353
|
scanForProjects(fullPath, workspaceRoot, mappings);
|
|
401
354
|
}
|
|
402
355
|
}
|
|
403
|
-
}
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
404
358
|
//const error = toError(err);
|
|
405
359
|
void err;
|
|
406
360
|
}
|
|
407
361
|
}
|
|
408
|
-
|
|
409
362
|
/**
|
|
410
363
|
* Get project name from file path
|
|
411
364
|
*/
|
|
412
|
-
function getProjectFromFile(filePath
|
|
365
|
+
function getProjectFromFile(filePath, workspaceRoot) {
|
|
413
366
|
const relativePath = path.relative(workspaceRoot, filePath).replace(/\\/g, '/');
|
|
414
367
|
const mappings = buildProjectMappings(workspaceRoot);
|
|
415
|
-
|
|
416
368
|
for (const mapping of mappings) {
|
|
417
369
|
if (relativePath.startsWith(mapping.root + '/') || relativePath.startsWith(mapping.root)) {
|
|
418
370
|
return mapping.name;
|
|
419
371
|
}
|
|
420
372
|
}
|
|
421
|
-
|
|
422
373
|
return null;
|
|
423
374
|
}
|
|
424
|
-
|
|
425
375
|
/**
|
|
426
376
|
* Compute all transitive dependencies for a project
|
|
427
377
|
*/
|
|
428
|
-
function computeTransitiveDependencies(project
|
|
429
|
-
const result = new Set
|
|
430
|
-
const visited = new Set
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
378
|
+
function computeTransitiveDependencies(project, graph) {
|
|
379
|
+
const result = new Set();
|
|
380
|
+
const visited = new Set();
|
|
381
|
+
function visit(currentProject) {
|
|
382
|
+
if (visited.has(currentProject))
|
|
383
|
+
return;
|
|
434
384
|
visited.add(currentProject);
|
|
435
|
-
|
|
436
385
|
const entry = graph[currentProject];
|
|
437
|
-
if (!entry || !entry.dependsOn)
|
|
438
|
-
|
|
386
|
+
if (!entry || !entry.dependsOn)
|
|
387
|
+
return;
|
|
439
388
|
for (const dep of entry.dependsOn) {
|
|
440
389
|
result.add(dep);
|
|
441
390
|
visit(dep);
|
|
442
391
|
}
|
|
443
392
|
}
|
|
444
|
-
|
|
445
393
|
visit(project);
|
|
446
394
|
return result;
|
|
447
395
|
}
|
|
448
|
-
|
|
449
|
-
function buildAllowedDepsList(allowedDeps: Set<string>, graph: EnhancedGraph): string {
|
|
396
|
+
function buildAllowedDepsList(allowedDeps, graph) {
|
|
450
397
|
const sorted = Array.from(allowedDeps).sort((a, b) => {
|
|
451
398
|
const diff = (graph[b]?.level ?? 0) - (graph[a]?.level ?? 0);
|
|
452
399
|
return diff !== 0 ? diff : a.localeCompare(b);
|
|
453
400
|
});
|
|
454
|
-
if (sorted.length === 0)
|
|
401
|
+
if (sorted.length === 0)
|
|
402
|
+
return ' (none - this is a foundation project)';
|
|
455
403
|
return sorted.map((dep) => ` - ${dep} (level ${graph[dep]?.level ?? '?'})`).join('\n');
|
|
456
404
|
}
|
|
457
|
-
|
|
458
|
-
const rule: Rule.RuleModule = {
|
|
405
|
+
const rule = {
|
|
459
406
|
meta: {
|
|
460
407
|
type: 'problem',
|
|
461
408
|
docs: {
|
|
@@ -465,72 +412,58 @@ const rule: Rule.RuleModule = {
|
|
|
465
412
|
url: 'https://github.com/deanhiller/webpieces-ts',
|
|
466
413
|
},
|
|
467
414
|
messages: {
|
|
468
|
-
illegalImport:
|
|
469
|
-
"⚠️ AI Agent: READ .webpieces/instruct-ai/webpieces.dependencies.md for resolution steps!\n\n" +
|
|
415
|
+
illegalImport: "⚠️ AI Agent: READ .webpieces/instruct-ai/webpieces.dependencies.md for resolution steps!\n\n" +
|
|
470
416
|
"Import '{{imported}}' violates architecture boundaries.\n\n" +
|
|
471
417
|
"Project '{{project}}' (level {{level}}) can only import from:\n" +
|
|
472
418
|
'{{allowedList}}',
|
|
473
|
-
noGraph:
|
|
474
|
-
'No architecture graph found at architecture/dependencies.json\n' +
|
|
419
|
+
noGraph: 'No architecture graph found at architecture/dependencies.json\n' +
|
|
475
420
|
'Run: nx run architecture:validate --mode=update',
|
|
476
421
|
},
|
|
477
422
|
schema: [],
|
|
478
423
|
},
|
|
479
|
-
|
|
480
424
|
// webpieces-disable max-lines-new-methods -- ESLint rule create method with AST validation
|
|
481
|
-
create(context
|
|
425
|
+
create(context) {
|
|
482
426
|
const filename = context.filename || context.getFilename();
|
|
483
427
|
const workspaceRoot = findWorkspaceRoot(filename);
|
|
484
|
-
|
|
485
428
|
return {
|
|
486
429
|
// webpieces-disable no-any-unknown -- ESLint visitor callback receives untyped AST node
|
|
487
|
-
ImportDeclaration(node
|
|
488
|
-
const importPath = node.source.value
|
|
489
|
-
|
|
430
|
+
ImportDeclaration(node) {
|
|
431
|
+
const importPath = node.source.value;
|
|
490
432
|
// Check if this is a workspace import (works for any scope or unscoped)
|
|
491
433
|
if (!isWorkspaceImport(importPath, workspaceRoot)) {
|
|
492
434
|
return; // Not a workspace import, skip validation
|
|
493
435
|
}
|
|
494
|
-
|
|
495
436
|
// Determine which project this file belongs to
|
|
496
437
|
const sourceProject = getProjectFromFile(filename, workspaceRoot);
|
|
497
438
|
if (!sourceProject) {
|
|
498
439
|
// File not in any known project (e.g., tools/, scripts/)
|
|
499
440
|
return;
|
|
500
441
|
}
|
|
501
|
-
|
|
502
442
|
// Convert import (package name) to project name
|
|
503
443
|
const targetProject = getProjectNameFromPackageName(importPath, workspaceRoot);
|
|
504
|
-
|
|
505
444
|
// Self-import is always allowed
|
|
506
445
|
if (targetProject === sourceProject) {
|
|
507
446
|
return;
|
|
508
447
|
}
|
|
509
|
-
|
|
510
448
|
// Load blessed graph
|
|
511
449
|
const graph = loadBlessedGraph(workspaceRoot);
|
|
512
450
|
if (!graph) {
|
|
513
451
|
// No graph file - warn but don't fail (allows gradual adoption)
|
|
514
452
|
return;
|
|
515
453
|
}
|
|
516
|
-
|
|
517
454
|
// Get project entry
|
|
518
455
|
const projectEntry = graph[sourceProject];
|
|
519
456
|
if (!projectEntry) {
|
|
520
457
|
// Project not in graph (new project?) - allow
|
|
521
458
|
return;
|
|
522
459
|
}
|
|
523
|
-
|
|
524
460
|
// Compute allowed dependencies (direct + transitive)
|
|
525
461
|
const allowedDeps = computeTransitiveDependencies(sourceProject, graph);
|
|
526
|
-
|
|
527
462
|
// Check if import is allowed (use project name, not package name)
|
|
528
463
|
if (!allowedDeps.has(targetProject)) {
|
|
529
464
|
// Write documentation file for AI/developer to read
|
|
530
465
|
ensureDependenciesDoc(workspaceRoot);
|
|
531
|
-
|
|
532
466
|
const allowedList = buildAllowedDepsList(allowedDeps, graph);
|
|
533
|
-
|
|
534
467
|
context.report({
|
|
535
468
|
node: node.source,
|
|
536
469
|
messageId: 'illegalImport',
|
|
@@ -546,5 +479,5 @@ const rule: Rule.RuleModule = {
|
|
|
546
479
|
};
|
|
547
480
|
},
|
|
548
481
|
};
|
|
549
|
-
|
|
550
|
-
|
|
482
|
+
module.exports = rule;
|
|
483
|
+
//# sourceMappingURL=enforce-architecture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforce-architecture.js","sourceRoot":"","sources":["../../../../../../packages/tooling/eslint-rules/src/rules/enforce-architecture.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAGH,+CAAyB;AACzB,mDAA6B;AAC7B,wCAAqC;AAErC,MAAM,wBAAwB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwIhC,CAAC;AAEF,uDAAuD;AACvD,IAAI,sBAAsB,GAAG,KAAK,CAAC;AAEnC;;GAEG;AACH,SAAS,aAAa,CAAC,OAAe,EAAE,OAAe;IACnD,8DAA8D;IAC9D,IAAI,CAAC;QACD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5C,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC;QACT,OAAO,CAAC,IAAI,CAAC,0CAA0C,OAAO,EAAE,CAAC,CAAC;QAClE,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,qBAAqB,CAAC,aAAqB;IAChD,IAAI,sBAAsB;QAAE,OAAO;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,WAAW,EAAE,2BAA2B,CAAC,CAAC;IAC1F,IAAI,aAAa,CAAC,OAAO,EAAE,wBAAwB,CAAC,EAAE,CAAC;QACnD,sBAAsB,GAAG,IAAI,CAAC;IAClC,CAAC;AACL,CAAC;AAoBD,qDAAqD;AACrD,IAAI,WAAW,GAAyB,IAAI,CAAC;AAC7C,IAAI,eAAe,GAAkB,IAAI,CAAC;AAE1C,6BAA6B;AAC7B,IAAI,qBAAqB,GAA4B,IAAI,CAAC;AAE1D;;GAEG;AACH,SAAS,iBAAiB,CAAC,SAAiB;IACxC,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;QAC1D,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7B,8DAA8D;YAC9D,IAAI,CAAC;gBACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC9D,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;oBAChD,OAAO,UAAU,CAAC;gBACtB,CAAC;YACL,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,6BAA6B;gBAC7B,KAAK,GAAG,CAAC;YACb,CAAC;QACL,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACxC,IAAI,MAAM,KAAK,UAAU;YAAE,MAAM;QACjC,UAAU,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;AACzB,CAAC;AAED;;GAEG;AACH,SAAS,gBAAgB,CAAC,aAAqB;IAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,cAAc,EAAE,mBAAmB,CAAC,CAAC;IAEhF,6BAA6B;IAC7B,IAAI,eAAe,KAAK,SAAS,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACxD,OAAO,WAAW,CAAC;IACvB,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACpD,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAkB,CAAC;QACnD,eAAe,GAAG,SAAS,CAAC;QAC5B,OAAO,WAAW,CAAC;IACvB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,kEAAkE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACjG,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,0BAA0B,CAAC,aAAqB;IACrD,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IACvC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAErD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QAC3E,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7B,8DAA8D;YAC9D,IAAI,CAAC;gBACD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;gBAClE,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;oBACf,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACnC,CAAC;YACL,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,6BAA6B;gBAC7B,KAAK,GAAG,CAAC,CAAC,sBAAsB;YACpC,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO,YAAY,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,UAAkB,EAAE,aAAqB;IAChE,MAAM,iBAAiB,GAAG,0BAA0B,CAAC,aAAa,CAAC,CAAC;IACpE,OAAO,iBAAiB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,SAAS,6BAA6B,CAAC,WAAmB,EAAE,aAAqB;IAC7E,MAAM,QAAQ,GAAG,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAErD,4CAA4C;IAC5C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QAC3E,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7B,8DAA8D;YAC9D,IAAI,CAAC;gBACD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;gBAClE,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;oBAC/B,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,sBAAsB;gBAC/C,CAAC;YACL,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,6BAA6B;gBAC7B,KAAK,GAAG,CAAC,CAAC,sBAAsB;YACpC,CAAC;QACL,CAAC;IACL,CAAC;IAED,uEAAuE;IACvE,OAAO,WAAW,CAAC;AACvB,CAAC;AAED;;GAEG;AACH,SAAS,oBAAoB,CAAC,aAAqB;IAC/C,IAAI,qBAAqB,KAAK,IAAI,EAAE,CAAC;QACjC,OAAO,qBAAqB,CAAC;IACjC,CAAC;IAED,MAAM,QAAQ,GAAqB,EAAE,CAAC;IAEtC,+CAA+C;IAC/C,MAAM,UAAU,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC;IAEzE,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACjC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QACvD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC;YAAE,SAAS;QAEzC,eAAe,CAAC,UAAU,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC;IACzD,CAAC;IAED,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEvD,qBAAqB,GAAG,QAAQ,CAAC;IACjC,OAAO,QAAQ,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CACpB,GAAW,EACX,aAAqB,EACrB,QAA0B;IAE1B,8DAA8D;IAC9D,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAE7D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAE5C,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;gBACtF,2CAA2C;gBAC3C,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;gBAC5D,IAAI,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;oBACjC,8DAA8D;oBAC9D,IAAI,CAAC;wBACD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC;wBAC1E,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;wBAE3D,8DAA8D;wBAC9D,MAAM,WAAW,GAAG,WAAW,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;wBAEnD,QAAQ,CAAC,IAAI,CAAC;4BACV,IAAI,EAAE,WAAW;4BACjB,IAAI,EAAE,WAAW;yBACpB,CAAC,CAAC;oBACP,CAAC;oBAAC,OAAO,GAAY,EAAE,CAAC;wBACpB,6BAA6B;wBAC7B,KAAK,GAAG,CAAC;oBACb,CAAC;gBACL,CAAC;gBAED,mCAAmC;gBACnC,eAAe,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC;YACvD,CAAC;QACL,CAAC;IACL,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,KAAK,GAAG,CAAC;IACb,CAAC;AACL,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,QAAgB,EAAE,aAAqB;IAC/D,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,oBAAoB,CAAC,aAAa,CAAC,CAAC;IAErD,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC7B,IAAI,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,YAAY,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACvF,OAAO,OAAO,CAAC,IAAI,CAAC;QACxB,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,6BAA6B,CAAC,OAAe,EAAE,KAAoB;IACxE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,CAAC;IACjC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,SAAS,KAAK,CAAC,cAAsB;QACjC,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;YAAE,OAAO;QACxC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAE5B,MAAM,KAAK,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,SAAS;YAAE,OAAO;QAEvC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAChB,KAAK,CAAC,GAAG,CAAC,CAAC;QACf,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,CAAC;IACf,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,oBAAoB,CAAC,WAAwB,EAAE,KAAoB;IACxE,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACjD,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC;QAC7D,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,yCAAyC,CAAC;IAC1E,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,WAAW,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,IAAI,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5F,CAAC;AAED,MAAM,IAAI,GAAoB;IAC1B,IAAI,EAAE;QACF,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACF,WAAW,EAAE,2CAA2C;YACxD,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,4CAA4C;SACpD;QACD,QAAQ,EAAE;YACN,aAAa,EACT,8FAA8F;gBAC9F,6DAA6D;gBAC7D,iEAAiE;gBACjE,iBAAiB;YACrB,OAAO,EACH,iEAAiE;gBACjE,iDAAiD;SACxD;QACD,MAAM,EAAE,EAAE;KACb;IAED,2FAA2F;IAC3F,MAAM,CAAC,OAAyB;QAC5B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QAC3D,MAAM,aAAa,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAElD,OAAO;YACH,wFAAwF;YACxF,iBAAiB,CAAC,IAAS;gBACvB,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,KAAe,CAAC;gBAE/C,wEAAwE;gBACxE,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,aAAa,CAAC,EAAE,CAAC;oBAChD,OAAO,CAAC,0CAA0C;gBACtD,CAAC;gBAED,+CAA+C;gBAC/C,MAAM,aAAa,GAAG,kBAAkB,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;gBAClE,IAAI,CAAC,aAAa,EAAE,CAAC;oBACjB,yDAAyD;oBACzD,OAAO;gBACX,CAAC;gBAED,gDAAgD;gBAChD,MAAM,aAAa,GAAG,6BAA6B,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;gBAE/E,gCAAgC;gBAChC,IAAI,aAAa,KAAK,aAAa,EAAE,CAAC;oBAClC,OAAO;gBACX,CAAC;gBAED,qBAAqB;gBACrB,MAAM,KAAK,GAAG,gBAAgB,CAAC,aAAa,CAAC,CAAC;gBAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;oBACT,gEAAgE;oBAChE,OAAO;gBACX,CAAC;gBAED,oBAAoB;gBACpB,MAAM,YAAY,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC;gBAC1C,IAAI,CAAC,YAAY,EAAE,CAAC;oBAChB,8CAA8C;oBAC9C,OAAO;gBACX,CAAC;gBAED,qDAAqD;gBACrD,MAAM,WAAW,GAAG,6BAA6B,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;gBAExE,kEAAkE;gBAClE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;oBAClC,oDAAoD;oBACpD,qBAAqB,CAAC,aAAa,CAAC,CAAC;oBAErC,MAAM,WAAW,GAAG,oBAAoB,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;oBAE7D,OAAO,CAAC,MAAM,CAAC;wBACX,IAAI,EAAE,IAAI,CAAC,MAAM;wBACjB,SAAS,EAAE,eAAe;wBAC1B,IAAI,EAAE;4BACF,QAAQ,EAAE,UAAU;4BACpB,OAAO,EAAE,aAAa;4BACtB,KAAK,EAAE,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC;4BACjC,WAAW,EAAE,WAAW;yBAC3B;qBACJ,CAAC,CAAC;gBACP,CAAC;YACL,CAAC;SACJ,CAAC;IACN,CAAC;CACJ,CAAC;AAEF,iBAAS,IAAI,CAAC","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"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to enforce maximum file length
|
|
3
|
+
*
|
|
4
|
+
* Enforces a configurable maximum line count for files.
|
|
5
|
+
* Default: 700 lines
|
|
6
|
+
*
|
|
7
|
+
* Configuration:
|
|
8
|
+
* '@webpieces/max-file-lines': ['error', { max: 700 }]
|
|
9
|
+
*/
|
|
10
|
+
import type { Rule } from 'eslint';
|
|
11
|
+
declare const rule: Rule.RuleModule;
|
|
12
|
+
export = rule;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* ESLint rule to enforce maximum file length
|
|
3
4
|
*
|
|
@@ -7,16 +8,10 @@
|
|
|
7
8
|
* Configuration:
|
|
8
9
|
* '@webpieces/max-file-lines': ['error', { max: 700 }]
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { toError } from '../toError';
|
|
15
|
-
|
|
16
|
-
interface FileLinesOptions {
|
|
17
|
-
max: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
11
|
+
const tslib_1 = require("tslib");
|
|
12
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
13
|
+
const path = tslib_1.__importStar(require("path"));
|
|
14
|
+
const toError_1 = require("../toError");
|
|
20
15
|
const FILE_DOC_CONTENT = `# AI Agent Instructions: File Too Long
|
|
21
16
|
|
|
22
17
|
**READ THIS FILE to fix files that are too long**
|
|
@@ -164,14 +159,11 @@ export class MyController {
|
|
|
164
159
|
|
|
165
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.
|
|
166
161
|
`;
|
|
167
|
-
|
|
168
162
|
// Module-level flag to prevent redundant file creation
|
|
169
163
|
let fileDocCreated = false;
|
|
170
|
-
|
|
171
|
-
function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
164
|
+
function getWorkspaceRoot(context) {
|
|
172
165
|
const filename = context.filename || context.getFilename();
|
|
173
166
|
let dir = path.dirname(filename);
|
|
174
|
-
|
|
175
167
|
// Walk up directory tree to find workspace root
|
|
176
168
|
while (dir !== path.dirname(dir)) {
|
|
177
169
|
const pkgPath = path.join(dir, 'package.json');
|
|
@@ -182,7 +174,8 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
182
174
|
if (pkg.workspaces || pkg.name === 'webpieces-ts') {
|
|
183
175
|
return dir;
|
|
184
176
|
}
|
|
185
|
-
}
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
186
179
|
//const error = toError(err);
|
|
187
180
|
void err; // Continue searching if JSON parse fails
|
|
188
181
|
}
|
|
@@ -191,32 +184,29 @@ function getWorkspaceRoot(context: Rule.RuleContext): string {
|
|
|
191
184
|
}
|
|
192
185
|
return process.cwd(); // Fallback
|
|
193
186
|
}
|
|
194
|
-
|
|
195
|
-
function ensureDocFile(docPath: string, content: string): boolean {
|
|
187
|
+
function ensureDocFile(docPath, content) {
|
|
196
188
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
197
189
|
try {
|
|
198
190
|
fs.mkdirSync(path.dirname(docPath), { recursive: true });
|
|
199
191
|
fs.writeFileSync(docPath, content, 'utf-8');
|
|
200
192
|
return true;
|
|
201
|
-
}
|
|
202
|
-
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
const error = (0, toError_1.toError)(err);
|
|
203
196
|
console.warn(`[webpieces] Could not create doc file: ${docPath}`, error);
|
|
204
197
|
return false;
|
|
205
198
|
}
|
|
206
199
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
200
|
+
function ensureFileDoc(context) {
|
|
201
|
+
if (fileDocCreated)
|
|
202
|
+
return; // Performance: only create once per lint run
|
|
211
203
|
const workspaceRoot = getWorkspaceRoot(context);
|
|
212
204
|
const docPath = path.join(workspaceRoot, '.webpieces', 'instruct-ai', 'webpieces.filesize.md');
|
|
213
|
-
|
|
214
205
|
if (ensureDocFile(docPath, FILE_DOC_CONTENT)) {
|
|
215
206
|
fileDocCreated = true;
|
|
216
207
|
}
|
|
217
208
|
}
|
|
218
|
-
|
|
219
|
-
const rule: Rule.RuleModule = {
|
|
209
|
+
const rule = {
|
|
220
210
|
meta: {
|
|
221
211
|
type: 'suggestion',
|
|
222
212
|
docs: {
|
|
@@ -226,8 +216,7 @@ const rule: Rule.RuleModule = {
|
|
|
226
216
|
url: 'https://github.com/deanhiller/webpieces-ts',
|
|
227
217
|
},
|
|
228
218
|
messages: {
|
|
229
|
-
tooLong:
|
|
230
|
-
'AI Agent: READ .webpieces/instruct-ai/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',
|
|
219
|
+
tooLong: 'AI Agent: READ .webpieces/instruct-ai/webpieces.filesize.md for fix instructions. File has {{actual}} lines (max: {{max}})',
|
|
231
220
|
},
|
|
232
221
|
fixable: undefined,
|
|
233
222
|
schema: [
|
|
@@ -243,20 +232,16 @@ const rule: Rule.RuleModule = {
|
|
|
243
232
|
},
|
|
244
233
|
],
|
|
245
234
|
},
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const options = context.options[0] as FileLinesOptions | undefined;
|
|
235
|
+
create(context) {
|
|
236
|
+
const options = context.options[0];
|
|
249
237
|
const maxLines = options?.max ?? 700;
|
|
250
|
-
|
|
251
238
|
return {
|
|
252
239
|
// webpieces-disable no-any-unknown -- ESTree AST nodes require any for dynamic properties
|
|
253
|
-
Program(node
|
|
240
|
+
Program(node) {
|
|
254
241
|
ensureFileDoc(context);
|
|
255
|
-
|
|
256
242
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
257
243
|
const lines = sourceCode.lines;
|
|
258
244
|
const lineCount = lines.length;
|
|
259
|
-
|
|
260
245
|
if (lineCount > maxLines) {
|
|
261
246
|
context.report({
|
|
262
247
|
node,
|
|
@@ -271,5 +256,5 @@ const rule: Rule.RuleModule = {
|
|
|
271
256
|
};
|
|
272
257
|
},
|
|
273
258
|
};
|
|
274
|
-
|
|
275
|
-
|
|
259
|
+
module.exports = rule;
|
|
260
|
+
//# sourceMappingURL=max-file-lines.js.map
|