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