@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/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.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 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
  }
@@ -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
- 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) {
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