@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 +6 -6
- package/dist/main.cjs +96 -39
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +96 -39
- package/dist/main.js.map +1 -1
- package/package.json +7 -2
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
|
-
|
|
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.
|
|
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
|
|
951
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
944
952
|
|
|
945
|
-
// src/_rules/_rules@shared/_utils/
|
|
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
|
|
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/
|
|
965
|
-
function
|
|
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 =
|
|
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
|
|
1012
|
-
if (
|
|
1013
|
-
const
|
|
1014
|
-
|
|
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 "${
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1254
|
+
const importedDir = import_path13.default.dirname(resolvedRelPath);
|
|
1216
1255
|
if (currentDir === importedDir) {
|
|
1217
|
-
const fileA =
|
|
1218
|
-
const fileB =
|
|
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: "${
|
|
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(
|
|
1273
|
-
const roleB = getDirectoryRole(
|
|
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
|
|
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 =
|
|
1339
|
-
const
|
|
1340
|
-
if (
|
|
1341
|
-
const
|
|
1342
|
-
|
|
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
|