@morriconeluca/eslint-plugin-faf 0.1.1 → 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/dist/main.js CHANGED
@@ -1,8 +1,12 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@morriconeluca/eslint-plugin-faf",
4
- version: "0.1.1",
4
+ version: "0.3.0",
5
5
  description: "An ESLint plugin for FAF projects.",
6
+ repository: {
7
+ type: "git",
8
+ url: "git+https://github.com/morriconeluca/eslint-plugin-faf.git"
9
+ },
6
10
  main: "./dist/main.cjs",
7
11
  module: "./dist/main.js",
8
12
  types: "./dist/main.d.ts",
@@ -14,7 +18,8 @@ var package_default = {
14
18
  }
15
19
  },
16
20
  publishConfig: {
17
- access: "public"
21
+ access: "public",
22
+ provenance: true
18
23
  },
19
24
  imports: {
20
25
  "#_rules@shared/*": "./src/_rules/_rules@shared/*",
@@ -315,6 +320,9 @@ var rule = {
315
320
  if (catConfig) {
316
321
  const ext = path5.extname(fileName);
317
322
  const isAllowedAsset = catConfig.allowedExtensions?.includes(ext) ?? false;
323
+ if (!isAllowedAsset && catConfig.allowedExtensions && catConfig.allowedExtensions.length > 0) {
324
+ return;
325
+ }
318
326
  if (!catConfig.allowSingleFiles && !isAllowedAsset) {
319
327
  context.report({
320
328
  message: `Category "${folderName}" cannot contain file nodes directly. File "${fileName}" must be encapsulated inside a Fragment.`,
@@ -434,6 +442,17 @@ function getProjectRoot() {
434
442
 
435
443
  // src/_rules/naming-conventions/naming-conventions.rule.ts
436
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
+ ];
437
456
  var rule3 = {
438
457
  create(context) {
439
458
  return {
@@ -597,15 +616,6 @@ var rule3 = {
597
616
  if (parentType === "invalid-fragment") {
598
617
  return;
599
618
  }
600
- if (fileName === "index.ts" || fileName === "index.js") {
601
- if (parentType !== "fragment") {
602
- context.report({
603
- message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
604
- node
605
- });
606
- }
607
- return;
608
- }
609
619
  const rfConfig = getRootFragmentConfig(relDir, config);
610
620
  if (rfConfig) {
611
621
  const flatRootNodes = rfConfig.rootNodes.flat();
@@ -621,11 +631,34 @@ var rule3 = {
621
631
  return;
622
632
  }
623
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
+ }
624
654
  if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
625
655
  return;
626
656
  }
627
657
  const role = getFileRole(fileName, config);
628
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.`;
629
662
  if (parentType === "fragment") {
630
663
  const matchedRoute = config.routeHierarchies?.find(
631
664
  (rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
@@ -639,6 +672,16 @@ var rule3 = {
639
672
  node
640
673
  });
641
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
+ }
642
685
  }
643
686
  let currentParent = path7.dirname(relDir).replace(/\\/g, "/");
644
687
  let categoryConfig = null;
@@ -677,6 +720,19 @@ var rule3 = {
677
720
  message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
678
721
  node
679
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
+ }
680
736
  }
681
737
  }
682
738
  if (parentType === "category") {
@@ -707,6 +763,8 @@ var rule3 = {
707
763
  message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
708
764
  node
709
765
  });
766
+ } else if (roleSegments.length > 1) {
767
+ context.report({ message: multiRoleMessage, node });
710
768
  }
711
769
  } else {
712
770
  if (role && role !== catConfig.role) {
@@ -714,6 +772,8 @@ var rule3 = {
714
772
  message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
715
773
  node
716
774
  });
775
+ } else if (role && roleSegments.length > 1) {
776
+ context.report({ message: multiRoleMessage, node });
717
777
  }
718
778
  }
719
779
  }
@@ -783,6 +843,16 @@ function _resolveImportPath(importPath, currentFileRelative, settings) {
783
843
  }
784
844
  return finalizeResolvedPath(relResolved);
785
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
+ ]);
786
856
  function finalizeResolvedPath(relResolved) {
787
857
  const resolvedDir = path8.dirname(relResolved).replace(/\\/g, "/");
788
858
  const resolvedBase = path8.basename(relResolved);
@@ -796,9 +866,12 @@ function finalizeResolvedPath(relResolved) {
796
866
  const parentContents = readDirCached(resolvedDir);
797
867
  const exactMatch = parentContents.files.find((f) => f === resolvedBase);
798
868
  if (!exactMatch) {
799
- const matchWithExt = parentContents.files.find(
869
+ const candidates = parentContents.files.filter(
800
870
  (f) => f.startsWith(resolvedBase + ".")
801
871
  );
872
+ const matchWithExt = candidates.find(
873
+ (f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
874
+ ) ?? candidates[0];
802
875
  if (matchWithExt) {
803
876
  return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
804
877
  }
@@ -867,7 +940,7 @@ var rule4 = {
867
940
  if (importedFolderType === "fragment") {
868
941
  const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
869
942
  if (!isAccessNode) {
870
- const isInside = relPath === importedDir || relPath.startsWith(importedDir + "/");
943
+ const isInside = path9.dirname(relPath) === importedDir;
871
944
  if (!isInside) {
872
945
  context.report({
873
946
  message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
@@ -902,43 +975,56 @@ var rule4 = {
902
975
  var no_direct_fragment_import_rule_default = rule4;
903
976
 
904
977
  // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
905
- import path11 from "path";
978
+ import path12 from "path";
906
979
 
907
- // src/_rules/_rules@shared/_utils/_systems/get-private-category-owner/get-private-category-owner.util.ts
980
+ // src/_rules/_rules@shared/_utils/_primitives/get-fractal-branch-owner/get-fractal-branch-owner.util.ts
908
981
  import path10 from "path";
909
- function getPrivateCategoryOwner(relPath, config) {
982
+ function getFractalBranchOwner(relPath) {
910
983
  let current = relPath;
911
984
  while (current && current !== "." && current !== "/") {
985
+ const folderName = path10.basename(current);
986
+ if (folderName.startsWith("_") && folderName.includes("@shared")) {
987
+ return path10.dirname(current).replace(/\\/g, "/");
988
+ }
912
989
  const parent = path10.dirname(current).replace(/\\/g, "/");
913
990
  if (parent === current) {
914
991
  break;
915
992
  }
916
- const currentType = classifyFolder(current, config);
917
- const parentType = classifyFolder(parent, config);
918
- if (currentType === "category" && (parentType === "fragment" || parentType === "root-fragment")) {
919
- return parent;
920
- }
921
993
  current = parent;
922
994
  }
923
995
  return null;
924
996
  }
925
997
 
926
- // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
927
- function getFractalBranchOwner(relPath) {
998
+ // src/_rules/_rules@shared/_utils/_primitives/get-immediate-sub-domain/get-immediate-sub-domain.util.ts
999
+ function getImmediateSubDomain(relPath, privateCategoryPath) {
1000
+ if (relPath !== privateCategoryPath && !relPath.startsWith(privateCategoryPath + "/")) {
1001
+ return "";
1002
+ }
1003
+ const relative = relPath.substring(privateCategoryPath.length + 1);
1004
+ const firstSegment = relative.split("/")[0];
1005
+ return firstSegment ? privateCategoryPath + "/" + firstSegment : privateCategoryPath;
1006
+ }
1007
+
1008
+ // src/_rules/_rules@shared/_utils/_systems/get-private-category-info/get-private-category-info.util.ts
1009
+ import path11 from "path";
1010
+ function getPrivateCategoryInfo(relPath, config) {
928
1011
  let current = relPath;
929
1012
  while (current && current !== "." && current !== "/") {
930
- const folderName = path11.basename(current);
931
- if (folderName.startsWith("_") && folderName.includes("@shared")) {
932
- return path11.dirname(current).replace(/\\/g, "/");
933
- }
934
1013
  const parent = path11.dirname(current).replace(/\\/g, "/");
935
1014
  if (parent === current) {
936
1015
  break;
937
1016
  }
1017
+ const currentType = classifyFolder(current, config);
1018
+ const parentType = classifyFolder(parent, config);
1019
+ if (currentType === "category" && (parentType === "fragment" || parentType === "root-fragment")) {
1020
+ return { owner: parent, privateCategoryPath: current };
1021
+ }
938
1022
  current = parent;
939
1023
  }
940
1024
  return null;
941
1025
  }
1026
+
1027
+ // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
942
1028
  var rule5 = {
943
1029
  create(context) {
944
1030
  const absPath = context.filename;
@@ -960,22 +1046,33 @@ var rule5 = {
960
1046
  if (!resolvedRelPath) {
961
1047
  return;
962
1048
  }
963
- const importedDir = path11.dirname(resolvedRelPath);
964
- const owner = getFractalBranchOwner(importedDir);
965
- if (owner) {
966
- const isDescendant = relPath === owner || relPath.startsWith(owner + "/");
1049
+ const importedDir = path12.dirname(resolvedRelPath);
1050
+ const fractalBranchOwner = getFractalBranchOwner(importedDir);
1051
+ if (fractalBranchOwner) {
1052
+ const isDescendant = relPath === fractalBranchOwner || relPath.startsWith(fractalBranchOwner + "/");
967
1053
  if (!isDescendant) {
968
1054
  context.report({
969
- message: `Importing from Fractal Branch is forbidden. The imported resource is private to "${owner}" and its descendants.`,
1055
+ message: `Importing from Fractal Branch is forbidden. The imported resource is private to "${fractalBranchOwner}" and its descendants.`,
970
1056
  node
971
1057
  });
972
1058
  } else {
973
- const privateOwner = getPrivateCategoryOwner(importedDir, config);
974
- if (privateOwner) {
975
- const isPrivateDescendant = relPath === privateOwner || relPath.startsWith(privateOwner + "/");
976
- if (!isPrivateDescendant) {
1059
+ const info = getPrivateCategoryInfo(importedDir, config);
1060
+ if (info) {
1061
+ const { owner, privateCategoryPath } = info;
1062
+ const isPrivateDirectChild = path12.dirname(relPath) === owner;
1063
+ const importerSubDomain = getImmediateSubDomain(
1064
+ relPath,
1065
+ privateCategoryPath
1066
+ );
1067
+ const importedSubDomain = getImmediateSubDomain(
1068
+ resolvedRelPath,
1069
+ privateCategoryPath
1070
+ );
1071
+ const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
1072
+ const isFractalBranchAllowed = fractalBranchOwner === privateCategoryPath || fractalBranchOwner.startsWith(privateCategoryPath + "/");
1073
+ if (!isPrivateDirectChild && !isInsideSameSubDomain && !isFractalBranchAllowed) {
977
1074
  context.report({
978
- message: `Importing from Private Category is forbidden. The imported resource is private to "${privateOwner}".`,
1075
+ message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
979
1076
  node
980
1077
  });
981
1078
  }
@@ -1008,7 +1105,7 @@ var rule5 = {
1008
1105
  var no_fractal_branch_leak_rule_default = rule5;
1009
1106
 
1010
1107
  // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1011
- import path12 from "path";
1108
+ import path13 from "path";
1012
1109
 
1013
1110
  // src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
1014
1111
  function findTreeConfigIncludingExcluded(relPath, settings) {
@@ -1086,7 +1183,7 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1086
1183
 
1087
1184
  // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1088
1185
  function getDirectoryRole(dirPath, config) {
1089
- const folderName = path12.basename(dirPath);
1186
+ const folderName = path13.basename(dirPath);
1090
1187
  if (folderName.startsWith("_")) {
1091
1188
  return getRoleFromFolderName(folderName, config);
1092
1189
  }
@@ -1104,7 +1201,7 @@ function getRoleFromFolderName(name, config) {
1104
1201
  return catConfig ? catConfig.role ?? null : null;
1105
1202
  }
1106
1203
  function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1107
- const relToLca = path12.relative(lcaPath, relPath).replace(/\\/g, "/");
1204
+ const relToLca = path13.relative(lcaPath, relPath).replace(/\\/g, "/");
1108
1205
  for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1109
1206
  const group = rfConfig.rootNodes[i];
1110
1207
  if (group && group.includes(relToLca)) {
@@ -1116,7 +1213,7 @@ function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1116
1213
  function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1117
1214
  let current = relPathB;
1118
1215
  while (current && current !== "." && current !== "/" && current !== lcaPath) {
1119
- const parent = path12.dirname(current).replace(/\\/g, "/");
1216
+ const parent = path13.dirname(current).replace(/\\/g, "/");
1120
1217
  if (parent === current) {
1121
1218
  break;
1122
1219
  }
@@ -1141,7 +1238,7 @@ var rule6 = {
1141
1238
  if (!config) {
1142
1239
  return {};
1143
1240
  }
1144
- const currentDir = path12.dirname(relPath);
1241
+ const currentDir = path13.dirname(relPath);
1145
1242
  const isCurrentFileExcluded = config.excludes?.some(
1146
1243
  (exc) => relPath === exc || relPath.startsWith(exc + "/")
1147
1244
  ) ?? false;
@@ -1174,10 +1271,10 @@ var rule6 = {
1174
1271
  if (isCurrentFileExcluded || isImportedFileExcluded) {
1175
1272
  return;
1176
1273
  }
1177
- const importedDir = path12.dirname(resolvedRelPath);
1274
+ const importedDir = path13.dirname(resolvedRelPath);
1178
1275
  if (currentDir === importedDir) {
1179
- const fileA = path12.basename(relPath);
1180
- const fileB = path12.basename(resolvedRelPath);
1276
+ const fileA = path13.basename(relPath);
1277
+ const fileB = path13.basename(resolvedRelPath);
1181
1278
  const roleA = getFileRole(fileA, config);
1182
1279
  const roleB = getFileRole(fileB, config);
1183
1280
  if (roleA && roleB) {
@@ -1206,6 +1303,10 @@ var rule6 = {
1206
1303
  return;
1207
1304
  }
1208
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
+ });
1209
1310
  return;
1210
1311
  }
1211
1312
  const rfConfig = getRootFragmentConfig(lca, config);
@@ -1215,13 +1316,22 @@ var rule6 = {
1215
1316
  if (idxRootA !== -1 && idxRootB !== -1) {
1216
1317
  if (idxRootB >= idxRootA) {
1217
1318
  context.report({
1218
- message: `Root Node import violation: "${path12.basename(relPath)}" cannot import from "${path12.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1319
+ message: `Root Node import violation: "${path13.basename(relPath)}" cannot import from "${path13.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1219
1320
  node
1220
1321
  });
1221
1322
  }
1222
1323
  return;
1223
1324
  }
1224
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
+ }
1225
1335
  const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
1226
1336
  if (hasDefinedHierarchy) {
1227
1337
  if (!allowed) {
@@ -1231,8 +1341,8 @@ var rule6 = {
1231
1341
  });
1232
1342
  }
1233
1343
  } else {
1234
- const roleA = getDirectoryRole(path12.posix.join(lca, subA), config);
1235
- const roleB = getDirectoryRole(path12.posix.join(lca, subB), config);
1344
+ const roleA = getDirectoryRole(path13.posix.join(lca, subA), config);
1345
+ const roleB = getDirectoryRole(path13.posix.join(lca, subB), config);
1236
1346
  const idxA = roleA ? getRoleHierarchyIndex(roleA, config) : -1;
1237
1347
  const idxB = roleB ? getRoleHierarchyIndex(roleB, config) : -1;
1238
1348
  if (idxA !== -1 && idxB !== -1) {
@@ -1275,7 +1385,7 @@ var rule6 = {
1275
1385
  var no_peer_dependency_rule_default = rule6;
1276
1386
 
1277
1387
  // src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
1278
- import path13 from "path";
1388
+ import path14 from "path";
1279
1389
  var rule7 = {
1280
1390
  create(context) {
1281
1391
  const absPath = context.filename;
@@ -1297,11 +1407,32 @@ var rule7 = {
1297
1407
  if (!resolvedRelPath) {
1298
1408
  return;
1299
1409
  }
1300
- const importedDir = path13.dirname(resolvedRelPath);
1301
- const owner = getPrivateCategoryOwner(importedDir, config);
1302
- if (owner) {
1303
- const isDescendant = relPath === owner || relPath.startsWith(owner + "/");
1304
- if (!isDescendant) {
1410
+ const importedDir = path14.dirname(resolvedRelPath);
1411
+ const info = getPrivateCategoryInfo(importedDir, config);
1412
+ if (info) {
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
+ }
1424
+ const isDirectChild = path14.dirname(relPath) === owner;
1425
+ const importerSubDomain = getImmediateSubDomain(
1426
+ relPath,
1427
+ privateCategoryPath
1428
+ );
1429
+ const importedSubDomain = getImmediateSubDomain(
1430
+ resolvedRelPath,
1431
+ privateCategoryPath
1432
+ );
1433
+ const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
1434
+ const isAllowed = isFractalBranchInPrivateCategory ? isFractalBranchAllowed : isDirectChild || isInsideSameSubDomain;
1435
+ if (!isAllowed) {
1305
1436
  context.report({
1306
1437
  message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
1307
1438
  node