@morriconeluca/eslint-plugin-faf 0.5.0 → 0.6.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/README.md +210 -277
- package/dist/main.cjs +14032 -1605
- package/dist/main.cjs.map +1 -1
- package/dist/main.d.cts +346 -37
- package/dist/main.d.ts +326 -37
- package/dist/main.js +14036 -1601
- package/dist/main.js.map +1 -1
- package/package.json +54 -25
package/README.md
CHANGED
|
@@ -1,66 +1,63 @@
|
|
|
1
1
|
# @morriconeluca/eslint-plugin-faf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official ESLint plugin for validating codebases against the **Fractal Architecture Framework (FAF)** specification.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`eslint-plugin-faf` enforces strict structural, classification, symbol, dependency, and acyclicity invariants across codebases using deterministic static analysis.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Requirements
|
|
10
10
|
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
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`, `ancestorChainCache`, `rolesCache`) to minimize disk I/O during linting runs.
|
|
11
|
+
- **Node.js**: `>= 24.0.0` (required for native TypeScript type stripping)
|
|
12
|
+
- **ESLint**: `^9.0.0 || ^10.0.0` (flat configuration)
|
|
15
13
|
|
|
16
14
|
---
|
|
17
15
|
|
|
18
|
-
##
|
|
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:
|
|
16
|
+
## Installation
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
-
|
|
18
|
+
```bash
|
|
19
|
+
npm install --save-dev @morriconeluca/eslint-plugin-faf eslint
|
|
20
|
+
```
|
|
24
21
|
|
|
25
22
|
---
|
|
26
23
|
|
|
27
|
-
##
|
|
24
|
+
## ESLint Flat Configuration
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
`eslint-plugin-faf` exposes a single, unified, zero-option rule: `faf/conformity`. FAF architectural constraints are non-negotiable and conjunctive: a codebase is either conformant or non-conformant. Granular rule toggles and severity demotions are not supported.
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
npm install @morriconeluca/eslint-plugin-faf --save-dev
|
|
33
|
-
# or
|
|
34
|
-
yarn add @morriconeluca/eslint-plugin-faf --dev
|
|
35
|
-
# or
|
|
36
|
-
pnpm add @morriconeluca/eslint-plugin-faf -D
|
|
37
|
-
```
|
|
28
|
+
### Recommended Setup
|
|
38
29
|
|
|
39
|
-
|
|
30
|
+
Add the plugin to your `eslint.config.js` or `eslint.config.ts`:
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
```javascript
|
|
33
|
+
import faf from '@morriconeluca/eslint-plugin-faf';
|
|
42
34
|
|
|
43
|
-
|
|
35
|
+
export default [
|
|
36
|
+
faf.configs.recommended,
|
|
37
|
+
// ...your other flat configs
|
|
38
|
+
];
|
|
39
|
+
```
|
|
44
40
|
|
|
45
|
-
|
|
41
|
+
The `recommended` configuration automatically:
|
|
46
42
|
|
|
47
|
-
|
|
43
|
+
- Registers the `faf` plugin.
|
|
44
|
+
- Targets all lintable script and module extensions (`.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.mts`, `.cts`).
|
|
45
|
+
- Enables `faf/conformity` with severity `error` (2).
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
### Manual Configuration
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
import fafPlugin from '@morriconeluca/eslint-plugin-faf';
|
|
53
|
-
import fafSettings from './faf.config';
|
|
49
|
+
If you prefer to configure rules manually:
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fafPlugin.configs.recommended,
|
|
51
|
+
```javascript
|
|
52
|
+
import faf from '@morriconeluca/eslint-plugin-faf';
|
|
58
53
|
|
|
59
|
-
|
|
54
|
+
export default [
|
|
60
55
|
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
plugins: {
|
|
57
|
+
faf,
|
|
58
|
+
},
|
|
59
|
+
rules: {
|
|
60
|
+
'faf/conformity': 'error',
|
|
64
61
|
},
|
|
65
62
|
},
|
|
66
63
|
];
|
|
@@ -68,329 +65,265 @@ export default [
|
|
|
68
65
|
|
|
69
66
|
---
|
|
70
67
|
|
|
71
|
-
##
|
|
68
|
+
## The Configuration Contract: `fafconfig.ts`
|
|
69
|
+
|
|
70
|
+
Every project governed by `eslint-plugin-faf` requires a configuration module named `fafconfig.ts` located at the root of the project.
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
### Example Configuration
|
|
74
73
|
|
|
75
74
|
```typescript
|
|
75
|
+
import type { TFAFConfig } from '@morriconeluca/eslint-plugin-faf';
|
|
76
|
+
|
|
76
77
|
export default {
|
|
77
78
|
faf: {
|
|
78
|
-
|
|
79
|
-
aliases: {
|
|
80
|
-
'#/_apis@shared': 'src/_src@shared/_network/_apis/_apis@shared',
|
|
81
|
-
'#/_app@shared': 'src/app/_app@shared',
|
|
82
|
-
'#/_src@shared': 'src/_src@shared',
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
// 2. Tree configurations
|
|
86
|
-
trees: [
|
|
79
|
+
locals: [
|
|
87
80
|
{
|
|
88
|
-
|
|
89
|
-
includes: ['src'],
|
|
90
|
-
|
|
91
|
-
// Paths to completely exclude from FAF validation (Foreign Domains)
|
|
92
|
-
excludes: ['src/configs'],
|
|
93
|
-
|
|
94
|
-
// Definition of organizational Categories (folders starting with "_")
|
|
95
|
-
categories: [
|
|
96
|
-
// name: Name of the folder
|
|
97
|
-
// role: Associated architectural role
|
|
98
|
-
// allowSingleFiles: Allow files (Logical Nodes) directly in the folder (defaults to false)
|
|
99
|
-
// allowedExtensions: Limit file types for static assets (disables role suffix check)
|
|
100
|
-
{ name: '_apis', role: 'api' },
|
|
101
|
-
{ name: '_components', role: 'component' },
|
|
102
|
-
{ name: '_utils', role: 'util' },
|
|
103
|
-
{ name: '_hooks', role: 'hook' },
|
|
104
|
-
{ name: '_layouts', role: 'layout' },
|
|
105
|
-
{ name: '_pages', role: 'page' },
|
|
106
|
-
{ name: '_routes', role: 'route' },
|
|
107
|
-
{ name: '_recipes', role: 'recipe' },
|
|
108
|
-
{ name: '_classes', role: 'class' },
|
|
109
|
-
{ name: '_contexts', role: 'context' },
|
|
110
|
-
{ name: '_stores', role: 'store' },
|
|
111
|
-
{ name: '_factories', role: 'factory' },
|
|
112
|
-
{ name: '_instances', role: 'instance' },
|
|
113
|
-
{ name: '_boundaries', role: 'boundary' },
|
|
114
|
-
|
|
115
|
-
// Categories that allow direct logical files (Logical Nodes)
|
|
116
|
-
{ allowSingleFiles: true, name: '_constants', role: 'constant' },
|
|
117
|
-
{ allowSingleFiles: true, name: '_types', role: 'type' },
|
|
118
|
-
{ allowSingleFiles: true, name: '_schemas', role: 'schema' },
|
|
119
|
-
{ allowSingleFiles: true, name: '_mocks', role: 'mock' },
|
|
120
|
-
|
|
121
|
-
// Asset-only categories (no logical role required)
|
|
122
|
-
{ allowedExtensions: ['.css'], name: '_styles' },
|
|
123
|
-
{ allowedExtensions: ['.ttf', '.woff', '.woff2'], name: '_fonts' },
|
|
124
|
-
{
|
|
125
|
-
allowedExtensions: ['.png', '.jpg', '.jpeg', '.svg'],
|
|
126
|
-
name: '_images',
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
|
|
130
|
-
// Global horizontal hierarchies: flow rules for peer imports
|
|
131
|
-
globalHorizontalHierarchies: [
|
|
132
|
-
[
|
|
133
|
-
['_atoms'],
|
|
134
|
-
['_molecules'],
|
|
135
|
-
['_organisms'],
|
|
136
|
-
['_templates'],
|
|
137
|
-
['_pages'],
|
|
138
|
-
],
|
|
139
|
-
[
|
|
140
|
-
['_primitives'],
|
|
141
|
-
['_compounds'],
|
|
142
|
-
['_aggregates'],
|
|
143
|
-
['_systems'],
|
|
144
|
-
['_ecosystems'],
|
|
145
|
-
],
|
|
146
|
-
],
|
|
147
|
-
|
|
148
|
-
// Local horizontal hierarchies: flow rules restricted to specific paths
|
|
149
|
-
// Imports flow from left to right (elements on the right can import from the left)
|
|
150
|
-
localHorizontalHierarchies: [
|
|
81
|
+
roles: [
|
|
151
82
|
{
|
|
152
|
-
|
|
153
|
-
|
|
83
|
+
name: 'constant',
|
|
84
|
+
category: '_constants',
|
|
85
|
+
isCategoryNode: true,
|
|
86
|
+
allowMultipleSymbols: true,
|
|
154
87
|
},
|
|
155
88
|
{
|
|
156
|
-
|
|
157
|
-
|
|
89
|
+
name: 'type',
|
|
90
|
+
category: '_types',
|
|
91
|
+
isCategoryNode: true,
|
|
92
|
+
allowMultipleSymbols: true,
|
|
158
93
|
},
|
|
159
94
|
{
|
|
160
|
-
|
|
161
|
-
|
|
95
|
+
name: 'mock',
|
|
96
|
+
category: '_mocks',
|
|
97
|
+
isCategoryNode: true,
|
|
98
|
+
allowMultipleSymbols: true,
|
|
162
99
|
},
|
|
163
100
|
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
['_recipes'],
|
|
169
|
-
['_components'],
|
|
170
|
-
['_pages'],
|
|
171
|
-
],
|
|
172
|
-
paths: ['src/_src@shared/_ui'],
|
|
101
|
+
name: 'config',
|
|
102
|
+
category: '_configs',
|
|
103
|
+
isCategoryNode: true,
|
|
104
|
+
allowMultipleSymbols: true,
|
|
173
105
|
},
|
|
106
|
+
{ name: 'util', category: '_utils' },
|
|
107
|
+
{ name: 'service', category: '_services' },
|
|
108
|
+
{ name: 'component', category: '_components' },
|
|
174
109
|
],
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Files with roles at higher indices can import from roles at lower indices.
|
|
178
|
-
roles: [
|
|
179
|
-
['constant'],
|
|
180
|
-
['schema'],
|
|
181
|
-
['type'],
|
|
182
|
-
['class'],
|
|
183
|
-
['config'],
|
|
184
|
-
['mock'],
|
|
185
|
-
['util'],
|
|
186
|
-
['style'],
|
|
187
|
-
['recipe'],
|
|
188
|
-
['factory'],
|
|
189
|
-
['instance'],
|
|
190
|
-
['hook'],
|
|
110
|
+
subGraphRoles: [{ name: 'spec' }],
|
|
111
|
+
horizontalHierarchies: [
|
|
191
112
|
[
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
'
|
|
195
|
-
'
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
'
|
|
199
|
-
'store',
|
|
113
|
+
['_constants'],
|
|
114
|
+
['_types'],
|
|
115
|
+
['_mocks'],
|
|
116
|
+
['_configs'],
|
|
117
|
+
['_utils'],
|
|
118
|
+
['_services'],
|
|
119
|
+
['_components'],
|
|
200
120
|
],
|
|
201
|
-
['index', 'spec', 'story'],
|
|
202
|
-
],
|
|
203
|
-
|
|
204
|
-
// Software entry points (Root Fragments) and their dependency rules
|
|
205
|
-
rootFragments: [
|
|
206
|
-
{
|
|
207
|
-
paths: ['src'],
|
|
208
|
-
// rootNodes specifies permitted layers of execution:
|
|
209
|
-
// main.tsx (index 1) can import main.css and app/app.tsx (index 0), but not vice-versa
|
|
210
|
-
rootNodes: [['main.css', 'app/app.tsx'], ['main.tsx']],
|
|
211
|
-
subRootFragments: [
|
|
212
|
-
{
|
|
213
|
-
paths: ['src/app'],
|
|
214
|
-
rootNodes: [['app.tsx']],
|
|
215
|
-
},
|
|
216
|
-
],
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
|
|
220
|
-
// Route-based structure (HTTP APIs or Pages)
|
|
221
|
-
routeHierarchies: [
|
|
222
|
-
{
|
|
223
|
-
// Subtree where intermediate folders are classified as organizational Layers,
|
|
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')
|
|
226
|
-
paths: ['src/_src@shared/_network/_apis'],
|
|
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'],
|
|
231
|
-
},
|
|
232
121
|
],
|
|
122
|
+
rootFragments: [{ path: 'src', rootNodes: [['main.ts']] }],
|
|
123
|
+
publicRootNodes: ['src/main.ts'],
|
|
124
|
+
externalDependencies: [{ from: ['src/main.ts'], to: ['package.json'] }],
|
|
233
125
|
},
|
|
234
126
|
],
|
|
235
127
|
},
|
|
236
|
-
};
|
|
128
|
+
} satisfies TFAFConfig;
|
|
237
129
|
```
|
|
238
130
|
|
|
239
|
-
|
|
131
|
+
### Configuration Structure: `common` and `locals`
|
|
240
132
|
|
|
241
|
-
|
|
133
|
+
The configuration object defines:
|
|
242
134
|
|
|
243
|
-
|
|
135
|
+
- **`locals`** (_required_, non-empty array): Declarations specific to each Fractal Tree within the workspace. Each element must designate either a `rootFragments` array or a `rootLayers` array defining tree origins.
|
|
136
|
+
- **`common`** (_optional_): Shared declarations (such as roles, sub-graph roles, foreign families, or aliases) inherited by all local trees unless explicitly overridden.
|
|
137
|
+
- **Whole-property replacement**: When a local tree defines a property that also exists in `common`, the local property completely replaces the common definition without shallow or deep merging. Defaults apply strictly after replacement.
|
|
244
138
|
|
|
245
|
-
|
|
139
|
+
### Native Type-Stripping and Execution Constraints
|
|
246
140
|
|
|
247
|
-
|
|
141
|
+
`fafconfig.ts` is evaluated synchronously within an isolated Node.js worker thread leveraging Node's native type stripping. Because native type stripping strips type annotations without full AST transformation, configuration files must adhere to the following rules:
|
|
248
142
|
|
|
249
|
-
|
|
143
|
+
1. **Explicit File Extensions on Relative Value Imports**:
|
|
144
|
+
Any relative value import inside `fafconfig.ts` or helper modules must specify an explicit extension (for example, `./roles.js` or `./roles.ts`). Type-only imports (`import type ...`) are erased during stripping and are exempt.
|
|
145
|
+
2. **Prohibition of Non-Strippable TypeScript Constructs**:
|
|
146
|
+
`enum`, runtime `namespace` declarations with executable values, and constructor parameter properties are forbidden in `fafconfig.ts` and its imported dependencies. Use union types and standard property definitions.
|
|
147
|
+
3. **Module Packaging in `package.json`**:
|
|
148
|
+
The nearest `package.json` must not declare `"type": "commonjs"`. It should specify `"type": "module"` or omit the field.
|
|
149
|
+
4. **Synchronous, Clonable Default Export**:
|
|
150
|
+
`fafconfig.ts` must export its configuration as a synchronous `export default`. It cannot be a function, cannot return a `Promise` (no top-level asynchronous configuration), and must produce a plain data object serializable via `structuredClone`.
|
|
250
151
|
|
|
251
|
-
###
|
|
152
|
+
### Configuration Discovery and Automatic Invalidation
|
|
252
153
|
|
|
253
|
-
|
|
154
|
+
- **Discovery**: The plugin inspects `settings.faf.configFile` if provided in ESLint settings. If unset, it searches upward from the linter's current working directory for `fafconfig.ts`. If not found, it reports `configuration/missing-config-file` on all linted files.
|
|
155
|
+
- **Freshness & Invalidation**: During worker evaluation, the loader tracks all local files loaded by `fafconfig.ts`. On subsequent lint passes, the plugin compares `mtimeMs` and `size` for all tracked files. If any file changes, the configuration cache is immediately invalidated and reloaded in a fresh worker without restarting your editor or language server.
|
|
254
156
|
|
|
255
|
-
|
|
157
|
+
---
|
|
256
158
|
|
|
257
|
-
|
|
159
|
+
## Diagnostic Outcomes & Enforcement Model
|
|
258
160
|
|
|
259
|
-
|
|
260
|
-
- `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.
|
|
261
|
-
- `allowedExtensions`: Whitelists file extensions for asset categories. Matches are treated as static assets, skipping role suffix validation (e.g. `logo.png` inside `_images`).
|
|
161
|
+
### Uniform Blocking Severity
|
|
262
162
|
|
|
263
|
-
|
|
163
|
+
In `eslint-plugin-faf`, all diagnostic outcomes have severity **`error` (2)**. There are no warnings, hints, or demoted diagnostics.
|
|
264
164
|
|
|
265
|
-
|
|
165
|
+
### The 6 Outcome Families
|
|
266
166
|
|
|
267
|
-
|
|
268
|
-
- Peer folders at the same index (e.g. `['_network', '_state', '_ui']`) cannot import from each other.
|
|
167
|
+
Diagnostics belong to one of six discriminated outcome families, corresponding to message identifiers in the ESLint rule:
|
|
269
168
|
|
|
270
|
-
|
|
169
|
+
1. **`configuration`**: Errors in `fafconfig.ts` loading, package configuration, schema validation, literal path grammar, foreign glob syntax, or declaration inconsistencies.
|
|
170
|
+
2. **`classification`**: Violations of the FAF classification model, including containment matrix violations, fragment anatomy defects, missing master or access nodes, invalid horizontal hierarchies, and nomenclature non-conformities.
|
|
171
|
+
3. **`symbol`**: Root-scope symbol violations, such as exceeding the single-symbol rule in strict nodes, missing master exports in access nodes, or illegal re-export of imported symbols.
|
|
172
|
+
4. **`dependency`**: Prohibited edges or paths in the Dependency Graph, including peer separation violations, fragment encapsulation bypasses, private category or fractal branch leakage, tree opacity breaches, and unauthorized external or cross-tree dependencies.
|
|
173
|
+
5. **`acyclicity`**: Cycle violations detected in the logical module dependency DAG or across the inter-tree authorization DAG.
|
|
174
|
+
6. **`unverified`**: Incomplete checks that static analysis cannot conclusively prove (such as encountering symbolic links inside trees, unreadable origins or files, non-literal dynamic imports, unsupported syntax, or unexercised reciprocal cyclic authorizations).
|
|
271
175
|
|
|
272
|
-
|
|
176
|
+
### Diagnostic Message Format
|
|
273
177
|
|
|
274
|
-
|
|
178
|
+
Every diagnostic outcome adheres to the canonical structure:
|
|
275
179
|
|
|
276
|
-
|
|
180
|
+
```text
|
|
181
|
+
{{constraint}}: {{message}} ({{reference}}).
|
|
182
|
+
```
|
|
277
183
|
|
|
278
|
-
|
|
279
|
-
- Requires an underscore prefix for Layers/Categories/Sub-Categories, and forbids it for Fragments/Sub-Fragments/Root Fragments.
|
|
280
|
-
- Validates that a Fractal Branch's name matches its parent scope (`_<Scope>@shared`).
|
|
281
|
-
- Restricts a Logical Node's name to a single Role segment, whether it is a Fragment Node or a loose file inside a Category.
|
|
282
|
-
- For loose files inside a Category: validates the file extension against the category's whitelist, requires a role suffix on code files, and validates that the role matches the Category's configured role.
|
|
184
|
+
For example:
|
|
283
185
|
|
|
284
|
-
|
|
186
|
+
```text
|
|
187
|
+
dependency/fragment-encapsulation: 'src/main.ts' imports 'src/_components/button/button.component.ts' through './_components/button/button.component.js': a Fragment is importable from outside only through its index.ts (FAF §6.1.1).
|
|
188
|
+
```
|
|
285
189
|
|
|
286
|
-
|
|
190
|
+
### Deterministic Outcome Anchoring
|
|
287
191
|
|
|
288
|
-
|
|
289
|
-
- Layers cannot reside inside Fragments, and cannot contain files or Fragments directly, except the terminal Layer of a configured Route Hierarchy.
|
|
290
|
-
- Fragments cannot be placed directly inside other Fragments (they must be nested within a Private Category), Layers (unless terminating a Route Hierarchy), Fractal Branches, or nested Root Fragments.
|
|
291
|
-
- Fractal Branches cannot be nested inside other Fractal Branches, and cannot contain files directly.
|
|
192
|
+
Because ESLint can only report diagnostics on files it is currently visiting, the plugin deterministically anchors outcomes to specific files:
|
|
292
193
|
|
|
293
|
-
|
|
194
|
+
- **Module-Level Outcomes**: Reported directly on the affected TypeScript/JavaScript file at the exact statement or import declaration location.
|
|
195
|
+
- **Domain-Level & Missing-File Outcomes**: If an outcome pertains to an entire directory, a missing mandatory file (such as a missing `index.ts`), or a non-lintable asset, it is deterministically anchored to the first lintable file in lexicographical order among direct children (anchoring to the Master Node if present). If no direct child is lintable, it ascends to the nearest ancestor domain containing a lintable file.
|
|
196
|
+
- **Global Configuration Outcomes**: Global format errors or catalog conflicts are anchored to `fafconfig.ts`.
|
|
197
|
+
- **Missing Configuration File**: If `fafconfig.ts` cannot be discovered, `configuration/missing-config-file` is anchored to every file ESLint visits.
|
|
198
|
+
- **Partial Linting Scope Limitation**: ESLint only presents diagnostics for files included in its current invocation. Linting a single file opened in an editor might not display architectural errors anchored to other files. Comprehensive architectural conformity requires running ESLint across the entire project (e.g., in CI via `eslint .`).
|
|
294
199
|
|
|
295
|
-
|
|
200
|
+
---
|
|
296
201
|
|
|
297
|
-
|
|
298
|
-
- Restricts Access Nodes to Fragments: an index file directly inside any other Logical Domain is forbidden.
|
|
299
|
-
- Requires every Fragment to contain at least one Fragment Node (its Master Node).
|
|
300
|
-
- Restricts the Access Node to import/export only its own sibling Fragment Nodes.
|
|
301
|
-
- Prevents import of nested Private Categories, Fractal Branches, or unrelated external paths.
|
|
202
|
+
## Scope of Verification
|
|
302
203
|
|
|
303
|
-
|
|
204
|
+
FAF defines both formal structural laws and qualitative design recommendations. Automated static analysis operates exclusively on deterministic source representations (ASTs, file inventories, and resolved import graphs).
|
|
304
205
|
|
|
305
|
-
|
|
206
|
+
The following five properties are explicitly **out of scope** for automated verification:
|
|
306
207
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
208
|
+
1. **Symbol Coherence with Role** (FAF §5): Whether the semantic responsibility of an exported function or constant truthfully matches its declared Role (e.g., distinguishing a pure `util` from a `service` when syntax alone is identical).
|
|
209
|
+
2. **Indivisibility of Symbols and Separation of Concerns** (FAF §6.1.2): Whether multiple Symbols grouped under an allowed multi-symbol catalog exemption are truly indivisible or constitute distinct architectural concerns.
|
|
210
|
+
3. **Semantic Cohesion of a Fragment** (FAF §6.1): Whether all internal files of a Fragment conceptually collaborate toward a single cohesive business responsibility.
|
|
211
|
+
4. **Semantic Fidelity of Mimetic Path Structures to External Addresses** (FAF §6.3.5): Whether declared Path Sub-Categories faithfully mirror real external HTTP routes or resource endpoints beyond syntactic validity.
|
|
212
|
+
5. **Architectural Recommendations Introduced by _"it is recommended"_ / _"è raccomandato"_** (FAF §1.2, §8): Qualitative recommendations such as placing pages next to components, placing private details as close as possible to consumers, or avoiding singleton layers.
|
|
311
213
|
|
|
312
|
-
|
|
214
|
+
The plugin emits **no diagnostic outcomes** and applies **no heuristics** for out-of-scope properties or recommendations.
|
|
313
215
|
|
|
314
|
-
|
|
216
|
+
**Conformity Certification**: The absence of diagnostic outcomes certifies architectural conformity for the entire verified scope.
|
|
315
217
|
|
|
316
|
-
|
|
218
|
+
---
|
|
317
219
|
|
|
318
|
-
|
|
220
|
+
## Operational Boundaries and Limitations
|
|
319
221
|
|
|
320
|
-
|
|
222
|
+
### Incompatibility with `eslint --cache`
|
|
321
223
|
|
|
322
|
-
|
|
224
|
+
`eslint-plugin-faf` is **fundamentally incompatible with `eslint --cache`**.
|
|
323
225
|
|
|
324
|
-
|
|
226
|
+
ESLint's native `--cache` flag operates on per-file timestamps. It assumes that if file `A` has not changed on disk, its lint status remains identical to the previous run. However, architectural invariants are whole-graph properties: if file `B` changes its exports or moves to another directory, file `A` (which imports file `B`) may suddenly become non-conformant even though file `A` was never modified.
|
|
325
227
|
|
|
326
|
-
|
|
228
|
+
**Do not run ESLint with `--cache` when validating FAF rules.**
|
|
327
229
|
|
|
328
|
-
|
|
329
|
-
- A Fractal Branch itself must never depend, not even indirectly, on the subtree it serves: importing anything from its owner's subtree that isn't part of the branch itself is forbidden.
|
|
230
|
+
The plugin maintains its own internal, sound caching mechanisms (memoizing module records and dependency graphs based on directory fingerprinting, `package.json`, and `fafconfig.ts`), ensuring high performance on full project runs.
|
|
330
231
|
|
|
331
|
-
###
|
|
232
|
+
### Purely Diagnostic Paradigm — Prohibition of Autofix
|
|
332
233
|
|
|
333
|
-
|
|
234
|
+
`eslint-plugin-faf` is strictly **diagnostic-only**:
|
|
334
235
|
|
|
335
|
-
-
|
|
336
|
-
-
|
|
236
|
+
- It provides **zero automated fixes** (`fix` functions) and zero automated refactorings.
|
|
237
|
+
- It will never automatically move files, create directories, rewrite imports, or synthesize barrel files.
|
|
337
238
|
|
|
338
|
-
|
|
239
|
+
Architectural corrections require intentional engineering judgment: deciding whether a private helper should be moved into a Private Category, extracted into a Sub-Fragment, or promoted to a Fractal Branch requires domain understanding that automated tools cannot possess.
|
|
339
240
|
|
|
340
|
-
###
|
|
241
|
+
### Static Analysis Capabilities
|
|
341
242
|
|
|
342
|
-
|
|
243
|
+
The plugin analyzes codebases using high-performance, deterministic static tooling:
|
|
343
244
|
|
|
344
|
-
-
|
|
345
|
-
-
|
|
245
|
+
- **TypeScript & JavaScript**: Parsed exclusively using `oxc-parser`. The plugin analyzes ESM static imports, dynamic imports with string literals (`import('...')`), CommonJS top-level `require()`, and TypeScript external references (`import x = require('...')`).
|
|
246
|
+
- **CSS Stylesheets**: Scanned with a dedicated parser for `@import` and `url()` references, establishing dependency edges to other stylesheets and static assets.
|
|
247
|
+
- **Binary Assets**: Recognized as terminal leaf nodes (sinks) with incoming edges but zero outgoing edges.
|
|
248
|
+
- **Boundary Resolution (`oxc-resolver`)**: Resolves module specifiers against your project's `tsconfig.json` (`paths`, `baseUrl`) and `package.json` (`imports`, `exports`). Workspace packages inside a monorepo are classified as local codebase files; external `node_modules` packages and Node.js built-ins (`node:*`) act as terminal sinks.
|
|
249
|
+
- **Incomplete Checks (`unverified`)**: Any unanalyzable construct (such as non-literal dynamic imports `import(variable)`, unreadable files, or symbolic links inside trees) emits a blocking `unverified/*` outcome. Non-literal `require(...)` calls, TypeScript `export =` assignments, and CommonJS `module.exports` / `exports.*` assignments are unsupported constructs that emit `unverified/unsupported-syntax`. Incomplete checks are never treated as proof of conformity.
|
|
346
250
|
|
|
347
|
-
###
|
|
251
|
+
### Nominal Kebab-Case Grammar
|
|
348
252
|
|
|
349
|
-
|
|
253
|
+
The lexical policy enforced by the plugin requires standard architectural element names to match the ASCII regular expression `^[a-z0-9]+(?:-[a-z0-9]+)*$`:
|
|
350
254
|
|
|
351
|
-
-
|
|
255
|
+
- **Permitted Characters**: Lowercase ASCII letters `a-z` and digits `0-9` separated by single hyphens.
|
|
256
|
+
- **Initial Digits**: Permitted, such as `2fa-service` or `v2-parser`.
|
|
257
|
+
- **Acronyms**: Must be lowercase, such as `http-client`.
|
|
258
|
+
- **Rejected Forms**: Uppercase letters (e.g. `HTTPClient`), consecutive, leading, or trailing hyphens, non-prefix underscores, and non-ASCII Unicode characters (e.g. `café-parser`).
|
|
259
|
+
- **Free Naming**: Free naming applies to designated Tree origins, Root Fragments, Root Nodes, Foreign Domains, and Foreign Nodes.
|
|
352
260
|
|
|
353
|
-
|
|
261
|
+
---
|
|
354
262
|
|
|
355
|
-
|
|
263
|
+
## Migration from 0.5.x (Breaking Changes)
|
|
356
264
|
|
|
357
|
-
-
|
|
358
|
-
- Blocks direct file placement inside categories configured with `allowSingleFiles: false`.
|
|
265
|
+
Version `0.6.0` is a **complete ground-up rewrite** of `@morriconeluca/eslint-plugin-faf` centered around the normative Fractal Architecture Framework specification and `fafconfig.ts`. Nothing from version `0.5.x` is compatible.
|
|
359
266
|
|
|
360
|
-
|
|
267
|
+
### What Changed
|
|
361
268
|
|
|
362
|
-
|
|
269
|
+
- **Removed `settings.faf.trees`**: Tree origins, catalog roles, and hierarchies are no longer declared inside `eslint.config.js` settings. They are declared in `fafconfig.ts`.
|
|
270
|
+
- **Removed `faf.config.ts`**: Replaced by the standardized `fafconfig.ts` specification module.
|
|
271
|
+
- **Removed Granular Rules**: All legacy individual rules (`faf/no-cross-imports`, `faf/enforce-hierarchy`, `faf/symbol-rules`, etc.) have been removed. The plugin now exposes exactly one rule: `faf/conformity`.
|
|
272
|
+
- **Removed Legacy Configs**: Previous configurations (such as `faf.configs['flat/recommended']`) are replaced by `faf.configs.recommended`.
|
|
363
273
|
|
|
364
|
-
|
|
274
|
+
### Step-by-Step Migration Guide
|
|
365
275
|
|
|
366
|
-
|
|
367
|
-
| ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- |
|
|
368
|
-
| `naming-conventions` — folder/file placement (Root Fragment, Layer, Fragment, Fractal Branch containment) | `logical-domain-placement` (new) |
|
|
369
|
-
| `naming-conventions` — Access Node existence, Master Node existence | `enforce-access-node` |
|
|
370
|
-
| `naming-conventions` — Fragment Node name prefix, role requirement/uniqueness, Category/Route Master Node role, route naming pattern | `fragment-master-node` (new) |
|
|
371
|
-
| `naming-conventions` — kebab-case, underscore prefix, Fractal Branch naming, single-Role-segment, Category loose-file naming | unchanged, stays in `naming-conventions` |
|
|
372
|
-
| `no-peer-dependency` — Foreign Domain imports (both directions) | `foreign-domain-isolation` (new) |
|
|
373
|
-
| `no-peer-dependency` — Root Node dependency direction (`rootFragments.rootNodes`) | `root-node-dependency-direction` (new) |
|
|
374
|
-
| `no-peer-dependency` — horizontal hierarchy resolution (Fragment Node roles, sibling folders) | unchanged, stays in `no-peer-dependency` |
|
|
276
|
+
1. **Upgrade Dependencies**:
|
|
375
277
|
|
|
376
|
-
|
|
278
|
+
```bash
|
|
279
|
+
npm install --save-dev @morriconeluca/eslint-plugin-faf@^0.6.0 eslint@^9.0.0
|
|
280
|
+
```
|
|
377
281
|
|
|
378
|
-
|
|
282
|
+
Ensure Node.js is `>= 24.0.0`.
|
|
283
|
+
|
|
284
|
+
2. **Create `fafconfig.ts`**:
|
|
285
|
+
In your repository root, create `fafconfig.ts` implementing `TFAFConfig`:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import type { TFAFConfig } from '@morriconeluca/eslint-plugin-faf';
|
|
289
|
+
|
|
290
|
+
export default {
|
|
291
|
+
faf: {
|
|
292
|
+
locals: [
|
|
293
|
+
{
|
|
294
|
+
roles: [
|
|
295
|
+
{ name: 'util', category: '_utils' },
|
|
296
|
+
{ name: 'component', category: '_components' },
|
|
297
|
+
],
|
|
298
|
+
subGraphRoles: [{ name: 'spec' }],
|
|
299
|
+
rootLayers: ['src'],
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
} satisfies TFAFConfig;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
3. **Update `eslint.config.js`**:
|
|
307
|
+
Remove any legacy rules and settings. Use the new recommended preset:
|
|
379
308
|
|
|
380
|
-
|
|
309
|
+
```javascript
|
|
310
|
+
import faf from '@morriconeluca/eslint-plugin-faf';
|
|
381
311
|
|
|
382
|
-
|
|
312
|
+
export default [faf.configs.recommended];
|
|
313
|
+
```
|
|
383
314
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
315
|
+
4. **Update CI and Scripts**:
|
|
316
|
+
Remove `--cache` from your ESLint linting scripts:
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"scripts": {
|
|
320
|
+
"lint": "eslint ."
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
391
324
|
|
|
392
325
|
---
|
|
393
326
|
|
|
394
327
|
## License
|
|
395
328
|
|
|
396
|
-
|
|
329
|
+
MIT © [Luca Morricone](https://github.com/morriconeluca)
|