@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/README.md +24 -10
- package/dist/main.cjs +185 -54
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +185 -54
- 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
|
|
|
@@ -15,23 +15,32 @@ This plugin is designed for complex TypeScript/JavaScript codebases to ensure **
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## Companion Tooling
|
|
19
|
+
|
|
20
|
+
`eslint-plugin-faf` does not detect dependency cycles. FAF requires the dependency graph to be a Directed Acyclic Graph, but enforcing that is intentionally left to dedicated tools, configured separately in your project:
|
|
21
|
+
|
|
22
|
+
- [`eslint-plugin-import-x`](https://github.com/un-ts/eslint-plugin-import-x)'s `import-x/no-cycle` rule.
|
|
23
|
+
- [`dependency-cruiser`](https://github.com/sverweij/dependency-cruiser), for cycle detection and other custom dependency fitness functions.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
18
27
|
## Installation
|
|
19
28
|
|
|
20
29
|
Install the plugin along with its peer dependencies:
|
|
21
30
|
|
|
22
31
|
```bash
|
|
23
|
-
npm install eslint-plugin-faf --save-dev
|
|
32
|
+
npm install @morriconeluca/eslint-plugin-faf --save-dev
|
|
24
33
|
# or
|
|
25
|
-
yarn add eslint-plugin-faf --dev
|
|
34
|
+
yarn add @morriconeluca/eslint-plugin-faf --dev
|
|
26
35
|
# or
|
|
27
|
-
pnpm add eslint-plugin-faf -D
|
|
36
|
+
pnpm add @morriconeluca/eslint-plugin-faf -D
|
|
28
37
|
```
|
|
29
38
|
|
|
30
39
|
---
|
|
31
40
|
|
|
32
41
|
## Configuration
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
`@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
44
|
|
|
36
45
|
### 1. Integration in `eslint.config.ts`
|
|
37
46
|
|
|
@@ -40,7 +49,7 @@ For a clean setup, we recommend defining your FAF taxonomy settings in a separat
|
|
|
40
49
|
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
50
|
|
|
42
51
|
```typescript
|
|
43
|
-
import fafPlugin from 'eslint-plugin-faf';
|
|
52
|
+
import fafPlugin from '@morriconeluca/eslint-plugin-faf';
|
|
44
53
|
import fafSettings from './faf.config';
|
|
45
54
|
|
|
46
55
|
export default [
|
|
@@ -213,8 +222,12 @@ export default {
|
|
|
213
222
|
{
|
|
214
223
|
// Subtree where intermediate folders are classified as organizational Layers,
|
|
215
224
|
// and the terminal Fragment must contain a Master Node with the specified role (e.g. '.api.ts')
|
|
225
|
+
// and a name following the "<method>-<object>" pattern (e.g. 'get-todo')
|
|
216
226
|
paths: ['src/_src@shared/_network/_apis'],
|
|
217
227
|
role: 'api',
|
|
228
|
+
// Optional: restricts which HTTP methods are valid as the Fragment name prefix.
|
|
229
|
+
// Defaults to all standard HTTP methods (get, post, put, patch, delete, head, options, connect, trace).
|
|
230
|
+
httpMethods: ['get', 'post', 'put', 'patch', 'delete'],
|
|
218
231
|
},
|
|
219
232
|
],
|
|
220
233
|
},
|
|
@@ -264,8 +277,9 @@ Verifies folders, files, and roles comply with FAF taxonomy:
|
|
|
264
277
|
|
|
265
278
|
- Enforces `kebab-case` naming for all folders and files in the logical domain.
|
|
266
279
|
- Validates that internal Fragment files share their parent Fragment's name prefix (e.g. files in folder `button/` must be named `button.<role>.<ext>`).
|
|
267
|
-
- Enforces role suffix validation on Fragment Nodes and Logical Nodes.
|
|
268
|
-
-
|
|
280
|
+
- Enforces role suffix validation on Fragment Nodes and Logical Nodes, restricted to a single Role segment per file name.
|
|
281
|
+
- Requires every Fragment to contain at least one Fragment Node (its Master Node), and forbids two Fragment Nodes from sharing the same Role.
|
|
282
|
+
- Assures route terminal Fragments contain the appropriate Master Node (e.g. a `.api.ts` file under `_apis`) and that the Fragment name follows the `<method>-<object>` pattern (e.g. `get-todo`), with the HTTP method restricted to `routeHierarchies[].httpMethods` or, by default, all standard HTTP methods.
|
|
269
283
|
- Enforces FAF logical domain containment rules:
|
|
270
284
|
- Root Fragments can only reside directly in the Root Container or another Root Fragment.
|
|
271
285
|
- Layers cannot reside inside Fragments.
|
|
@@ -289,7 +303,7 @@ Applies **Fragment Encapsulation**:
|
|
|
289
303
|
|
|
290
304
|
Applies **Private Category Encapsulation**:
|
|
291
305
|
|
|
292
|
-
- Restricts consumption of elements inside a Private Category (e.g. `_components/` nested inside a Fragment) to
|
|
306
|
+
- Restricts consumption of elements inside a Private Category (e.g. `_components/` nested inside a Fragment) to the direct child Fragment Nodes of the owning Fragment/Root Fragment, and to sibling nodes within the same immediate sub-domain of the Private Category. Access does not extend to other sub-domains, nor to a Fractal Branch nested inside the Private Category (governed instead by its own encapsulation rule).
|
|
293
307
|
|
|
294
308
|
### 5. `faf/no-fractal-branch-leak`
|
|
295
309
|
|
|
@@ -304,7 +318,7 @@ Applies **Peer Isolation**:
|
|
|
304
318
|
- Prevents sibling files and folders from importing each other unless an explicit hierarchy is configured:
|
|
305
319
|
- **Inside a Fragment**: Flow is governed by the `roles` array order.
|
|
306
320
|
- **Between folders**: Flow is governed by `localHorizontalHierarchies` or `globalHorizontalHierarchies`. If undefined, the linter falls back to mapped roles. Otherwise, peer isolation is strictly enforced.
|
|
307
|
-
- **Root Nodes**: Flow is governed by the defined index order in `rootFragments.rootNodes`.
|
|
321
|
+
- **Root Nodes**: Flow is governed exclusively by the defined index order in `rootFragments.rootNodes`. Root Nodes are exempt from the Role naming convention, so relationships between them never fall back to the `roles` scale: any relationship without a matching `rootNodes` entry is denied.
|
|
308
322
|
|
|
309
323
|
### 7. `faf/category-mutually-exclusive`
|
|
310
324
|
|
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.3.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.`,
|
|
@@ -472,6 +480,17 @@ function getProjectRoot() {
|
|
|
472
480
|
|
|
473
481
|
// src/_rules/naming-conventions/naming-conventions.rule.ts
|
|
474
482
|
var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
483
|
+
var DEFAULT_HTTP_METHODS = [
|
|
484
|
+
"connect",
|
|
485
|
+
"delete",
|
|
486
|
+
"get",
|
|
487
|
+
"head",
|
|
488
|
+
"options",
|
|
489
|
+
"patch",
|
|
490
|
+
"post",
|
|
491
|
+
"put",
|
|
492
|
+
"trace"
|
|
493
|
+
];
|
|
475
494
|
var rule3 = {
|
|
476
495
|
create(context) {
|
|
477
496
|
return {
|
|
@@ -635,15 +654,6 @@ var rule3 = {
|
|
|
635
654
|
if (parentType === "invalid-fragment") {
|
|
636
655
|
return;
|
|
637
656
|
}
|
|
638
|
-
if (fileName === "index.ts" || fileName === "index.js") {
|
|
639
|
-
if (parentType !== "fragment") {
|
|
640
|
-
context.report({
|
|
641
|
-
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
|
|
642
|
-
node
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
657
|
const rfConfig = getRootFragmentConfig(relDir, config);
|
|
648
658
|
if (rfConfig) {
|
|
649
659
|
const flatRootNodes = rfConfig.rootNodes.flat();
|
|
@@ -659,11 +669,34 @@ var rule3 = {
|
|
|
659
669
|
return;
|
|
660
670
|
}
|
|
661
671
|
}
|
|
672
|
+
if (fileName === "index.ts" || fileName === "index.js") {
|
|
673
|
+
if (parentType !== "fragment") {
|
|
674
|
+
context.report({
|
|
675
|
+
message: `Access Nodes ("index.ts/index.js") are exclusive to Fragments. Found index file directly inside "${folderName}" (classified as ${parentType}).`,
|
|
676
|
+
node
|
|
677
|
+
});
|
|
678
|
+
} else {
|
|
679
|
+
const contents = readDirCached(relDir);
|
|
680
|
+
const hasFragmentNode = contents.files.some(
|
|
681
|
+
(f) => f !== "index.ts" && f !== "index.js" && f !== "README.md" && f !== "package.json" && f !== "tsconfig.json"
|
|
682
|
+
);
|
|
683
|
+
if (!hasFragmentNode) {
|
|
684
|
+
context.report({
|
|
685
|
+
message: `Fragment "${folderName}" has no Master Node. Every Fragment must contain at least one Fragment Node establishing its Role.`,
|
|
686
|
+
node
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
662
692
|
if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
|
|
663
693
|
return;
|
|
664
694
|
}
|
|
665
695
|
const role = getFileRole(fileName, config);
|
|
666
696
|
const ext = import_path7.default.extname(fileName);
|
|
697
|
+
const allRoleNames = new Set(config.roles.flat());
|
|
698
|
+
const roleSegments = fileName.slice(0, fileName.length - ext.length).split(".").slice(1).filter((part) => allRoleNames.has(part));
|
|
699
|
+
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.`;
|
|
667
700
|
if (parentType === "fragment") {
|
|
668
701
|
const matchedRoute = config.routeHierarchies?.find(
|
|
669
702
|
(rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
|
|
@@ -677,6 +710,16 @@ var rule3 = {
|
|
|
677
710
|
node
|
|
678
711
|
});
|
|
679
712
|
}
|
|
713
|
+
const httpMethods = matchedRoute.httpMethods ?? DEFAULT_HTTP_METHODS;
|
|
714
|
+
const routeNameRegex = new RegExp(
|
|
715
|
+
`^(${httpMethods.join("|")})-[a-z0-9]+(-[a-z0-9]+)*$`
|
|
716
|
+
);
|
|
717
|
+
if (!routeNameRegex.test(folderName)) {
|
|
718
|
+
context.report({
|
|
719
|
+
message: `Route terminal Fragment "${folderName}" must follow the "<method>-<object>" pattern with a lowercase HTTP method (${httpMethods.join(", ")}).`,
|
|
720
|
+
node
|
|
721
|
+
});
|
|
722
|
+
}
|
|
680
723
|
}
|
|
681
724
|
let currentParent = import_path7.default.dirname(relDir).replace(/\\/g, "/");
|
|
682
725
|
let categoryConfig = null;
|
|
@@ -715,6 +758,19 @@ var rule3 = {
|
|
|
715
758
|
message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
|
|
716
759
|
node
|
|
717
760
|
});
|
|
761
|
+
} else if (roleSegments.length > 1) {
|
|
762
|
+
context.report({ message: multiRoleMessage, node });
|
|
763
|
+
} else {
|
|
764
|
+
const contents = readDirCached(relDir);
|
|
765
|
+
const duplicate = contents.files.find(
|
|
766
|
+
(f) => f !== fileName && f !== "index.ts" && f !== "index.js" && getFileRole(f, config) === role
|
|
767
|
+
);
|
|
768
|
+
if (duplicate) {
|
|
769
|
+
context.report({
|
|
770
|
+
message: `Fragment Node "${fileName}" shares Role "${role}" with sibling "${duplicate}". Each Fragment Node must have a unique Role within its Fragment.`,
|
|
771
|
+
node
|
|
772
|
+
});
|
|
773
|
+
}
|
|
718
774
|
}
|
|
719
775
|
}
|
|
720
776
|
if (parentType === "category") {
|
|
@@ -745,6 +801,8 @@ var rule3 = {
|
|
|
745
801
|
message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
746
802
|
node
|
|
747
803
|
});
|
|
804
|
+
} else if (roleSegments.length > 1) {
|
|
805
|
+
context.report({ message: multiRoleMessage, node });
|
|
748
806
|
}
|
|
749
807
|
} else {
|
|
750
808
|
if (role && role !== catConfig.role) {
|
|
@@ -752,6 +810,8 @@ var rule3 = {
|
|
|
752
810
|
message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
|
|
753
811
|
node
|
|
754
812
|
});
|
|
813
|
+
} else if (role && roleSegments.length > 1) {
|
|
814
|
+
context.report({ message: multiRoleMessage, node });
|
|
755
815
|
}
|
|
756
816
|
}
|
|
757
817
|
}
|
|
@@ -821,6 +881,16 @@ function _resolveImportPath(importPath, currentFileRelative, settings) {
|
|
|
821
881
|
}
|
|
822
882
|
return finalizeResolvedPath(relResolved);
|
|
823
883
|
}
|
|
884
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
885
|
+
"cjs",
|
|
886
|
+
"cts",
|
|
887
|
+
"js",
|
|
888
|
+
"jsx",
|
|
889
|
+
"mjs",
|
|
890
|
+
"mts",
|
|
891
|
+
"ts",
|
|
892
|
+
"tsx"
|
|
893
|
+
]);
|
|
824
894
|
function finalizeResolvedPath(relResolved) {
|
|
825
895
|
const resolvedDir = import_path8.default.dirname(relResolved).replace(/\\/g, "/");
|
|
826
896
|
const resolvedBase = import_path8.default.basename(relResolved);
|
|
@@ -834,9 +904,12 @@ function finalizeResolvedPath(relResolved) {
|
|
|
834
904
|
const parentContents = readDirCached(resolvedDir);
|
|
835
905
|
const exactMatch = parentContents.files.find((f) => f === resolvedBase);
|
|
836
906
|
if (!exactMatch) {
|
|
837
|
-
const
|
|
907
|
+
const candidates = parentContents.files.filter(
|
|
838
908
|
(f) => f.startsWith(resolvedBase + ".")
|
|
839
909
|
);
|
|
910
|
+
const matchWithExt = candidates.find(
|
|
911
|
+
(f) => SCRIPT_EXTENSIONS.has(f.slice(resolvedBase.length + 1))
|
|
912
|
+
) ?? candidates[0];
|
|
840
913
|
if (matchWithExt) {
|
|
841
914
|
return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
|
|
842
915
|
}
|
|
@@ -905,7 +978,7 @@ var rule4 = {
|
|
|
905
978
|
if (importedFolderType === "fragment") {
|
|
906
979
|
const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
|
|
907
980
|
if (!isAccessNode) {
|
|
908
|
-
const isInside = relPath === importedDir
|
|
981
|
+
const isInside = import_path9.default.dirname(relPath) === importedDir;
|
|
909
982
|
if (!isInside) {
|
|
910
983
|
context.report({
|
|
911
984
|
message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
|
|
@@ -940,43 +1013,56 @@ var rule4 = {
|
|
|
940
1013
|
var no_direct_fragment_import_rule_default = rule4;
|
|
941
1014
|
|
|
942
1015
|
// src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
|
|
943
|
-
var
|
|
1016
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
944
1017
|
|
|
945
|
-
// src/_rules/_rules@shared/_utils/
|
|
1018
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-fractal-branch-owner/get-fractal-branch-owner.util.ts
|
|
946
1019
|
var import_path10 = __toESM(require("path"), 1);
|
|
947
|
-
function
|
|
1020
|
+
function getFractalBranchOwner(relPath) {
|
|
948
1021
|
let current = relPath;
|
|
949
1022
|
while (current && current !== "." && current !== "/") {
|
|
1023
|
+
const folderName = import_path10.default.basename(current);
|
|
1024
|
+
if (folderName.startsWith("_") && folderName.includes("@shared")) {
|
|
1025
|
+
return import_path10.default.dirname(current).replace(/\\/g, "/");
|
|
1026
|
+
}
|
|
950
1027
|
const parent = import_path10.default.dirname(current).replace(/\\/g, "/");
|
|
951
1028
|
if (parent === current) {
|
|
952
1029
|
break;
|
|
953
1030
|
}
|
|
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
1031
|
current = parent;
|
|
960
1032
|
}
|
|
961
1033
|
return null;
|
|
962
1034
|
}
|
|
963
1035
|
|
|
964
|
-
// src/_rules/
|
|
965
|
-
function
|
|
1036
|
+
// src/_rules/_rules@shared/_utils/_primitives/get-immediate-sub-domain/get-immediate-sub-domain.util.ts
|
|
1037
|
+
function getImmediateSubDomain(relPath, privateCategoryPath) {
|
|
1038
|
+
if (relPath !== privateCategoryPath && !relPath.startsWith(privateCategoryPath + "/")) {
|
|
1039
|
+
return "";
|
|
1040
|
+
}
|
|
1041
|
+
const relative = relPath.substring(privateCategoryPath.length + 1);
|
|
1042
|
+
const firstSegment = relative.split("/")[0];
|
|
1043
|
+
return firstSegment ? privateCategoryPath + "/" + firstSegment : privateCategoryPath;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/_rules/_rules@shared/_utils/_systems/get-private-category-info/get-private-category-info.util.ts
|
|
1047
|
+
var import_path11 = __toESM(require("path"), 1);
|
|
1048
|
+
function getPrivateCategoryInfo(relPath, config) {
|
|
966
1049
|
let current = relPath;
|
|
967
1050
|
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
1051
|
const parent = import_path11.default.dirname(current).replace(/\\/g, "/");
|
|
973
1052
|
if (parent === current) {
|
|
974
1053
|
break;
|
|
975
1054
|
}
|
|
1055
|
+
const currentType = classifyFolder(current, config);
|
|
1056
|
+
const parentType = classifyFolder(parent, config);
|
|
1057
|
+
if (currentType === "category" && (parentType === "fragment" || parentType === "root-fragment")) {
|
|
1058
|
+
return { owner: parent, privateCategoryPath: current };
|
|
1059
|
+
}
|
|
976
1060
|
current = parent;
|
|
977
1061
|
}
|
|
978
1062
|
return null;
|
|
979
1063
|
}
|
|
1064
|
+
|
|
1065
|
+
// src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
|
|
980
1066
|
var rule5 = {
|
|
981
1067
|
create(context) {
|
|
982
1068
|
const absPath = context.filename;
|
|
@@ -998,22 +1084,33 @@ var rule5 = {
|
|
|
998
1084
|
if (!resolvedRelPath) {
|
|
999
1085
|
return;
|
|
1000
1086
|
}
|
|
1001
|
-
const importedDir =
|
|
1002
|
-
const
|
|
1003
|
-
if (
|
|
1004
|
-
const isDescendant = relPath ===
|
|
1087
|
+
const importedDir = import_path12.default.dirname(resolvedRelPath);
|
|
1088
|
+
const fractalBranchOwner = getFractalBranchOwner(importedDir);
|
|
1089
|
+
if (fractalBranchOwner) {
|
|
1090
|
+
const isDescendant = relPath === fractalBranchOwner || relPath.startsWith(fractalBranchOwner + "/");
|
|
1005
1091
|
if (!isDescendant) {
|
|
1006
1092
|
context.report({
|
|
1007
|
-
message: `Importing from Fractal Branch is forbidden. The imported resource is private to "${
|
|
1093
|
+
message: `Importing from Fractal Branch is forbidden. The imported resource is private to "${fractalBranchOwner}" and its descendants.`,
|
|
1008
1094
|
node
|
|
1009
1095
|
});
|
|
1010
1096
|
} else {
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1097
|
+
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1098
|
+
if (info) {
|
|
1099
|
+
const { owner, privateCategoryPath } = info;
|
|
1100
|
+
const isPrivateDirectChild = import_path12.default.dirname(relPath) === owner;
|
|
1101
|
+
const importerSubDomain = getImmediateSubDomain(
|
|
1102
|
+
relPath,
|
|
1103
|
+
privateCategoryPath
|
|
1104
|
+
);
|
|
1105
|
+
const importedSubDomain = getImmediateSubDomain(
|
|
1106
|
+
resolvedRelPath,
|
|
1107
|
+
privateCategoryPath
|
|
1108
|
+
);
|
|
1109
|
+
const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
|
|
1110
|
+
const isFractalBranchAllowed = fractalBranchOwner === privateCategoryPath || fractalBranchOwner.startsWith(privateCategoryPath + "/");
|
|
1111
|
+
if (!isPrivateDirectChild && !isInsideSameSubDomain && !isFractalBranchAllowed) {
|
|
1015
1112
|
context.report({
|
|
1016
|
-
message: `Importing from Private Category is forbidden. The imported resource is private to "${
|
|
1113
|
+
message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
|
|
1017
1114
|
node
|
|
1018
1115
|
});
|
|
1019
1116
|
}
|
|
@@ -1046,7 +1143,7 @@ var rule5 = {
|
|
|
1046
1143
|
var no_fractal_branch_leak_rule_default = rule5;
|
|
1047
1144
|
|
|
1048
1145
|
// src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
|
|
1049
|
-
var
|
|
1146
|
+
var import_path13 = __toESM(require("path"), 1);
|
|
1050
1147
|
|
|
1051
1148
|
// src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
|
|
1052
1149
|
function findTreeConfigIncludingExcluded(relPath, settings) {
|
|
@@ -1124,7 +1221,7 @@ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
|
|
|
1124
1221
|
|
|
1125
1222
|
// src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
|
|
1126
1223
|
function getDirectoryRole(dirPath, config) {
|
|
1127
|
-
const folderName =
|
|
1224
|
+
const folderName = import_path13.default.basename(dirPath);
|
|
1128
1225
|
if (folderName.startsWith("_")) {
|
|
1129
1226
|
return getRoleFromFolderName(folderName, config);
|
|
1130
1227
|
}
|
|
@@ -1142,7 +1239,7 @@ function getRoleFromFolderName(name, config) {
|
|
|
1142
1239
|
return catConfig ? catConfig.role ?? null : null;
|
|
1143
1240
|
}
|
|
1144
1241
|
function getRootNodeIndex(relPath, rfConfig, lcaPath) {
|
|
1145
|
-
const relToLca =
|
|
1242
|
+
const relToLca = import_path13.default.relative(lcaPath, relPath).replace(/\\/g, "/");
|
|
1146
1243
|
for (let i = 0; i < rfConfig.rootNodes.length; i++) {
|
|
1147
1244
|
const group = rfConfig.rootNodes[i];
|
|
1148
1245
|
if (group && group.includes(relToLca)) {
|
|
@@ -1154,7 +1251,7 @@ function getRootNodeIndex(relPath, rfConfig, lcaPath) {
|
|
|
1154
1251
|
function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
|
|
1155
1252
|
let current = relPathB;
|
|
1156
1253
|
while (current && current !== "." && current !== "/" && current !== lcaPath) {
|
|
1157
|
-
const parent =
|
|
1254
|
+
const parent = import_path13.default.dirname(current).replace(/\\/g, "/");
|
|
1158
1255
|
if (parent === current) {
|
|
1159
1256
|
break;
|
|
1160
1257
|
}
|
|
@@ -1179,7 +1276,7 @@ var rule6 = {
|
|
|
1179
1276
|
if (!config) {
|
|
1180
1277
|
return {};
|
|
1181
1278
|
}
|
|
1182
|
-
const currentDir =
|
|
1279
|
+
const currentDir = import_path13.default.dirname(relPath);
|
|
1183
1280
|
const isCurrentFileExcluded = config.excludes?.some(
|
|
1184
1281
|
(exc) => relPath === exc || relPath.startsWith(exc + "/")
|
|
1185
1282
|
) ?? false;
|
|
@@ -1212,10 +1309,10 @@ var rule6 = {
|
|
|
1212
1309
|
if (isCurrentFileExcluded || isImportedFileExcluded) {
|
|
1213
1310
|
return;
|
|
1214
1311
|
}
|
|
1215
|
-
const importedDir =
|
|
1312
|
+
const importedDir = import_path13.default.dirname(resolvedRelPath);
|
|
1216
1313
|
if (currentDir === importedDir) {
|
|
1217
|
-
const fileA =
|
|
1218
|
-
const fileB =
|
|
1314
|
+
const fileA = import_path13.default.basename(relPath);
|
|
1315
|
+
const fileB = import_path13.default.basename(resolvedRelPath);
|
|
1219
1316
|
const roleA = getFileRole(fileA, config);
|
|
1220
1317
|
const roleB = getFileRole(fileB, config);
|
|
1221
1318
|
if (roleA && roleB) {
|
|
@@ -1244,6 +1341,10 @@ var rule6 = {
|
|
|
1244
1341
|
return;
|
|
1245
1342
|
}
|
|
1246
1343
|
if (subB === "" && classifyFolder(lca, config) === "fragment") {
|
|
1344
|
+
context.report({
|
|
1345
|
+
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.`,
|
|
1346
|
+
node
|
|
1347
|
+
});
|
|
1247
1348
|
return;
|
|
1248
1349
|
}
|
|
1249
1350
|
const rfConfig = getRootFragmentConfig(lca, config);
|
|
@@ -1253,13 +1354,22 @@ var rule6 = {
|
|
|
1253
1354
|
if (idxRootA !== -1 && idxRootB !== -1) {
|
|
1254
1355
|
if (idxRootB >= idxRootA) {
|
|
1255
1356
|
context.report({
|
|
1256
|
-
message: `Root Node import violation: "${
|
|
1357
|
+
message: `Root Node import violation: "${import_path13.default.basename(relPath)}" cannot import from "${import_path13.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
|
|
1257
1358
|
node
|
|
1258
1359
|
});
|
|
1259
1360
|
}
|
|
1260
1361
|
return;
|
|
1261
1362
|
}
|
|
1262
1363
|
}
|
|
1364
|
+
const isSubARootFragment = classifyFolder(import_path13.default.posix.join(lca, subA), config) === "root-fragment";
|
|
1365
|
+
const isSubBRootFragment = classifyFolder(import_path13.default.posix.join(lca, subB), config) === "root-fragment";
|
|
1366
|
+
if (isSubARootFragment || isSubBRootFragment) {
|
|
1367
|
+
context.report({
|
|
1368
|
+
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.`,
|
|
1369
|
+
node
|
|
1370
|
+
});
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1263
1373
|
const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
|
|
1264
1374
|
if (hasDefinedHierarchy) {
|
|
1265
1375
|
if (!allowed) {
|
|
@@ -1269,8 +1379,8 @@ var rule6 = {
|
|
|
1269
1379
|
});
|
|
1270
1380
|
}
|
|
1271
1381
|
} else {
|
|
1272
|
-
const roleA = getDirectoryRole(
|
|
1273
|
-
const roleB = getDirectoryRole(
|
|
1382
|
+
const roleA = getDirectoryRole(import_path13.default.posix.join(lca, subA), config);
|
|
1383
|
+
const roleB = getDirectoryRole(import_path13.default.posix.join(lca, subB), config);
|
|
1274
1384
|
const idxA = roleA ? getRoleHierarchyIndex(roleA, config) : -1;
|
|
1275
1385
|
const idxB = roleB ? getRoleHierarchyIndex(roleB, config) : -1;
|
|
1276
1386
|
if (idxA !== -1 && idxB !== -1) {
|
|
@@ -1313,7 +1423,7 @@ var rule6 = {
|
|
|
1313
1423
|
var no_peer_dependency_rule_default = rule6;
|
|
1314
1424
|
|
|
1315
1425
|
// src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
|
|
1316
|
-
var
|
|
1426
|
+
var import_path14 = __toESM(require("path"), 1);
|
|
1317
1427
|
var rule7 = {
|
|
1318
1428
|
create(context) {
|
|
1319
1429
|
const absPath = context.filename;
|
|
@@ -1335,11 +1445,32 @@ var rule7 = {
|
|
|
1335
1445
|
if (!resolvedRelPath) {
|
|
1336
1446
|
return;
|
|
1337
1447
|
}
|
|
1338
|
-
const importedDir =
|
|
1339
|
-
const
|
|
1340
|
-
if (
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1448
|
+
const importedDir = import_path14.default.dirname(resolvedRelPath);
|
|
1449
|
+
const info = getPrivateCategoryInfo(importedDir, config);
|
|
1450
|
+
if (info) {
|
|
1451
|
+
const { owner, privateCategoryPath } = info;
|
|
1452
|
+
let isFractalBranchInPrivateCategory = false;
|
|
1453
|
+
let isFractalBranchAllowed = false;
|
|
1454
|
+
const fbOwner = getFractalBranchOwner(importedDir);
|
|
1455
|
+
if (fbOwner) {
|
|
1456
|
+
const isFbInsidePrivateCategory = fbOwner === privateCategoryPath || fbOwner.startsWith(privateCategoryPath + "/");
|
|
1457
|
+
if (isFbInsidePrivateCategory) {
|
|
1458
|
+
isFractalBranchInPrivateCategory = true;
|
|
1459
|
+
isFractalBranchAllowed = relPath === fbOwner || relPath.startsWith(fbOwner + "/");
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const isDirectChild = import_path14.default.dirname(relPath) === owner;
|
|
1463
|
+
const importerSubDomain = getImmediateSubDomain(
|
|
1464
|
+
relPath,
|
|
1465
|
+
privateCategoryPath
|
|
1466
|
+
);
|
|
1467
|
+
const importedSubDomain = getImmediateSubDomain(
|
|
1468
|
+
resolvedRelPath,
|
|
1469
|
+
privateCategoryPath
|
|
1470
|
+
);
|
|
1471
|
+
const isInsideSameSubDomain = importerSubDomain !== "" && importerSubDomain === importedSubDomain;
|
|
1472
|
+
const isAllowed = isFractalBranchInPrivateCategory ? isFractalBranchAllowed : isDirectChild || isInsideSameSubDomain;
|
|
1473
|
+
if (!isAllowed) {
|
|
1343
1474
|
context.report({
|
|
1344
1475
|
message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
|
|
1345
1476
|
node
|