@morriconeluca/eslint-plugin-faf 0.1.1 → 0.2.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 CHANGED
@@ -1,4 +1,4 @@
1
- # eslint-plugin-faf
1
+ # @morriconeluca/eslint-plugin-faf
2
2
 
3
3
  An ESLint plugin to deterministically enforce the rules and constraints of the **[Fractal Architecture Framework (FAF)](https://github.com/morriconeluca/fractal-architecture-framework)**.
4
4
 
@@ -20,18 +20,18 @@ This plugin is designed for complex TypeScript/JavaScript codebases to ensure **
20
20
  Install the plugin along with its peer dependencies:
21
21
 
22
22
  ```bash
23
- npm install eslint-plugin-faf --save-dev
23
+ npm install @morriconeluca/eslint-plugin-faf --save-dev
24
24
  # or
25
- yarn add eslint-plugin-faf --dev
25
+ yarn add @morriconeluca/eslint-plugin-faf --dev
26
26
  # or
27
- pnpm add eslint-plugin-faf -D
27
+ pnpm add @morriconeluca/eslint-plugin-faf -D
28
28
  ```
29
29
 
30
30
  ---
31
31
 
32
32
  ## Configuration
33
33
 
34
- `eslint-plugin-faf` is configured via the ESLint flat config file (`eslint.config.ts` or `eslint.config.js`). All architectural settings are loaded from the global `settings.faf` object.
34
+ `@morriconeluca/eslint-plugin-faf` is configured via the ESLint flat config file (`eslint.config.ts` or `eslint.config.js`). All architectural settings are loaded from the global `settings.faf` object.
35
35
 
36
36
  ### 1. Integration in `eslint.config.ts`
37
37
 
@@ -40,7 +40,7 @@ For a clean setup, we recommend defining your FAF taxonomy settings in a separat
40
40
  Since FAF is an **all-or-nothing architectural system**, all rules must be active as blocking errors to prevent architectural drift. The canonical way to integrate it is by extending `fafPlugin.configs.recommended` (which pre-configures all rules as `"error"`) and injecting your settings:
41
41
 
42
42
  ```typescript
43
- import fafPlugin from 'eslint-plugin-faf';
43
+ import fafPlugin from '@morriconeluca/eslint-plugin-faf';
44
44
  import fafSettings from './faf.config';
45
45
 
46
46
  export default [
package/dist/main.cjs CHANGED
@@ -39,8 +39,12 @@ module.exports = __toCommonJS(main_exports);
39
39
  // package.json
40
40
  var package_default = {
41
41
  name: "@morriconeluca/eslint-plugin-faf",
42
- version: "0.1.1",
42
+ version: "0.2.0",
43
43
  description: "An ESLint plugin for FAF projects.",
44
+ repository: {
45
+ type: "git",
46
+ url: "git+https://github.com/morriconeluca/eslint-plugin-faf.git"
47
+ },
44
48
  main: "./dist/main.cjs",
45
49
  module: "./dist/main.js",
46
50
  types: "./dist/main.d.ts",
@@ -52,7 +56,8 @@ var package_default = {
52
56
  }
53
57
  },
54
58
  publishConfig: {
55
- access: "public"
59
+ access: "public",
60
+ provenance: true
56
61
  },
57
62
  imports: {
58
63
  "#_rules@shared/*": "./src/_rules/_rules@shared/*",
@@ -353,6 +358,9 @@ var rule = {
353
358
  if (catConfig) {
354
359
  const ext = import_path5.default.extname(fileName);
355
360
  const isAllowedAsset = catConfig.allowedExtensions?.includes(ext) ?? false;
361
+ if (!isAllowedAsset && catConfig.allowedExtensions && catConfig.allowedExtensions.length > 0) {
362
+ return;
363
+ }
356
364
  if (!catConfig.allowSingleFiles && !isAllowedAsset) {
357
365
  context.report({
358
366
  message: `Category "${folderName}" cannot contain file nodes directly. File "${fileName}" must be encapsulated inside a Fragment.`,
@@ -940,43 +948,56 @@ var rule4 = {
940
948
  var no_direct_fragment_import_rule_default = rule4;
941
949
 
942
950
  // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
943
- var import_path11 = __toESM(require("path"), 1);
951
+ var import_path12 = __toESM(require("path"), 1);
944
952
 
945
- // src/_rules/_rules@shared/_utils/_systems/get-private-category-owner/get-private-category-owner.util.ts
953
+ // src/_rules/_rules@shared/_utils/_primitives/get-fractal-branch-owner/get-fractal-branch-owner.util.ts
946
954
  var import_path10 = __toESM(require("path"), 1);
947
- function getPrivateCategoryOwner(relPath, config) {
955
+ function getFractalBranchOwner(relPath) {
948
956
  let current = relPath;
949
957
  while (current && current !== "." && current !== "/") {
958
+ const folderName = import_path10.default.basename(current);
959
+ if (folderName.startsWith("_") && folderName.includes("@shared")) {
960
+ return import_path10.default.dirname(current).replace(/\\/g, "/");
961
+ }
950
962
  const parent = import_path10.default.dirname(current).replace(/\\/g, "/");
951
963
  if (parent === current) {
952
964
  break;
953
965
  }
954
- const currentType = classifyFolder(current, config);
955
- const parentType = classifyFolder(parent, config);
956
- if (currentType === "category" && (parentType === "fragment" || parentType === "root-fragment")) {
957
- return parent;
958
- }
959
966
  current = parent;
960
967
  }
961
968
  return null;
962
969
  }
963
970
 
964
- // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
965
- function getFractalBranchOwner(relPath) {
971
+ // src/_rules/_rules@shared/_utils/_primitives/get-immediate-sub-domain/get-immediate-sub-domain.util.ts
972
+ function getImmediateSubDomain(relPath, privateCategoryPath) {
973
+ if (relPath !== privateCategoryPath && !relPath.startsWith(privateCategoryPath + "/")) {
974
+ return "";
975
+ }
976
+ const relative = relPath.substring(privateCategoryPath.length + 1);
977
+ const firstSegment = relative.split("/")[0];
978
+ return firstSegment ? privateCategoryPath + "/" + firstSegment : privateCategoryPath;
979
+ }
980
+
981
+ // src/_rules/_rules@shared/_utils/_systems/get-private-category-info/get-private-category-info.util.ts
982
+ var import_path11 = __toESM(require("path"), 1);
983
+ function getPrivateCategoryInfo(relPath, config) {
966
984
  let current = relPath;
967
985
  while (current && current !== "." && current !== "/") {
968
- const folderName = import_path11.default.basename(current);
969
- if (folderName.startsWith("_") && folderName.includes("@shared")) {
970
- return import_path11.default.dirname(current).replace(/\\/g, "/");
971
- }
972
986
  const parent = import_path11.default.dirname(current).replace(/\\/g, "/");
973
987
  if (parent === current) {
974
988
  break;
975
989
  }
990
+ const currentType = classifyFolder(current, config);
991
+ const parentType = classifyFolder(parent, config);
992
+ if (currentType === "category" && (parentType === "fragment" || parentType === "root-fragment")) {
993
+ return { owner: parent, privateCategoryPath: current };
994
+ }
976
995
  current = parent;
977
996
  }
978
997
  return null;
979
998
  }
999
+
1000
+ // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
980
1001
  var rule5 = {
981
1002
  create(context) {
982
1003
  const absPath = context.filename;
@@ -998,7 +1019,7 @@ var rule5 = {
998
1019
  if (!resolvedRelPath) {
999
1020
  return;
1000
1021
  }
1001
- const importedDir = import_path11.default.dirname(resolvedRelPath);
1022
+ const importedDir = import_path12.default.dirname(resolvedRelPath);
1002
1023
  const owner = getFractalBranchOwner(importedDir);
1003
1024
  if (owner) {
1004
1025
  const isDescendant = relPath === owner || relPath.startsWith(owner + "/");
@@ -1008,12 +1029,30 @@ var rule5 = {
1008
1029
  node
1009
1030
  });
1010
1031
  } else {
1011
- const privateOwner = getPrivateCategoryOwner(importedDir, config);
1012
- if (privateOwner) {
1013
- const isPrivateDescendant = relPath === privateOwner || relPath.startsWith(privateOwner + "/");
1014
- if (!isPrivateDescendant) {
1032
+ const info = getPrivateCategoryInfo(importedDir, config);
1033
+ if (info) {
1034
+ const { owner: owner2, privateCategoryPath } = info;
1035
+ const isPrivateDirectChild = import_path12.default.dirname(relPath) === owner2;
1036
+ const importerSubDomain = getImmediateSubDomain(
1037
+ relPath,
1038
+ privateCategoryPath
1039
+ );
1040
+ const importedSubDomain = getImmediateSubDomain(
1041
+ resolvedRelPath,
1042
+ privateCategoryPath
1043
+ );
1044
+ const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
1045
+ let isFractalBranchAllowed = false;
1046
+ const fbOwner = getFractalBranchOwner(importedDir);
1047
+ if (fbOwner) {
1048
+ const isFbInsidePrivateCategory = fbOwner === privateCategoryPath || fbOwner.startsWith(privateCategoryPath + "/");
1049
+ if (isFbInsidePrivateCategory) {
1050
+ isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
1051
+ }
1052
+ }
1053
+ if (!isPrivateDirectChild && !isInsideSameSubDomain && !isFractalBranchAllowed) {
1015
1054
  context.report({
1016
- message: `Importing from Private Category is forbidden. The imported resource is private to "${privateOwner}".`,
1055
+ message: `Importing from Private Category is forbidden. The imported resource is private to "${owner2}".`,
1017
1056
  node
1018
1057
  });
1019
1058
  }
@@ -1046,7 +1085,7 @@ var rule5 = {
1046
1085
  var no_fractal_branch_leak_rule_default = rule5;
1047
1086
 
1048
1087
  // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1049
- var import_path12 = __toESM(require("path"), 1);
1088
+ var import_path13 = __toESM(require("path"), 1);
1050
1089
 
1051
1090
  // src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
1052
1091
  function findTreeConfigIncludingExcluded(relPath, settings) {
@@ -1124,7 +1163,7 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1124
1163
 
1125
1164
  // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1126
1165
  function getDirectoryRole(dirPath, config) {
1127
- const folderName = import_path12.default.basename(dirPath);
1166
+ const folderName = import_path13.default.basename(dirPath);
1128
1167
  if (folderName.startsWith("_")) {
1129
1168
  return getRoleFromFolderName(folderName, config);
1130
1169
  }
@@ -1142,7 +1181,7 @@ function getRoleFromFolderName(name, config) {
1142
1181
  return catConfig ? catConfig.role ?? null : null;
1143
1182
  }
1144
1183
  function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1145
- const relToLca = import_path12.default.relative(lcaPath, relPath).replace(/\\/g, "/");
1184
+ const relToLca = import_path13.default.relative(lcaPath, relPath).replace(/\\/g, "/");
1146
1185
  for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1147
1186
  const group = rfConfig.rootNodes[i];
1148
1187
  if (group && group.includes(relToLca)) {
@@ -1154,7 +1193,7 @@ function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1154
1193
  function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1155
1194
  let current = relPathB;
1156
1195
  while (current && current !== "." && current !== "/" && current !== lcaPath) {
1157
- const parent = import_path12.default.dirname(current).replace(/\\/g, "/");
1196
+ const parent = import_path13.default.dirname(current).replace(/\\/g, "/");
1158
1197
  if (parent === current) {
1159
1198
  break;
1160
1199
  }
@@ -1179,7 +1218,7 @@ var rule6 = {
1179
1218
  if (!config) {
1180
1219
  return {};
1181
1220
  }
1182
- const currentDir = import_path12.default.dirname(relPath);
1221
+ const currentDir = import_path13.default.dirname(relPath);
1183
1222
  const isCurrentFileExcluded = config.excludes?.some(
1184
1223
  (exc) => relPath === exc || relPath.startsWith(exc + "/")
1185
1224
  ) ?? false;
@@ -1212,10 +1251,10 @@ var rule6 = {
1212
1251
  if (isCurrentFileExcluded || isImportedFileExcluded) {
1213
1252
  return;
1214
1253
  }
1215
- const importedDir = import_path12.default.dirname(resolvedRelPath);
1254
+ const importedDir = import_path13.default.dirname(resolvedRelPath);
1216
1255
  if (currentDir === importedDir) {
1217
- const fileA = import_path12.default.basename(relPath);
1218
- const fileB = import_path12.default.basename(resolvedRelPath);
1256
+ const fileA = import_path13.default.basename(relPath);
1257
+ const fileB = import_path13.default.basename(resolvedRelPath);
1219
1258
  const roleA = getFileRole(fileA, config);
1220
1259
  const roleB = getFileRole(fileB, config);
1221
1260
  if (roleA && roleB) {
@@ -1253,7 +1292,7 @@ var rule6 = {
1253
1292
  if (idxRootA !== -1 && idxRootB !== -1) {
1254
1293
  if (idxRootB >= idxRootA) {
1255
1294
  context.report({
1256
- message: `Root Node import violation: "${import_path12.default.basename(relPath)}" cannot import from "${import_path12.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1295
+ message: `Root Node import violation: "${import_path13.default.basename(relPath)}" cannot import from "${import_path13.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1257
1296
  node
1258
1297
  });
1259
1298
  }
@@ -1269,8 +1308,8 @@ var rule6 = {
1269
1308
  });
1270
1309
  }
1271
1310
  } else {
1272
- const roleA = getDirectoryRole(import_path12.default.posix.join(lca, subA), config);
1273
- const roleB = getDirectoryRole(import_path12.default.posix.join(lca, subB), config);
1311
+ const roleA = getDirectoryRole(import_path13.default.posix.join(lca, subA), config);
1312
+ const roleB = getDirectoryRole(import_path13.default.posix.join(lca, subB), config);
1274
1313
  const idxA = roleA ? getRoleHierarchyIndex(roleA, config) : -1;
1275
1314
  const idxB = roleB ? getRoleHierarchyIndex(roleB, config) : -1;
1276
1315
  if (idxA !== -1 && idxB !== -1) {
@@ -1313,7 +1352,7 @@ var rule6 = {
1313
1352
  var no_peer_dependency_rule_default = rule6;
1314
1353
 
1315
1354
  // src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
1316
- var import_path13 = __toESM(require("path"), 1);
1355
+ var import_path14 = __toESM(require("path"), 1);
1317
1356
  var rule7 = {
1318
1357
  create(context) {
1319
1358
  const absPath = context.filename;
@@ -1335,11 +1374,29 @@ var rule7 = {
1335
1374
  if (!resolvedRelPath) {
1336
1375
  return;
1337
1376
  }
1338
- const importedDir = import_path13.default.dirname(resolvedRelPath);
1339
- const owner = getPrivateCategoryOwner(importedDir, config);
1340
- if (owner) {
1341
- const isDescendant = relPath === owner || relPath.startsWith(owner + "/");
1342
- if (!isDescendant) {
1377
+ const importedDir = import_path14.default.dirname(resolvedRelPath);
1378
+ const info = getPrivateCategoryInfo(importedDir, config);
1379
+ if (info) {
1380
+ const { owner, privateCategoryPath } = info;
1381
+ const isDirectChild = import_path14.default.dirname(relPath) === owner;
1382
+ const importerSubDomain = getImmediateSubDomain(
1383
+ relPath,
1384
+ privateCategoryPath
1385
+ );
1386
+ const importedSubDomain = getImmediateSubDomain(
1387
+ resolvedRelPath,
1388
+ privateCategoryPath
1389
+ );
1390
+ const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
1391
+ let isFractalBranchAllowed = false;
1392
+ const fbOwner = getFractalBranchOwner(importedDir);
1393
+ if (fbOwner) {
1394
+ const isFbInsidePrivateCategory = fbOwner === privateCategoryPath || fbOwner.startsWith(privateCategoryPath + "/");
1395
+ if (isFbInsidePrivateCategory) {
1396
+ isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
1397
+ }
1398
+ }
1399
+ if (!isDirectChild && !isInsideSameSubDomain && !isFractalBranchAllowed) {
1343
1400
  context.report({
1344
1401
  message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
1345
1402
  node