@morriconeluca/eslint-plugin-faf 0.3.0 → 0.5.0
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/README.md +71 -22
- package/dist/main.cjs +835 -511
- package/dist/main.cjs.map +1 -1
- package/dist/main.d.cts +4 -0
- package/dist/main.d.ts +4 -0
- package/dist/main.js +835 -511
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// package.json
|
|
2
2
|
var package_default = {
|
|
3
3
|
name: "@morriconeluca/eslint-plugin-faf",
|
|
4
|
-
version: "0.
|
|
4
|
+
version: "0.5.0",
|
|
5
5
|
description: "An ESLint plugin for FAF projects.",
|
|
6
6
|
repository: {
|
|
7
7
|
type: "git",
|
|
@@ -89,6 +89,7 @@ import path3 from "path";
|
|
|
89
89
|
|
|
90
90
|
// src/_rules/_rules@shared/_utils/_utils@shared/_stores/cache/cache.store.ts
|
|
91
91
|
var state = {
|
|
92
|
+
ancestorChainCache: /* @__PURE__ */ new Map(),
|
|
92
93
|
categoryConfigCache: /* @__PURE__ */ new Map(),
|
|
93
94
|
classifyCache: /* @__PURE__ */ new Map(),
|
|
94
95
|
dirCache: {},
|
|
@@ -345,27 +346,139 @@ var rule = {
|
|
|
345
346
|
var category_mutually_exclusive_rule_default = rule;
|
|
346
347
|
|
|
347
348
|
// src/_rules/enforce-access-node/enforce-access-node.rule.ts
|
|
349
|
+
import path7 from "path";
|
|
350
|
+
|
|
351
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-project-root/get-project-root.util.ts
|
|
352
|
+
function getProjectRoot() {
|
|
353
|
+
return state.projectRoot;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/_rules/_rules@shared/_utils/_systems/walk-ancestors/walk-ancestors.util.ts
|
|
348
357
|
import path6 from "path";
|
|
358
|
+
function walkAncestors(relDir, config) {
|
|
359
|
+
const cached = state.ancestorChainCache.get(relDir);
|
|
360
|
+
if (cached !== void 0) {
|
|
361
|
+
return cached;
|
|
362
|
+
}
|
|
363
|
+
const result = [];
|
|
364
|
+
let currentDir = relDir;
|
|
365
|
+
while (currentDir && currentDir !== "." && currentDir !== "/") {
|
|
366
|
+
const isInTree = config.includes.some(
|
|
367
|
+
(inc) => currentDir === inc || currentDir.startsWith(inc + "/")
|
|
368
|
+
);
|
|
369
|
+
const isExcluded = config.excludes?.some(
|
|
370
|
+
(exc) => currentDir === exc || currentDir.startsWith(exc + "/")
|
|
371
|
+
) ?? false;
|
|
372
|
+
if (!isInTree || isExcluded) {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
const folderName = path6.basename(currentDir);
|
|
376
|
+
const type = classifyFolder(currentDir, config);
|
|
377
|
+
const parentDir = path6.dirname(currentDir).replace(/\\/g, "/");
|
|
378
|
+
const parentFolderName = path6.basename(parentDir);
|
|
379
|
+
const parentType = parentDir === "." || parentDir === "/" ? "unknown" : classifyFolder(parentDir, config);
|
|
380
|
+
result.push({
|
|
381
|
+
dir: currentDir,
|
|
382
|
+
folderName,
|
|
383
|
+
parentDir,
|
|
384
|
+
parentFolderName,
|
|
385
|
+
parentType,
|
|
386
|
+
type
|
|
387
|
+
});
|
|
388
|
+
currentDir = parentDir;
|
|
389
|
+
}
|
|
390
|
+
state.ancestorChainCache.set(relDir, result);
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/_rules/enforce-access-node/enforce-access-node.rule.ts
|
|
349
395
|
var rule2 = {
|
|
350
396
|
create(context) {
|
|
351
397
|
const absPath = context.filename;
|
|
352
398
|
const relPath = toRelativePath(absPath);
|
|
353
|
-
const
|
|
399
|
+
const settings = context.settings;
|
|
400
|
+
const listeners = {
|
|
401
|
+
Program(node) {
|
|
402
|
+
if (!settings.faf) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const config2 = findTreeConfig(relPath, settings.faf);
|
|
406
|
+
if (!config2) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const relDir2 = path7.dirname(relPath);
|
|
410
|
+
const folderName = path7.basename(relDir2);
|
|
411
|
+
const parentType2 = classifyFolder(relDir2, config2);
|
|
412
|
+
if (parentType2 === "foreign") {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const fileName2 = path7.basename(relPath);
|
|
416
|
+
for (const {
|
|
417
|
+
folderName: currentFolderName,
|
|
418
|
+
type: currentType
|
|
419
|
+
} of walkAncestors(relDir2, config2)) {
|
|
420
|
+
if (currentType === "invalid-fragment") {
|
|
421
|
+
context.report({
|
|
422
|
+
message: `Fragment directory "${currentFolderName}" is missing an Access Node (index.ts/index.js).`,
|
|
423
|
+
node
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (parentType2 === "layer" || parentType2 === "fractal-branch" || parentType2 === "invalid-fragment") {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const rfConfig = getRootFragmentConfig(relDir2, config2);
|
|
431
|
+
if (rfConfig) {
|
|
432
|
+
const flatRootNodes = rfConfig.rootNodes.flat();
|
|
433
|
+
const isMatchedRootNode = flatRootNodes.some((rn) => {
|
|
434
|
+
if (rn.includes("/")) {
|
|
435
|
+
const rfPath = rfConfig.paths[0] ?? "";
|
|
436
|
+
const absRn = path7.resolve(getProjectRoot(), rfPath, rn);
|
|
437
|
+
return toRelativePath(absRn) === relPath;
|
|
438
|
+
}
|
|
439
|
+
return rn === fileName2;
|
|
440
|
+
});
|
|
441
|
+
if (isMatchedRootNode) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (fileName2 !== "index.ts" && fileName2 !== "index.js") {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (parentType2 !== "fragment") {
|
|
449
|
+
context.report({
|
|
450
|
+
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType2}).`,
|
|
451
|
+
node
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const contents = readDirCached(relDir2);
|
|
456
|
+
const hasFragmentNode = contents.files.some(
|
|
457
|
+
(f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
|
|
458
|
+
);
|
|
459
|
+
if (!hasFragmentNode) {
|
|
460
|
+
context.report({
|
|
461
|
+
message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
|
|
462
|
+
node
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
const fileName = path7.basename(relPath);
|
|
354
468
|
if (fileName !== "index.ts" && fileName !== "index.js") {
|
|
355
|
-
return
|
|
469
|
+
return listeners;
|
|
356
470
|
}
|
|
357
|
-
const settings = context.settings;
|
|
358
471
|
if (!settings.faf) {
|
|
359
|
-
return
|
|
472
|
+
return listeners;
|
|
360
473
|
}
|
|
361
474
|
const config = findTreeConfig(relPath, settings.faf);
|
|
362
475
|
if (!config) {
|
|
363
|
-
return
|
|
476
|
+
return listeners;
|
|
364
477
|
}
|
|
365
|
-
const relDir =
|
|
478
|
+
const relDir = path7.dirname(relPath);
|
|
366
479
|
const parentType = classifyFolder(relDir, config);
|
|
367
480
|
if (parentType !== "fragment") {
|
|
368
|
-
return
|
|
481
|
+
return listeners;
|
|
369
482
|
}
|
|
370
483
|
function checkSource(node, sourceVal) {
|
|
371
484
|
const isValidSibling = /^\.\/[^/]+$/.test(sourceVal);
|
|
@@ -376,11 +489,11 @@ var rule2 = {
|
|
|
376
489
|
});
|
|
377
490
|
return;
|
|
378
491
|
}
|
|
379
|
-
const importedFilename =
|
|
492
|
+
const importedFilename = path7.basename(sourceVal);
|
|
380
493
|
if (importedFilename === "index" || importedFilename === "index.ts" || importedFilename === "index.js" || importedFilename === "README.md" || importedFilename === "package.json") {
|
|
381
494
|
return;
|
|
382
495
|
}
|
|
383
|
-
const fragmentName =
|
|
496
|
+
const fragmentName = path7.basename(relDir);
|
|
384
497
|
if (!importedFilename.startsWith(fragmentName + ".")) {
|
|
385
498
|
context.report({
|
|
386
499
|
message: `Access Node (index file) can only re-export its own Fragment Nodes (names starting with "${fragmentName}."). Got "${importedFilename}".`,
|
|
@@ -388,23 +501,22 @@ var rule2 = {
|
|
|
388
501
|
});
|
|
389
502
|
}
|
|
390
503
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (node.source) {
|
|
397
|
-
checkSource(node, node.source.value);
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
ImportDeclaration(node) {
|
|
504
|
+
listeners.ExportAllDeclaration = (node) => {
|
|
505
|
+
checkSource(node, node.source.value);
|
|
506
|
+
};
|
|
507
|
+
listeners.ExportNamedDeclaration = (node) => {
|
|
508
|
+
if (node.source) {
|
|
401
509
|
checkSource(node, node.source.value);
|
|
402
510
|
}
|
|
403
511
|
};
|
|
512
|
+
listeners.ImportDeclaration = (node) => {
|
|
513
|
+
checkSource(node, node.source.value);
|
|
514
|
+
};
|
|
515
|
+
return listeners;
|
|
404
516
|
},
|
|
405
517
|
meta: {
|
|
406
518
|
docs: {
|
|
407
|
-
description: "Enforce Access Node
|
|
519
|
+
description: "Enforce Access Node integrity for Fragments"
|
|
408
520
|
},
|
|
409
521
|
schema: [],
|
|
410
522
|
type: "problem"
|
|
@@ -412,136 +524,410 @@ var rule2 = {
|
|
|
412
524
|
};
|
|
413
525
|
var enforce_access_node_rule_default = rule2;
|
|
414
526
|
|
|
415
|
-
// src/_rules/
|
|
416
|
-
import
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
527
|
+
// src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
|
|
528
|
+
import fs2 from "fs";
|
|
529
|
+
import path8 from "path";
|
|
530
|
+
function resolveImportPath(importPath, currentFileRelative, settings) {
|
|
531
|
+
const cacheKey = `${currentFileRelative}::${importPath}`;
|
|
532
|
+
const cached = state.resolvedImportPathCache.get(cacheKey);
|
|
533
|
+
if (cached !== void 0) {
|
|
534
|
+
return cached;
|
|
422
535
|
}
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
536
|
+
const result = _resolveImportPath(importPath, currentFileRelative, settings);
|
|
537
|
+
state.resolvedImportPathCache.set(cacheKey, result);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
function _resolveImportPath(importPath, currentFileRelative, settings) {
|
|
541
|
+
const pkgImports = loadPackageImports(state.projectRoot);
|
|
542
|
+
const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
|
|
543
|
+
let matchedAlias = null;
|
|
544
|
+
let longestMatch = 0;
|
|
545
|
+
for (const alias of Object.keys(aliases)) {
|
|
546
|
+
if (importPath === alias || importPath.startsWith(alias + "/")) {
|
|
547
|
+
if (alias.length > longestMatch) {
|
|
548
|
+
longestMatch = alias.length;
|
|
549
|
+
matchedAlias = alias;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
428
552
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
553
|
+
let relResolved;
|
|
554
|
+
if (matchedAlias) {
|
|
555
|
+
const target = aliases[matchedAlias];
|
|
556
|
+
if (target) {
|
|
557
|
+
const remainder = importPath.substring(matchedAlias.length);
|
|
558
|
+
const absTarget = path8.resolve(state.projectRoot, target + remainder);
|
|
559
|
+
relResolved = toRelativePath(absTarget);
|
|
560
|
+
} else {
|
|
561
|
+
return "";
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
565
|
+
const absCurrentDir = path8.dirname(
|
|
566
|
+
path8.resolve(state.projectRoot, currentFileRelative)
|
|
567
|
+
);
|
|
568
|
+
const absResolved = path8.resolve(absCurrentDir, importPath);
|
|
569
|
+
relResolved = toRelativePath(absResolved);
|
|
570
|
+
} else {
|
|
571
|
+
return "";
|
|
433
572
|
}
|
|
434
573
|
}
|
|
435
|
-
return
|
|
574
|
+
return finalizeResolvedPath(relResolved);
|
|
575
|
+
}
|
|
576
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
577
|
+
"cjs",
|
|
578
|
+
"cts",
|
|
579
|
+
"js",
|
|
580
|
+
"jsx",
|
|
581
|
+
"mjs",
|
|
582
|
+
"mts",
|
|
583
|
+
"ts",
|
|
584
|
+
"tsx"
|
|
585
|
+
]);
|
|
586
|
+
function finalizeResolvedPath(relResolved) {
|
|
587
|
+
const resolvedDir = path8.dirname(relResolved).replace(/\\/g, "/");
|
|
588
|
+
const resolvedBase = path8.basename(relResolved);
|
|
589
|
+
const dirContents = readDirCached(relResolved);
|
|
590
|
+
if (dirContents.files.includes("index.ts")) {
|
|
591
|
+
return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
|
|
592
|
+
}
|
|
593
|
+
if (dirContents.files.includes("index.js")) {
|
|
594
|
+
return (relResolved === "." ? "" : relResolved + "/") + "index.js";
|
|
595
|
+
}
|
|
596
|
+
const parentContents = readDirCached(resolvedDir);
|
|
597
|
+
const exactMatch = parentContents.files.find((f) => f === resolvedBase);
|
|
598
|
+
if (!exactMatch) {
|
|
599
|
+
const candidates = parentContents.files.filter(
|
|
600
|
+
(f) => f.startsWith(resolvedBase + ".")
|
|
601
|
+
);
|
|
602
|
+
const matchWithExt = candidates.find(
|
|
603
|
+
(f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
|
|
604
|
+
) ?? candidates[0];
|
|
605
|
+
if (matchWithExt) {
|
|
606
|
+
return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return relResolved;
|
|
610
|
+
}
|
|
611
|
+
function loadPackageImports(projectRootPath) {
|
|
612
|
+
if (state.packageImports !== null) {
|
|
613
|
+
return state.packageImports;
|
|
614
|
+
}
|
|
615
|
+
state.packageImports = {};
|
|
616
|
+
const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
|
|
617
|
+
if (isTest) {
|
|
618
|
+
return state.packageImports;
|
|
619
|
+
}
|
|
620
|
+
const pkgPath = path8.resolve(projectRootPath, "package.json");
|
|
621
|
+
if (fs2.existsSync(pkgPath)) {
|
|
622
|
+
try {
|
|
623
|
+
const pkgContent = fs2.readFileSync(pkgPath, "utf8");
|
|
624
|
+
const pkg = JSON.parse(pkgContent);
|
|
625
|
+
if (pkg.imports) {
|
|
626
|
+
for (const [key, val] of Object.entries(pkg.imports)) {
|
|
627
|
+
if (typeof val === "string") {
|
|
628
|
+
const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
|
|
629
|
+
let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
|
|
630
|
+
if (cleanVal.startsWith("./")) {
|
|
631
|
+
cleanVal = cleanVal.substring(2);
|
|
632
|
+
} else if (cleanVal.startsWith("/")) {
|
|
633
|
+
cleanVal = cleanVal.substring(1);
|
|
634
|
+
}
|
|
635
|
+
state.packageImports[cleanKey] = cleanVal;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return state.packageImports;
|
|
436
643
|
}
|
|
437
644
|
|
|
438
|
-
// src/_rules/_rules@shared/_utils/_primitives/
|
|
439
|
-
function
|
|
440
|
-
|
|
645
|
+
// src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
|
|
646
|
+
function findTreeConfigIncludingExcluded(relPath, settings) {
|
|
647
|
+
if (!settings || !settings.trees) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const cached = state.treeConfigIncludingExcludedCache.get(relPath);
|
|
651
|
+
if (cached !== void 0) {
|
|
652
|
+
return cached;
|
|
653
|
+
}
|
|
654
|
+
let result = null;
|
|
655
|
+
for (const tree of settings.trees) {
|
|
656
|
+
const isIncluded = tree.includes.some(
|
|
657
|
+
(inc) => relPath === inc || relPath.startsWith(inc + "/")
|
|
658
|
+
);
|
|
659
|
+
if (isIncluded) {
|
|
660
|
+
result = tree;
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
state.treeConfigIncludingExcludedCache.set(relPath, result);
|
|
665
|
+
return result;
|
|
441
666
|
}
|
|
442
667
|
|
|
443
|
-
// src/_rules/
|
|
444
|
-
var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
445
|
-
var DEFAULT_HTTP_METHODS = [
|
|
446
|
-
"connect",
|
|
447
|
-
"delete",
|
|
448
|
-
"get",
|
|
449
|
-
"head",
|
|
450
|
-
"options",
|
|
451
|
-
"patch",
|
|
452
|
-
"post",
|
|
453
|
-
"put",
|
|
454
|
-
"trace"
|
|
455
|
-
];
|
|
668
|
+
// src/_rules/foreign-domain-isolation/foreign-domain-isolation.rule.ts
|
|
456
669
|
var rule3 = {
|
|
457
670
|
create(context) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
671
|
+
const absPath = context.filename;
|
|
672
|
+
const relPath = toRelativePath(absPath);
|
|
673
|
+
const settings = context.settings;
|
|
674
|
+
if (!settings.faf) {
|
|
675
|
+
return {};
|
|
676
|
+
}
|
|
677
|
+
const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
|
|
678
|
+
if (!config) {
|
|
679
|
+
return {};
|
|
680
|
+
}
|
|
681
|
+
const isCurrentFileExcluded = config.excludes?.some(
|
|
682
|
+
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
683
|
+
) ?? false;
|
|
684
|
+
function checkImport(node, importValue) {
|
|
685
|
+
const resolvedRelPath = resolveImportPath(
|
|
686
|
+
importValue,
|
|
687
|
+
relPath,
|
|
688
|
+
settings.faf
|
|
689
|
+
);
|
|
690
|
+
if (!resolvedRelPath) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const isImportedFileExcluded = config?.excludes?.some(
|
|
694
|
+
(exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
|
|
695
|
+
) ?? false;
|
|
696
|
+
if (!isCurrentFileExcluded && isImportedFileExcluded) {
|
|
697
|
+
context.report({
|
|
698
|
+
message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
|
|
699
|
+
node
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (isCurrentFileExcluded && !isImportedFileExcluded) {
|
|
704
|
+
context.report({
|
|
705
|
+
message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
|
|
706
|
+
node
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
ExportAllDeclaration(node) {
|
|
712
|
+
checkImport(node, node.source.value);
|
|
713
|
+
},
|
|
714
|
+
ExportNamedDeclaration(node) {
|
|
715
|
+
if (node.source) {
|
|
716
|
+
checkImport(node, node.source.value);
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
ImportDeclaration(node) {
|
|
720
|
+
checkImport(node, node.source.value);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
},
|
|
724
|
+
meta: {
|
|
725
|
+
docs: {
|
|
726
|
+
description: "Enforce isolation between Foreign Domains and Logical Nodes"
|
|
727
|
+
},
|
|
728
|
+
schema: [],
|
|
729
|
+
type: "problem"
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var foreign_domain_isolation_rule_default = rule3;
|
|
733
|
+
|
|
734
|
+
// src/_rules/fragment-master-node/fragment-master-node.rule.ts
|
|
735
|
+
import path9 from "path";
|
|
736
|
+
|
|
737
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-file-role/get-file-role.util.ts
|
|
738
|
+
function getFileRole(fileName, config) {
|
|
739
|
+
if (fileName === "index.ts" || fileName === "index.js") {
|
|
740
|
+
return "index";
|
|
741
|
+
}
|
|
742
|
+
const parts = fileName.split(".");
|
|
743
|
+
let allRoles = state.rolesCache.get(config);
|
|
744
|
+
if (!allRoles) {
|
|
745
|
+
allRoles = new Set(config.roles.flat());
|
|
746
|
+
state.rolesCache.set(config, allRoles);
|
|
747
|
+
}
|
|
748
|
+
for (let i = parts.length - 2; i >= 1; i--) {
|
|
749
|
+
const part = parts[i];
|
|
750
|
+
if (part && allRoles.has(part)) {
|
|
751
|
+
return part;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/_rules/fragment-master-node/fragment-master-node.rule.ts
|
|
758
|
+
var DEFAULT_HTTP_METHODS = [
|
|
759
|
+
"connect",
|
|
760
|
+
"delete",
|
|
761
|
+
"get",
|
|
762
|
+
"head",
|
|
763
|
+
"options",
|
|
764
|
+
"patch",
|
|
765
|
+
"post",
|
|
766
|
+
"put",
|
|
767
|
+
"trace"
|
|
768
|
+
];
|
|
769
|
+
var rule4 = {
|
|
770
|
+
create(context) {
|
|
771
|
+
return {
|
|
772
|
+
Program(node) {
|
|
773
|
+
const absPath = context.filename;
|
|
774
|
+
const relPath = toRelativePath(absPath);
|
|
775
|
+
const settings = context.settings;
|
|
776
|
+
if (!settings.faf) {
|
|
468
777
|
return;
|
|
469
778
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
779
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
780
|
+
if (!config) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const relDir = path9.dirname(relPath);
|
|
784
|
+
const folderName = path9.basename(relDir);
|
|
472
785
|
const parentType = classifyFolder(relDir, config);
|
|
473
|
-
if (parentType
|
|
786
|
+
if (parentType !== "fragment") {
|
|
474
787
|
return;
|
|
475
788
|
}
|
|
476
|
-
const fileName =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
789
|
+
const fileName = path9.basename(relPath);
|
|
790
|
+
const rfConfig = getRootFragmentConfig(relDir, config);
|
|
791
|
+
if (rfConfig) {
|
|
792
|
+
const flatRootNodes = rfConfig.rootNodes.flat();
|
|
793
|
+
const isMatchedRootNode = flatRootNodes.some((rn) => {
|
|
794
|
+
if (rn.includes("/")) {
|
|
795
|
+
const rfPath = rfConfig.paths[0] ?? "";
|
|
796
|
+
const absRn = path9.resolve(getProjectRoot(), rfPath, rn);
|
|
797
|
+
return toRelativePath(absRn) === relPath;
|
|
798
|
+
}
|
|
799
|
+
return rn === fileName;
|
|
800
|
+
});
|
|
801
|
+
if (isMatchedRootNode) {
|
|
802
|
+
return;
|
|
487
803
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
804
|
+
}
|
|
805
|
+
if (fileName === "index.ts" || fileName === "index.js") {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const role = getFileRole(fileName, config);
|
|
812
|
+
const ext = path9.extname(fileName);
|
|
813
|
+
const allRoleNames = new Set(config.roles.flat());
|
|
814
|
+
const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
|
|
815
|
+
const matchedRoute = config.routeHierarchies?.find(
|
|
816
|
+
(rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
|
|
817
|
+
);
|
|
818
|
+
if (matchedRoute) {
|
|
819
|
+
const contents = readDirCached(relDir);
|
|
820
|
+
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
821
|
+
if (!fileRoles.includes(matchedRoute.role)) {
|
|
491
822
|
context.report({
|
|
492
|
-
message: `
|
|
823
|
+
message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
|
|
493
824
|
node
|
|
494
825
|
});
|
|
495
826
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
827
|
+
const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
|
|
828
|
+
const routeNameRegex = new RegExp(
|
|
829
|
+
`^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
|
|
830
|
+
);
|
|
831
|
+
if (!routeNameRegex.test(folderName)) {
|
|
832
|
+
context.report({
|
|
833
|
+
message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
|
|
834
|
+
node
|
|
835
|
+
});
|
|
505
836
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (!isKebab) {
|
|
519
|
-
context.report({
|
|
520
|
-
message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
|
|
521
|
-
node
|
|
522
|
-
});
|
|
523
|
-
}
|
|
837
|
+
}
|
|
838
|
+
let currentParent = path9.dirname(relDir).replace(/\\/g, "/");
|
|
839
|
+
let categoryConfig = null;
|
|
840
|
+
let categoryFolderName = "";
|
|
841
|
+
while (currentParent && currentParent !== "." && currentParent !== "/") {
|
|
842
|
+
const currentParentType = classifyFolder(currentParent, config);
|
|
843
|
+
if (currentParentType === "category") {
|
|
844
|
+
categoryConfig = getCategoryConfig(currentParent, config);
|
|
845
|
+
categoryFolderName = path9.basename(currentParent);
|
|
846
|
+
break;
|
|
847
|
+
} else if (currentParentType === "fragment" || currentParentType === "root-fragment" || currentParentType === "layer") {
|
|
848
|
+
break;
|
|
524
849
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
850
|
+
const nextParent = path9.dirname(currentParent).replace(/\\/g, "/");
|
|
851
|
+
if (nextParent === currentParent) break;
|
|
852
|
+
currentParent = nextParent;
|
|
853
|
+
}
|
|
854
|
+
if (categoryConfig && categoryConfig.role) {
|
|
855
|
+
const contents = readDirCached(relDir);
|
|
856
|
+
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
857
|
+
if (!fileRoles.includes(categoryConfig.role)) {
|
|
858
|
+
context.report({
|
|
859
|
+
message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
|
|
860
|
+
node
|
|
861
|
+
});
|
|
535
862
|
}
|
|
536
|
-
|
|
863
|
+
}
|
|
864
|
+
if (!fileName.startsWith(folderName + ".")) {
|
|
865
|
+
context.report({
|
|
866
|
+
message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
|
|
867
|
+
node
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
if (!role) {
|
|
871
|
+
context.report({
|
|
872
|
+
message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
|
|
873
|
+
node
|
|
874
|
+
});
|
|
875
|
+
} else if (roleSegments.length > 1) {
|
|
876
|
+
} else {
|
|
877
|
+
const contents = readDirCached(relDir);
|
|
878
|
+
const duplicate = contents.files.find(
|
|
879
|
+
(f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
|
|
880
|
+
);
|
|
881
|
+
if (duplicate) {
|
|
537
882
|
context.report({
|
|
538
|
-
message: `Fragment
|
|
883
|
+
message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
|
|
539
884
|
node
|
|
540
885
|
});
|
|
541
886
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
},
|
|
891
|
+
meta: {
|
|
892
|
+
docs: {
|
|
893
|
+
description: "Enforce Fragment Node identity and Master Node integrity in FAF Fragments"
|
|
894
|
+
},
|
|
895
|
+
schema: [],
|
|
896
|
+
type: "problem"
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var fragment_master_node_rule_default = rule4;
|
|
900
|
+
|
|
901
|
+
// src/_rules/logical-domain-placement/logical-domain-placement.rule.ts
|
|
902
|
+
import path10 from "path";
|
|
903
|
+
var rule5 = {
|
|
904
|
+
create(context) {
|
|
905
|
+
return {
|
|
906
|
+
Program(node) {
|
|
907
|
+
const absPath = context.filename;
|
|
908
|
+
const relPath = toRelativePath(absPath);
|
|
909
|
+
const settings = context.settings;
|
|
910
|
+
if (!settings.faf) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
914
|
+
if (!config) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const relDir = path10.dirname(relPath);
|
|
918
|
+
const folderName = path10.basename(relDir);
|
|
919
|
+
const parentType = classifyFolder(relDir, config);
|
|
920
|
+
if (parentType === "foreign") {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const fileName = path10.basename(relPath);
|
|
924
|
+
for (const {
|
|
925
|
+
folderName: currentFolderName,
|
|
926
|
+
parentDir: relParentDir,
|
|
927
|
+
parentFolderName,
|
|
928
|
+
parentType: relParentType,
|
|
929
|
+
type: currentType
|
|
930
|
+
} of walkAncestors(relDir, config)) {
|
|
545
931
|
if (currentType === "root-fragment") {
|
|
546
932
|
if (relParentType !== "root-fragment" && relParentType !== "unknown") {
|
|
547
933
|
context.report({
|
|
@@ -597,7 +983,6 @@ var rule3 = {
|
|
|
597
983
|
});
|
|
598
984
|
}
|
|
599
985
|
}
|
|
600
|
-
currentDir = path7.dirname(currentDir).replace(/\\/g, "/");
|
|
601
986
|
}
|
|
602
987
|
if (parentType === "layer") {
|
|
603
988
|
context.report({
|
|
@@ -611,9 +996,96 @@ var rule3 = {
|
|
|
611
996
|
message: `Fractal Branches cannot contain files directly. File "${fileName}" is placed directly inside Fractal Branch "${folderName}".`,
|
|
612
997
|
node
|
|
613
998
|
});
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
},
|
|
1003
|
+
meta: {
|
|
1004
|
+
docs: {
|
|
1005
|
+
description: "Enforce topological placement constraints between FAF Logical Domains"
|
|
1006
|
+
},
|
|
1007
|
+
schema: [],
|
|
1008
|
+
type: "problem"
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
var logical_domain_placement_rule_default = rule5;
|
|
1012
|
+
|
|
1013
|
+
// src/_rules/naming-conventions/naming-conventions.rule.ts
|
|
1014
|
+
import path11 from "path";
|
|
1015
|
+
var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
1016
|
+
var rule6 = {
|
|
1017
|
+
create(context) {
|
|
1018
|
+
return {
|
|
1019
|
+
Program(node) {
|
|
1020
|
+
const absPath = context.filename;
|
|
1021
|
+
const relPath = toRelativePath(absPath);
|
|
1022
|
+
const settings = context.settings;
|
|
1023
|
+
if (!settings.faf) {
|
|
614
1024
|
return;
|
|
615
1025
|
}
|
|
616
|
-
|
|
1026
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
1027
|
+
if (!config) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const relDir = path11.dirname(relPath);
|
|
1031
|
+
const parentType = classifyFolder(relDir, config);
|
|
1032
|
+
if (parentType === "foreign") {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const fileName = path11.basename(relPath);
|
|
1036
|
+
for (const {
|
|
1037
|
+
dir: currentDir,
|
|
1038
|
+
folderName: currentFolderName,
|
|
1039
|
+
type: currentType
|
|
1040
|
+
} of walkAncestors(relDir, config)) {
|
|
1041
|
+
if ((currentType === "layer" || currentType === "category") && !currentFolderName.startsWith("_")) {
|
|
1042
|
+
context.report({
|
|
1043
|
+
message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
|
|
1044
|
+
node
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
if (currentFolderName.startsWith("_") && !currentFolderName.includes("@shared")) {
|
|
1048
|
+
const contents = readDirCached(currentDir);
|
|
1049
|
+
const hasIndex = contents.files.includes("index.ts") || contents.files.includes("index.js");
|
|
1050
|
+
if (hasIndex) {
|
|
1051
|
+
context.report({
|
|
1052
|
+
message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
|
|
1053
|
+
node
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (currentFolderName !== "src") {
|
|
1058
|
+
let baseFolderName = currentFolderName.replace(/@shared$/, "");
|
|
1059
|
+
if (baseFolderName.startsWith("_")) {
|
|
1060
|
+
baseFolderName = baseFolderName.substring(1);
|
|
1061
|
+
}
|
|
1062
|
+
if (baseFolderName.startsWith("[") && baseFolderName.endsWith("]")) {
|
|
1063
|
+
baseFolderName = baseFolderName.substring(
|
|
1064
|
+
1,
|
|
1065
|
+
baseFolderName.length - 1
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const isKebab = KEBAB_CASE_REGEX.test(baseFolderName);
|
|
1069
|
+
if (!isKebab) {
|
|
1070
|
+
context.report({
|
|
1071
|
+
message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
|
|
1072
|
+
node
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
if (currentType === "fractal-branch") {
|
|
1077
|
+
const expectedScope = path11.basename(path11.dirname(currentDir));
|
|
1078
|
+
const normalizedExpectedScope = expectedScope.startsWith("_") ? expectedScope.substring(1) : expectedScope;
|
|
1079
|
+
const expectedName = `_${normalizedExpectedScope}@shared`;
|
|
1080
|
+
if (currentFolderName !== expectedName) {
|
|
1081
|
+
context.report({
|
|
1082
|
+
message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
|
|
1083
|
+
node
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (parentType === "layer" || parentType === "fractal-branch" || parentType === "invalid-fragment") {
|
|
617
1089
|
return;
|
|
618
1090
|
}
|
|
619
1091
|
const rfConfig = getRootFragmentConfig(relDir, config);
|
|
@@ -622,7 +1094,7 @@ var rule3 = {
|
|
|
622
1094
|
const isMatchedRootNode = flatRootNodes.some((rn) => {
|
|
623
1095
|
if (rn.includes("/")) {
|
|
624
1096
|
const rfPath = rfConfig.paths[0] ?? "";
|
|
625
|
-
const absRn =
|
|
1097
|
+
const absRn = path11.resolve(getProjectRoot(), rfPath, rn);
|
|
626
1098
|
return toRelativePath(absRn) === relPath;
|
|
627
1099
|
}
|
|
628
1100
|
return rn === fileName;
|
|
@@ -632,288 +1104,79 @@ var rule3 = {
|
|
|
632
1104
|
}
|
|
633
1105
|
}
|
|
634
1106
|
if (fileName === "index.ts" || fileName === "index.js") {
|
|
635
|
-
if (parentType !== "fragment") {
|
|
636
|
-
context.report({
|
|
637
|
-
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
|
|
638
|
-
node
|
|
639
|
-
});
|
|
640
|
-
} else {
|
|
641
|
-
const contents = readDirCached(relDir);
|
|
642
|
-
const hasFragmentNode = contents.files.some(
|
|
643
|
-
(f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
|
|
644
|
-
);
|
|
645
|
-
if (!hasFragmentNode) {
|
|
646
|
-
context.report({
|
|
647
|
-
message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
|
|
648
|
-
node
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
1107
|
return;
|
|
653
1108
|
}
|
|
654
1109
|
if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
|
|
655
1110
|
return;
|
|
656
1111
|
}
|
|
657
1112
|
const role = getFileRole(fileName, config);
|
|
658
|
-
const ext =
|
|
1113
|
+
const ext = path11.extname(fileName);
|
|
659
1114
|
const allRoleNames = new Set(config.roles.flat());
|
|
660
1115
|
const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
|
|
661
1116
|
const multiRoleMessage = `File "${fileName}" composes multiple Roles ("${roleSegments.join('", "')}") in its name. A Logical Node may declare only one Role; promote the detail that needs its own Role to an autonomous Sub-Fragment.`;
|
|
662
|
-
if (parentType === "fragment") {
|
|
663
|
-
|
|
664
|
-
(rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
|
|
665
|
-
);
|
|
666
|
-
if (matchedRoute) {
|
|
667
|
-
const contents = readDirCached(relDir);
|
|
668
|
-
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
669
|
-
if (!fileRoles.includes(matchedRoute.role)) {
|
|
670
|
-
context.report({
|
|
671
|
-
message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
|
|
672
|
-
node
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
|
|
676
|
-
const routeNameRegex = new RegExp(
|
|
677
|
-
`^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
|
|
678
|
-
);
|
|
679
|
-
if (!routeNameRegex.test(folderName)) {
|
|
680
|
-
context.report({
|
|
681
|
-
message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
|
|
682
|
-
node
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
let currentParent = path7.dirname(relDir).replace(/\\/g, "/");
|
|
687
|
-
let categoryConfig = null;
|
|
688
|
-
let categoryFolderName = "";
|
|
689
|
-
while (currentParent && currentParent !== "." && currentParent !== "/") {
|
|
690
|
-
const parentType2 = classifyFolder(currentParent, config);
|
|
691
|
-
if (parentType2 === "category") {
|
|
692
|
-
categoryConfig = getCategoryConfig(currentParent, config);
|
|
693
|
-
categoryFolderName = path7.basename(currentParent);
|
|
694
|
-
break;
|
|
695
|
-
} else if (parentType2 === "fragment" || parentType2 === "root-fragment" || parentType2 === "layer") {
|
|
696
|
-
break;
|
|
697
|
-
}
|
|
698
|
-
const nextParent = path7.dirname(currentParent).replace(/\\/g, "/");
|
|
699
|
-
if (nextParent === currentParent) break;
|
|
700
|
-
currentParent = nextParent;
|
|
701
|
-
}
|
|
702
|
-
if (categoryConfig && categoryConfig.role) {
|
|
703
|
-
const contents = readDirCached(relDir);
|
|
704
|
-
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
705
|
-
if (!fileRoles.includes(categoryConfig.role)) {
|
|
706
|
-
context.report({
|
|
707
|
-
message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
|
|
708
|
-
node
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
if (!fileName.startsWith(folderName + ".")) {
|
|
713
|
-
context.report({
|
|
714
|
-
message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
|
|
715
|
-
node
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
if (!role) {
|
|
719
|
-
context.report({
|
|
720
|
-
message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
|
|
721
|
-
node
|
|
722
|
-
});
|
|
723
|
-
} else if (roleSegments.length > 1) {
|
|
724
|
-
context.report({ message: multiRoleMessage, node });
|
|
725
|
-
} else {
|
|
726
|
-
const contents = readDirCached(relDir);
|
|
727
|
-
const duplicate = contents.files.find(
|
|
728
|
-
(f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
|
|
729
|
-
);
|
|
730
|
-
if (duplicate) {
|
|
731
|
-
context.report({
|
|
732
|
-
message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
|
|
733
|
-
node
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
}
|
|
1117
|
+
if (parentType === "fragment" && role && roleSegments.length > 1) {
|
|
1118
|
+
context.report({ message: multiRoleMessage, node });
|
|
737
1119
|
}
|
|
738
1120
|
if (parentType === "category") {
|
|
1121
|
+
const folderName = path11.basename(relDir);
|
|
739
1122
|
const catConfig = getCategoryConfig(relDir, config);
|
|
740
1123
|
if (catConfig) {
|
|
741
1124
|
const allowedExts = catConfig.allowedExtensions ?? [
|
|
742
1125
|
".js",
|
|
743
1126
|
".jsx",
|
|
744
1127
|
".ts",
|
|
745
|
-
".tsx"
|
|
746
|
-
];
|
|
747
|
-
if (!allowedExts.includes(ext)) {
|
|
748
|
-
context.report({
|
|
749
|
-
message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
|
|
750
|
-
node
|
|
751
|
-
});
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
|
|
755
|
-
if (isCodeExt) {
|
|
756
|
-
if (!role) {
|
|
757
|
-
context.report({
|
|
758
|
-
message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
|
|
759
|
-
node
|
|
760
|
-
});
|
|
761
|
-
} else if (role !== catConfig.role) {
|
|
762
|
-
context.report({
|
|
763
|
-
message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
764
|
-
node
|
|
765
|
-
});
|
|
766
|
-
} else if (roleSegments.length > 1) {
|
|
767
|
-
context.report({ message: multiRoleMessage, node });
|
|
768
|
-
}
|
|
769
|
-
} else {
|
|
770
|
-
if (role && role !== catConfig.role) {
|
|
771
|
-
context.report({
|
|
772
|
-
message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
773
|
-
node
|
|
774
|
-
});
|
|
775
|
-
} else if (role && roleSegments.length > 1) {
|
|
776
|
-
context.report({ message: multiRoleMessage, node });
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
},
|
|
784
|
-
meta: {
|
|
785
|
-
docs: {
|
|
786
|
-
description: "Enforce naming conventions and folder taxonomy in FAF architecture"
|
|
787
|
-
},
|
|
788
|
-
schema: [],
|
|
789
|
-
type: "problem"
|
|
790
|
-
}
|
|
791
|
-
};
|
|
792
|
-
var naming_conventions_rule_default = rule3;
|
|
793
|
-
|
|
794
|
-
// src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
|
|
795
|
-
import path9 from "path";
|
|
796
|
-
|
|
797
|
-
// src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
|
|
798
|
-
import fs2 from "fs";
|
|
799
|
-
import path8 from "path";
|
|
800
|
-
function resolveImportPath(importPath, currentFileRelative, settings) {
|
|
801
|
-
const cacheKey = `${currentFileRelative}::${importPath}`;
|
|
802
|
-
const cached = state.resolvedImportPathCache.get(cacheKey);
|
|
803
|
-
if (cached !== void 0) {
|
|
804
|
-
return cached;
|
|
805
|
-
}
|
|
806
|
-
const result = _resolveImportPath(importPath, currentFileRelative, settings);
|
|
807
|
-
state.resolvedImportPathCache.set(cacheKey, result);
|
|
808
|
-
return result;
|
|
809
|
-
}
|
|
810
|
-
function _resolveImportPath(importPath, currentFileRelative, settings) {
|
|
811
|
-
const pkgImports = loadPackageImports(state.projectRoot);
|
|
812
|
-
const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
|
|
813
|
-
let matchedAlias = null;
|
|
814
|
-
let longestMatch = 0;
|
|
815
|
-
for (const alias of Object.keys(aliases)) {
|
|
816
|
-
if (importPath === alias || importPath.startsWith(alias + "/")) {
|
|
817
|
-
if (alias.length > longestMatch) {
|
|
818
|
-
longestMatch = alias.length;
|
|
819
|
-
matchedAlias = alias;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
let relResolved;
|
|
824
|
-
if (matchedAlias) {
|
|
825
|
-
const target = aliases[matchedAlias];
|
|
826
|
-
if (target) {
|
|
827
|
-
const remainder = importPath.substring(matchedAlias.length);
|
|
828
|
-
const absTarget = path8.resolve(state.projectRoot, target + remainder);
|
|
829
|
-
relResolved = toRelativePath(absTarget);
|
|
830
|
-
} else {
|
|
831
|
-
return "";
|
|
832
|
-
}
|
|
833
|
-
} else {
|
|
834
|
-
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
835
|
-
const absCurrentDir = path8.dirname(
|
|
836
|
-
path8.resolve(state.projectRoot, currentFileRelative)
|
|
837
|
-
);
|
|
838
|
-
const absResolved = path8.resolve(absCurrentDir, importPath);
|
|
839
|
-
relResolved = toRelativePath(absResolved);
|
|
840
|
-
} else {
|
|
841
|
-
return "";
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return finalizeResolvedPath(relResolved);
|
|
845
|
-
}
|
|
846
|
-
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
847
|
-
"cjs",
|
|
848
|
-
"cts",
|
|
849
|
-
"js",
|
|
850
|
-
"jsx",
|
|
851
|
-
"mjs",
|
|
852
|
-
"mts",
|
|
853
|
-
"ts",
|
|
854
|
-
"tsx"
|
|
855
|
-
]);
|
|
856
|
-
function finalizeResolvedPath(relResolved) {
|
|
857
|
-
const resolvedDir = path8.dirname(relResolved).replace(/\\/g, "/");
|
|
858
|
-
const resolvedBase = path8.basename(relResolved);
|
|
859
|
-
const dirContents = readDirCached(relResolved);
|
|
860
|
-
if (dirContents.files.includes("index.ts")) {
|
|
861
|
-
return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
|
|
862
|
-
}
|
|
863
|
-
if (dirContents.files.includes("index.js")) {
|
|
864
|
-
return (relResolved === "." ? "" : relResolved + "/") + "index.js";
|
|
865
|
-
}
|
|
866
|
-
const parentContents = readDirCached(resolvedDir);
|
|
867
|
-
const exactMatch = parentContents.files.find((f) => f === resolvedBase);
|
|
868
|
-
if (!exactMatch) {
|
|
869
|
-
const candidates = parentContents.files.filter(
|
|
870
|
-
(f) => f.startsWith(resolvedBase + ".")
|
|
871
|
-
);
|
|
872
|
-
const matchWithExt = candidates.find(
|
|
873
|
-
(f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
|
|
874
|
-
) ?? candidates[0];
|
|
875
|
-
if (matchWithExt) {
|
|
876
|
-
return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return relResolved;
|
|
880
|
-
}
|
|
881
|
-
function loadPackageImports(projectRootPath) {
|
|
882
|
-
if (state.packageImports !== null) {
|
|
883
|
-
return state.packageImports;
|
|
884
|
-
}
|
|
885
|
-
state.packageImports = {};
|
|
886
|
-
const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
|
|
887
|
-
if (isTest) {
|
|
888
|
-
return state.packageImports;
|
|
889
|
-
}
|
|
890
|
-
const pkgPath = path8.resolve(projectRootPath, "package.json");
|
|
891
|
-
if (fs2.existsSync(pkgPath)) {
|
|
892
|
-
try {
|
|
893
|
-
const pkgContent = fs2.readFileSync(pkgPath, "utf8");
|
|
894
|
-
const pkg = JSON.parse(pkgContent);
|
|
895
|
-
if (pkg.imports) {
|
|
896
|
-
for (const [key, val] of Object.entries(pkg.imports)) {
|
|
897
|
-
if (typeof val === "string") {
|
|
898
|
-
const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
|
|
899
|
-
let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
|
|
900
|
-
if (cleanVal.startsWith("./")) {
|
|
901
|
-
cleanVal = cleanVal.substring(2);
|
|
902
|
-
} else if (cleanVal.startsWith("/")) {
|
|
903
|
-
cleanVal = cleanVal.substring(1);
|
|
1128
|
+
".tsx"
|
|
1129
|
+
];
|
|
1130
|
+
if (!allowedExts.includes(ext)) {
|
|
1131
|
+
context.report({
|
|
1132
|
+
message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
|
|
1133
|
+
node
|
|
1134
|
+
});
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
|
|
1138
|
+
if (isCodeExt) {
|
|
1139
|
+
if (!role) {
|
|
1140
|
+
context.report({
|
|
1141
|
+
message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
|
|
1142
|
+
node
|
|
1143
|
+
});
|
|
1144
|
+
} else if (role !== catConfig.role) {
|
|
1145
|
+
context.report({
|
|
1146
|
+
message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
1147
|
+
node
|
|
1148
|
+
});
|
|
1149
|
+
} else if (roleSegments.length > 1) {
|
|
1150
|
+
context.report({ message: multiRoleMessage, node });
|
|
1151
|
+
}
|
|
1152
|
+
} else {
|
|
1153
|
+
if (role && role !== catConfig.role) {
|
|
1154
|
+
context.report({
|
|
1155
|
+
message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
1156
|
+
node
|
|
1157
|
+
});
|
|
1158
|
+
} else if (role && roleSegments.length > 1) {
|
|
1159
|
+
context.report({ message: multiRoleMessage, node });
|
|
1160
|
+
}
|
|
904
1161
|
}
|
|
905
|
-
state.packageImports[cleanKey] = cleanVal;
|
|
906
1162
|
}
|
|
907
1163
|
}
|
|
908
1164
|
}
|
|
909
|
-
}
|
|
910
|
-
|
|
1165
|
+
};
|
|
1166
|
+
},
|
|
1167
|
+
meta: {
|
|
1168
|
+
docs: {
|
|
1169
|
+
description: "Enforce lexical naming conventions for FAF folders and Logical Nodes"
|
|
1170
|
+
},
|
|
1171
|
+
schema: [],
|
|
1172
|
+
type: "problem"
|
|
911
1173
|
}
|
|
912
|
-
|
|
913
|
-
|
|
1174
|
+
};
|
|
1175
|
+
var naming_conventions_rule_default = rule6;
|
|
914
1176
|
|
|
915
1177
|
// src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
|
|
916
|
-
|
|
1178
|
+
import path12 from "path";
|
|
1179
|
+
var rule7 = {
|
|
917
1180
|
create(context) {
|
|
918
1181
|
const absPath = context.filename;
|
|
919
1182
|
const relPath = toRelativePath(absPath);
|
|
@@ -934,13 +1197,13 @@ var rule4 = {
|
|
|
934
1197
|
if (!resolvedRelPath) {
|
|
935
1198
|
return;
|
|
936
1199
|
}
|
|
937
|
-
const importedDir =
|
|
938
|
-
const importedFile =
|
|
1200
|
+
const importedDir = path12.dirname(resolvedRelPath);
|
|
1201
|
+
const importedFile = path12.basename(resolvedRelPath);
|
|
939
1202
|
const importedFolderType = classifyFolder(importedDir, config);
|
|
940
1203
|
if (importedFolderType === "fragment") {
|
|
941
1204
|
const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
|
|
942
1205
|
if (!isAccessNode) {
|
|
943
|
-
const isInside =
|
|
1206
|
+
const isInside = path12.dirname(relPath) === importedDir;
|
|
944
1207
|
if (!isInside) {
|
|
945
1208
|
context.report({
|
|
946
1209
|
message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
|
|
@@ -972,21 +1235,21 @@ var rule4 = {
|
|
|
972
1235
|
type: "problem"
|
|
973
1236
|
}
|
|
974
1237
|
};
|
|
975
|
-
var no_direct_fragment_import_rule_default =
|
|
1238
|
+
var no_direct_fragment_import_rule_default = rule7;
|
|
976
1239
|
|
|
977
1240
|
// src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
|
|
978
|
-
import
|
|
1241
|
+
import path15 from "path";
|
|
979
1242
|
|
|
980
1243
|
// src/_rules/_rules@shared/_utils/_primitives/get-fractal-branch-owner/get-fractal-branch-owner.util.ts
|
|
981
|
-
import
|
|
1244
|
+
import path13 from "path";
|
|
982
1245
|
function getFractalBranchOwner(relPath) {
|
|
983
1246
|
let current = relPath;
|
|
984
1247
|
while (current && current !== "." && current !== "/") {
|
|
985
|
-
const folderName =
|
|
1248
|
+
const folderName = path13.basename(current);
|
|
986
1249
|
if (folderName.startsWith("_") && folderName.includes("@shared")) {
|
|
987
|
-
return
|
|
1250
|
+
return path13.dirname(current).replace(/\\/g, "/");
|
|
988
1251
|
}
|
|
989
|
-
const parent =
|
|
1252
|
+
const parent = path13.dirname(current).replace(/\\/g, "/");
|
|
990
1253
|
if (parent === current) {
|
|
991
1254
|
break;
|
|
992
1255
|
}
|
|
@@ -1006,11 +1269,11 @@ function getImmediateSubDomain(relPath, privateCategoryPath) {
|
|
|
1006
1269
|
}
|
|
1007
1270
|
|
|
1008
1271
|
// src/_rules/_rules@shared/_utils/_systems/get-private-category-info/get-private-category-info.util.ts
|
|
1009
|
-
import
|
|
1272
|
+
import path14 from "path";
|
|
1010
1273
|
function getPrivateCategoryInfo(relPath, config) {
|
|
1011
1274
|
let current = relPath;
|
|
1012
1275
|
while (current && current !== "." && current !== "/") {
|
|
1013
|
-
const parent =
|
|
1276
|
+
const parent = path14.dirname(current).replace(/\\/g, "/");
|
|
1014
1277
|
if (parent === current) {
|
|
1015
1278
|
break;
|
|
1016
1279
|
}
|
|
@@ -1025,7 +1288,22 @@ function getPrivateCategoryInfo(relPath, config) {
|
|
|
1025
1288
|
}
|
|
1026
1289
|
|
|
1027
1290
|
// src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
|
|
1028
|
-
|
|
1291
|
+
function getFractalBranchPath(relPath) {
|
|
1292
|
+
let current = relPath;
|
|
1293
|
+
while (current && current !== "." && current !== "/") {
|
|
1294
|
+
const folderName = path15.basename(current);
|
|
1295
|
+
if (folderName.startsWith("_") && folderName.includes("@shared")) {
|
|
1296
|
+
return current;
|
|
1297
|
+
}
|
|
1298
|
+
const parent = path15.dirname(current).replace(/\\/g, "/");
|
|
1299
|
+
if (parent === current) {
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
current = parent;
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
var rule8 = {
|
|
1029
1307
|
create(context) {
|
|
1030
1308
|
const absPath = context.filename;
|
|
1031
1309
|
const relPath = toRelativePath(absPath);
|
|
@@ -1046,7 +1324,7 @@ var rule5 = {
|
|
|
1046
1324
|
if (!resolvedRelPath) {
|
|
1047
1325
|
return;
|
|
1048
1326
|
}
|
|
1049
|
-
const importedDir =
|
|
1327
|
+
const importedDir = path15.dirname(resolvedRelPath);
|
|
1050
1328
|
const fractalBranchOwner = getFractalBranchOwner(importedDir);
|
|
1051
1329
|
if (fractalBranchOwner) {
|
|
1052
1330
|
const isDescendant = relPath === fractalBranchOwner || relPath.startsWith(fractalBranchOwner + "/");
|
|
@@ -1059,7 +1337,7 @@ var rule5 = {
|
|
|
1059
1337
|
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1060
1338
|
if (info) {
|
|
1061
1339
|
const { owner, privateCategoryPath } = info;
|
|
1062
|
-
const isPrivateDirectChild =
|
|
1340
|
+
const isPrivateDirectChild = path15.dirname(relPath) === owner;
|
|
1063
1341
|
const importerSubDomain = getImmediateSubDomain(
|
|
1064
1342
|
relPath,
|
|
1065
1343
|
privateCategoryPath
|
|
@@ -1079,6 +1357,18 @@ var rule5 = {
|
|
|
1079
1357
|
}
|
|
1080
1358
|
}
|
|
1081
1359
|
}
|
|
1360
|
+
const importerBranchPath = getFractalBranchPath(path15.dirname(relPath));
|
|
1361
|
+
if (importerBranchPath) {
|
|
1362
|
+
const importerBranchOwner = path15.dirname(importerBranchPath).replace(/\\/g, "/");
|
|
1363
|
+
const isImportedInsideOwnBranch = resolvedRelPath === importerBranchPath || resolvedRelPath.startsWith(importerBranchPath + "/");
|
|
1364
|
+
const isImportedInsideServedSubtree = resolvedRelPath === importerBranchOwner || resolvedRelPath.startsWith(importerBranchOwner + "/");
|
|
1365
|
+
if (isImportedInsideServedSubtree && !isImportedInsideOwnBranch) {
|
|
1366
|
+
context.report({
|
|
1367
|
+
message: `Fractal Branch "${importerBranchPath}" cannot depend on "${resolvedRelPath}" from the subtree it serves. A Fractal Branch may only provide implementation details to its parent scope; it must never depend on it, not even indirectly.`,
|
|
1368
|
+
node
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1082
1372
|
}
|
|
1083
1373
|
return {
|
|
1084
1374
|
ExportAllDeclaration(node) {
|
|
@@ -1102,33 +1392,10 @@ var rule5 = {
|
|
|
1102
1392
|
type: "problem"
|
|
1103
1393
|
}
|
|
1104
1394
|
};
|
|
1105
|
-
var no_fractal_branch_leak_rule_default =
|
|
1395
|
+
var no_fractal_branch_leak_rule_default = rule8;
|
|
1106
1396
|
|
|
1107
1397
|
// src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
|
|
1108
|
-
import
|
|
1109
|
-
|
|
1110
|
-
// src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
|
|
1111
|
-
function findTreeConfigIncludingExcluded(relPath, settings) {
|
|
1112
|
-
if (!settings || !settings.trees) {
|
|
1113
|
-
return null;
|
|
1114
|
-
}
|
|
1115
|
-
const cached = state.treeConfigIncludingExcludedCache.get(relPath);
|
|
1116
|
-
if (cached !== void 0) {
|
|
1117
|
-
return cached;
|
|
1118
|
-
}
|
|
1119
|
-
let result = null;
|
|
1120
|
-
for (const tree of settings.trees) {
|
|
1121
|
-
const isIncluded = tree.includes.some(
|
|
1122
|
-
(inc) => relPath === inc || relPath.startsWith(inc + "/")
|
|
1123
|
-
);
|
|
1124
|
-
if (isIncluded) {
|
|
1125
|
-
result = tree;
|
|
1126
|
-
break;
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
state.treeConfigIncludingExcludedCache.set(relPath, result);
|
|
1130
|
-
return result;
|
|
1131
|
-
}
|
|
1398
|
+
import path18 from "path";
|
|
1132
1399
|
|
|
1133
1400
|
// src/_rules/_rules@shared/_utils/_primitives/get-lca-and-sub-branches/get-lca-and-sub-branches.util.ts
|
|
1134
1401
|
function getLcaAndSubBranches(pathA, pathB) {
|
|
@@ -1158,6 +1425,19 @@ function getRoleHierarchyIndex(role, config) {
|
|
|
1158
1425
|
return -1;
|
|
1159
1426
|
}
|
|
1160
1427
|
|
|
1428
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-root-node-index/get-root-node-index.util.ts
|
|
1429
|
+
import path16 from "path";
|
|
1430
|
+
function getRootNodeIndex(relPath, rfConfig, lcaPath) {
|
|
1431
|
+
const relToLca = path16.relative(lcaPath, relPath).replace(/\\/g, "/");
|
|
1432
|
+
for (let i = 0; i < rfConfig.rootNodes.length; i++) {
|
|
1433
|
+
const group = rfConfig.rootNodes[i];
|
|
1434
|
+
if (group && group.includes(relToLca)) {
|
|
1435
|
+
return i;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return -1;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1161
1441
|
// src/_rules/_rules@shared/_utils/_primitives/resolve-horizontal-hierarchy/resolve-horizontal-hierarchy.util.ts
|
|
1162
1442
|
function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
|
|
1163
1443
|
const local = config.localHorizontalHierarchies?.find(
|
|
@@ -1181,39 +1461,12 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
|
|
|
1181
1461
|
return { allowed: false, defined: false };
|
|
1182
1462
|
}
|
|
1183
1463
|
|
|
1184
|
-
// src/_rules/
|
|
1185
|
-
|
|
1186
|
-
const folderName = path13.basename(dirPath);
|
|
1187
|
-
if (folderName.startsWith("_")) {
|
|
1188
|
-
return getRoleFromFolderName(folderName, config);
|
|
1189
|
-
}
|
|
1190
|
-
const contents = readDirCached(dirPath);
|
|
1191
|
-
for (const file of contents.files) {
|
|
1192
|
-
if (file.startsWith(folderName + ".")) {
|
|
1193
|
-
const role = getFileRole(file, config);
|
|
1194
|
-
if (role) return role;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
return null;
|
|
1198
|
-
}
|
|
1199
|
-
function getRoleFromFolderName(name, config) {
|
|
1200
|
-
const catConfig = config.categories?.find((c) => c.name === name);
|
|
1201
|
-
return catConfig ? catConfig.role ?? null : null;
|
|
1202
|
-
}
|
|
1203
|
-
function getRootNodeIndex(relPath, rfConfig, lcaPath) {
|
|
1204
|
-
const relToLca = path13.relative(lcaPath, relPath).replace(/\\/g, "/");
|
|
1205
|
-
for (let i = 0; i < rfConfig.rootNodes.length; i++) {
|
|
1206
|
-
const group = rfConfig.rootNodes[i];
|
|
1207
|
-
if (group && group.includes(relToLca)) {
|
|
1208
|
-
return i;
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
return -1;
|
|
1212
|
-
}
|
|
1464
|
+
// src/_rules/_rules@shared/_utils/_systems/is-inside-private-category-of-lca/is-inside-private-category-of-lca.util.ts
|
|
1465
|
+
import path17 from "path";
|
|
1213
1466
|
function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
|
|
1214
1467
|
let current = relPathB;
|
|
1215
1468
|
while (current && current !== "." && current !== "/" && current !== lcaPath) {
|
|
1216
|
-
const parent =
|
|
1469
|
+
const parent = path17.dirname(current).replace(/\\/g, "/");
|
|
1217
1470
|
if (parent === current) {
|
|
1218
1471
|
break;
|
|
1219
1472
|
}
|
|
@@ -1226,7 +1479,9 @@ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
|
|
|
1226
1479
|
}
|
|
1227
1480
|
return false;
|
|
1228
1481
|
}
|
|
1229
|
-
|
|
1482
|
+
|
|
1483
|
+
// src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
|
|
1484
|
+
var rule9 = {
|
|
1230
1485
|
create(context) {
|
|
1231
1486
|
const absPath = context.filename;
|
|
1232
1487
|
const relPath = toRelativePath(absPath);
|
|
@@ -1234,11 +1489,11 @@ var rule6 = {
|
|
|
1234
1489
|
if (!settings.faf) {
|
|
1235
1490
|
return {};
|
|
1236
1491
|
}
|
|
1237
|
-
const config =
|
|
1492
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
1238
1493
|
if (!config) {
|
|
1239
1494
|
return {};
|
|
1240
1495
|
}
|
|
1241
|
-
const currentDir =
|
|
1496
|
+
const currentDir = path18.dirname(relPath);
|
|
1242
1497
|
const isCurrentFileExcluded = config.excludes?.some(
|
|
1243
1498
|
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
1244
1499
|
) ?? false;
|
|
@@ -1254,27 +1509,13 @@ var rule6 = {
|
|
|
1254
1509
|
const isImportedFileExcluded = config?.excludes?.some(
|
|
1255
1510
|
(exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
|
|
1256
1511
|
) ?? false;
|
|
1257
|
-
if (!isCurrentFileExcluded && isImportedFileExcluded) {
|
|
1258
|
-
context.report({
|
|
1259
|
-
message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
|
|
1260
|
-
node
|
|
1261
|
-
});
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
if (isCurrentFileExcluded && !isImportedFileExcluded) {
|
|
1265
|
-
context.report({
|
|
1266
|
-
message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
|
|
1267
|
-
node
|
|
1268
|
-
});
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
1512
|
if (isCurrentFileExcluded || isImportedFileExcluded) {
|
|
1272
1513
|
return;
|
|
1273
1514
|
}
|
|
1274
|
-
const importedDir =
|
|
1515
|
+
const importedDir = path18.dirname(resolvedRelPath);
|
|
1275
1516
|
if (currentDir === importedDir) {
|
|
1276
|
-
const fileA =
|
|
1277
|
-
const fileB =
|
|
1517
|
+
const fileA = path18.basename(relPath);
|
|
1518
|
+
const fileB = path18.basename(resolvedRelPath);
|
|
1278
1519
|
const roleA = getFileRole(fileA, config);
|
|
1279
1520
|
const roleB = getFileRole(fileB, config);
|
|
1280
1521
|
if (roleA && roleB) {
|
|
@@ -1314,22 +1555,12 @@ var rule6 = {
|
|
|
1314
1555
|
const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
|
|
1315
1556
|
const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
|
|
1316
1557
|
if (idxRootA !== -1 && idxRootB !== -1) {
|
|
1317
|
-
if (idxRootB >= idxRootA) {
|
|
1318
|
-
context.report({
|
|
1319
|
-
message: `Root Node import violation: "${path13.basename(relPath)}" cannot import from "${path13.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
|
|
1320
|
-
node
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
1558
|
return;
|
|
1324
1559
|
}
|
|
1325
1560
|
}
|
|
1326
|
-
const isSubARootFragment = classifyFolder(
|
|
1327
|
-
const isSubBRootFragment = classifyFolder(
|
|
1561
|
+
const isSubARootFragment = classifyFolder(path18.posix.join(lca, subA), config) === "root-fragment";
|
|
1562
|
+
const isSubBRootFragment = classifyFolder(path18.posix.join(lca, subB), config) === "root-fragment";
|
|
1328
1563
|
if (isSubARootFragment || isSubBRootFragment) {
|
|
1329
|
-
context.report({
|
|
1330
|
-
message: `Root Node import violation: "${relPath}" cannot import from "${resolvedRelPath}" because no "rootNodes" relationship is configured under Root Fragment "${lca}". Relationships between Root Nodes must always be explicitly authorized by the architect.`,
|
|
1331
|
-
node
|
|
1332
|
-
});
|
|
1333
1564
|
return;
|
|
1334
1565
|
}
|
|
1335
1566
|
const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
|
|
@@ -1341,23 +1572,10 @@ var rule6 = {
|
|
|
1341
1572
|
});
|
|
1342
1573
|
}
|
|
1343
1574
|
} else {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (idxA !== -1 && idxB !== -1) {
|
|
1349
|
-
if (idxB >= idxA) {
|
|
1350
|
-
context.report({
|
|
1351
|
-
message: `Horizontal hierarchy violation (fallback to roles): sibling directory "${subA}" (mapped to role "${roleA}", level ${idxA}) cannot import from "${subB}" (mapped to role "${roleB}", level ${idxB}) under parent "${lca}".`,
|
|
1352
|
-
node
|
|
1353
|
-
});
|
|
1354
|
-
}
|
|
1355
|
-
} else {
|
|
1356
|
-
context.report({
|
|
1357
|
-
message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
|
|
1358
|
-
node
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1575
|
+
context.report({
|
|
1576
|
+
message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
|
|
1577
|
+
node
|
|
1578
|
+
});
|
|
1361
1579
|
}
|
|
1362
1580
|
}
|
|
1363
1581
|
return {
|
|
@@ -1382,11 +1600,11 @@ var rule6 = {
|
|
|
1382
1600
|
type: "problem"
|
|
1383
1601
|
}
|
|
1384
1602
|
};
|
|
1385
|
-
var no_peer_dependency_rule_default =
|
|
1603
|
+
var no_peer_dependency_rule_default = rule9;
|
|
1386
1604
|
|
|
1387
1605
|
// src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
|
|
1388
|
-
import
|
|
1389
|
-
var
|
|
1606
|
+
import path19 from "path";
|
|
1607
|
+
var rule10 = {
|
|
1390
1608
|
create(context) {
|
|
1391
1609
|
const absPath = context.filename;
|
|
1392
1610
|
const relPath = toRelativePath(absPath);
|
|
@@ -1407,7 +1625,7 @@ var rule7 = {
|
|
|
1407
1625
|
if (!resolvedRelPath) {
|
|
1408
1626
|
return;
|
|
1409
1627
|
}
|
|
1410
|
-
const importedDir =
|
|
1628
|
+
const importedDir = path19.dirname(resolvedRelPath);
|
|
1411
1629
|
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1412
1630
|
if (info) {
|
|
1413
1631
|
const { owner, privateCategoryPath } = info;
|
|
@@ -1421,7 +1639,7 @@ var rule7 = {
|
|
|
1421
1639
|
isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
|
|
1422
1640
|
}
|
|
1423
1641
|
}
|
|
1424
|
-
const isDirectChild =
|
|
1642
|
+
const isDirectChild = path19.dirname(relPath) === owner;
|
|
1425
1643
|
const importerSubDomain = getImmediateSubDomain(
|
|
1426
1644
|
relPath,
|
|
1427
1645
|
privateCategoryPath
|
|
@@ -1462,17 +1680,119 @@ var rule7 = {
|
|
|
1462
1680
|
type: "problem"
|
|
1463
1681
|
}
|
|
1464
1682
|
};
|
|
1465
|
-
var no_private_category_leak_rule_default =
|
|
1683
|
+
var no_private_category_leak_rule_default = rule10;
|
|
1684
|
+
|
|
1685
|
+
// src/_rules/root-node-dependency-direction/root-node-dependency-direction.rule.ts
|
|
1686
|
+
import path20 from "path";
|
|
1687
|
+
var rule11 = {
|
|
1688
|
+
create(context) {
|
|
1689
|
+
const absPath = context.filename;
|
|
1690
|
+
const relPath = toRelativePath(absPath);
|
|
1691
|
+
const settings = context.settings;
|
|
1692
|
+
if (!settings.faf) {
|
|
1693
|
+
return {};
|
|
1694
|
+
}
|
|
1695
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
1696
|
+
if (!config) {
|
|
1697
|
+
return {};
|
|
1698
|
+
}
|
|
1699
|
+
const currentDir = path20.dirname(relPath);
|
|
1700
|
+
const isCurrentFileExcluded = config.excludes?.some(
|
|
1701
|
+
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
1702
|
+
) ?? false;
|
|
1703
|
+
function checkImport(node, importValue) {
|
|
1704
|
+
const resolvedRelPath = resolveImportPath(
|
|
1705
|
+
importValue,
|
|
1706
|
+
relPath,
|
|
1707
|
+
settings.faf
|
|
1708
|
+
);
|
|
1709
|
+
if (!resolvedRelPath) {
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
const isImportedFileExcluded = config?.excludes?.some(
|
|
1713
|
+
(exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
|
|
1714
|
+
) ?? false;
|
|
1715
|
+
if (isCurrentFileExcluded || isImportedFileExcluded) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const importedDir = path20.dirname(resolvedRelPath);
|
|
1719
|
+
if (currentDir === importedDir) {
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
const lcaInfo = getLcaAndSubBranches(currentDir, importedDir);
|
|
1723
|
+
if (!lcaInfo) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const { lca, subA, subB } = lcaInfo;
|
|
1727
|
+
if (subB.startsWith("_") && subB.includes("@shared")) {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (isInsidePrivateCategoryOfLca(importedDir, lca, config)) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (subB === "" && classifyFolder(lca, config) === "fragment") {
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
const rfConfig = getRootFragmentConfig(lca, config);
|
|
1737
|
+
if (rfConfig) {
|
|
1738
|
+
const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
|
|
1739
|
+
const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
|
|
1740
|
+
if (idxRootA !== -1 && idxRootB !== -1) {
|
|
1741
|
+
if (idxRootB >= idxRootA) {
|
|
1742
|
+
context.report({
|
|
1743
|
+
message: `Root Node import violation: "${path20.basename(relPath)}" cannot import from "${path20.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
|
|
1744
|
+
node
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const isSubARootFragment = classifyFolder(path20.posix.join(lca, subA), config) === "root-fragment";
|
|
1751
|
+
const isSubBRootFragment = classifyFolder(path20.posix.join(lca, subB), config) === "root-fragment";
|
|
1752
|
+
if (isSubARootFragment || isSubBRootFragment) {
|
|
1753
|
+
context.report({
|
|
1754
|
+
message: `Root Node import violation: "${relPath}" cannot import from "${resolvedRelPath}" because no "rootNodes" relationship is configured under Root Fragment "${lca}". Relationships between Root Nodes must always be explicitly authorized by the architect.`,
|
|
1755
|
+
node
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
ExportAllDeclaration(node) {
|
|
1761
|
+
checkImport(node, node.source.value);
|
|
1762
|
+
},
|
|
1763
|
+
ExportNamedDeclaration(node) {
|
|
1764
|
+
if (node.source) {
|
|
1765
|
+
checkImport(node, node.source.value);
|
|
1766
|
+
}
|
|
1767
|
+
},
|
|
1768
|
+
ImportDeclaration(node) {
|
|
1769
|
+
checkImport(node, node.source.value);
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
},
|
|
1773
|
+
meta: {
|
|
1774
|
+
docs: {
|
|
1775
|
+
description: "Enforce explicit, non-Role-based dependency direction between Root Nodes"
|
|
1776
|
+
},
|
|
1777
|
+
schema: [],
|
|
1778
|
+
type: "problem"
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
var root_node_dependency_direction_rule_default = rule11;
|
|
1466
1782
|
|
|
1467
1783
|
// src/rules.ts
|
|
1468
1784
|
var rules = {
|
|
1469
1785
|
"category-mutually-exclusive": category_mutually_exclusive_rule_default,
|
|
1470
1786
|
"enforce-access-node": enforce_access_node_rule_default,
|
|
1787
|
+
"foreign-domain-isolation": foreign_domain_isolation_rule_default,
|
|
1788
|
+
"fragment-master-node": fragment_master_node_rule_default,
|
|
1789
|
+
"logical-domain-placement": logical_domain_placement_rule_default,
|
|
1471
1790
|
"naming-conventions": naming_conventions_rule_default,
|
|
1472
1791
|
"no-direct-fragment-import": no_direct_fragment_import_rule_default,
|
|
1473
1792
|
"no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
|
|
1474
1793
|
"no-peer-dependency": no_peer_dependency_rule_default,
|
|
1475
|
-
"no-private-category-leak": no_private_category_leak_rule_default
|
|
1794
|
+
"no-private-category-leak": no_private_category_leak_rule_default,
|
|
1795
|
+
"root-node-dependency-direction": root_node_dependency_direction_rule_default
|
|
1476
1796
|
};
|
|
1477
1797
|
|
|
1478
1798
|
// src/configs.ts
|
|
@@ -1486,11 +1806,15 @@ var configs = {
|
|
|
1486
1806
|
rules: {
|
|
1487
1807
|
"faf/category-mutually-exclusive": "error",
|
|
1488
1808
|
"faf/enforce-access-node": "error",
|
|
1809
|
+
"faf/foreign-domain-isolation": "error",
|
|
1810
|
+
"faf/fragment-master-node": "error",
|
|
1811
|
+
"faf/logical-domain-placement": "error",
|
|
1489
1812
|
"faf/naming-conventions": "error",
|
|
1490
1813
|
"faf/no-direct-fragment-import": "error",
|
|
1491
1814
|
"faf/no-fractal-branch-leak": "error",
|
|
1492
1815
|
"faf/no-peer-dependency": "error",
|
|
1493
|
-
"faf/no-private-category-leak": "error"
|
|
1816
|
+
"faf/no-private-category-leak": "error",
|
|
1817
|
+
"faf/root-node-dependency-direction": "error"
|
|
1494
1818
|
}
|
|
1495
1819
|
}
|
|
1496
1820
|
};
|