@morriconeluca/eslint-plugin-faf 0.4.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 +839 -484
- 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 +839 -484
- 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) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
818
|
+
if (!config) {
|
|
506
819
|
return;
|
|
507
820
|
}
|
|
508
|
-
const relDir =
|
|
509
|
-
const folderName =
|
|
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,309 +1034,187 @@ 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) {
|
|
655
1066
|
return;
|
|
656
1067
|
}
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (rn.includes("/")) {
|
|
662
|
-
const rfPath = rfConfig.paths[0] ?? "";
|
|
663
|
-
const absRn = import_path7.default.resolve(getProjectRoot(), rfPath, rn);
|
|
664
|
-
return toRelativePath(absRn) === relPath;
|
|
665
|
-
}
|
|
666
|
-
return rn === fileName;
|
|
667
|
-
});
|
|
668
|
-
if (isMatchedRootNode) {
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
1068
|
+
const relDir = import_path11.default.dirname(relPath);
|
|
1069
|
+
const parentType = classifyFolder(relDir, config);
|
|
1070
|
+
if (parentType === "foreign") {
|
|
1071
|
+
return;
|
|
671
1072
|
}
|
|
672
|
-
|
|
673
|
-
|
|
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("_")) {
|
|
674
1080
|
context.report({
|
|
675
|
-
message: `
|
|
1081
|
+
message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
|
|
676
1082
|
node
|
|
677
1083
|
});
|
|
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
1084
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
const role = getFileRole(fileName, config);
|
|
696
|
-
const ext = import_path7.default.extname(fileName);
|
|
697
|
-
const allRoleNames = new Set(config.roles.flat());
|
|
698
|
-
const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
|
|
699
|
-
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
|
-
const matchedRoute = config.routeHierarchies?.find(
|
|
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)) {
|
|
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) {
|
|
718
1089
|
context.report({
|
|
719
|
-
message: `
|
|
1090
|
+
message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
|
|
720
1091
|
node
|
|
721
1092
|
});
|
|
722
1093
|
}
|
|
723
1094
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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;
|
|
1095
|
+
if (currentFolderName !== "src") {
|
|
1096
|
+
let baseFolderName = currentFolderName.replace(/@shared$/, "");
|
|
1097
|
+
if (baseFolderName.startsWith("_")) {
|
|
1098
|
+
baseFolderName = baseFolderName.substring(1);
|
|
735
1099
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
if (!
|
|
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) {
|
|
744
1108
|
context.report({
|
|
745
|
-
message: `
|
|
1109
|
+
message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
|
|
746
1110
|
node
|
|
747
1111
|
});
|
|
748
1112
|
}
|
|
749
1113
|
}
|
|
750
|
-
if (
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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) {
|
|
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) {
|
|
769
1119
|
context.report({
|
|
770
|
-
message: `
|
|
1120
|
+
message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
|
|
771
1121
|
node
|
|
772
1122
|
});
|
|
773
1123
|
}
|
|
774
1124
|
}
|
|
775
1125
|
}
|
|
776
|
-
if (parentType === "
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
}
|
|
1126
|
+
if (parentType === "layer" || parentType === "fractal-branch" || parentType === "invalid-fragment") {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const rfConfig = getRootFragmentConfig(relDir, config);
|
|
1130
|
+
if (rfConfig) {
|
|
1131
|
+
const flatRootNodes = rfConfig.rootNodes.flat();
|
|
1132
|
+
const isMatchedRootNode = flatRootNodes.some((rn) => {
|
|
1133
|
+
if (rn.includes("/")) {
|
|
1134
|
+
const rfPath = rfConfig.paths[0] ?? "";
|
|
1135
|
+
const absRn = import_path11.default.resolve(getProjectRoot(), rfPath, rn);
|
|
1136
|
+
return toRelativePath(absRn) === relPath;
|
|
816
1137
|
}
|
|
1138
|
+
return rn === fileName;
|
|
1139
|
+
});
|
|
1140
|
+
if (isMatchedRootNode) {
|
|
1141
|
+
return;
|
|
817
1142
|
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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);
|
|
1143
|
+
}
|
|
1144
|
+
if (fileName === "index.ts" || fileName === "index.js") {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const role = getFileRole(fileName, config);
|
|
1151
|
+
const ext = import_path11.default.extname(fileName);
|
|
1152
|
+
const allRoleNames = new Set(config.roles.flat());
|
|
1153
|
+
const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
|
|
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.`;
|
|
1155
|
+
if (parentType === "fragment" && role && roleSegments.length > 1) {
|
|
1156
|
+
context.report({ message: multiRoleMessage, node });
|
|
1157
|
+
}
|
|
1158
|
+
if (parentType === "category") {
|
|
1159
|
+
const folderName = import_path11.default.basename(relDir);
|
|
1160
|
+
const catConfig = getCategoryConfig(relDir, config);
|
|
1161
|
+
if (catConfig) {
|
|
1162
|
+
const allowedExts = catConfig.allowedExtensions ?? [
|
|
1163
|
+
".js",
|
|
1164
|
+
".jsx",
|
|
1165
|
+
".ts",
|
|
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,21 +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 relToLca = import_path13.default.relative(lcaPath, relPath).replace(/\\/g, "/");
|
|
1225
|
-
for (let i = 0; i < rfConfig.rootNodes.length; i++) {
|
|
1226
|
-
const group = rfConfig.rootNodes[i];
|
|
1227
|
-
if (group && group.includes(relToLca)) {
|
|
1228
|
-
return i;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
return -1;
|
|
1232
|
-
}
|
|
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);
|
|
1233
1504
|
function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
|
|
1234
1505
|
let current = relPathB;
|
|
1235
1506
|
while (current && current !== "." && current !== "/" && current !== lcaPath) {
|
|
1236
|
-
const parent =
|
|
1507
|
+
const parent = import_path17.default.dirname(current).replace(/\\/g, "/");
|
|
1237
1508
|
if (parent === current) {
|
|
1238
1509
|
break;
|
|
1239
1510
|
}
|
|
@@ -1246,7 +1517,9 @@ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
|
|
|
1246
1517
|
}
|
|
1247
1518
|
return false;
|
|
1248
1519
|
}
|
|
1249
|
-
|
|
1520
|
+
|
|
1521
|
+
// src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
|
|
1522
|
+
var rule9 = {
|
|
1250
1523
|
create(context) {
|
|
1251
1524
|
const absPath = context.filename;
|
|
1252
1525
|
const relPath = toRelativePath(absPath);
|
|
@@ -1254,11 +1527,11 @@ var rule6 = {
|
|
|
1254
1527
|
if (!settings.faf) {
|
|
1255
1528
|
return {};
|
|
1256
1529
|
}
|
|
1257
|
-
const config =
|
|
1530
|
+
const config = findTreeConfig(relPath, settings.faf);
|
|
1258
1531
|
if (!config) {
|
|
1259
1532
|
return {};
|
|
1260
1533
|
}
|
|
1261
|
-
const currentDir =
|
|
1534
|
+
const currentDir = import_path18.default.dirname(relPath);
|
|
1262
1535
|
const isCurrentFileExcluded = config.excludes?.some(
|
|
1263
1536
|
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
1264
1537
|
) ?? false;
|
|
@@ -1274,27 +1547,13 @@ var rule6 = {
|
|
|
1274
1547
|
const isImportedFileExcluded = config?.excludes?.some(
|
|
1275
1548
|
(exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
|
|
1276
1549
|
) ?? false;
|
|
1277
|
-
if (!isCurrentFileExcluded && isImportedFileExcluded) {
|
|
1278
|
-
context.report({
|
|
1279
|
-
message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
|
|
1280
|
-
node
|
|
1281
|
-
});
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
if (isCurrentFileExcluded && !isImportedFileExcluded) {
|
|
1285
|
-
context.report({
|
|
1286
|
-
message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
|
|
1287
|
-
node
|
|
1288
|
-
});
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
1550
|
if (isCurrentFileExcluded || isImportedFileExcluded) {
|
|
1292
1551
|
return;
|
|
1293
1552
|
}
|
|
1294
|
-
const importedDir =
|
|
1553
|
+
const importedDir = import_path18.default.dirname(resolvedRelPath);
|
|
1295
1554
|
if (currentDir === importedDir) {
|
|
1296
|
-
const fileA =
|
|
1297
|
-
const fileB =
|
|
1555
|
+
const fileA = import_path18.default.basename(relPath);
|
|
1556
|
+
const fileB = import_path18.default.basename(resolvedRelPath);
|
|
1298
1557
|
const roleA = getFileRole(fileA, config);
|
|
1299
1558
|
const roleB = getFileRole(fileB, config);
|
|
1300
1559
|
if (roleA && roleB) {
|
|
@@ -1334,22 +1593,12 @@ var rule6 = {
|
|
|
1334
1593
|
const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
|
|
1335
1594
|
const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
|
|
1336
1595
|
if (idxRootA !== -1 && idxRootB !== -1) {
|
|
1337
|
-
if (idxRootB >= idxRootA) {
|
|
1338
|
-
context.report({
|
|
1339
|
-
message: `Root Node import violation: "${import_path13.default.basename(relPath)}" cannot import from "${import_path13.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
|
|
1340
|
-
node
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
1596
|
return;
|
|
1344
1597
|
}
|
|
1345
1598
|
}
|
|
1346
|
-
const isSubARootFragment = classifyFolder(
|
|
1347
|
-
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";
|
|
1348
1601
|
if (isSubARootFragment || isSubBRootFragment) {
|
|
1349
|
-
context.report({
|
|
1350
|
-
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.`,
|
|
1351
|
-
node
|
|
1352
|
-
});
|
|
1353
1602
|
return;
|
|
1354
1603
|
}
|
|
1355
1604
|
const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
|
|
@@ -1389,11 +1638,11 @@ var rule6 = {
|
|
|
1389
1638
|
type: "problem"
|
|
1390
1639
|
}
|
|
1391
1640
|
};
|
|
1392
|
-
var no_peer_dependency_rule_default =
|
|
1641
|
+
var no_peer_dependency_rule_default = rule9;
|
|
1393
1642
|
|
|
1394
1643
|
// src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
|
|
1395
|
-
var
|
|
1396
|
-
var
|
|
1644
|
+
var import_path19 = __toESM(require("path"), 1);
|
|
1645
|
+
var rule10 = {
|
|
1397
1646
|
create(context) {
|
|
1398
1647
|
const absPath = context.filename;
|
|
1399
1648
|
const relPath = toRelativePath(absPath);
|
|
@@ -1414,7 +1663,7 @@ var rule7 = {
|
|
|
1414
1663
|
if (!resolvedRelPath) {
|
|
1415
1664
|
return;
|
|
1416
1665
|
}
|
|
1417
|
-
const importedDir =
|
|
1666
|
+
const importedDir = import_path19.default.dirname(resolvedRelPath);
|
|
1418
1667
|
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1419
1668
|
if (info) {
|
|
1420
1669
|
const { owner, privateCategoryPath } = info;
|
|
@@ -1428,7 +1677,7 @@ var rule7 = {
|
|
|
1428
1677
|
isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
|
|
1429
1678
|
}
|
|
1430
1679
|
}
|
|
1431
|
-
const isDirectChild =
|
|
1680
|
+
const isDirectChild = import_path19.default.dirname(relPath) === owner;
|
|
1432
1681
|
const importerSubDomain = getImmediateSubDomain(
|
|
1433
1682
|
relPath,
|
|
1434
1683
|
privateCategoryPath
|
|
@@ -1469,17 +1718,119 @@ var rule7 = {
|
|
|
1469
1718
|
type: "problem"
|
|
1470
1719
|
}
|
|
1471
1720
|
};
|
|
1472
|
-
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;
|
|
1473
1820
|
|
|
1474
1821
|
// src/rules.ts
|
|
1475
1822
|
var rules = {
|
|
1476
1823
|
"category-mutually-exclusive": category_mutually_exclusive_rule_default,
|
|
1477
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,
|
|
1478
1828
|
"naming-conventions": naming_conventions_rule_default,
|
|
1479
1829
|
"no-direct-fragment-import": no_direct_fragment_import_rule_default,
|
|
1480
1830
|
"no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
|
|
1481
1831
|
"no-peer-dependency": no_peer_dependency_rule_default,
|
|
1482
|
-
"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
|
|
1483
1834
|
};
|
|
1484
1835
|
|
|
1485
1836
|
// src/configs.ts
|
|
@@ -1493,11 +1844,15 @@ var configs = {
|
|
|
1493
1844
|
rules: {
|
|
1494
1845
|
"faf/category-mutually-exclusive": "error",
|
|
1495
1846
|
"faf/enforce-access-node": "error",
|
|
1847
|
+
"faf/foreign-domain-isolation": "error",
|
|
1848
|
+
"faf/fragment-master-node": "error",
|
|
1849
|
+
"faf/logical-domain-placement": "error",
|
|
1496
1850
|
"faf/naming-conventions": "error",
|
|
1497
1851
|
"faf/no-direct-fragment-import": "error",
|
|
1498
1852
|
"faf/no-fractal-branch-leak": "error",
|
|
1499
1853
|
"faf/no-peer-dependency": "error",
|
|
1500
|
-
"faf/no-private-category-leak": "error"
|
|
1854
|
+
"faf/no-private-category-leak": "error",
|
|
1855
|
+
"faf/root-node-dependency-direction": "error"
|
|
1501
1856
|
}
|
|
1502
1857
|
}
|
|
1503
1858
|
};
|