@morriconeluca/eslint-plugin-faf 0.2.0 → 0.4.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,7 +1,7 @@
1
1
  // package.json
2
2
  var package_default = {
3
3
  name: "@morriconeluca/eslint-plugin-faf",
4
- version: "0.2.0",
4
+ version: "0.4.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 matchWithExt = parentContents.files.find(
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 || relPath.startsWith(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 owner = getFractalBranchOwner(importedDir);
986
- if (owner) {
987
- const isDescendant = relPath === owner || relPath.startsWith(owner + "/");
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 "${owner}" and its descendants.`,
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: owner2, privateCategoryPath } = info;
997
- const isPrivateDirectChild = path12.dirname(relPath) === owner2;
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
- let isFractalBranchAllowed = false;
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 "${owner2}".`,
1075
+ message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
1018
1076
  node
1019
1077
  });
1020
1078
  }
@@ -1124,24 +1182,6 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1124
1182
  }
1125
1183
 
1126
1184
  // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1127
- function getDirectoryRole(dirPath, config) {
1128
- const folderName = path13.basename(dirPath);
1129
- if (folderName.startsWith("_")) {
1130
- return getRoleFromFolderName(folderName, config);
1131
- }
1132
- const contents = readDirCached(dirPath);
1133
- for (const file of contents.files) {
1134
- if (file.startsWith(folderName + ".")) {
1135
- const role = getFileRole(file, config);
1136
- if (role) return role;
1137
- }
1138
- }
1139
- return null;
1140
- }
1141
- function getRoleFromFolderName(name, config) {
1142
- const catConfig = config.categories?.find((c) => c.name === name);
1143
- return catConfig ? catConfig.role ?? null : null;
1144
- }
1145
1185
  function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1146
1186
  const relToLca = path13.relative(lcaPath, relPath).replace(/\\/g, "/");
1147
1187
  for (let i = 0; i < rfConfig.rootNodes.length; i++) {
@@ -1245,6 +1285,10 @@ var rule6 = {
1245
1285
  return;
1246
1286
  }
1247
1287
  if (subB === "" && classifyFolder(lca, config) === "fragment") {
1288
+ context.report({
1289
+ 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.`,
1290
+ node
1291
+ });
1248
1292
  return;
1249
1293
  }
1250
1294
  const rfConfig = getRootFragmentConfig(lca, config);
@@ -1261,6 +1305,15 @@ var rule6 = {
1261
1305
  return;
1262
1306
  }
1263
1307
  }
1308
+ const isSubARootFragment = classifyFolder(path13.posix.join(lca, subA), config) === "root-fragment";
1309
+ const isSubBRootFragment = classifyFolder(path13.posix.join(lca, subB), config) === "root-fragment";
1310
+ 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
+ return;
1316
+ }
1264
1317
  const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
1265
1318
  if (hasDefinedHierarchy) {
1266
1319
  if (!allowed) {
@@ -1270,23 +1323,10 @@ var rule6 = {
1270
1323
  });
1271
1324
  }
1272
1325
  } else {
1273
- const roleA = getDirectoryRole(path13.posix.join(lca, subA), config);
1274
- const roleB = getDirectoryRole(path13.posix.join(lca, subB), config);
1275
- const idxA = roleA ? getRoleHierarchyIndex(roleA, config) : -1;
1276
- const idxB = roleB ? getRoleHierarchyIndex(roleB, config) : -1;
1277
- if (idxA !== -1 && idxB !== -1) {
1278
- if (idxB >= idxA) {
1279
- context.report({
1280
- message: `Horizontal hierarchy violation (fallback to roles): sibling directory "${subA}" (mapped to role "${roleA}", level ${idxA}) cannot import from "${subB}" (mapped to role "${roleB}", level ${idxB}) under parent "${lca}".`,
1281
- node
1282
- });
1283
- }
1284
- } else {
1285
- context.report({
1286
- message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
1287
- node
1288
- });
1289
- }
1326
+ context.report({
1327
+ message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
1328
+ node
1329
+ });
1290
1330
  }
1291
1331
  }
1292
1332
  return {
@@ -1340,6 +1380,16 @@ var rule7 = {
1340
1380
  const info = getPrivateCategoryInfo(importedDir, config);
1341
1381
  if (info) {
1342
1382
  const { owner, privateCategoryPath } = info;
1383
+ let isFractalBranchInPrivateCategory = false;
1384
+ let isFractalBranchAllowed = false;
1385
+ const fbOwner = getFractalBranchOwner(importedDir);
1386
+ if (fbOwner) {
1387
+ const isFbInsidePrivateCategory = fbOwner === privateCategoryPath || fbOwner.startsWith(privateCategoryPath + "/");
1388
+ if (isFbInsidePrivateCategory) {
1389
+ isFractalBranchInPrivateCategory = true;
1390
+ isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
1391
+ }
1392
+ }
1343
1393
  const isDirectChild = path14.dirname(relPath) === owner;
1344
1394
  const importerSubDomain = getImmediateSubDomain(
1345
1395
  relPath,
@@ -1350,15 +1400,8 @@ var rule7 = {
1350
1400
  privateCategoryPath
1351
1401
  );
1352
1402
  const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
1353
- let isFractalBranchAllowed = false;
1354
- const fbOwner = getFractalBranchOwner(importedDir);
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) {
1403
+ const isAllowed = isFractalBranchInPrivateCategory ? isFractalBranchAllowed : isDirectChild || isInsideSameSubDomain;
1404
+ if (!isAllowed) {
1362
1405
  context.report({
1363
1406
  message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
1364
1407
  node