@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.cjs
CHANGED
|
@@ -39,7 +39,7 @@ module.exports = __toCommonJS(main_exports);
|
|
|
39
39
|
// package.json
|
|
40
40
|
var package_default = {
|
|
41
41
|
name: "@morriconeluca/eslint-plugin-faf",
|
|
42
|
-
version: "0.
|
|
42
|
+
version: "0.5.0",
|
|
43
43
|
description: "An ESLint plugin for FAF projects.",
|
|
44
44
|
repository: {
|
|
45
45
|
type: "git",
|
|
@@ -127,6 +127,7 @@ var import_path3 = __toESM(require("path"), 1);
|
|
|
127
127
|
|
|
128
128
|
// src/_rules/_rules@shared/_utils/_utils@shared/_stores/cache/cache.store.ts
|
|
129
129
|
var state = {
|
|
130
|
+
ancestorChainCache: /* @__PURE__ */ new Map(),
|
|
130
131
|
categoryConfigCache: /* @__PURE__ */ new Map(),
|
|
131
132
|
classifyCache: /* @__PURE__ */ new Map(),
|
|
132
133
|
dirCache: {},
|
|
@@ -383,27 +384,139 @@ var rule = {
|
|
|
383
384
|
var category_mutually_exclusive_rule_default = rule;
|
|
384
385
|
|
|
385
386
|
// src/_rules/enforce-access-node/enforce-access-node.rule.ts
|
|
387
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
388
|
+
|
|
389
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-project-root/get-project-root.util.ts
|
|
390
|
+
function getProjectRoot() {
|
|
391
|
+
return state.projectRoot;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/_rules/_rules@shared/_utils/_systems/walk-ancestors/walk-ancestors.util.ts
|
|
386
395
|
var import_path6 = __toESM(require("path"), 1);
|
|
396
|
+
function walkAncestors(relDir, config) {
|
|
397
|
+
const cached = state.ancestorChainCache.get(relDir);
|
|
398
|
+
if (cached !== void 0) {
|
|
399
|
+
return cached;
|
|
400
|
+
}
|
|
401
|
+
const result = [];
|
|
402
|
+
let currentDir = relDir;
|
|
403
|
+
while (currentDir && currentDir !== "." && currentDir !== "/") {
|
|
404
|
+
const isInTree = config.includes.some(
|
|
405
|
+
(inc) => currentDir === inc || currentDir.startsWith(inc + "/")
|
|
406
|
+
);
|
|
407
|
+
const isExcluded = config.excludes?.some(
|
|
408
|
+
(exc) => currentDir === exc || currentDir.startsWith(exc + "/")
|
|
409
|
+
) ?? false;
|
|
410
|
+
if (!isInTree || isExcluded) {
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
const folderName = import_path6.default.basename(currentDir);
|
|
414
|
+
const type = classifyFolder(currentDir, config);
|
|
415
|
+
const parentDir = import_path6.default.dirname(currentDir).replace(/\\/g, "/");
|
|
416
|
+
const parentFolderName = import_path6.default.basename(parentDir);
|
|
417
|
+
const parentType = parentDir === "." || parentDir === "/" ? "unknown" : classifyFolder(parentDir, config);
|
|
418
|
+
result.push({
|
|
419
|
+
dir: currentDir,
|
|
420
|
+
folderName,
|
|
421
|
+
parentDir,
|
|
422
|
+
parentFolderName,
|
|
423
|
+
parentType,
|
|
424
|
+
type
|
|
425
|
+
});
|
|
426
|
+
currentDir = parentDir;
|
|
427
|
+
}
|
|
428
|
+
state.ancestorChainCache.set(relDir, result);
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/_rules/enforce-access-node/enforce-access-node.rule.ts
|
|
387
433
|
var rule2 = {
|
|
388
434
|
create(context) {
|
|
389
435
|
const absPath = context.filename;
|
|
390
436
|
const relPath = toRelativePath(absPath);
|
|
391
|
-
const
|
|
437
|
+
const settings = context.settings;
|
|
438
|
+
const listeners = {
|
|
439
|
+
Program(node) {
|
|
440
|
+
if (!settings.faf) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const config2 = findTreeConfig(relPath, settings.faf);
|
|
444
|
+
if (!config2) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const relDir2 = import_path7.default.dirname(relPath);
|
|
448
|
+
const folderName = import_path7.default.basename(relDir2);
|
|
449
|
+
const parentType2 = classifyFolder(relDir2, config2);
|
|
450
|
+
if (parentType2 === "foreign") {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const fileName2 = import_path7.default.basename(relPath);
|
|
454
|
+
for (const {
|
|
455
|
+
folderName: currentFolderName,
|
|
456
|
+
type: currentType
|
|
457
|
+
} of walkAncestors(relDir2, config2)) {
|
|
458
|
+
if (currentType === "invalid-fragment") {
|
|
459
|
+
context.report({
|
|
460
|
+
message: `Fragment directory "${currentFolderName}" is missing an Access Node (index.ts/index.js).`,
|
|
461
|
+
node
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (parentType2 === "layer" || parentType2 === "fractal-branch" || parentType2 === "invalid-fragment") {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const rfConfig = getRootFragmentConfig(relDir2, config2);
|
|
469
|
+
if (rfConfig) {
|
|
470
|
+
const flatRootNodes = rfConfig.rootNodes.flat();
|
|
471
|
+
const isMatchedRootNode = flatRootNodes.some((rn) => {
|
|
472
|
+
if (rn.includes("/")) {
|
|
473
|
+
const rfPath = rfConfig.paths[0] ?? "";
|
|
474
|
+
const absRn = import_path7.default.resolve(getProjectRoot(), rfPath, rn);
|
|
475
|
+
return toRelativePath(absRn) === relPath;
|
|
476
|
+
}
|
|
477
|
+
return rn === fileName2;
|
|
478
|
+
});
|
|
479
|
+
if (isMatchedRootNode) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (fileName2 !== "index.ts" && fileName2 !== "index.js") {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (parentType2 !== "fragment") {
|
|
487
|
+
context.report({
|
|
488
|
+
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType2}).`,
|
|
489
|
+
node
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const contents = readDirCached(relDir2);
|
|
494
|
+
const hasFragmentNode = contents.files.some(
|
|
495
|
+
(f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
|
|
496
|
+
);
|
|
497
|
+
if (!hasFragmentNode) {
|
|
498
|
+
context.report({
|
|
499
|
+
message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
|
|
500
|
+
node
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
const fileName = import_path7.default.basename(relPath);
|
|
392
506
|
if (fileName !== "index.ts" && fileName !== "index.js") {
|
|
393
|
-
return
|
|
507
|
+
return listeners;
|
|
394
508
|
}
|
|
395
|
-
const settings = context.settings;
|
|
396
509
|
if (!settings.faf) {
|
|
397
|
-
return
|
|
510
|
+
return listeners;
|
|
398
511
|
}
|
|
399
512
|
const config = findTreeConfig(relPath, settings.faf);
|
|
400
513
|
if (!config) {
|
|
401
|
-
return
|
|
514
|
+
return listeners;
|
|
402
515
|
}
|
|
403
|
-
const relDir =
|
|
516
|
+
const relDir = import_path7.default.dirname(relPath);
|
|
404
517
|
const parentType = classifyFolder(relDir, config);
|
|
405
518
|
if (parentType !== "fragment") {
|
|
406
|
-
return
|
|
519
|
+
return listeners;
|
|
407
520
|
}
|
|
408
521
|
function checkSource(node, sourceVal) {
|
|
409
522
|
const isValidSibling = /^\.\/[^/]+$/.test(sourceVal);
|
|
@@ -414,11 +527,11 @@ var rule2 = {
|
|
|
414
527
|
});
|
|
415
528
|
return;
|
|
416
529
|
}
|
|
417
|
-
const importedFilename =
|
|
530
|
+
const importedFilename = import_path7.default.basename(sourceVal);
|
|
418
531
|
if (importedFilename === "index" || importedFilename === "index.ts" || importedFilename === "index.js" || importedFilename === "README.md" || importedFilename === "package.json") {
|
|
419
532
|
return;
|
|
420
533
|
}
|
|
421
|
-
const fragmentName =
|
|
534
|
+
const fragmentName = import_path7.default.basename(relDir);
|
|
422
535
|
if (!importedFilename.startsWith(fragmentName + ".")) {
|
|
423
536
|
context.report({
|
|
424
537
|
message: `Access Node (index file) can only re-export its own Fragment Nodes (names starting with "${fragmentName}."). Got "${importedFilename}".`,
|
|
@@ -426,23 +539,22 @@ var rule2 = {
|
|
|
426
539
|
});
|
|
427
540
|
}
|
|
428
541
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (node.source) {
|
|
435
|
-
checkSource(node, node.source.value);
|
|
436
|
-
}
|
|
437
|
-
},
|
|
438
|
-
ImportDeclaration(node) {
|
|
542
|
+
listeners.ExportAllDeclaration = (node) => {
|
|
543
|
+
checkSource(node, node.source.value);
|
|
544
|
+
};
|
|
545
|
+
listeners.ExportNamedDeclaration = (node) => {
|
|
546
|
+
if (node.source) {
|
|
439
547
|
checkSource(node, node.source.value);
|
|
440
548
|
}
|
|
441
549
|
};
|
|
550
|
+
listeners.ImportDeclaration = (node) => {
|
|
551
|
+
checkSource(node, node.source.value);
|
|
552
|
+
};
|
|
553
|
+
return listeners;
|
|
442
554
|
},
|
|
443
555
|
meta: {
|
|
444
556
|
docs: {
|
|
445
|
-
description: "Enforce Access Node
|
|
557
|
+
description: "Enforce Access Node integrity for Fragments"
|
|
446
558
|
},
|
|
447
559
|
schema: [],
|
|
448
560
|
type: "problem"
|
|
@@ -450,136 +562,410 @@ var rule2 = {
|
|
|
450
562
|
};
|
|
451
563
|
var enforce_access_node_rule_default = rule2;
|
|
452
564
|
|
|
453
|
-
// src/_rules/
|
|
454
|
-
var
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
565
|
+
// src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
|
|
566
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
567
|
+
var import_path8 = __toESM(require("path"), 1);
|
|
568
|
+
function resolveImportPath(importPath, currentFileRelative, settings) {
|
|
569
|
+
const cacheKey = `${currentFileRelative}::${importPath}`;
|
|
570
|
+
const cached = state.resolvedImportPathCache.get(cacheKey);
|
|
571
|
+
if (cached !== void 0) {
|
|
572
|
+
return cached;
|
|
460
573
|
}
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
574
|
+
const result = _resolveImportPath(importPath, currentFileRelative, settings);
|
|
575
|
+
state.resolvedImportPathCache.set(cacheKey, result);
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
function _resolveImportPath(importPath, currentFileRelative, settings) {
|
|
579
|
+
const pkgImports = loadPackageImports(state.projectRoot);
|
|
580
|
+
const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
|
|
581
|
+
let matchedAlias = null;
|
|
582
|
+
let longestMatch = 0;
|
|
583
|
+
for (const alias of Object.keys(aliases)) {
|
|
584
|
+
if (importPath === alias || importPath.startsWith(alias + "/")) {
|
|
585
|
+
if (alias.length > longestMatch) {
|
|
586
|
+
longestMatch = alias.length;
|
|
587
|
+
matchedAlias = alias;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
466
590
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
591
|
+
let relResolved;
|
|
592
|
+
if (matchedAlias) {
|
|
593
|
+
const target = aliases[matchedAlias];
|
|
594
|
+
if (target) {
|
|
595
|
+
const remainder = importPath.substring(matchedAlias.length);
|
|
596
|
+
const absTarget = import_path8.default.resolve(state.projectRoot, target + remainder);
|
|
597
|
+
relResolved = toRelativePath(absTarget);
|
|
598
|
+
} else {
|
|
599
|
+
return "";
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
603
|
+
const absCurrentDir = import_path8.default.dirname(
|
|
604
|
+
import_path8.default.resolve(state.projectRoot, currentFileRelative)
|
|
605
|
+
);
|
|
606
|
+
const absResolved = import_path8.default.resolve(absCurrentDir, importPath);
|
|
607
|
+
relResolved = toRelativePath(absResolved);
|
|
608
|
+
} else {
|
|
609
|
+
return "";
|
|
471
610
|
}
|
|
472
611
|
}
|
|
473
|
-
return
|
|
612
|
+
return finalizeResolvedPath(relResolved);
|
|
613
|
+
}
|
|
614
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
615
|
+
"cjs",
|
|
616
|
+
"cts",
|
|
617
|
+
"js",
|
|
618
|
+
"jsx",
|
|
619
|
+
"mjs",
|
|
620
|
+
"mts",
|
|
621
|
+
"ts",
|
|
622
|
+
"tsx"
|
|
623
|
+
]);
|
|
624
|
+
function finalizeResolvedPath(relResolved) {
|
|
625
|
+
const resolvedDir = import_path8.default.dirname(relResolved).replace(/\\/g, "/");
|
|
626
|
+
const resolvedBase = import_path8.default.basename(relResolved);
|
|
627
|
+
const dirContents = readDirCached(relResolved);
|
|
628
|
+
if (dirContents.files.includes("index.ts")) {
|
|
629
|
+
return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
|
|
630
|
+
}
|
|
631
|
+
if (dirContents.files.includes("index.js")) {
|
|
632
|
+
return (relResolved === "." ? "" : relResolved + "/") + "index.js";
|
|
633
|
+
}
|
|
634
|
+
const parentContents = readDirCached(resolvedDir);
|
|
635
|
+
const exactMatch = parentContents.files.find((f) => f === resolvedBase);
|
|
636
|
+
if (!exactMatch) {
|
|
637
|
+
const candidates = parentContents.files.filter(
|
|
638
|
+
(f) => f.startsWith(resolvedBase + ".")
|
|
639
|
+
);
|
|
640
|
+
const matchWithExt = candidates.find(
|
|
641
|
+
(f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
|
|
642
|
+
) ?? candidates[0];
|
|
643
|
+
if (matchWithExt) {
|
|
644
|
+
return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return relResolved;
|
|
648
|
+
}
|
|
649
|
+
function loadPackageImports(projectRootPath) {
|
|
650
|
+
if (state.packageImports !== null) {
|
|
651
|
+
return state.packageImports;
|
|
652
|
+
}
|
|
653
|
+
state.packageImports = {};
|
|
654
|
+
const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
|
|
655
|
+
if (isTest) {
|
|
656
|
+
return state.packageImports;
|
|
657
|
+
}
|
|
658
|
+
const pkgPath = import_path8.default.resolve(projectRootPath, "package.json");
|
|
659
|
+
if (import_fs2.default.existsSync(pkgPath)) {
|
|
660
|
+
try {
|
|
661
|
+
const pkgContent = import_fs2.default.readFileSync(pkgPath, "utf8");
|
|
662
|
+
const pkg = JSON.parse(pkgContent);
|
|
663
|
+
if (pkg.imports) {
|
|
664
|
+
for (const [key, val] of Object.entries(pkg.imports)) {
|
|
665
|
+
if (typeof val === "string") {
|
|
666
|
+
const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
|
|
667
|
+
let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
|
|
668
|
+
if (cleanVal.startsWith("./")) {
|
|
669
|
+
cleanVal = cleanVal.substring(2);
|
|
670
|
+
} else if (cleanVal.startsWith("/")) {
|
|
671
|
+
cleanVal = cleanVal.substring(1);
|
|
672
|
+
}
|
|
673
|
+
state.packageImports[cleanKey] = cleanVal;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return state.packageImports;
|
|
474
681
|
}
|
|
475
682
|
|
|
476
|
-
// src/_rules/_rules@shared/_utils/_primitives/
|
|
477
|
-
function
|
|
478
|
-
|
|
683
|
+
// src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
|
|
684
|
+
function findTreeConfigIncludingExcluded(relPath, settings) {
|
|
685
|
+
if (!settings || !settings.trees) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const cached = state.treeConfigIncludingExcludedCache.get(relPath);
|
|
689
|
+
if (cached !== void 0) {
|
|
690
|
+
return cached;
|
|
691
|
+
}
|
|
692
|
+
let result = null;
|
|
693
|
+
for (const tree of settings.trees) {
|
|
694
|
+
const isIncluded = tree.includes.some(
|
|
695
|
+
(inc) => relPath === inc || relPath.startsWith(inc + "/")
|
|
696
|
+
);
|
|
697
|
+
if (isIncluded) {
|
|
698
|
+
result = tree;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
state.treeConfigIncludingExcludedCache.set(relPath, result);
|
|
703
|
+
return result;
|
|
479
704
|
}
|
|
480
705
|
|
|
481
|
-
// src/_rules/
|
|
482
|
-
var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
483
|
-
var DEFAULT_HTTP_METHODS = [
|
|
484
|
-
"connect",
|
|
485
|
-
"delete",
|
|
486
|
-
"get",
|
|
487
|
-
"head",
|
|
488
|
-
"options",
|
|
489
|
-
"patch",
|
|
490
|
-
"post",
|
|
491
|
-
"put",
|
|
492
|
-
"trace"
|
|
493
|
-
];
|
|
706
|
+
// src/_rules/foreign-domain-isolation/foreign-domain-isolation.rule.ts
|
|
494
707
|
var rule3 = {
|
|
495
708
|
create(context) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
709
|
+
const absPath = context.filename;
|
|
710
|
+
const relPath = toRelativePath(absPath);
|
|
711
|
+
const settings = context.settings;
|
|
712
|
+
if (!settings.faf) {
|
|
713
|
+
return {};
|
|
714
|
+
}
|
|
715
|
+
const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
|
|
716
|
+
if (!config) {
|
|
717
|
+
return {};
|
|
718
|
+
}
|
|
719
|
+
const isCurrentFileExcluded = config.excludes?.some(
|
|
720
|
+
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
721
|
+
) ?? false;
|
|
722
|
+
function checkImport(node, importValue) {
|
|
723
|
+
const resolvedRelPath = resolveImportPath(
|
|
724
|
+
importValue,
|
|
725
|
+
relPath,
|
|
726
|
+
settings.faf
|
|
727
|
+
);
|
|
728
|
+
if (!resolvedRelPath) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const isImportedFileExcluded = config?.excludes?.some(
|
|
732
|
+
(exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
|
|
733
|
+
) ?? false;
|
|
734
|
+
if (!isCurrentFileExcluded && isImportedFileExcluded) {
|
|
735
|
+
context.report({
|
|
736
|
+
message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
|
|
737
|
+
node
|
|
738
|
+
});
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (isCurrentFileExcluded && !isImportedFileExcluded) {
|
|
742
|
+
context.report({
|
|
743
|
+
message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
|
|
744
|
+
node
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
ExportAllDeclaration(node) {
|
|
750
|
+
checkImport(node, node.source.value);
|
|
751
|
+
},
|
|
752
|
+
ExportNamedDeclaration(node) {
|
|
753
|
+
if (node.source) {
|
|
754
|
+
checkImport(node, node.source.value);
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
ImportDeclaration(node) {
|
|
758
|
+
checkImport(node, node.source.value);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
},
|
|
762
|
+
meta: {
|
|
763
|
+
docs: {
|
|
764
|
+
description: "Enforce isolation between Foreign Domains and Logical Nodes"
|
|
765
|
+
},
|
|
766
|
+
schema: [],
|
|
767
|
+
type: "problem"
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
var foreign_domain_isolation_rule_default = rule3;
|
|
771
|
+
|
|
772
|
+
// src/_rules/fragment-master-node/fragment-master-node.rule.ts
|
|
773
|
+
var import_path9 = __toESM(require("path"), 1);
|
|
774
|
+
|
|
775
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-file-role/get-file-role.util.ts
|
|
776
|
+
function getFileRole(fileName, config) {
|
|
777
|
+
if (fileName === "index.ts" || fileName === "index.js") {
|
|
778
|
+
return "index";
|
|
779
|
+
}
|
|
780
|
+
const parts = fileName.split(".");
|
|
781
|
+
let allRoles = state.rolesCache.get(config);
|
|
782
|
+
if (!allRoles) {
|
|
783
|
+
allRoles = new Set(config.roles.flat());
|
|
784
|
+
state.rolesCache.set(config, allRoles);
|
|
785
|
+
}
|
|
786
|
+
for (let i = parts.length - 2; i >= 1; i--) {
|
|
787
|
+
const part = parts[i];
|
|
788
|
+
if (part && allRoles.has(part)) {
|
|
789
|
+
return part;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/_rules/fragment-master-node/fragment-master-node.rule.ts
|
|
796
|
+
var DEFAULT_HTTP_METHODS = [
|
|
797
|
+
"connect",
|
|
798
|
+
"delete",
|
|
799
|
+
"get",
|
|
800
|
+
"head",
|
|
801
|
+
"options",
|
|
802
|
+
"patch",
|
|
803
|
+
"post",
|
|
804
|
+
"put",
|
|
805
|
+
"trace"
|
|
806
|
+
];
|
|
807
|
+
var rule4 = {
|
|
808
|
+
create(context) {
|
|
809
|
+
return {
|
|
810
|
+
Program(node) {
|
|
811
|
+
const absPath = context.filename;
|
|
812
|
+
const relPath = toRelativePath(absPath);
|
|
813
|
+
const settings = context.settings;
|
|
814
|
+
if (!settings.faf) {
|
|
506
815
|
return;
|
|
507
816
|
}
|
|
508
|
-
const
|
|
509
|
-
|
|
817
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
818
|
+
if (!config) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const relDir = import_path9.default.dirname(relPath);
|
|
822
|
+
const folderName = import_path9.default.basename(relDir);
|
|
510
823
|
const parentType = classifyFolder(relDir, config);
|
|
511
|
-
if (parentType
|
|
824
|
+
if (parentType !== "fragment") {
|
|
512
825
|
return;
|
|
513
826
|
}
|
|
514
|
-
const fileName =
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
827
|
+
const fileName = import_path9.default.basename(relPath);
|
|
828
|
+
const rfConfig = getRootFragmentConfig(relDir, config);
|
|
829
|
+
if (rfConfig) {
|
|
830
|
+
const flatRootNodes = rfConfig.rootNodes.flat();
|
|
831
|
+
const isMatchedRootNode = flatRootNodes.some((rn) => {
|
|
832
|
+
if (rn.includes("/")) {
|
|
833
|
+
const rfPath = rfConfig.paths[0] ?? "";
|
|
834
|
+
const absRn = import_path9.default.resolve(getProjectRoot(), rfPath, rn);
|
|
835
|
+
return toRelativePath(absRn) === relPath;
|
|
836
|
+
}
|
|
837
|
+
return rn === fileName;
|
|
838
|
+
});
|
|
839
|
+
if (isMatchedRootNode) {
|
|
840
|
+
return;
|
|
525
841
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
842
|
+
}
|
|
843
|
+
if (fileName === "index.ts" || fileName === "index.js") {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const role = getFileRole(fileName, config);
|
|
850
|
+
const ext = import_path9.default.extname(fileName);
|
|
851
|
+
const allRoleNames = new Set(config.roles.flat());
|
|
852
|
+
const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
|
|
853
|
+
const matchedRoute = config.routeHierarchies?.find(
|
|
854
|
+
(rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
|
|
855
|
+
);
|
|
856
|
+
if (matchedRoute) {
|
|
857
|
+
const contents = readDirCached(relDir);
|
|
858
|
+
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
859
|
+
if (!fileRoles.includes(matchedRoute.role)) {
|
|
529
860
|
context.report({
|
|
530
|
-
message: `
|
|
861
|
+
message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
|
|
531
862
|
node
|
|
532
863
|
});
|
|
533
864
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
865
|
+
const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
|
|
866
|
+
const routeNameRegex = new RegExp(
|
|
867
|
+
`^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
|
|
868
|
+
);
|
|
869
|
+
if (!routeNameRegex.test(folderName)) {
|
|
870
|
+
context.report({
|
|
871
|
+
message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
|
|
872
|
+
node
|
|
873
|
+
});
|
|
543
874
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (!isKebab) {
|
|
557
|
-
context.report({
|
|
558
|
-
message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
|
|
559
|
-
node
|
|
560
|
-
});
|
|
561
|
-
}
|
|
875
|
+
}
|
|
876
|
+
let currentParent = import_path9.default.dirname(relDir).replace(/\\/g, "/");
|
|
877
|
+
let categoryConfig = null;
|
|
878
|
+
let categoryFolderName = "";
|
|
879
|
+
while (currentParent && currentParent !== "." && currentParent !== "/") {
|
|
880
|
+
const currentParentType = classifyFolder(currentParent, config);
|
|
881
|
+
if (currentParentType === "category") {
|
|
882
|
+
categoryConfig = getCategoryConfig(currentParent, config);
|
|
883
|
+
categoryFolderName = import_path9.default.basename(currentParent);
|
|
884
|
+
break;
|
|
885
|
+
} else if (currentParentType === "fragment" || currentParentType === "root-fragment" || currentParentType === "layer") {
|
|
886
|
+
break;
|
|
562
887
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
888
|
+
const nextParent = import_path9.default.dirname(currentParent).replace(/\\/g, "/");
|
|
889
|
+
if (nextParent === currentParent) break;
|
|
890
|
+
currentParent = nextParent;
|
|
891
|
+
}
|
|
892
|
+
if (categoryConfig && categoryConfig.role) {
|
|
893
|
+
const contents = readDirCached(relDir);
|
|
894
|
+
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
895
|
+
if (!fileRoles.includes(categoryConfig.role)) {
|
|
896
|
+
context.report({
|
|
897
|
+
message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
|
|
898
|
+
node
|
|
899
|
+
});
|
|
573
900
|
}
|
|
574
|
-
|
|
901
|
+
}
|
|
902
|
+
if (!fileName.startsWith(folderName + ".")) {
|
|
903
|
+
context.report({
|
|
904
|
+
message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
|
|
905
|
+
node
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
if (!role) {
|
|
909
|
+
context.report({
|
|
910
|
+
message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
|
|
911
|
+
node
|
|
912
|
+
});
|
|
913
|
+
} else if (roleSegments.length > 1) {
|
|
914
|
+
} else {
|
|
915
|
+
const contents = readDirCached(relDir);
|
|
916
|
+
const duplicate = contents.files.find(
|
|
917
|
+
(f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
|
|
918
|
+
);
|
|
919
|
+
if (duplicate) {
|
|
575
920
|
context.report({
|
|
576
|
-
message: `Fragment
|
|
921
|
+
message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
|
|
577
922
|
node
|
|
578
923
|
});
|
|
579
924
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
},
|
|
929
|
+
meta: {
|
|
930
|
+
docs: {
|
|
931
|
+
description: "Enforce Fragment Node identity and Master Node integrity in FAF Fragments"
|
|
932
|
+
},
|
|
933
|
+
schema: [],
|
|
934
|
+
type: "problem"
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
var fragment_master_node_rule_default = rule4;
|
|
938
|
+
|
|
939
|
+
// src/_rules/logical-domain-placement/logical-domain-placement.rule.ts
|
|
940
|
+
var import_path10 = __toESM(require("path"), 1);
|
|
941
|
+
var rule5 = {
|
|
942
|
+
create(context) {
|
|
943
|
+
return {
|
|
944
|
+
Program(node) {
|
|
945
|
+
const absPath = context.filename;
|
|
946
|
+
const relPath = toRelativePath(absPath);
|
|
947
|
+
const settings = context.settings;
|
|
948
|
+
if (!settings.faf) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
952
|
+
if (!config) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const relDir = import_path10.default.dirname(relPath);
|
|
956
|
+
const folderName = import_path10.default.basename(relDir);
|
|
957
|
+
const parentType = classifyFolder(relDir, config);
|
|
958
|
+
if (parentType === "foreign") {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const fileName = import_path10.default.basename(relPath);
|
|
962
|
+
for (const {
|
|
963
|
+
folderName: currentFolderName,
|
|
964
|
+
parentDir: relParentDir,
|
|
965
|
+
parentFolderName,
|
|
966
|
+
parentType: relParentType,
|
|
967
|
+
type: currentType
|
|
968
|
+
} of walkAncestors(relDir, config)) {
|
|
583
969
|
if (currentType === "root-fragment") {
|
|
584
970
|
if (relParentType !== "root-fragment" && relParentType !== "unknown") {
|
|
585
971
|
context.report({
|
|
@@ -635,7 +1021,6 @@ var rule3 = {
|
|
|
635
1021
|
});
|
|
636
1022
|
}
|
|
637
1023
|
}
|
|
638
|
-
currentDir = import_path7.default.dirname(currentDir).replace(/\\/g, "/");
|
|
639
1024
|
}
|
|
640
1025
|
if (parentType === "layer") {
|
|
641
1026
|
context.report({
|
|
@@ -649,9 +1034,96 @@ var rule3 = {
|
|
|
649
1034
|
message: `Fractal Branches cannot contain files directly. File "${fileName}" is placed directly inside Fractal Branch "${folderName}".`,
|
|
650
1035
|
node
|
|
651
1036
|
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
},
|
|
1041
|
+
meta: {
|
|
1042
|
+
docs: {
|
|
1043
|
+
description: "Enforce topological placement constraints between FAF Logical Domains"
|
|
1044
|
+
},
|
|
1045
|
+
schema: [],
|
|
1046
|
+
type: "problem"
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
var logical_domain_placement_rule_default = rule5;
|
|
1050
|
+
|
|
1051
|
+
// src/_rules/naming-conventions/naming-conventions.rule.ts
|
|
1052
|
+
var import_path11 = __toESM(require("path"), 1);
|
|
1053
|
+
var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
1054
|
+
var rule6 = {
|
|
1055
|
+
create(context) {
|
|
1056
|
+
return {
|
|
1057
|
+
Program(node) {
|
|
1058
|
+
const absPath = context.filename;
|
|
1059
|
+
const relPath = toRelativePath(absPath);
|
|
1060
|
+
const settings = context.settings;
|
|
1061
|
+
if (!settings.faf) {
|
|
652
1062
|
return;
|
|
653
1063
|
}
|
|
654
|
-
|
|
1064
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
1065
|
+
if (!config) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const relDir = import_path11.default.dirname(relPath);
|
|
1069
|
+
const parentType = classifyFolder(relDir, config);
|
|
1070
|
+
if (parentType === "foreign") {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const fileName = import_path11.default.basename(relPath);
|
|
1074
|
+
for (const {
|
|
1075
|
+
dir: currentDir,
|
|
1076
|
+
folderName: currentFolderName,
|
|
1077
|
+
type: currentType
|
|
1078
|
+
} of walkAncestors(relDir, config)) {
|
|
1079
|
+
if ((currentType === "layer" || currentType === "category") && !currentFolderName.startsWith("_")) {
|
|
1080
|
+
context.report({
|
|
1081
|
+
message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
|
|
1082
|
+
node
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
if (currentFolderName.startsWith("_") && !currentFolderName.includes("@shared")) {
|
|
1086
|
+
const contents = readDirCached(currentDir);
|
|
1087
|
+
const hasIndex = contents.files.includes("index.ts") || contents.files.includes("index.js");
|
|
1088
|
+
if (hasIndex) {
|
|
1089
|
+
context.report({
|
|
1090
|
+
message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
|
|
1091
|
+
node
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
if (currentFolderName !== "src") {
|
|
1096
|
+
let baseFolderName = currentFolderName.replace(/@shared$/, "");
|
|
1097
|
+
if (baseFolderName.startsWith("_")) {
|
|
1098
|
+
baseFolderName = baseFolderName.substring(1);
|
|
1099
|
+
}
|
|
1100
|
+
if (baseFolderName.startsWith("[") && baseFolderName.endsWith("]")) {
|
|
1101
|
+
baseFolderName = baseFolderName.substring(
|
|
1102
|
+
1,
|
|
1103
|
+
baseFolderName.length - 1
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const isKebab = KEBAB_CASE_REGEX.test(baseFolderName);
|
|
1107
|
+
if (!isKebab) {
|
|
1108
|
+
context.report({
|
|
1109
|
+
message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
|
|
1110
|
+
node
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (currentType === "fractal-branch") {
|
|
1115
|
+
const expectedScope = import_path11.default.basename(import_path11.default.dirname(currentDir));
|
|
1116
|
+
const normalizedExpectedScope = expectedScope.startsWith("_") ? expectedScope.substring(1) : expectedScope;
|
|
1117
|
+
const expectedName = `_${normalizedExpectedScope}@shared`;
|
|
1118
|
+
if (currentFolderName !== expectedName) {
|
|
1119
|
+
context.report({
|
|
1120
|
+
message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
|
|
1121
|
+
node
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (parentType === "layer" || parentType === "fractal-branch" || parentType === "invalid-fragment") {
|
|
655
1127
|
return;
|
|
656
1128
|
}
|
|
657
1129
|
const rfConfig = getRootFragmentConfig(relDir, config);
|
|
@@ -660,7 +1132,7 @@ var rule3 = {
|
|
|
660
1132
|
const isMatchedRootNode = flatRootNodes.some((rn) => {
|
|
661
1133
|
if (rn.includes("/")) {
|
|
662
1134
|
const rfPath = rfConfig.paths[0] ?? "";
|
|
663
|
-
const absRn =
|
|
1135
|
+
const absRn = import_path11.default.resolve(getProjectRoot(), rfPath, rn);
|
|
664
1136
|
return toRelativePath(absRn) === relPath;
|
|
665
1137
|
}
|
|
666
1138
|
return rn === fileName;
|
|
@@ -670,288 +1142,79 @@ var rule3 = {
|
|
|
670
1142
|
}
|
|
671
1143
|
}
|
|
672
1144
|
if (fileName === "index.ts" || fileName === "index.js") {
|
|
673
|
-
if (parentType !== "fragment") {
|
|
674
|
-
context.report({
|
|
675
|
-
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
|
|
676
|
-
node
|
|
677
|
-
});
|
|
678
|
-
} else {
|
|
679
|
-
const contents = readDirCached(relDir);
|
|
680
|
-
const hasFragmentNode = contents.files.some(
|
|
681
|
-
(f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
|
|
682
|
-
);
|
|
683
|
-
if (!hasFragmentNode) {
|
|
684
|
-
context.report({
|
|
685
|
-
message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
|
|
686
|
-
node
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
1145
|
return;
|
|
691
1146
|
}
|
|
692
1147
|
if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
|
|
693
1148
|
return;
|
|
694
1149
|
}
|
|
695
1150
|
const role = getFileRole(fileName, config);
|
|
696
|
-
const ext =
|
|
1151
|
+
const ext = import_path11.default.extname(fileName);
|
|
697
1152
|
const allRoleNames = new Set(config.roles.flat());
|
|
698
1153
|
const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
|
|
699
1154
|
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.`;
|
|
700
|
-
if (parentType === "fragment") {
|
|
701
|
-
|
|
702
|
-
(rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
|
|
703
|
-
);
|
|
704
|
-
if (matchedRoute) {
|
|
705
|
-
const contents = readDirCached(relDir);
|
|
706
|
-
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
707
|
-
if (!fileRoles.includes(matchedRoute.role)) {
|
|
708
|
-
context.report({
|
|
709
|
-
message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
|
|
710
|
-
node
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
|
|
714
|
-
const routeNameRegex = new RegExp(
|
|
715
|
-
`^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
|
|
716
|
-
);
|
|
717
|
-
if (!routeNameRegex.test(folderName)) {
|
|
718
|
-
context.report({
|
|
719
|
-
message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
|
|
720
|
-
node
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
let currentParent = import_path7.default.dirname(relDir).replace(/\\/g, "/");
|
|
725
|
-
let categoryConfig = null;
|
|
726
|
-
let categoryFolderName = "";
|
|
727
|
-
while (currentParent && currentParent !== "." && currentParent !== "/") {
|
|
728
|
-
const parentType2 = classifyFolder(currentParent, config);
|
|
729
|
-
if (parentType2 === "category") {
|
|
730
|
-
categoryConfig = getCategoryConfig(currentParent, config);
|
|
731
|
-
categoryFolderName = import_path7.default.basename(currentParent);
|
|
732
|
-
break;
|
|
733
|
-
} else if (parentType2 === "fragment" || parentType2 === "root-fragment" || parentType2 === "layer") {
|
|
734
|
-
break;
|
|
735
|
-
}
|
|
736
|
-
const nextParent = import_path7.default.dirname(currentParent).replace(/\\/g, "/");
|
|
737
|
-
if (nextParent === currentParent) break;
|
|
738
|
-
currentParent = nextParent;
|
|
739
|
-
}
|
|
740
|
-
if (categoryConfig && categoryConfig.role) {
|
|
741
|
-
const contents = readDirCached(relDir);
|
|
742
|
-
const fileRoles = contents.files.map((f) => getFileRole(f, config));
|
|
743
|
-
if (!fileRoles.includes(categoryConfig.role)) {
|
|
744
|
-
context.report({
|
|
745
|
-
message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
|
|
746
|
-
node
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
if (!fileName.startsWith(folderName + ".")) {
|
|
751
|
-
context.report({
|
|
752
|
-
message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
|
|
753
|
-
node
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
if (!role) {
|
|
757
|
-
context.report({
|
|
758
|
-
message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
|
|
759
|
-
node
|
|
760
|
-
});
|
|
761
|
-
} else if (roleSegments.length > 1) {
|
|
762
|
-
context.report({ message: multiRoleMessage, node });
|
|
763
|
-
} else {
|
|
764
|
-
const contents = readDirCached(relDir);
|
|
765
|
-
const duplicate = contents.files.find(
|
|
766
|
-
(f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
|
|
767
|
-
);
|
|
768
|
-
if (duplicate) {
|
|
769
|
-
context.report({
|
|
770
|
-
message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
|
|
771
|
-
node
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
}
|
|
1155
|
+
if (parentType === "fragment" && role && roleSegments.length > 1) {
|
|
1156
|
+
context.report({ message: multiRoleMessage, node });
|
|
775
1157
|
}
|
|
776
1158
|
if (parentType === "category") {
|
|
1159
|
+
const folderName = import_path11.default.basename(relDir);
|
|
777
1160
|
const catConfig = getCategoryConfig(relDir, config);
|
|
778
1161
|
if (catConfig) {
|
|
779
1162
|
const allowedExts = catConfig.allowedExtensions ?? [
|
|
780
1163
|
".js",
|
|
781
1164
|
".jsx",
|
|
782
1165
|
".ts",
|
|
783
|
-
".tsx"
|
|
784
|
-
];
|
|
785
|
-
if (!allowedExts.includes(ext)) {
|
|
786
|
-
context.report({
|
|
787
|
-
message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
|
|
788
|
-
node
|
|
789
|
-
});
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
|
|
793
|
-
if (isCodeExt) {
|
|
794
|
-
if (!role) {
|
|
795
|
-
context.report({
|
|
796
|
-
message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
|
|
797
|
-
node
|
|
798
|
-
});
|
|
799
|
-
} else if (role !== catConfig.role) {
|
|
800
|
-
context.report({
|
|
801
|
-
message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
802
|
-
node
|
|
803
|
-
});
|
|
804
|
-
} else if (roleSegments.length > 1) {
|
|
805
|
-
context.report({ message: multiRoleMessage, node });
|
|
806
|
-
}
|
|
807
|
-
} else {
|
|
808
|
-
if (role && role !== catConfig.role) {
|
|
809
|
-
context.report({
|
|
810
|
-
message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
811
|
-
node
|
|
812
|
-
});
|
|
813
|
-
} else if (role && roleSegments.length > 1) {
|
|
814
|
-
context.report({ message: multiRoleMessage, node });
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
},
|
|
822
|
-
meta: {
|
|
823
|
-
docs: {
|
|
824
|
-
description: "Enforce naming conventions and folder taxonomy in FAF architecture"
|
|
825
|
-
},
|
|
826
|
-
schema: [],
|
|
827
|
-
type: "problem"
|
|
828
|
-
}
|
|
829
|
-
};
|
|
830
|
-
var naming_conventions_rule_default = rule3;
|
|
831
|
-
|
|
832
|
-
// src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
|
|
833
|
-
var import_path9 = __toESM(require("path"), 1);
|
|
834
|
-
|
|
835
|
-
// src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
|
|
836
|
-
var import_fs2 = __toESM(require("fs"), 1);
|
|
837
|
-
var import_path8 = __toESM(require("path"), 1);
|
|
838
|
-
function resolveImportPath(importPath, currentFileRelative, settings) {
|
|
839
|
-
const cacheKey = `${currentFileRelative}::${importPath}`;
|
|
840
|
-
const cached = state.resolvedImportPathCache.get(cacheKey);
|
|
841
|
-
if (cached !== void 0) {
|
|
842
|
-
return cached;
|
|
843
|
-
}
|
|
844
|
-
const result = _resolveImportPath(importPath, currentFileRelative, settings);
|
|
845
|
-
state.resolvedImportPathCache.set(cacheKey, result);
|
|
846
|
-
return result;
|
|
847
|
-
}
|
|
848
|
-
function _resolveImportPath(importPath, currentFileRelative, settings) {
|
|
849
|
-
const pkgImports = loadPackageImports(state.projectRoot);
|
|
850
|
-
const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
|
|
851
|
-
let matchedAlias = null;
|
|
852
|
-
let longestMatch = 0;
|
|
853
|
-
for (const alias of Object.keys(aliases)) {
|
|
854
|
-
if (importPath === alias || importPath.startsWith(alias + "/")) {
|
|
855
|
-
if (alias.length > longestMatch) {
|
|
856
|
-
longestMatch = alias.length;
|
|
857
|
-
matchedAlias = alias;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
let relResolved;
|
|
862
|
-
if (matchedAlias) {
|
|
863
|
-
const target = aliases[matchedAlias];
|
|
864
|
-
if (target) {
|
|
865
|
-
const remainder = importPath.substring(matchedAlias.length);
|
|
866
|
-
const absTarget = import_path8.default.resolve(state.projectRoot, target + remainder);
|
|
867
|
-
relResolved = toRelativePath(absTarget);
|
|
868
|
-
} else {
|
|
869
|
-
return "";
|
|
870
|
-
}
|
|
871
|
-
} else {
|
|
872
|
-
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
873
|
-
const absCurrentDir = import_path8.default.dirname(
|
|
874
|
-
import_path8.default.resolve(state.projectRoot, currentFileRelative)
|
|
875
|
-
);
|
|
876
|
-
const absResolved = import_path8.default.resolve(absCurrentDir, importPath);
|
|
877
|
-
relResolved = toRelativePath(absResolved);
|
|
878
|
-
} else {
|
|
879
|
-
return "";
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
return finalizeResolvedPath(relResolved);
|
|
883
|
-
}
|
|
884
|
-
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
885
|
-
"cjs",
|
|
886
|
-
"cts",
|
|
887
|
-
"js",
|
|
888
|
-
"jsx",
|
|
889
|
-
"mjs",
|
|
890
|
-
"mts",
|
|
891
|
-
"ts",
|
|
892
|
-
"tsx"
|
|
893
|
-
]);
|
|
894
|
-
function finalizeResolvedPath(relResolved) {
|
|
895
|
-
const resolvedDir = import_path8.default.dirname(relResolved).replace(/\\/g, "/");
|
|
896
|
-
const resolvedBase = import_path8.default.basename(relResolved);
|
|
897
|
-
const dirContents = readDirCached(relResolved);
|
|
898
|
-
if (dirContents.files.includes("index.ts")) {
|
|
899
|
-
return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
|
|
900
|
-
}
|
|
901
|
-
if (dirContents.files.includes("index.js")) {
|
|
902
|
-
return (relResolved === "." ? "" : relResolved + "/") + "index.js";
|
|
903
|
-
}
|
|
904
|
-
const parentContents = readDirCached(resolvedDir);
|
|
905
|
-
const exactMatch = parentContents.files.find((f) => f === resolvedBase);
|
|
906
|
-
if (!exactMatch) {
|
|
907
|
-
const candidates = parentContents.files.filter(
|
|
908
|
-
(f) => f.startsWith(resolvedBase + ".")
|
|
909
|
-
);
|
|
910
|
-
const matchWithExt = candidates.find(
|
|
911
|
-
(f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
|
|
912
|
-
) ?? candidates[0];
|
|
913
|
-
if (matchWithExt) {
|
|
914
|
-
return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
return relResolved;
|
|
918
|
-
}
|
|
919
|
-
function loadPackageImports(projectRootPath) {
|
|
920
|
-
if (state.packageImports !== null) {
|
|
921
|
-
return state.packageImports;
|
|
922
|
-
}
|
|
923
|
-
state.packageImports = {};
|
|
924
|
-
const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
|
|
925
|
-
if (isTest) {
|
|
926
|
-
return state.packageImports;
|
|
927
|
-
}
|
|
928
|
-
const pkgPath = import_path8.default.resolve(projectRootPath, "package.json");
|
|
929
|
-
if (import_fs2.default.existsSync(pkgPath)) {
|
|
930
|
-
try {
|
|
931
|
-
const pkgContent = import_fs2.default.readFileSync(pkgPath, "utf8");
|
|
932
|
-
const pkg = JSON.parse(pkgContent);
|
|
933
|
-
if (pkg.imports) {
|
|
934
|
-
for (const [key, val] of Object.entries(pkg.imports)) {
|
|
935
|
-
if (typeof val === "string") {
|
|
936
|
-
const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
|
|
937
|
-
let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
|
|
938
|
-
if (cleanVal.startsWith("./")) {
|
|
939
|
-
cleanVal = cleanVal.substring(2);
|
|
940
|
-
} else if (cleanVal.startsWith("/")) {
|
|
941
|
-
cleanVal = cleanVal.substring(1);
|
|
1166
|
+
".tsx"
|
|
1167
|
+
];
|
|
1168
|
+
if (!allowedExts.includes(ext)) {
|
|
1169
|
+
context.report({
|
|
1170
|
+
message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
|
|
1171
|
+
node
|
|
1172
|
+
});
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
|
|
1176
|
+
if (isCodeExt) {
|
|
1177
|
+
if (!role) {
|
|
1178
|
+
context.report({
|
|
1179
|
+
message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
|
|
1180
|
+
node
|
|
1181
|
+
});
|
|
1182
|
+
} else if (role !== catConfig.role) {
|
|
1183
|
+
context.report({
|
|
1184
|
+
message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
1185
|
+
node
|
|
1186
|
+
});
|
|
1187
|
+
} else if (roleSegments.length > 1) {
|
|
1188
|
+
context.report({ message: multiRoleMessage, node });
|
|
1189
|
+
}
|
|
1190
|
+
} else {
|
|
1191
|
+
if (role && role !== catConfig.role) {
|
|
1192
|
+
context.report({
|
|
1193
|
+
message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
1194
|
+
node
|
|
1195
|
+
});
|
|
1196
|
+
} else if (role && roleSegments.length > 1) {
|
|
1197
|
+
context.report({ message: multiRoleMessage, node });
|
|
1198
|
+
}
|
|
942
1199
|
}
|
|
943
|
-
state.packageImports[cleanKey] = cleanVal;
|
|
944
1200
|
}
|
|
945
1201
|
}
|
|
946
1202
|
}
|
|
947
|
-
}
|
|
948
|
-
|
|
1203
|
+
};
|
|
1204
|
+
},
|
|
1205
|
+
meta: {
|
|
1206
|
+
docs: {
|
|
1207
|
+
description: "Enforce lexical naming conventions for FAF folders and Logical Nodes"
|
|
1208
|
+
},
|
|
1209
|
+
schema: [],
|
|
1210
|
+
type: "problem"
|
|
949
1211
|
}
|
|
950
|
-
|
|
951
|
-
|
|
1212
|
+
};
|
|
1213
|
+
var naming_conventions_rule_default = rule6;
|
|
952
1214
|
|
|
953
1215
|
// src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
|
|
954
|
-
var
|
|
1216
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
1217
|
+
var rule7 = {
|
|
955
1218
|
create(context) {
|
|
956
1219
|
const absPath = context.filename;
|
|
957
1220
|
const relPath = toRelativePath(absPath);
|
|
@@ -972,13 +1235,13 @@ var rule4 = {
|
|
|
972
1235
|
if (!resolvedRelPath) {
|
|
973
1236
|
return;
|
|
974
1237
|
}
|
|
975
|
-
const importedDir =
|
|
976
|
-
const importedFile =
|
|
1238
|
+
const importedDir = import_path12.default.dirname(resolvedRelPath);
|
|
1239
|
+
const importedFile = import_path12.default.basename(resolvedRelPath);
|
|
977
1240
|
const importedFolderType = classifyFolder(importedDir, config);
|
|
978
1241
|
if (importedFolderType === "fragment") {
|
|
979
1242
|
const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
|
|
980
1243
|
if (!isAccessNode) {
|
|
981
|
-
const isInside =
|
|
1244
|
+
const isInside = import_path12.default.dirname(relPath) === importedDir;
|
|
982
1245
|
if (!isInside) {
|
|
983
1246
|
context.report({
|
|
984
1247
|
message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
|
|
@@ -1010,21 +1273,21 @@ var rule4 = {
|
|
|
1010
1273
|
type: "problem"
|
|
1011
1274
|
}
|
|
1012
1275
|
};
|
|
1013
|
-
var no_direct_fragment_import_rule_default =
|
|
1276
|
+
var no_direct_fragment_import_rule_default = rule7;
|
|
1014
1277
|
|
|
1015
1278
|
// src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
|
|
1016
|
-
var
|
|
1279
|
+
var import_path15 = __toESM(require("path"), 1);
|
|
1017
1280
|
|
|
1018
1281
|
// src/_rules/_rules@shared/_utils/_primitives/get-fractal-branch-owner/get-fractal-branch-owner.util.ts
|
|
1019
|
-
var
|
|
1282
|
+
var import_path13 = __toESM(require("path"), 1);
|
|
1020
1283
|
function getFractalBranchOwner(relPath) {
|
|
1021
1284
|
let current = relPath;
|
|
1022
1285
|
while (current && current !== "." && current !== "/") {
|
|
1023
|
-
const folderName =
|
|
1286
|
+
const folderName = import_path13.default.basename(current);
|
|
1024
1287
|
if (folderName.startsWith("_") && folderName.includes("@shared")) {
|
|
1025
|
-
return
|
|
1288
|
+
return import_path13.default.dirname(current).replace(/\\/g, "/");
|
|
1026
1289
|
}
|
|
1027
|
-
const parent =
|
|
1290
|
+
const parent = import_path13.default.dirname(current).replace(/\\/g, "/");
|
|
1028
1291
|
if (parent === current) {
|
|
1029
1292
|
break;
|
|
1030
1293
|
}
|
|
@@ -1044,11 +1307,11 @@ function getImmediateSubDomain(relPath, privateCategoryPath) {
|
|
|
1044
1307
|
}
|
|
1045
1308
|
|
|
1046
1309
|
// src/_rules/_rules@shared/_utils/_systems/get-private-category-info/get-private-category-info.util.ts
|
|
1047
|
-
var
|
|
1310
|
+
var import_path14 = __toESM(require("path"), 1);
|
|
1048
1311
|
function getPrivateCategoryInfo(relPath, config) {
|
|
1049
1312
|
let current = relPath;
|
|
1050
1313
|
while (current && current !== "." && current !== "/") {
|
|
1051
|
-
const parent =
|
|
1314
|
+
const parent = import_path14.default.dirname(current).replace(/\\/g, "/");
|
|
1052
1315
|
if (parent === current) {
|
|
1053
1316
|
break;
|
|
1054
1317
|
}
|
|
@@ -1063,7 +1326,22 @@ function getPrivateCategoryInfo(relPath, config) {
|
|
|
1063
1326
|
}
|
|
1064
1327
|
|
|
1065
1328
|
// src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
|
|
1066
|
-
|
|
1329
|
+
function getFractalBranchPath(relPath) {
|
|
1330
|
+
let current = relPath;
|
|
1331
|
+
while (current && current !== "." && current !== "/") {
|
|
1332
|
+
const folderName = import_path15.default.basename(current);
|
|
1333
|
+
if (folderName.startsWith("_") && folderName.includes("@shared")) {
|
|
1334
|
+
return current;
|
|
1335
|
+
}
|
|
1336
|
+
const parent = import_path15.default.dirname(current).replace(/\\/g, "/");
|
|
1337
|
+
if (parent === current) {
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
current = parent;
|
|
1341
|
+
}
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
var rule8 = {
|
|
1067
1345
|
create(context) {
|
|
1068
1346
|
const absPath = context.filename;
|
|
1069
1347
|
const relPath = toRelativePath(absPath);
|
|
@@ -1084,7 +1362,7 @@ var rule5 = {
|
|
|
1084
1362
|
if (!resolvedRelPath) {
|
|
1085
1363
|
return;
|
|
1086
1364
|
}
|
|
1087
|
-
const importedDir =
|
|
1365
|
+
const importedDir = import_path15.default.dirname(resolvedRelPath);
|
|
1088
1366
|
const fractalBranchOwner = getFractalBranchOwner(importedDir);
|
|
1089
1367
|
if (fractalBranchOwner) {
|
|
1090
1368
|
const isDescendant = relPath === fractalBranchOwner || relPath.startsWith(fractalBranchOwner + "/");
|
|
@@ -1097,7 +1375,7 @@ var rule5 = {
|
|
|
1097
1375
|
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1098
1376
|
if (info) {
|
|
1099
1377
|
const { owner, privateCategoryPath } = info;
|
|
1100
|
-
const isPrivateDirectChild =
|
|
1378
|
+
const isPrivateDirectChild = import_path15.default.dirname(relPath) === owner;
|
|
1101
1379
|
const importerSubDomain = getImmediateSubDomain(
|
|
1102
1380
|
relPath,
|
|
1103
1381
|
privateCategoryPath
|
|
@@ -1117,6 +1395,18 @@ var rule5 = {
|
|
|
1117
1395
|
}
|
|
1118
1396
|
}
|
|
1119
1397
|
}
|
|
1398
|
+
const importerBranchPath = getFractalBranchPath(import_path15.default.dirname(relPath));
|
|
1399
|
+
if (importerBranchPath) {
|
|
1400
|
+
const importerBranchOwner = import_path15.default.dirname(importerBranchPath).replace(/\\/g, "/");
|
|
1401
|
+
const isImportedInsideOwnBranch = resolvedRelPath === importerBranchPath || resolvedRelPath.startsWith(importerBranchPath + "/");
|
|
1402
|
+
const isImportedInsideServedSubtree = resolvedRelPath === importerBranchOwner || resolvedRelPath.startsWith(importerBranchOwner + "/");
|
|
1403
|
+
if (isImportedInsideServedSubtree && !isImportedInsideOwnBranch) {
|
|
1404
|
+
context.report({
|
|
1405
|
+
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.`,
|
|
1406
|
+
node
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1120
1410
|
}
|
|
1121
1411
|
return {
|
|
1122
1412
|
ExportAllDeclaration(node) {
|
|
@@ -1140,33 +1430,10 @@ var rule5 = {
|
|
|
1140
1430
|
type: "problem"
|
|
1141
1431
|
}
|
|
1142
1432
|
};
|
|
1143
|
-
var no_fractal_branch_leak_rule_default =
|
|
1433
|
+
var no_fractal_branch_leak_rule_default = rule8;
|
|
1144
1434
|
|
|
1145
1435
|
// src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
|
|
1146
|
-
var
|
|
1147
|
-
|
|
1148
|
-
// src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
|
|
1149
|
-
function findTreeConfigIncludingExcluded(relPath, settings) {
|
|
1150
|
-
if (!settings || !settings.trees) {
|
|
1151
|
-
return null;
|
|
1152
|
-
}
|
|
1153
|
-
const cached = state.treeConfigIncludingExcludedCache.get(relPath);
|
|
1154
|
-
if (cached !== void 0) {
|
|
1155
|
-
return cached;
|
|
1156
|
-
}
|
|
1157
|
-
let result = null;
|
|
1158
|
-
for (const tree of settings.trees) {
|
|
1159
|
-
const isIncluded = tree.includes.some(
|
|
1160
|
-
(inc) => relPath === inc || relPath.startsWith(inc + "/")
|
|
1161
|
-
);
|
|
1162
|
-
if (isIncluded) {
|
|
1163
|
-
result = tree;
|
|
1164
|
-
break;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
state.treeConfigIncludingExcludedCache.set(relPath, result);
|
|
1168
|
-
return result;
|
|
1169
|
-
}
|
|
1436
|
+
var import_path18 = __toESM(require("path"), 1);
|
|
1170
1437
|
|
|
1171
1438
|
// src/_rules/_rules@shared/_utils/_primitives/get-lca-and-sub-branches/get-lca-and-sub-branches.util.ts
|
|
1172
1439
|
function getLcaAndSubBranches(pathA, pathB) {
|
|
@@ -1196,6 +1463,19 @@ function getRoleHierarchyIndex(role, config) {
|
|
|
1196
1463
|
return -1;
|
|
1197
1464
|
}
|
|
1198
1465
|
|
|
1466
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-root-node-index/get-root-node-index.util.ts
|
|
1467
|
+
var import_path16 = __toESM(require("path"), 1);
|
|
1468
|
+
function getRootNodeIndex(relPath, rfConfig, lcaPath) {
|
|
1469
|
+
const relToLca = import_path16.default.relative(lcaPath, relPath).replace(/\\/g, "/");
|
|
1470
|
+
for (let i = 0; i < rfConfig.rootNodes.length; i++) {
|
|
1471
|
+
const group = rfConfig.rootNodes[i];
|
|
1472
|
+
if (group && group.includes(relToLca)) {
|
|
1473
|
+
return i;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return -1;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1199
1479
|
// src/_rules/_rules@shared/_utils/_primitives/resolve-horizontal-hierarchy/resolve-horizontal-hierarchy.util.ts
|
|
1200
1480
|
function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
|
|
1201
1481
|
const local = config.localHorizontalHierarchies?.find(
|
|
@@ -1219,39 +1499,12 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
|
|
|
1219
1499
|
return { allowed: false, defined: false };
|
|
1220
1500
|
}
|
|
1221
1501
|
|
|
1222
|
-
// src/_rules/
|
|
1223
|
-
|
|
1224
|
-
const folderName = import_path13.default.basename(dirPath);
|
|
1225
|
-
if (folderName.startsWith("_")) {
|
|
1226
|
-
return getRoleFromFolderName(folderName, config);
|
|
1227
|
-
}
|
|
1228
|
-
const contents = readDirCached(dirPath);
|
|
1229
|
-
for (const file of contents.files) {
|
|
1230
|
-
if (file.startsWith(folderName + ".")) {
|
|
1231
|
-
const role = getFileRole(file, config);
|
|
1232
|
-
if (role) return role;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return null;
|
|
1236
|
-
}
|
|
1237
|
-
function getRoleFromFolderName(name, config) {
|
|
1238
|
-
const catConfig = config.categories?.find((c) => c.name === name);
|
|
1239
|
-
return catConfig ? catConfig.role ?? null : null;
|
|
1240
|
-
}
|
|
1241
|
-
function getRootNodeIndex(relPath, rfConfig, lcaPath) {
|
|
1242
|
-
const relToLca = import_path13.default.relative(lcaPath, relPath).replace(/\\/g, "/");
|
|
1243
|
-
for (let i = 0; i < rfConfig.rootNodes.length; i++) {
|
|
1244
|
-
const group = rfConfig.rootNodes[i];
|
|
1245
|
-
if (group && group.includes(relToLca)) {
|
|
1246
|
-
return i;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
return -1;
|
|
1250
|
-
}
|
|
1502
|
+
// src/_rules/_rules@shared/_utils/_systems/is-inside-private-category-of-lca/is-inside-private-category-of-lca.util.ts
|
|
1503
|
+
var import_path17 = __toESM(require("path"), 1);
|
|
1251
1504
|
function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
|
|
1252
1505
|
let current = relPathB;
|
|
1253
1506
|
while (current && current !== "." && current !== "/" && current !== lcaPath) {
|
|
1254
|
-
const parent =
|
|
1507
|
+
const parent = import_path17.default.dirname(current).replace(/\\/g, "/");
|
|
1255
1508
|
if (parent === current) {
|
|
1256
1509
|
break;
|
|
1257
1510
|
}
|
|
@@ -1264,7 +1517,9 @@ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
|
|
|
1264
1517
|
}
|
|
1265
1518
|
return false;
|
|
1266
1519
|
}
|
|
1267
|
-
|
|
1520
|
+
|
|
1521
|
+
// src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
|
|
1522
|
+
var rule9 = {
|
|
1268
1523
|
create(context) {
|
|
1269
1524
|
const absPath = context.filename;
|
|
1270
1525
|
const relPath = toRelativePath(absPath);
|
|
@@ -1272,11 +1527,11 @@ var rule6 = {
|
|
|
1272
1527
|
if (!settings.faf) {
|
|
1273
1528
|
return {};
|
|
1274
1529
|
}
|
|
1275
|
-
const config =
|
|
1530
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
1276
1531
|
if (!config) {
|
|
1277
1532
|
return {};
|
|
1278
1533
|
}
|
|
1279
|
-
const currentDir =
|
|
1534
|
+
const currentDir = import_path18.default.dirname(relPath);
|
|
1280
1535
|
const isCurrentFileExcluded = config.excludes?.some(
|
|
1281
1536
|
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
1282
1537
|
) ?? false;
|
|
@@ -1292,27 +1547,13 @@ var rule6 = {
|
|
|
1292
1547
|
const isImportedFileExcluded = config?.excludes?.some(
|
|
1293
1548
|
(exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
|
|
1294
1549
|
) ?? false;
|
|
1295
|
-
if (!isCurrentFileExcluded && isImportedFileExcluded) {
|
|
1296
|
-
context.report({
|
|
1297
|
-
message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
|
|
1298
|
-
node
|
|
1299
|
-
});
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
if (isCurrentFileExcluded && !isImportedFileExcluded) {
|
|
1303
|
-
context.report({
|
|
1304
|
-
message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
|
|
1305
|
-
node
|
|
1306
|
-
});
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
1550
|
if (isCurrentFileExcluded || isImportedFileExcluded) {
|
|
1310
1551
|
return;
|
|
1311
1552
|
}
|
|
1312
|
-
const importedDir =
|
|
1553
|
+
const importedDir = import_path18.default.dirname(resolvedRelPath);
|
|
1313
1554
|
if (currentDir === importedDir) {
|
|
1314
|
-
const fileA =
|
|
1315
|
-
const fileB =
|
|
1555
|
+
const fileA = import_path18.default.basename(relPath);
|
|
1556
|
+
const fileB = import_path18.default.basename(resolvedRelPath);
|
|
1316
1557
|
const roleA = getFileRole(fileA, config);
|
|
1317
1558
|
const roleB = getFileRole(fileB, config);
|
|
1318
1559
|
if (roleA && roleB) {
|
|
@@ -1352,22 +1593,12 @@ var rule6 = {
|
|
|
1352
1593
|
const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
|
|
1353
1594
|
const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
|
|
1354
1595
|
if (idxRootA !== -1 && idxRootB !== -1) {
|
|
1355
|
-
if (idxRootB >= idxRootA) {
|
|
1356
|
-
context.report({
|
|
1357
|
-
message: `Root Node import violation: "${import_path13.default.basename(relPath)}" cannot import from "${import_path13.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
|
|
1358
|
-
node
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1361
1596
|
return;
|
|
1362
1597
|
}
|
|
1363
1598
|
}
|
|
1364
|
-
const isSubARootFragment = classifyFolder(
|
|
1365
|
-
const isSubBRootFragment = classifyFolder(
|
|
1599
|
+
const isSubARootFragment = classifyFolder(import_path18.default.posix.join(lca, subA), config) === "root-fragment";
|
|
1600
|
+
const isSubBRootFragment = classifyFolder(import_path18.default.posix.join(lca, subB), config) === "root-fragment";
|
|
1366
1601
|
if (isSubARootFragment || isSubBRootFragment) {
|
|
1367
|
-
context.report({
|
|
1368
|
-
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.`,
|
|
1369
|
-
node
|
|
1370
|
-
});
|
|
1371
1602
|
return;
|
|
1372
1603
|
}
|
|
1373
1604
|
const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
|
|
@@ -1379,23 +1610,10 @@ var rule6 = {
|
|
|
1379
1610
|
});
|
|
1380
1611
|
}
|
|
1381
1612
|
} else {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
if (idxA !== -1 && idxB !== -1) {
|
|
1387
|
-
if (idxB >= idxA) {
|
|
1388
|
-
context.report({
|
|
1389
|
-
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}".`,
|
|
1390
|
-
node
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
} else {
|
|
1394
|
-
context.report({
|
|
1395
|
-
message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
|
|
1396
|
-
node
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1613
|
+
context.report({
|
|
1614
|
+
message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
|
|
1615
|
+
node
|
|
1616
|
+
});
|
|
1399
1617
|
}
|
|
1400
1618
|
}
|
|
1401
1619
|
return {
|
|
@@ -1420,11 +1638,11 @@ var rule6 = {
|
|
|
1420
1638
|
type: "problem"
|
|
1421
1639
|
}
|
|
1422
1640
|
};
|
|
1423
|
-
var no_peer_dependency_rule_default =
|
|
1641
|
+
var no_peer_dependency_rule_default = rule9;
|
|
1424
1642
|
|
|
1425
1643
|
// src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
|
|
1426
|
-
var
|
|
1427
|
-
var
|
|
1644
|
+
var import_path19 = __toESM(require("path"), 1);
|
|
1645
|
+
var rule10 = {
|
|
1428
1646
|
create(context) {
|
|
1429
1647
|
const absPath = context.filename;
|
|
1430
1648
|
const relPath = toRelativePath(absPath);
|
|
@@ -1445,7 +1663,7 @@ var rule7 = {
|
|
|
1445
1663
|
if (!resolvedRelPath) {
|
|
1446
1664
|
return;
|
|
1447
1665
|
}
|
|
1448
|
-
const importedDir =
|
|
1666
|
+
const importedDir = import_path19.default.dirname(resolvedRelPath);
|
|
1449
1667
|
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1450
1668
|
if (info) {
|
|
1451
1669
|
const { owner, privateCategoryPath } = info;
|
|
@@ -1459,7 +1677,7 @@ var rule7 = {
|
|
|
1459
1677
|
isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
|
|
1460
1678
|
}
|
|
1461
1679
|
}
|
|
1462
|
-
const isDirectChild =
|
|
1680
|
+
const isDirectChild = import_path19.default.dirname(relPath) === owner;
|
|
1463
1681
|
const importerSubDomain = getImmediateSubDomain(
|
|
1464
1682
|
relPath,
|
|
1465
1683
|
privateCategoryPath
|
|
@@ -1500,17 +1718,119 @@ var rule7 = {
|
|
|
1500
1718
|
type: "problem"
|
|
1501
1719
|
}
|
|
1502
1720
|
};
|
|
1503
|
-
var no_private_category_leak_rule_default =
|
|
1721
|
+
var no_private_category_leak_rule_default = rule10;
|
|
1722
|
+
|
|
1723
|
+
// src/_rules/root-node-dependency-direction/root-node-dependency-direction.rule.ts
|
|
1724
|
+
var import_path20 = __toESM(require("path"), 1);
|
|
1725
|
+
var rule11 = {
|
|
1726
|
+
create(context) {
|
|
1727
|
+
const absPath = context.filename;
|
|
1728
|
+
const relPath = toRelativePath(absPath);
|
|
1729
|
+
const settings = context.settings;
|
|
1730
|
+
if (!settings.faf) {
|
|
1731
|
+
return {};
|
|
1732
|
+
}
|
|
1733
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
1734
|
+
if (!config) {
|
|
1735
|
+
return {};
|
|
1736
|
+
}
|
|
1737
|
+
const currentDir = import_path20.default.dirname(relPath);
|
|
1738
|
+
const isCurrentFileExcluded = config.excludes?.some(
|
|
1739
|
+
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
1740
|
+
) ?? false;
|
|
1741
|
+
function checkImport(node, importValue) {
|
|
1742
|
+
const resolvedRelPath = resolveImportPath(
|
|
1743
|
+
importValue,
|
|
1744
|
+
relPath,
|
|
1745
|
+
settings.faf
|
|
1746
|
+
);
|
|
1747
|
+
if (!resolvedRelPath) {
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
const isImportedFileExcluded = config?.excludes?.some(
|
|
1751
|
+
(exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
|
|
1752
|
+
) ?? false;
|
|
1753
|
+
if (isCurrentFileExcluded || isImportedFileExcluded) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
const importedDir = import_path20.default.dirname(resolvedRelPath);
|
|
1757
|
+
if (currentDir === importedDir) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const lcaInfo = getLcaAndSubBranches(currentDir, importedDir);
|
|
1761
|
+
if (!lcaInfo) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const { lca, subA, subB } = lcaInfo;
|
|
1765
|
+
if (subB.startsWith("_") && subB.includes("@shared")) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (isInsidePrivateCategoryOfLca(importedDir, lca, config)) {
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (subB === "" && classifyFolder(lca, config) === "fragment") {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const rfConfig = getRootFragmentConfig(lca, config);
|
|
1775
|
+
if (rfConfig) {
|
|
1776
|
+
const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
|
|
1777
|
+
const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
|
|
1778
|
+
if (idxRootA !== -1 && idxRootB !== -1) {
|
|
1779
|
+
if (idxRootB >= idxRootA) {
|
|
1780
|
+
context.report({
|
|
1781
|
+
message: `Root Node import violation: "${import_path20.default.basename(relPath)}" cannot import from "${import_path20.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
|
|
1782
|
+
node
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
const isSubARootFragment = classifyFolder(import_path20.default.posix.join(lca, subA), config) === "root-fragment";
|
|
1789
|
+
const isSubBRootFragment = classifyFolder(import_path20.default.posix.join(lca, subB), config) === "root-fragment";
|
|
1790
|
+
if (isSubARootFragment || isSubBRootFragment) {
|
|
1791
|
+
context.report({
|
|
1792
|
+
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.`,
|
|
1793
|
+
node
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
ExportAllDeclaration(node) {
|
|
1799
|
+
checkImport(node, node.source.value);
|
|
1800
|
+
},
|
|
1801
|
+
ExportNamedDeclaration(node) {
|
|
1802
|
+
if (node.source) {
|
|
1803
|
+
checkImport(node, node.source.value);
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
ImportDeclaration(node) {
|
|
1807
|
+
checkImport(node, node.source.value);
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
},
|
|
1811
|
+
meta: {
|
|
1812
|
+
docs: {
|
|
1813
|
+
description: "Enforce explicit, non-Role-based dependency direction between Root Nodes"
|
|
1814
|
+
},
|
|
1815
|
+
schema: [],
|
|
1816
|
+
type: "problem"
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
var root_node_dependency_direction_rule_default = rule11;
|
|
1504
1820
|
|
|
1505
1821
|
// src/rules.ts
|
|
1506
1822
|
var rules = {
|
|
1507
1823
|
"category-mutually-exclusive": category_mutually_exclusive_rule_default,
|
|
1508
1824
|
"enforce-access-node": enforce_access_node_rule_default,
|
|
1825
|
+
"foreign-domain-isolation": foreign_domain_isolation_rule_default,
|
|
1826
|
+
"fragment-master-node": fragment_master_node_rule_default,
|
|
1827
|
+
"logical-domain-placement": logical_domain_placement_rule_default,
|
|
1509
1828
|
"naming-conventions": naming_conventions_rule_default,
|
|
1510
1829
|
"no-direct-fragment-import": no_direct_fragment_import_rule_default,
|
|
1511
1830
|
"no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
|
|
1512
1831
|
"no-peer-dependency": no_peer_dependency_rule_default,
|
|
1513
|
-
"no-private-category-leak": no_private_category_leak_rule_default
|
|
1832
|
+
"no-private-category-leak": no_private_category_leak_rule_default,
|
|
1833
|
+
"root-node-dependency-direction": root_node_dependency_direction_rule_default
|
|
1514
1834
|
};
|
|
1515
1835
|
|
|
1516
1836
|
// src/configs.ts
|
|
@@ -1524,11 +1844,15 @@ var configs = {
|
|
|
1524
1844
|
rules: {
|
|
1525
1845
|
"faf/category-mutually-exclusive": "error",
|
|
1526
1846
|
"faf/enforce-access-node": "error",
|
|
1847
|
+
"faf/foreign-domain-isolation": "error",
|
|
1848
|
+
"faf/fragment-master-node": "error",
|
|
1849
|
+
"faf/logical-domain-placement": "error",
|
|
1527
1850
|
"faf/naming-conventions": "error",
|
|
1528
1851
|
"faf/no-direct-fragment-import": "error",
|
|
1529
1852
|
"faf/no-fractal-branch-leak": "error",
|
|
1530
1853
|
"faf/no-peer-dependency": "error",
|
|
1531
|
-
"faf/no-private-category-leak": "error"
|
|
1854
|
+
"faf/no-private-category-leak": "error",
|
|
1855
|
+
"faf/root-node-dependency-direction": "error"
|
|
1532
1856
|
}
|
|
1533
1857
|
}
|
|
1534
1858
|
};
|