@morriconeluca/eslint-plugin-faf 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2026 Luca Morricone
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # eslint-plugin-faf
2
+
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
+
5
+ This plugin is designed for complex TypeScript/JavaScript codebases to ensure **predictability**, **testability**, and strict **isolation** of dependency flows. It is fully compatible with modern ESLint flat configurations (ESLint v9 and v10).
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Strict Enforcing of FAF Taxonomy**: Ensures your folder structure strictly maps to Layers, Categories, Fragments, and Fractal Branches.
12
+ - **Law of Separation between Peers**: Prevents lateral dependency coupling between sibling modules without explicit hierarchy.
13
+ - **Access Node Encapsulation**: Guarantees that internal Fragment files are never imported directly, enforcing consumption solely through the Fragment's barrel file (`index.ts`).
14
+ - **High-Performance Architecture**: Features optimized caching (`dirCache`, `classifyCache`, `rolesCache`) to minimize disk I/O during linting runs.
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ Install the plugin along with its peer dependencies:
21
+
22
+ ```bash
23
+ npm install eslint-plugin-faf --save-dev
24
+ # or
25
+ yarn add eslint-plugin-faf --dev
26
+ # or
27
+ pnpm add eslint-plugin-faf -D
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Configuration
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.
35
+
36
+ ### 1. Integration in `eslint.config.ts`
37
+
38
+ For a clean setup, we recommend defining your FAF taxonomy settings in a separate file (e.g. `faf.config.ts`) and importing it into your ESLint configuration.
39
+
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
+
42
+ ```typescript
43
+ import fafPlugin from 'eslint-plugin-faf';
44
+ import fafSettings from './faf.config';
45
+
46
+ export default [
47
+ // 1. Extend the recommended FAF configuration (registers the plugin and enables all rules as errors)
48
+ fafPlugin.configs.recommended,
49
+
50
+ // 2. Inject FAF taxonomy settings
51
+ {
52
+ files: ['**/*.{js,cjs,ts,cts,tsx}'],
53
+ settings: {
54
+ ...fafSettings,
55
+ },
56
+ },
57
+ ];
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Anatomy of `faf.config.ts`
63
+
64
+ Here is a comprehensive configuration example mapping a typical project structure:
65
+
66
+ ```typescript
67
+ export default {
68
+ faf: {
69
+ // 1. Path aliases mapping (resolves absolute imports)
70
+ aliases: {
71
+ '#/_apis@shared': 'src/_src@shared/_network/_apis/_apis@shared',
72
+ '#/_app@shared': 'src/app/_app@shared',
73
+ '#/_src@shared': 'src/_src@shared',
74
+ },
75
+
76
+ // 2. Tree configurations
77
+ trees: [
78
+ {
79
+ // Root path to apply these rules to
80
+ includes: ['src'],
81
+
82
+ // Paths to completely exclude from FAF validation (Foreign Domains)
83
+ excludes: ['src/configs'],
84
+
85
+ // Definition of organizational Categories (folders starting with "_")
86
+ categories: [
87
+ // name: Name of the folder
88
+ // role: Associated architectural role
89
+ // allowSingleFiles: Allow files (Logical Nodes) directly in the folder (defaults to false)
90
+ // allowedExtensions: Limit file types for static assets (disables role suffix check)
91
+ { name: '_apis', role: 'api' },
92
+ { name: '_components', role: 'component' },
93
+ { name: '_utils', role: 'util' },
94
+ { name: '_hooks', role: 'hook' },
95
+ { name: '_layouts', role: 'layout' },
96
+ { name: '_pages', role: 'page' },
97
+ { name: '_routes', role: 'route' },
98
+ { name: '_recipes', role: 'recipe' },
99
+ { name: '_classes', role: 'class' },
100
+ { name: '_contexts', role: 'context' },
101
+ { name: '_stores', role: 'store' },
102
+ { name: '_factories', role: 'factory' },
103
+ { name: '_instances', role: 'instance' },
104
+ { name: '_boundaries', role: 'boundary' },
105
+
106
+ // Categories that allow direct logical files (Logical Nodes)
107
+ { allowSingleFiles: true, name: '_constants', role: 'constant' },
108
+ { allowSingleFiles: true, name: '_types', role: 'type' },
109
+ { allowSingleFiles: true, name: '_schemas', role: 'schema' },
110
+ { allowSingleFiles: true, name: '_mocks', role: 'mock' },
111
+
112
+ // Asset-only categories (no logical role required)
113
+ { allowedExtensions: ['.css'], name: '_styles' },
114
+ { allowedExtensions: ['.ttf', '.woff', '.woff2'], name: '_fonts' },
115
+ {
116
+ allowedExtensions: ['.png', '.jpg', '.jpeg', '.svg'],
117
+ name: '_images',
118
+ },
119
+ ],
120
+
121
+ // Global horizontal hierarchies: flow rules for peer imports
122
+ globalHorizontalHierarchies: [
123
+ [
124
+ ['_atoms'],
125
+ ['_molecules'],
126
+ ['_organisms'],
127
+ ['_templates'],
128
+ ['_pages'],
129
+ ],
130
+ [
131
+ ['_primitives'],
132
+ ['_compounds'],
133
+ ['_aggregates'],
134
+ ['_systems'],
135
+ ['_ecosystems'],
136
+ ],
137
+ ],
138
+
139
+ // Local horizontal hierarchies: flow rules restricted to specific paths
140
+ // Imports flow from left to right (elements on the right can import from the left)
141
+ localHorizontalHierarchies: [
142
+ {
143
+ hierarchies: [['_domain'], ['_network', '_state', '_ui']],
144
+ paths: ['src/_src@shared'],
145
+ },
146
+ {
147
+ hierarchies: [['_enums'], ['_dtos']],
148
+ paths: ['src/_src@shared/_domain/_schemas'],
149
+ },
150
+ {
151
+ hierarchies: [['_types'], ['_classes'], ['_utils'], ['_apis']],
152
+ paths: ['src/_src@shared/_network'],
153
+ },
154
+ {
155
+ hierarchies: [
156
+ ['_constants'],
157
+ ['_types'],
158
+ ['_utils'],
159
+ ['_recipes'],
160
+ ['_components'],
161
+ ['_pages'],
162
+ ],
163
+ paths: ['src/_src@shared/_ui'],
164
+ },
165
+ ],
166
+
167
+ // Suffix hierarchy role scale for files inside a Fragment.
168
+ // Files with roles at higher indices can import from roles at lower indices.
169
+ roles: [
170
+ ['constant'],
171
+ ['schema'],
172
+ ['type'],
173
+ ['class'],
174
+ ['config'],
175
+ ['mock'],
176
+ ['util'],
177
+ ['style'],
178
+ ['recipe'],
179
+ ['factory'],
180
+ ['instance'],
181
+ ['hook'],
182
+ [
183
+ 'api',
184
+ 'boundary',
185
+ 'component',
186
+ 'context',
187
+ 'layout',
188
+ 'page',
189
+ 'route',
190
+ 'store',
191
+ ],
192
+ ['index', 'spec', 'story'],
193
+ ],
194
+
195
+ // Software entry points (Root Fragments) and their dependency rules
196
+ rootFragments: [
197
+ {
198
+ paths: ['src'],
199
+ // rootNodes specifies permitted layers of execution:
200
+ // main.tsx (index 1) can import main.css and app/app.tsx (index 0), but not vice-versa
201
+ rootNodes: [['main.css', 'app/app.tsx'], ['main.tsx']],
202
+ subRootFragments: [
203
+ {
204
+ paths: ['src/app'],
205
+ rootNodes: [['app.tsx']],
206
+ },
207
+ ],
208
+ },
209
+ ],
210
+
211
+ // Route-based structure (HTTP APIs or Pages)
212
+ routeHierarchies: [
213
+ {
214
+ // Subtree where intermediate folders are classified as organizational Layers,
215
+ // and the terminal Fragment must contain a Master Node with the specified role (e.g. '.api.ts')
216
+ paths: ['src/_src@shared/_network/_apis'],
217
+ role: 'api',
218
+ },
219
+ ],
220
+ },
221
+ ],
222
+ },
223
+ };
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Configuration Settings Reference
229
+
230
+ ### `aliases`
231
+
232
+ Maps import aliases (e.g., `#/_src@shared`) to relative paths from the project root. This is critical for the linter to trace boundaries and verify import encapsulation.
233
+
234
+ **Single Source of Truth**: By default, `eslint-plugin-faf` automatically resolves your path aliases using the `"imports"` field in your `package.json`. If `"imports"` contains any keys mapped to flat path strings, it is treated as the exclusive source of truth, and any `aliases` configured in `faf.config.ts` will be ignored. If `"imports"` is not defined, empty, or consists only of conditional import objects (which are skipped by the linter for simplicity), the plugin falls back to using the `aliases` object defined in `faf.config.ts`.
235
+
236
+ _Note: If your codebase uses conditional imports in `package.json` (e.g. mapping aliases to objects for ESM/CJS or types/default targets), you must define their flat equivalents in `faf.config.ts` `aliases` to ensure correct linting._
237
+
238
+ ### `excludes`
239
+
240
+ Array of path strings relative to the project root. Any folder matching these paths (or their subfolders) will be completely ignored by the linter.
241
+
242
+ ### `categories`
243
+
244
+ Configures taxomonic categories (`_` prefixed directories).
245
+
246
+ - `role`: Maps the folder to an architectural role. It removes language plural heuristics (e.g., mapping `_components` to the `component` role suffix).
247
+ - `allowSingleFiles`: Set to `true` to allow single files (`Logical Nodes`) directly in the category. If `false` or omitted, only subfolder `Fragments` are permitted, except for static assets matching the `allowedExtensions` whitelist.
248
+ - `allowedExtensions`: Whitelists file extensions for asset categories. Matches are treated as static assets, skipping role suffix validation (e.g. `logo.png` inside `_images`).
249
+
250
+ ### `localHorizontalHierarchies` / `globalHorizontalHierarchies`
251
+
252
+ Determines horizontal import flow (Peer Separation). Each hierarchy is an array of string arrays.
253
+
254
+ - Sub-folders at index `i` can import from sub-folders at index `j` only if `j < i`.
255
+ - Peer folders at the same index (e.g. `['_network', '_state', '_ui']`) cannot import from each other.
256
+
257
+ ---
258
+
259
+ ## Rules Reference
260
+
261
+ ### 1. `faf/naming-conventions`
262
+
263
+ Verifies folders, files, and roles comply with FAF taxonomy:
264
+
265
+ - Enforces `kebab-case` naming for all folders and files in the logical domain.
266
+ - 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
+ - Assures route terminal Fragments contain the appropriate Master Node (e.g. a `.api.ts` file under `_apis`).
269
+ - Enforces FAF logical domain containment rules:
270
+ - Root Fragments can only reside directly in the Root Container or another Root Fragment.
271
+ - Layers cannot reside inside Fragments.
272
+ - Fragments cannot be placed directly inside other Fragments (they must be nested within a Private Category), Fractal Branches, or nested Root Fragments.
273
+ - Fractal Branches cannot be nested inside other Fractal Branches.
274
+
275
+ ### 2. `faf/enforce-access-node`
276
+
277
+ Enforces Access Node (`index.ts` / `index.js`) integrity:
278
+
279
+ - Restricts the Access Node to import/export only its own sibling Fragment Nodes.
280
+ - Prevents import of nested Private Categories, Fractal Branches, or unrelated external paths.
281
+
282
+ ### 3. `faf/no-direct-fragment-import`
283
+
284
+ Applies **Fragment Encapsulation**:
285
+
286
+ - Prevents external files from directly importing a Fragment's internal files. All imports must pass through the Fragment's Access Node (`index.ts`).
287
+
288
+ ### 4. `faf/no-private-category-leak`
289
+
290
+ Applies **Private Category Encapsulation**:
291
+
292
+ - Restricts consumption of elements inside a Private Category (e.g. `_components/` nested inside a Fragment) to files within the parent Fragment or its sub-fragments.
293
+
294
+ ### 5. `faf/no-fractal-branch-leak`
295
+
296
+ Applies **Fractal Branch Encapsulation**:
297
+
298
+ - Fractal Branches named `_<Scope>@shared` are only importable by modules residing inside the parent Scope's subtree.
299
+
300
+ ### 6. `faf/no-peer-dependency`
301
+
302
+ Applies **Peer Isolation**:
303
+
304
+ - Prevents sibling files and folders from importing each other unless an explicit hierarchy is configured:
305
+ - **Inside a Fragment**: Flow is governed by the `roles` array order.
306
+ - **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`.
308
+
309
+ ### 7. `faf/category-mutually-exclusive`
310
+
311
+ Ensures category purity:
312
+
313
+ - Blocks mixing single files (Logical Nodes) and subfolders (Fragments) in the same category.
314
+ - Blocks direct file placement inside categories configured with `allowSingleFiles: false`.
315
+
316
+ ---
317
+
318
+ ## Performance Optimizations
319
+
320
+ To prevent sluggish linting in large codebases, the core engine implements aggressive, state-cached optimizations:
321
+
322
+ - **Directory Cache (`dirCache`)**: Inspects the physical disk exactly once per folder, storing the contents in-memory. It is optimized to perform exactly one system call on cache misses by removing redundant `fs.existsSync` checks.
323
+ - **Classification Cache (`classifyCache`)**: Memoizes the structural type (`FolderType`) of each directory for the lifetime of the ESLint run.
324
+ - **Role Cache (`rolesCache`)**: Uses a `WeakMap` to cache the allowed roles per tree configuration as a native `Set`, avoiding memory leaks, redundant array flattening allocations, and speeding up role lookup checks to $O(1)$.
325
+ - **Category Configuration Cache (`categoryConfigCache`)**: Memoizes resolved category configurations per folder path to eliminate repetitive directory walks and path parent traversals.
326
+ - **Tree Configuration Cache (`treeConfigCache` / `treeConfigIncludingExcludedCache`)**: Memoizes resolved tree configurations per file path, reducing tree lookup checks to a simple map-lookup.
327
+ - **Resolved Import Path Cache (`resolvedImportPathCache`)**: Caches project-root-relative resolved import paths to avoid repeating expensive path resolutions, alias matching, and package.json parsing on duplicate imports.
328
+
329
+ ---
330
+
331
+ ## License
332
+
333
+ This project is licensed under the terms of the [MIT License](LICENSE).