@morriconeluca/eslint-plugin-faf 0.2.0 → 0.3.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 +18 -4
- package/dist/main.cjs +110 -36
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +110 -36
- 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.3.0",
|
|
5
5
|
description: "An ESLint plugin for FAF projects.",
|
|
6
6
|
repository: {
|
|
7
7
|
type: "git",
|
|
@@ -442,6 +442,17 @@ function getProjectRoot() {
|
|
|
442
442
|
|
|
443
443
|
// src/_rules/naming-conventions/naming-conventions.rule.ts
|
|
444
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
|
+
];
|
|
445
456
|
var rule3 = {
|
|
446
457
|
create(context) {
|
|
447
458
|
return {
|
|
@@ -605,15 +616,6 @@ var rule3 = {
|
|
|
605
616
|
if (parentType === "invalid-fragment") {
|
|
606
617
|
return;
|
|
607
618
|
}
|
|
608
|
-
if (fileName === "index.ts" || fileName === "index.js") {
|
|
609
|
-
if (parentType !== "fragment") {
|
|
610
|
-
context.report({
|
|
611
|
-
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
|
|
612
|
-
node
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
619
|
const rfConfig = getRootFragmentConfig(relDir, config);
|
|
618
620
|
if (rfConfig) {
|
|
619
621
|
const flatRootNodes = rfConfig.rootNodes.flat();
|
|
@@ -629,11 +631,34 @@ var rule3 = {
|
|
|
629
631
|
return;
|
|
630
632
|
}
|
|
631
633
|
}
|
|
634
|
+
if (fileName === "index.ts" || fileName === "index.js") {
|
|
635
|
+
if (parentType !== "fragment") {
|
|
636
|
+
context.report({
|
|
637
|
+
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
|
|
638
|
+
node
|
|
639
|
+
});
|
|
640
|
+
} else {
|
|
641
|
+
const contents = readDirCached(relDir);
|
|
642
|
+
const hasFragmentNode = contents.files.some(
|
|
643
|
+
(f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
|
|
644
|
+
);
|
|
645
|
+
if (!hasFragmentNode) {
|
|
646
|
+
context.report({
|
|
647
|
+
message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
|
|
648
|
+
node
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
632
654
|
if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
|
|
633
655
|
return;
|
|
634
656
|
}
|
|
635
657
|
const role = getFileRole(fileName, config);
|
|
636
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.`;
|
|
637
662
|
if (parentType === "fragment") {
|
|
638
663
|
const matchedRoute = config.routeHierarchies?.find(
|
|
639
664
|
(rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
|
|
@@ -647,6 +672,16 @@ var rule3 = {
|
|
|
647
672
|
node
|
|
648
673
|
});
|
|
649
674
|
}
|
|
675
|
+
const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
|
|
676
|
+
const routeNameRegex = new RegExp(
|
|
677
|
+
`^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
|
|
678
|
+
);
|
|
679
|
+
if (!routeNameRegex.test(folderName)) {
|
|
680
|
+
context.report({
|
|
681
|
+
message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
|
|
682
|
+
node
|
|
683
|
+
});
|
|
684
|
+
}
|
|
650
685
|
}
|
|
651
686
|
let currentParent = path7.dirname(relDir).replace(/\\/g, "/");
|
|
652
687
|
let categoryConfig = null;
|
|
@@ -685,6 +720,19 @@ var rule3 = {
|
|
|
685
720
|
message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
|
|
686
721
|
node
|
|
687
722
|
});
|
|
723
|
+
} else if (roleSegments.length > 1) {
|
|
724
|
+
context.report({ message: multiRoleMessage, node });
|
|
725
|
+
} else {
|
|
726
|
+
const contents = readDirCached(relDir);
|
|
727
|
+
const duplicate = contents.files.find(
|
|
728
|
+
(f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
|
|
729
|
+
);
|
|
730
|
+
if (duplicate) {
|
|
731
|
+
context.report({
|
|
732
|
+
message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
|
|
733
|
+
node
|
|
734
|
+
});
|
|
735
|
+
}
|
|
688
736
|
}
|
|
689
737
|
}
|
|
690
738
|
if (parentType === "category") {
|
|
@@ -715,6 +763,8 @@ var rule3 = {
|
|
|
715
763
|
message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
716
764
|
node
|
|
717
765
|
});
|
|
766
|
+
} else if (roleSegments.length > 1) {
|
|
767
|
+
context.report({ message: multiRoleMessage, node });
|
|
718
768
|
}
|
|
719
769
|
} else {
|
|
720
770
|
if (role && role !== catConfig.role) {
|
|
@@ -722,6 +772,8 @@ var rule3 = {
|
|
|
722
772
|
message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
723
773
|
node
|
|
724
774
|
});
|
|
775
|
+
} else if (role && roleSegments.length > 1) {
|
|
776
|
+
context.report({ message: multiRoleMessage, node });
|
|
725
777
|
}
|
|
726
778
|
}
|
|
727
779
|
}
|
|
@@ -791,6 +843,16 @@ function _resolveImportPath(importPath, currentFileRelative, settings) {
|
|
|
791
843
|
}
|
|
792
844
|
return finalizeResolvedPath(relResolved);
|
|
793
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
|
+
]);
|
|
794
856
|
function finalizeResolvedPath(relResolved) {
|
|
795
857
|
const resolvedDir = path8.dirname(relResolved).replace(/\\/g, "/");
|
|
796
858
|
const resolvedBase = path8.basename(relResolved);
|
|
@@ -804,9 +866,12 @@ function finalizeResolvedPath(relResolved) {
|
|
|
804
866
|
const parentContents = readDirCached(resolvedDir);
|
|
805
867
|
const exactMatch = parentContents.files.find((f) => f === resolvedBase);
|
|
806
868
|
if (!exactMatch) {
|
|
807
|
-
const
|
|
869
|
+
const candidates = parentContents.files.filter(
|
|
808
870
|
(f) => f.startsWith(resolvedBase + ".")
|
|
809
871
|
);
|
|
872
|
+
const matchWithExt = candidates.find(
|
|
873
|
+
(f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
|
|
874
|
+
) ?? candidates[0];
|
|
810
875
|
if (matchWithExt) {
|
|
811
876
|
return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
|
|
812
877
|
}
|
|
@@ -875,7 +940,7 @@ var rule4 = {
|
|
|
875
940
|
if (importedFolderType === "fragment") {
|
|
876
941
|
const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
|
|
877
942
|
if (!isAccessNode) {
|
|
878
|
-
const isInside = relPath === importedDir
|
|
943
|
+
const isInside = path9.dirname(relPath) === importedDir;
|
|
879
944
|
if (!isInside) {
|
|
880
945
|
context.report({
|
|
881
946
|
message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
|
|
@@ -982,19 +1047,19 @@ var rule5 = {
|
|
|
982
1047
|
return;
|
|
983
1048
|
}
|
|
984
1049
|
const importedDir = path12.dirname(resolvedRelPath);
|
|
985
|
-
const
|
|
986
|
-
if (
|
|
987
|
-
const isDescendant = relPath ===
|
|
1050
|
+
const fractalBranchOwner = getFractalBranchOwner(importedDir);
|
|
1051
|
+
if (fractalBranchOwner) {
|
|
1052
|
+
const isDescendant = relPath === fractalBranchOwner || relPath.startsWith(fractalBranchOwner + "/");
|
|
988
1053
|
if (!isDescendant) {
|
|
989
1054
|
context.report({
|
|
990
|
-
message: `Importing from Fractal Branch is forbidden. The imported resource is private to "${
|
|
1055
|
+
message: `Importing from Fractal Branch is forbidden. The imported resource is private to "${fractalBranchOwner}" and its descendants.`,
|
|
991
1056
|
node
|
|
992
1057
|
});
|
|
993
1058
|
} else {
|
|
994
1059
|
const info = getPrivateCategoryInfo(importedDir, config);
|
|
995
1060
|
if (info) {
|
|
996
|
-
const { owner
|
|
997
|
-
const isPrivateDirectChild = path12.dirname(relPath) ===
|
|
1061
|
+
const { owner, privateCategoryPath } = info;
|
|
1062
|
+
const isPrivateDirectChild = path12.dirname(relPath) === owner;
|
|
998
1063
|
const importerSubDomain = getImmediateSubDomain(
|
|
999
1064
|
relPath,
|
|
1000
1065
|
privateCategoryPath
|
|
@@ -1004,17 +1069,10 @@ var rule5 = {
|
|
|
1004
1069
|
privateCategoryPath
|
|
1005
1070
|
);
|
|
1006
1071
|
const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
|
|
1007
|
-
|
|
1008
|
-
const fbOwner = getFractalBranchOwner(importedDir);
|
|
1009
|
-
if (fbOwner) {
|
|
1010
|
-
const isFbInsidePrivateCategory = fbOwner === privateCategoryPath || fbOwner.startsWith(privateCategoryPath + "/");
|
|
1011
|
-
if (isFbInsidePrivateCategory) {
|
|
1012
|
-
isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1072
|
+
const isFractalBranchAllowed = fractalBranchOwner === privateCategoryPath || fractalBranchOwner.startsWith(privateCategoryPath + "/");
|
|
1015
1073
|
if (!isPrivateDirectChild && !isInsideSameSubDomain && !isFractalBranchAllowed) {
|
|
1016
1074
|
context.report({
|
|
1017
|
-
message: `Importing from Private Category is forbidden. The imported resource is private to "${
|
|
1075
|
+
message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
|
|
1018
1076
|
node
|
|
1019
1077
|
});
|
|
1020
1078
|
}
|
|
@@ -1245,6 +1303,10 @@ var rule6 = {
|
|
|
1245
1303
|
return;
|
|
1246
1304
|
}
|
|
1247
1305
|
if (subB === "" && classifyFolder(lca, config) === "fragment") {
|
|
1306
|
+
context.report({
|
|
1307
|
+
message: `Peer separation violation: "${subA}" cannot import directly from the root of Fragment "${lca}". A node nested inside a Private Category or Fractal Branch has no privileged access to its owning Fragment's other direct children; promote the shared resource to a Fractal Branch if the sharing need is genuine.`,
|
|
1308
|
+
node
|
|
1309
|
+
});
|
|
1248
1310
|
return;
|
|
1249
1311
|
}
|
|
1250
1312
|
const rfConfig = getRootFragmentConfig(lca, config);
|
|
@@ -1261,6 +1323,15 @@ var rule6 = {
|
|
|
1261
1323
|
return;
|
|
1262
1324
|
}
|
|
1263
1325
|
}
|
|
1326
|
+
const isSubARootFragment = classifyFolder(path13.posix.join(lca, subA), config) === "root-fragment";
|
|
1327
|
+
const isSubBRootFragment = classifyFolder(path13.posix.join(lca, subB), config) === "root-fragment";
|
|
1328
|
+
if (isSubARootFragment || isSubBRootFragment) {
|
|
1329
|
+
context.report({
|
|
1330
|
+
message: `Root Node import violation: "${relPath}" cannot import from "${resolvedRelPath}" because no "rootNodes" relationship is configured under Root Fragment "${lca}". Relationships between Root Nodes must always be explicitly authorized by the architect.`,
|
|
1331
|
+
node
|
|
1332
|
+
});
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1264
1335
|
const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
|
|
1265
1336
|
if (hasDefinedHierarchy) {
|
|
1266
1337
|
if (!allowed) {
|
|
@@ -1340,6 +1411,16 @@ var rule7 = {
|
|
|
1340
1411
|
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1341
1412
|
if (info) {
|
|
1342
1413
|
const { owner, privateCategoryPath } = info;
|
|
1414
|
+
let isFractalBranchInPrivateCategory = false;
|
|
1415
|
+
let isFractalBranchAllowed = false;
|
|
1416
|
+
const fbOwner = getFractalBranchOwner(importedDir);
|
|
1417
|
+
if (fbOwner) {
|
|
1418
|
+
const isFbInsidePrivateCategory = fbOwner === privateCategoryPath || fbOwner.startsWith(privateCategoryPath + "/");
|
|
1419
|
+
if (isFbInsidePrivateCategory) {
|
|
1420
|
+
isFractalBranchInPrivateCategory = true;
|
|
1421
|
+
isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1343
1424
|
const isDirectChild = path14.dirname(relPath) === owner;
|
|
1344
1425
|
const importerSubDomain = getImmediateSubDomain(
|
|
1345
1426
|
relPath,
|
|
@@ -1350,15 +1431,8 @@ var rule7 = {
|
|
|
1350
1431
|
privateCategoryPath
|
|
1351
1432
|
);
|
|
1352
1433
|
const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
if (fbOwner) {
|
|
1356
|
-
const isFbInsidePrivateCategory = fbOwner === privateCategoryPath || fbOwner.startsWith(privateCategoryPath + "/");
|
|
1357
|
-
if (isFbInsidePrivateCategory) {
|
|
1358
|
-
isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
if (!isDirectChild && !isInsideSameSubDomain && !isFractalBranchAllowed) {
|
|
1434
|
+
const isAllowed = isFractalBranchInPrivateCategory ? isFractalBranchAllowed : isDirectChild || isInsideSameSubDomain;
|
|
1435
|
+
if (!isAllowed) {
|
|
1362
1436
|
context.report({
|
|
1363
1437
|
message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
|
|
1364
1438
|
node
|