@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/dist/main.cjs ADDED
@@ -0,0 +1,1420 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/main.ts
31
+ var main_exports = {};
32
+ __export(main_exports, {
33
+ configs: () => configs,
34
+ default: () => main_default,
35
+ rules: () => rules
36
+ });
37
+ module.exports = __toCommonJS(main_exports);
38
+
39
+ // package.json
40
+ var package_default = {
41
+ name: "@morriconeluca/eslint-plugin-faf",
42
+ version: "0.1.1",
43
+ description: "An ESLint plugin for FAF projects.",
44
+ main: "./dist/main.cjs",
45
+ module: "./dist/main.js",
46
+ types: "./dist/main.d.ts",
47
+ exports: {
48
+ ".": {
49
+ types: "./dist/main.d.ts",
50
+ import: "./dist/main.js",
51
+ require: "./dist/main.cjs"
52
+ }
53
+ },
54
+ publishConfig: {
55
+ access: "public"
56
+ },
57
+ imports: {
58
+ "#_rules@shared/*": "./src/_rules/_rules@shared/*",
59
+ "#_utils@shared/*": "./src/_rules/_rules@shared/_utils/_utils@shared/*"
60
+ },
61
+ type: "module",
62
+ files: [
63
+ "dist"
64
+ ],
65
+ scripts: {
66
+ build: "npm run lint && tsup",
67
+ test: "vitest run",
68
+ dev: "vitest",
69
+ lint: "npm run lint:types && npm run lint:eslint",
70
+ "lint:eslint": "eslint .",
71
+ "lint:types": "tsc --noEmit",
72
+ "lint:fix": "eslint --fix .",
73
+ format: "prettier --write .",
74
+ "check-format": "prettier --check .",
75
+ ci: "npm run build && npm run check-format && npm run test",
76
+ prepublishOnly: "npm run ci",
77
+ "local-release": "changeset version && changeset publish",
78
+ prepare: "husky"
79
+ },
80
+ keywords: [
81
+ "eslint",
82
+ "eslintplugin",
83
+ "faf"
84
+ ],
85
+ author: "Luca Morricone",
86
+ license: "MIT",
87
+ peerDependencies: {
88
+ eslint: "^8.57.0 || ^9.0.0 || ^10.0.0"
89
+ },
90
+ devDependencies: {
91
+ "@changesets/cli": "^2.31.0",
92
+ "@commitlint/cli": "^21.0.2",
93
+ "@commitlint/config-conventional": "^21.0.2",
94
+ "@eslint/js": "^10.0.1",
95
+ "@types/eslint": "^9.6.1",
96
+ "@types/node": "^25.9.3",
97
+ "@typescript-eslint/utils": "^8.24.1",
98
+ "dependency-cruiser": "^17.4.3",
99
+ eslint: "^10.5.0",
100
+ "eslint-import-resolver-typescript": "^4.4.5",
101
+ "eslint-plugin-import-x": "^4.16.2",
102
+ "eslint-plugin-perfectionist": "^5.9.0",
103
+ husky: "^9.1.7",
104
+ "lint-staged": "^17.0.7",
105
+ prettier: "^3.8.4",
106
+ tsup: "^8.3.6",
107
+ typescript: "^6.0.3",
108
+ "typescript-eslint": "^8.61.0",
109
+ vitest: "^4.1.9"
110
+ },
111
+ "lint-staged": {
112
+ "*": "prettier --write --ignore-unknown",
113
+ "src/**/*.ts": "eslint --fix"
114
+ }
115
+ };
116
+
117
+ // src/_rules/category-mutually-exclusive/category-mutually-exclusive.rule.ts
118
+ var import_path5 = __toESM(require("path"), 1);
119
+
120
+ // src/_rules/_rules@shared/_utils/_aggregates/classify-folder/classify-folder.util.ts
121
+ var import_path3 = __toESM(require("path"), 1);
122
+
123
+ // src/_rules/_rules@shared/_utils/_utils@shared/_stores/cache/cache.store.ts
124
+ var state = {
125
+ categoryConfigCache: /* @__PURE__ */ new Map(),
126
+ classifyCache: /* @__PURE__ */ new Map(),
127
+ dirCache: {},
128
+ packageImports: null,
129
+ projectRoot: process.cwd(),
130
+ resolvedImportPathCache: /* @__PURE__ */ new Map(),
131
+ rolesCache: /* @__PURE__ */ new WeakMap(),
132
+ treeConfigCache: /* @__PURE__ */ new Map(),
133
+ treeConfigIncludingExcludedCache: /* @__PURE__ */ new Map()
134
+ };
135
+
136
+ // src/_rules/_rules@shared/_utils/_primitives/get-root-fragment-config/get-root-fragment-config.util.ts
137
+ function getRootFragmentConfig(relDirPath, config) {
138
+ const checkConfig = (rfConfig) => {
139
+ if (rfConfig.paths.includes(relDirPath)) {
140
+ return rfConfig;
141
+ }
142
+ if (rfConfig.subRootFragments) {
143
+ for (const sub of rfConfig.subRootFragments) {
144
+ const res = checkConfig(sub);
145
+ if (res) return res;
146
+ }
147
+ }
148
+ return null;
149
+ };
150
+ if (config.rootFragments) {
151
+ for (const rf of config.rootFragments) {
152
+ const res = checkConfig(rf);
153
+ if (res) return res;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ // src/_rules/_rules@shared/_utils/_compounds/is-root-fragment/is-root-fragment.util.ts
160
+ function isRootFragment(relDirPath, config) {
161
+ return getRootFragmentConfig(relDirPath, config) !== null;
162
+ }
163
+
164
+ // src/_rules/_rules@shared/_utils/_primitives/get-category-config/get-category-config.util.ts
165
+ var import_path = __toESM(require("path"), 1);
166
+ function getCategoryConfig(relDirPath, config) {
167
+ const cached = state.categoryConfigCache.get(relDirPath);
168
+ if (cached !== void 0) {
169
+ return cached;
170
+ }
171
+ let result = null;
172
+ let current = relDirPath;
173
+ while (current && current !== "." && current !== "/") {
174
+ const folderName = import_path.default.basename(current);
175
+ if (folderName.startsWith("_") && !folderName.includes("@shared")) {
176
+ const catConfig = config.categories?.find((c) => c.name === folderName);
177
+ if (catConfig) {
178
+ result = catConfig;
179
+ break;
180
+ }
181
+ } else {
182
+ break;
183
+ }
184
+ const parent = import_path.default.dirname(current).replace(/\\/g, "/");
185
+ if (parent === current) break;
186
+ current = parent;
187
+ }
188
+ state.categoryConfigCache.set(relDirPath, result);
189
+ return result;
190
+ }
191
+
192
+ // src/_rules/_rules@shared/_utils/_primitives/read-dir-cached/read-dir-cached.util.ts
193
+ var import_fs = __toESM(require("fs"), 1);
194
+ var import_path2 = __toESM(require("path"), 1);
195
+ function readDirCached(relDirPath) {
196
+ const absPath = import_path2.default.resolve(state.projectRoot, relDirPath).replace(/\\/g, "/");
197
+ if (state.dirCache[absPath]) {
198
+ return state.dirCache[absPath];
199
+ }
200
+ try {
201
+ const entries = import_fs.default.readdirSync(absPath, { withFileTypes: true });
202
+ const files = [];
203
+ const dirs = [];
204
+ for (const entry of entries) {
205
+ if (entry.isFile()) {
206
+ files.push(entry.name);
207
+ } else if (entry.isDirectory()) {
208
+ dirs.push(entry.name);
209
+ }
210
+ }
211
+ state.dirCache[absPath] = { dirs, files };
212
+ return state.dirCache[absPath];
213
+ } catch {
214
+ return { dirs: [], files: [] };
215
+ }
216
+ }
217
+
218
+ // src/_rules/_rules@shared/_utils/_aggregates/classify-folder/classify-folder.util.ts
219
+ function classifyFolder(relDirPath, config) {
220
+ const cached = state.classifyCache.get(relDirPath);
221
+ if (cached !== void 0) {
222
+ return cached;
223
+ }
224
+ const result = _classifyFolder(relDirPath, config);
225
+ state.classifyCache.set(relDirPath, result);
226
+ return result;
227
+ }
228
+ function _classifyFolder(relDirPath, config) {
229
+ const isExcluded = config.excludes?.some(
230
+ (exc) => relDirPath === exc || relDirPath.startsWith(exc + "/")
231
+ );
232
+ if (isExcluded) {
233
+ return "foreign";
234
+ }
235
+ if (isRootFragment(relDirPath, config)) {
236
+ return "root-fragment";
237
+ }
238
+ const folderName = import_path3.default.basename(relDirPath);
239
+ if (folderName.startsWith("_") && folderName.includes("@shared")) {
240
+ return "fractal-branch";
241
+ }
242
+ const contents = readDirCached(relDirPath);
243
+ const hasAccessNode = contents.files.some(
244
+ (f) => f === "index.ts" || f === "index.js"
245
+ );
246
+ const isRoute = config.routeHierarchies && config.routeHierarchies.some(
247
+ (rh) => rh.paths.some((rp) => {
248
+ if (relDirPath.startsWith(rp + "/")) {
249
+ const remainder = relDirPath.substring(rp.length + 1);
250
+ const segments = remainder.split("/");
251
+ return !segments.some(
252
+ (seg) => seg.endsWith("@shared") || seg.includes("@shared")
253
+ );
254
+ }
255
+ return false;
256
+ })
257
+ );
258
+ if (isRoute && !hasAccessNode) {
259
+ return "layer";
260
+ }
261
+ if (folderName.startsWith("_")) {
262
+ const catConfig = getCategoryConfig(relDirPath, config);
263
+ if (catConfig) {
264
+ return "category";
265
+ }
266
+ return "layer";
267
+ }
268
+ const isCategory = config.categories && config.categories.some((c) => c.name === "_" + folderName);
269
+ if (isCategory && !hasAccessNode) {
270
+ return "category";
271
+ }
272
+ if (hasAccessNode) {
273
+ return "fragment";
274
+ }
275
+ return "invalid-fragment";
276
+ }
277
+
278
+ // src/_rules/_rules@shared/_utils/_primitives/find-tree-config/find-tree-config.util.ts
279
+ function findTreeConfig(relPath, settings) {
280
+ if (!settings || !settings.trees) {
281
+ return null;
282
+ }
283
+ const cached = state.treeConfigCache.get(relPath);
284
+ if (cached !== void 0) {
285
+ return cached;
286
+ }
287
+ let result = null;
288
+ for (const tree of settings.trees) {
289
+ const isIncluded = tree.includes.some(
290
+ (inc) => relPath === inc || relPath.startsWith(inc + "/")
291
+ );
292
+ const isExcluded = tree.excludes?.some(
293
+ (exc) => relPath === exc || relPath.startsWith(exc + "/")
294
+ );
295
+ if (isIncluded && !isExcluded) {
296
+ result = tree;
297
+ break;
298
+ }
299
+ }
300
+ state.treeConfigCache.set(relPath, result);
301
+ return result;
302
+ }
303
+
304
+ // src/_rules/_rules@shared/_utils/_primitives/to-relative-path/to-relative-path.util.ts
305
+ var import_path4 = __toESM(require("path"), 1);
306
+ function toRelativePath(absoluteOrRelative) {
307
+ let abs = absoluteOrRelative;
308
+ if (!import_path4.default.isAbsolute(abs)) {
309
+ abs = import_path4.default.resolve(state.projectRoot, abs);
310
+ }
311
+ const rel = import_path4.default.relative(state.projectRoot, abs);
312
+ return rel.replace(/\\/g, "/");
313
+ }
314
+
315
+ // src/_rules/category-mutually-exclusive/category-mutually-exclusive.rule.ts
316
+ var rule = {
317
+ create(context) {
318
+ const absPath = context.filename;
319
+ const relPath = toRelativePath(absPath);
320
+ const settings = context.settings;
321
+ if (!settings.faf) {
322
+ return {};
323
+ }
324
+ const config = findTreeConfig(relPath, settings.faf);
325
+ if (!config) {
326
+ return {};
327
+ }
328
+ const relDir = import_path5.default.dirname(relPath);
329
+ const parentType = classifyFolder(relDir, config);
330
+ if (parentType !== "category") {
331
+ return {};
332
+ }
333
+ const fileName = import_path5.default.basename(relPath);
334
+ return {
335
+ Program(node) {
336
+ const contents = readDirCached(relDir);
337
+ const activeFiles = contents.files.filter(
338
+ (f) => f !== "README.md" && f !== "package.json" && !f.startsWith(".")
339
+ );
340
+ const activeDirs = contents.dirs.filter((d) => !d.startsWith("_"));
341
+ const hasFiles = activeFiles.length > 0;
342
+ const hasDirs = activeDirs.length > 0;
343
+ if (hasFiles && hasDirs) {
344
+ context.report({
345
+ loc: { column: 0, line: 1 },
346
+ message: `Mutua Esclusivit\xE0 violation in Category "${import_path5.default.basename(relDir)}": cannot mix single files (${activeFiles.join(", ")}) and Fragment directories (${activeDirs.join(", ")}).`,
347
+ node
348
+ });
349
+ }
350
+ if (hasFiles) {
351
+ const folderName = import_path5.default.basename(relDir);
352
+ const catConfig = getCategoryConfig(relDir, config);
353
+ if (catConfig) {
354
+ const ext = import_path5.default.extname(fileName);
355
+ const isAllowedAsset = catConfig.allowedExtensions?.includes(ext) ?? false;
356
+ if (!catConfig.allowSingleFiles && !isAllowedAsset) {
357
+ context.report({
358
+ message: `Category "${folderName}" cannot contain file nodes directly. File "${fileName}" must be encapsulated inside a Fragment.`,
359
+ node
360
+ });
361
+ }
362
+ }
363
+ }
364
+ }
365
+ };
366
+ },
367
+ meta: {
368
+ docs: {
369
+ description: "Enforce that Categories contain either only file nodes or only Fragment folders, and check whitelist roles"
370
+ },
371
+ schema: [],
372
+ type: "problem"
373
+ }
374
+ };
375
+ var category_mutually_exclusive_rule_default = rule;
376
+
377
+ // src/_rules/enforce-access-node/enforce-access-node.rule.ts
378
+ var import_path6 = __toESM(require("path"), 1);
379
+ var rule2 = {
380
+ create(context) {
381
+ const absPath = context.filename;
382
+ const relPath = toRelativePath(absPath);
383
+ const fileName = import_path6.default.basename(relPath);
384
+ if (fileName !== "index.ts" && fileName !== "index.js") {
385
+ return {};
386
+ }
387
+ const settings = context.settings;
388
+ if (!settings.faf) {
389
+ return {};
390
+ }
391
+ const config = findTreeConfig(relPath, settings.faf);
392
+ if (!config) {
393
+ return {};
394
+ }
395
+ const relDir = import_path6.default.dirname(relPath);
396
+ const parentType = classifyFolder(relDir, config);
397
+ if (parentType !== "fragment") {
398
+ return {};
399
+ }
400
+ function checkSource(node, sourceVal) {
401
+ const isValidSibling = /^\.\/[^/]+$/.test(sourceVal);
402
+ if (!isValidSibling) {
403
+ context.report({
404
+ message: `Access Node (index file) can only import or export its own sibling Fragment Nodes using simple relative paths (e.g. "./example.component"). Got "${sourceVal}".`,
405
+ node
406
+ });
407
+ return;
408
+ }
409
+ const importedFilename = import_path6.default.basename(sourceVal);
410
+ if (importedFilename === "index" || importedFilename === "index.ts" || importedFilename === "index.js" || importedFilename === "README.md" || importedFilename === "package.json") {
411
+ return;
412
+ }
413
+ const fragmentName = import_path6.default.basename(relDir);
414
+ if (!importedFilename.startsWith(fragmentName + ".")) {
415
+ context.report({
416
+ message: `Access Node (index file) can only re-export its own Fragment Nodes (names starting with "${fragmentName}."). Got "${importedFilename}".`,
417
+ node
418
+ });
419
+ }
420
+ }
421
+ return {
422
+ ExportAllDeclaration(node) {
423
+ checkSource(node, node.source.value);
424
+ },
425
+ ExportNamedDeclaration(node) {
426
+ if (node.source) {
427
+ checkSource(node, node.source.value);
428
+ }
429
+ },
430
+ ImportDeclaration(node) {
431
+ checkSource(node, node.source.value);
432
+ }
433
+ };
434
+ },
435
+ meta: {
436
+ docs: {
437
+ description: "Enforce Access Node rules for Fragments"
438
+ },
439
+ schema: [],
440
+ type: "problem"
441
+ }
442
+ };
443
+ var enforce_access_node_rule_default = rule2;
444
+
445
+ // src/_rules/naming-conventions/naming-conventions.rule.ts
446
+ var import_path7 = __toESM(require("path"), 1);
447
+
448
+ // src/_rules/_rules@shared/_utils/_primitives/get-file-role/get-file-role.util.ts
449
+ function getFileRole(fileName, config) {
450
+ if (fileName === "index.ts" || fileName === "index.js") {
451
+ return "index";
452
+ }
453
+ const parts = fileName.split(".");
454
+ let allRoles = state.rolesCache.get(config);
455
+ if (!allRoles) {
456
+ allRoles = new Set(config.roles.flat());
457
+ state.rolesCache.set(config, allRoles);
458
+ }
459
+ for (let i = parts.length - 2; i >= 1; i--) {
460
+ const part = parts[i];
461
+ if (part && allRoles.has(part)) {
462
+ return part;
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+
468
+ // src/_rules/_rules@shared/_utils/_primitives/get-project-root/get-project-root.util.ts
469
+ function getProjectRoot() {
470
+ return state.projectRoot;
471
+ }
472
+
473
+ // src/_rules/naming-conventions/naming-conventions.rule.ts
474
+ var KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
475
+ var rule3 = {
476
+ create(context) {
477
+ return {
478
+ Program(node) {
479
+ const absPath = context.filename;
480
+ const relPath = toRelativePath(absPath);
481
+ const settings = context.settings;
482
+ if (!settings.faf) {
483
+ return;
484
+ }
485
+ const config = findTreeConfig(relPath, settings.faf);
486
+ if (!config) {
487
+ return;
488
+ }
489
+ const relDir = import_path7.default.dirname(relPath);
490
+ const folderName = import_path7.default.basename(relDir);
491
+ const parentType = classifyFolder(relDir, config);
492
+ if (parentType === "foreign") {
493
+ return;
494
+ }
495
+ const fileName = import_path7.default.basename(relPath);
496
+ let currentDir = relDir;
497
+ while (currentDir && currentDir !== "." && currentDir !== "/") {
498
+ const isInTree = config.includes.some(
499
+ (inc) => currentDir === inc || currentDir.startsWith(inc + "/")
500
+ );
501
+ const isExcluded = config.excludes?.some(
502
+ (exc) => currentDir === exc || currentDir.startsWith(exc + "/")
503
+ ) ?? false;
504
+ if (!isInTree || isExcluded) {
505
+ break;
506
+ }
507
+ const currentFolderName = import_path7.default.basename(currentDir);
508
+ const currentType = classifyFolder(currentDir, config);
509
+ if ((currentType === "layer" || currentType === "category") && !currentFolderName.startsWith("_")) {
510
+ context.report({
511
+ message: `Layer/Category directory "${currentFolderName}" must be prefixed with an underscore (e.g. "_${currentFolderName}").`,
512
+ node
513
+ });
514
+ }
515
+ if (currentFolderName.startsWith("_") && !currentFolderName.includes("@shared")) {
516
+ const contents = readDirCached(currentDir);
517
+ const hasIndex = contents.files.includes("index.ts") || contents.files.includes("index.js");
518
+ if (hasIndex) {
519
+ context.report({
520
+ message: `Directory "${currentFolderName}" has an Access Node (index.ts) but its name starts with "_". Fragments must not be prefixed with underscore.`,
521
+ node
522
+ });
523
+ }
524
+ }
525
+ if (currentFolderName !== "src") {
526
+ let baseFolderName = currentFolderName.replace(/@shared$/, "");
527
+ if (baseFolderName.startsWith("_")) {
528
+ baseFolderName = baseFolderName.substring(1);
529
+ }
530
+ if (baseFolderName.startsWith("[") && baseFolderName.endsWith("]")) {
531
+ baseFolderName = baseFolderName.substring(
532
+ 1,
533
+ baseFolderName.length - 1
534
+ );
535
+ }
536
+ const isKebab = KEBAB_CASE_REGEX.test(baseFolderName);
537
+ if (!isKebab) {
538
+ context.report({
539
+ message: `Folder name "${currentFolderName}" must be in kebab-case (e.g. "_my-folder" or "_[my-param]").`,
540
+ node
541
+ });
542
+ }
543
+ }
544
+ if (currentType === "fractal-branch") {
545
+ const expectedScope = import_path7.default.basename(import_path7.default.dirname(currentDir));
546
+ const normalizedExpectedScope = expectedScope.startsWith("_") ? expectedScope.substring(1) : expectedScope;
547
+ const expectedName = `_${normalizedExpectedScope}@shared`;
548
+ if (currentFolderName !== expectedName) {
549
+ context.report({
550
+ message: `Fractal Branch name "${currentFolderName}" must match its parent scope name: "${expectedName}".`,
551
+ node
552
+ });
553
+ }
554
+ }
555
+ if (currentType === "invalid-fragment") {
556
+ context.report({
557
+ message: `Fragment directory "${currentFolderName}" is missing an Access Node (index.ts/index.js).`,
558
+ node
559
+ });
560
+ }
561
+ const relParentDir = import_path7.default.dirname(currentDir).replace(/\\/g, "/");
562
+ const parentFolderName = import_path7.default.basename(relParentDir);
563
+ const relParentType = relParentDir === "." || relParentDir === "/" ? "unknown" : classifyFolder(relParentDir, config);
564
+ if (currentType === "root-fragment") {
565
+ if (relParentType !== "root-fragment" && relParentType !== "unknown") {
566
+ context.report({
567
+ message: `Root Fragment directory "${currentFolderName}" must be placed directly inside a Root Container or another Root Fragment, not inside "${parentFolderName}" (classified as ${relParentType}).`,
568
+ node
569
+ });
570
+ }
571
+ }
572
+ if (currentType === "layer") {
573
+ if (relParentType === "fragment" || relParentType === "invalid-fragment") {
574
+ context.report({
575
+ message: `Layer directory "${currentFolderName}" cannot be placed inside "${parentFolderName}" (classified as ${relParentType}). Layers cannot reside within Fragments.`,
576
+ node
577
+ });
578
+ }
579
+ }
580
+ if (currentType === "fragment" || currentType === "invalid-fragment") {
581
+ const isParentRootContainer = config.includes.includes(relParentDir);
582
+ if (relParentType === "layer") {
583
+ const isParentRoute = config.routeHierarchies && config.routeHierarchies.some(
584
+ (rh) => rh.paths.some(
585
+ (rp) => relParentDir === rp || relParentDir.startsWith(rp + "/")
586
+ )
587
+ );
588
+ if (!isParentRoute) {
589
+ context.report({
590
+ message: `Fragment directory "${currentFolderName}" cannot be placed directly inside Layer "${parentFolderName}". Fragments must be contained within a Category.`,
591
+ node
592
+ });
593
+ }
594
+ } else if (relParentType === "fragment" || relParentType === "invalid-fragment") {
595
+ context.report({
596
+ message: `Fragment directory "${currentFolderName}" cannot be placed directly inside Fragment "${parentFolderName}". Sub-Fragments must be contained within a Private Category.`,
597
+ node
598
+ });
599
+ } else if (relParentType === "fractal-branch") {
600
+ context.report({
601
+ message: `Fragment directory "${currentFolderName}" cannot be placed directly inside Fractal Branch "${parentFolderName}". Fragments must be contained within a Category.`,
602
+ node
603
+ });
604
+ } else if (relParentType === "root-fragment" && !isParentRootContainer) {
605
+ context.report({
606
+ message: `Fragment directory "${currentFolderName}" cannot be placed directly inside Root Fragment "${parentFolderName}". Fragments must be contained within a Category.`,
607
+ node
608
+ });
609
+ }
610
+ }
611
+ if (currentType === "fractal-branch") {
612
+ if (relParentType === "fractal-branch") {
613
+ context.report({
614
+ message: `Fractal Branch "${currentFolderName}" cannot be placed inside another Fractal Branch "${parentFolderName}".`,
615
+ node
616
+ });
617
+ }
618
+ }
619
+ currentDir = import_path7.default.dirname(currentDir).replace(/\\/g, "/");
620
+ }
621
+ if (parentType === "layer") {
622
+ context.report({
623
+ message: `Layers cannot contain files directly. File "${fileName}" is placed directly inside Layer "${folderName}".`,
624
+ node
625
+ });
626
+ return;
627
+ }
628
+ if (parentType === "fractal-branch") {
629
+ context.report({
630
+ message: `Fractal Branches cannot contain files directly. File "${fileName}" is placed directly inside Fractal Branch "${folderName}".`,
631
+ node
632
+ });
633
+ return;
634
+ }
635
+ if (parentType === "invalid-fragment") {
636
+ return;
637
+ }
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
+ const rfConfig = getRootFragmentConfig(relDir, config);
648
+ if (rfConfig) {
649
+ const flatRootNodes = rfConfig.rootNodes.flat();
650
+ const isMatchedRootNode = flatRootNodes.some((rn) => {
651
+ if (rn.includes("/")) {
652
+ const rfPath = rfConfig.paths[0] ?? "";
653
+ const absRn = import_path7.default.resolve(getProjectRoot(), rfPath, rn);
654
+ return toRelativePath(absRn) === relPath;
655
+ }
656
+ return rn === fileName;
657
+ });
658
+ if (isMatchedRootNode) {
659
+ return;
660
+ }
661
+ }
662
+ if (fileName === "README.md" || fileName === "package.json" || fileName === "tsconfig.json") {
663
+ return;
664
+ }
665
+ const role = getFileRole(fileName, config);
666
+ const ext = import_path7.default.extname(fileName);
667
+ if (parentType === "fragment") {
668
+ const matchedRoute = config.routeHierarchies?.find(
669
+ (rh) => rh.paths.some((rp) => relDir === rp || relDir.startsWith(rp + "/"))
670
+ );
671
+ if (matchedRoute) {
672
+ const contents = readDirCached(relDir);
673
+ const fileRoles = contents.files.map((f) => getFileRole(f, config));
674
+ if (!fileRoles.includes(matchedRoute.role)) {
675
+ context.report({
676
+ message: `Route terminal Fragment "${folderName}" must contain a Master Node with role "${matchedRoute.role}" (e.g. "${folderName}.${matchedRoute.role}.ts").`,
677
+ node
678
+ });
679
+ }
680
+ }
681
+ let currentParent = import_path7.default.dirname(relDir).replace(/\\/g, "/");
682
+ let categoryConfig = null;
683
+ let categoryFolderName = "";
684
+ while (currentParent && currentParent !== "." && currentParent !== "/") {
685
+ const parentType2 = classifyFolder(currentParent, config);
686
+ if (parentType2 === "category") {
687
+ categoryConfig = getCategoryConfig(currentParent, config);
688
+ categoryFolderName = import_path7.default.basename(currentParent);
689
+ break;
690
+ } else if (parentType2 === "fragment" || parentType2 === "root-fragment" || parentType2 === "layer") {
691
+ break;
692
+ }
693
+ const nextParent = import_path7.default.dirname(currentParent).replace(/\\/g, "/");
694
+ if (nextParent === currentParent) break;
695
+ currentParent = nextParent;
696
+ }
697
+ if (categoryConfig && categoryConfig.role) {
698
+ const contents = readDirCached(relDir);
699
+ const fileRoles = contents.files.map((f) => getFileRole(f, config));
700
+ if (!fileRoles.includes(categoryConfig.role)) {
701
+ context.report({
702
+ message: `Fragment "${folderName}" inside Category "${categoryFolderName}" must contain a Master Node with role "${categoryConfig.role}" (e.g. "${folderName}.${categoryConfig.role}.ts").`,
703
+ node
704
+ });
705
+ }
706
+ }
707
+ if (!fileName.startsWith(folderName + ".")) {
708
+ context.report({
709
+ message: `Fragment Node "${fileName}" must share the parent Fragment name: "${folderName}.<role>${ext}".`,
710
+ node
711
+ });
712
+ }
713
+ if (!role) {
714
+ context.report({
715
+ message: `File "${fileName}" inside Fragment "${folderName}" must have an explicit role suffix (e.g. "${folderName}.style${ext}").`,
716
+ node
717
+ });
718
+ }
719
+ }
720
+ if (parentType === "category") {
721
+ const catConfig = getCategoryConfig(relDir, config);
722
+ if (catConfig) {
723
+ const allowedExts = catConfig.allowedExtensions ?? [
724
+ ".js",
725
+ ".jsx",
726
+ ".ts",
727
+ ".tsx"
728
+ ];
729
+ if (!allowedExts.includes(ext)) {
730
+ context.report({
731
+ message: `File extension "${ext}" is not allowed in Category "${folderName}". Allowed extensions: ${allowedExts.join(", ")}.`,
732
+ node
733
+ });
734
+ return;
735
+ }
736
+ const isCodeExt = [".js", ".jsx", ".ts", ".tsx"].includes(ext);
737
+ if (isCodeExt) {
738
+ if (!role) {
739
+ context.report({
740
+ message: `Logical Node "${fileName}" inside Category "${folderName}" must have a role suffix.`,
741
+ node
742
+ });
743
+ } else if (role !== catConfig.role) {
744
+ context.report({
745
+ message: `Role "${role}" for file "${fileName}" does not match the expected Category role "${catConfig.role}".`,
746
+ node
747
+ });
748
+ }
749
+ } else {
750
+ if (role && role !== catConfig.role) {
751
+ context.report({
752
+ message: `Role "${role}" for asset "${fileName}" does not match the expected Category role "${catConfig.role}".`,
753
+ node
754
+ });
755
+ }
756
+ }
757
+ }
758
+ }
759
+ }
760
+ };
761
+ },
762
+ meta: {
763
+ docs: {
764
+ description: "Enforce naming conventions and folder taxonomy in FAF architecture"
765
+ },
766
+ schema: [],
767
+ type: "problem"
768
+ }
769
+ };
770
+ var naming_conventions_rule_default = rule3;
771
+
772
+ // src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
773
+ var import_path9 = __toESM(require("path"), 1);
774
+
775
+ // src/_rules/_rules@shared/_utils/_aggregates/resolve-import-path/resolve-import-path.util.ts
776
+ var import_fs2 = __toESM(require("fs"), 1);
777
+ var import_path8 = __toESM(require("path"), 1);
778
+ function resolveImportPath(importPath, currentFileRelative, settings) {
779
+ const cacheKey = `${currentFileRelative}::${importPath}`;
780
+ const cached = state.resolvedImportPathCache.get(cacheKey);
781
+ if (cached !== void 0) {
782
+ return cached;
783
+ }
784
+ const result = _resolveImportPath(importPath, currentFileRelative, settings);
785
+ state.resolvedImportPathCache.set(cacheKey, result);
786
+ return result;
787
+ }
788
+ function _resolveImportPath(importPath, currentFileRelative, settings) {
789
+ const pkgImports = loadPackageImports(state.projectRoot);
790
+ const aliases = Object.keys(pkgImports).length > 0 ? pkgImports : settings?.aliases || {};
791
+ let matchedAlias = null;
792
+ let longestMatch = 0;
793
+ for (const alias of Object.keys(aliases)) {
794
+ if (importPath === alias || importPath.startsWith(alias + "/")) {
795
+ if (alias.length > longestMatch) {
796
+ longestMatch = alias.length;
797
+ matchedAlias = alias;
798
+ }
799
+ }
800
+ }
801
+ let relResolved;
802
+ if (matchedAlias) {
803
+ const target = aliases[matchedAlias];
804
+ if (target) {
805
+ const remainder = importPath.substring(matchedAlias.length);
806
+ const absTarget = import_path8.default.resolve(state.projectRoot, target + remainder);
807
+ relResolved = toRelativePath(absTarget);
808
+ } else {
809
+ return "";
810
+ }
811
+ } else {
812
+ if (importPath.startsWith(".") || importPath.startsWith("/")) {
813
+ const absCurrentDir = import_path8.default.dirname(
814
+ import_path8.default.resolve(state.projectRoot, currentFileRelative)
815
+ );
816
+ const absResolved = import_path8.default.resolve(absCurrentDir, importPath);
817
+ relResolved = toRelativePath(absResolved);
818
+ } else {
819
+ return "";
820
+ }
821
+ }
822
+ return finalizeResolvedPath(relResolved);
823
+ }
824
+ function finalizeResolvedPath(relResolved) {
825
+ const resolvedDir = import_path8.default.dirname(relResolved).replace(/\\/g, "/");
826
+ const resolvedBase = import_path8.default.basename(relResolved);
827
+ const dirContents = readDirCached(relResolved);
828
+ if (dirContents.files.includes("index.ts")) {
829
+ return (relResolved === "." ? "" : relResolved + "/") + "index.ts";
830
+ }
831
+ if (dirContents.files.includes("index.js")) {
832
+ return (relResolved === "." ? "" : relResolved + "/") + "index.js";
833
+ }
834
+ const parentContents = readDirCached(resolvedDir);
835
+ const exactMatch = parentContents.files.find((f) => f === resolvedBase);
836
+ if (!exactMatch) {
837
+ const matchWithExt = parentContents.files.find(
838
+ (f) => f.startsWith(resolvedBase + ".")
839
+ );
840
+ if (matchWithExt) {
841
+ return (resolvedDir === "." ? "" : resolvedDir + "/") + matchWithExt;
842
+ }
843
+ }
844
+ return relResolved;
845
+ }
846
+ function loadPackageImports(projectRootPath) {
847
+ if (state.packageImports !== null) {
848
+ return state.packageImports;
849
+ }
850
+ state.packageImports = {};
851
+ const isTest = typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.VITEST === "true");
852
+ if (isTest) {
853
+ return state.packageImports;
854
+ }
855
+ const pkgPath = import_path8.default.resolve(projectRootPath, "package.json");
856
+ if (import_fs2.default.existsSync(pkgPath)) {
857
+ try {
858
+ const pkgContent = import_fs2.default.readFileSync(pkgPath, "utf8");
859
+ const pkg = JSON.parse(pkgContent);
860
+ if (pkg.imports) {
861
+ for (const [key, val] of Object.entries(pkg.imports)) {
862
+ if (typeof val === "string") {
863
+ const cleanKey = key.endsWith("/*") ? key.slice(0, -2) : key.endsWith("*") ? key.slice(0, -1) : key;
864
+ let cleanVal = val.endsWith("/*") ? val.slice(0, -2) : val.endsWith("*") ? val.slice(0, -1) : val;
865
+ if (cleanVal.startsWith("./")) {
866
+ cleanVal = cleanVal.substring(2);
867
+ } else if (cleanVal.startsWith("/")) {
868
+ cleanVal = cleanVal.substring(1);
869
+ }
870
+ state.packageImports[cleanKey] = cleanVal;
871
+ }
872
+ }
873
+ }
874
+ } catch {
875
+ }
876
+ }
877
+ return state.packageImports;
878
+ }
879
+
880
+ // src/_rules/no-direct-fragment-import/no-direct-fragment-import.rule.ts
881
+ var rule4 = {
882
+ create(context) {
883
+ const absPath = context.filename;
884
+ const relPath = toRelativePath(absPath);
885
+ const settings = context.settings;
886
+ if (!settings.faf) {
887
+ return {};
888
+ }
889
+ const config = findTreeConfig(relPath, settings.faf);
890
+ if (!config) {
891
+ return {};
892
+ }
893
+ function checkImport(node, importValue) {
894
+ const resolvedRelPath = resolveImportPath(
895
+ importValue,
896
+ relPath,
897
+ settings.faf
898
+ );
899
+ if (!resolvedRelPath) {
900
+ return;
901
+ }
902
+ const importedDir = import_path9.default.dirname(resolvedRelPath);
903
+ const importedFile = import_path9.default.basename(resolvedRelPath);
904
+ const importedFolderType = classifyFolder(importedDir, config);
905
+ if (importedFolderType === "fragment") {
906
+ const isAccessNode = importedFile === "index.ts" || importedFile === "index.js";
907
+ if (!isAccessNode) {
908
+ const isInside = relPath === importedDir || relPath.startsWith(importedDir + "/");
909
+ if (!isInside) {
910
+ context.report({
911
+ message: `Direct import of Fragment internal file "${importedFile}" is forbidden. You must import through its Access Node (index file) in "${importedDir}".`,
912
+ node
913
+ });
914
+ }
915
+ }
916
+ }
917
+ }
918
+ return {
919
+ ExportAllDeclaration(node) {
920
+ checkImport(node, node.source.value);
921
+ },
922
+ ExportNamedDeclaration(node) {
923
+ if (node.source) {
924
+ checkImport(node, node.source.value);
925
+ }
926
+ },
927
+ ImportDeclaration(node) {
928
+ checkImport(node, node.source.value);
929
+ }
930
+ };
931
+ },
932
+ meta: {
933
+ docs: {
934
+ description: "Prevent direct import of Fragment internal files from outside the Fragment"
935
+ },
936
+ schema: [],
937
+ type: "problem"
938
+ }
939
+ };
940
+ var no_direct_fragment_import_rule_default = rule4;
941
+
942
+ // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
943
+ var import_path11 = __toESM(require("path"), 1);
944
+
945
+ // src/_rules/_rules@shared/_utils/_systems/get-private-category-owner/get-private-category-owner.util.ts
946
+ var import_path10 = __toESM(require("path"), 1);
947
+ function getPrivateCategoryOwner(relPath, config) {
948
+ let current = relPath;
949
+ while (current && current !== "." && current !== "/") {
950
+ const parent = import_path10.default.dirname(current).replace(/\\/g, "/");
951
+ if (parent === current) {
952
+ break;
953
+ }
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
+ current = parent;
960
+ }
961
+ return null;
962
+ }
963
+
964
+ // src/_rules/no-fractal-branch-leak/no-fractal-branch-leak.rule.ts
965
+ function getFractalBranchOwner(relPath) {
966
+ let current = relPath;
967
+ 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
+ const parent = import_path11.default.dirname(current).replace(/\\/g, "/");
973
+ if (parent === current) {
974
+ break;
975
+ }
976
+ current = parent;
977
+ }
978
+ return null;
979
+ }
980
+ var rule5 = {
981
+ create(context) {
982
+ const absPath = context.filename;
983
+ const relPath = toRelativePath(absPath);
984
+ const settings = context.settings;
985
+ if (!settings.faf) {
986
+ return {};
987
+ }
988
+ const config = findTreeConfig(relPath, settings.faf);
989
+ if (!config) {
990
+ return {};
991
+ }
992
+ function checkImport(node, importValue) {
993
+ const resolvedRelPath = resolveImportPath(
994
+ importValue,
995
+ relPath,
996
+ settings.faf
997
+ );
998
+ if (!resolvedRelPath) {
999
+ return;
1000
+ }
1001
+ const importedDir = import_path11.default.dirname(resolvedRelPath);
1002
+ const owner = getFractalBranchOwner(importedDir);
1003
+ if (owner) {
1004
+ const isDescendant = relPath === owner || relPath.startsWith(owner + "/");
1005
+ if (!isDescendant) {
1006
+ context.report({
1007
+ message: `Importing from Fractal Branch is forbidden. The imported resource is private to "${owner}" and its descendants.`,
1008
+ node
1009
+ });
1010
+ } else {
1011
+ const privateOwner = getPrivateCategoryOwner(importedDir, config);
1012
+ if (privateOwner) {
1013
+ const isPrivateDescendant = relPath === privateOwner || relPath.startsWith(privateOwner + "/");
1014
+ if (!isPrivateDescendant) {
1015
+ context.report({
1016
+ message: `Importing from Private Category is forbidden. The imported resource is private to "${privateOwner}".`,
1017
+ node
1018
+ });
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ return {
1025
+ ExportAllDeclaration(node) {
1026
+ checkImport(node, node.source.value);
1027
+ },
1028
+ ExportNamedDeclaration(node) {
1029
+ if (node.source) {
1030
+ checkImport(node, node.source.value);
1031
+ }
1032
+ },
1033
+ ImportDeclaration(node) {
1034
+ checkImport(node, node.source.value);
1035
+ }
1036
+ };
1037
+ },
1038
+ meta: {
1039
+ docs: {
1040
+ description: "Prevent leakage of Fractal Branch internals outside their owner scope"
1041
+ },
1042
+ schema: [],
1043
+ type: "problem"
1044
+ }
1045
+ };
1046
+ var no_fractal_branch_leak_rule_default = rule5;
1047
+
1048
+ // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1049
+ var import_path12 = __toESM(require("path"), 1);
1050
+
1051
+ // src/_rules/_rules@shared/_utils/_primitives/find-tree-config-including-excluded/find-tree-config-including-excluded.util.ts
1052
+ function findTreeConfigIncludingExcluded(relPath, settings) {
1053
+ if (!settings || !settings.trees) {
1054
+ return null;
1055
+ }
1056
+ const cached = state.treeConfigIncludingExcludedCache.get(relPath);
1057
+ if (cached !== void 0) {
1058
+ return cached;
1059
+ }
1060
+ let result = null;
1061
+ for (const tree of settings.trees) {
1062
+ const isIncluded = tree.includes.some(
1063
+ (inc) => relPath === inc || relPath.startsWith(inc + "/")
1064
+ );
1065
+ if (isIncluded) {
1066
+ result = tree;
1067
+ break;
1068
+ }
1069
+ }
1070
+ state.treeConfigIncludingExcludedCache.set(relPath, result);
1071
+ return result;
1072
+ }
1073
+
1074
+ // src/_rules/_rules@shared/_utils/_primitives/get-lca-and-sub-branches/get-lca-and-sub-branches.util.ts
1075
+ function getLcaAndSubBranches(pathA, pathB) {
1076
+ const partsA = pathA.split("/");
1077
+ const partsB = pathB.split("/");
1078
+ let commonLength = 0;
1079
+ while (commonLength < partsA.length && commonLength < partsB.length && partsA[commonLength] === partsB[commonLength]) {
1080
+ commonLength++;
1081
+ }
1082
+ if (commonLength === 0) {
1083
+ return null;
1084
+ }
1085
+ const lca = partsA.slice(0, commonLength).join("/");
1086
+ const subA = partsA[commonLength] || "";
1087
+ const subB = partsB[commonLength] || "";
1088
+ return { lca, subA, subB };
1089
+ }
1090
+
1091
+ // src/_rules/_rules@shared/_utils/_primitives/get-role-hierarchy-index/get-role-hierarchy-index.util.ts
1092
+ function getRoleHierarchyIndex(role, config) {
1093
+ for (let i = 0; i < config.roles.length; i++) {
1094
+ const roleGroup = config.roles[i];
1095
+ if (roleGroup && roleGroup.includes(role)) {
1096
+ return i;
1097
+ }
1098
+ }
1099
+ return -1;
1100
+ }
1101
+
1102
+ // src/_rules/_rules@shared/_utils/_primitives/resolve-horizontal-hierarchy/resolve-horizontal-hierarchy.util.ts
1103
+ function resolveHorizontalHierarchy(lcaPath, subA, subB, config) {
1104
+ const local = config.localHorizontalHierarchies?.find(
1105
+ (lh) => lh.paths.includes(lcaPath)
1106
+ );
1107
+ if (local) {
1108
+ const idxA = local.hierarchies.findIndex((group) => group.includes(subA));
1109
+ const idxB = local.hierarchies.findIndex((group) => group.includes(subB));
1110
+ if (idxA !== -1 && idxB !== -1) {
1111
+ return { allowed: idxB < idxA, defined: true };
1112
+ }
1113
+ }
1114
+ const globalHierarchies = config.globalHorizontalHierarchies || [];
1115
+ for (const gh of globalHierarchies) {
1116
+ const idxA = gh.findIndex((group) => group.includes(subA));
1117
+ const idxB = gh.findIndex((group) => group.includes(subB));
1118
+ if (idxA !== -1 && idxB !== -1) {
1119
+ return { allowed: idxB < idxA, defined: true };
1120
+ }
1121
+ }
1122
+ return { allowed: false, defined: false };
1123
+ }
1124
+
1125
+ // src/_rules/no-peer-dependency/no-peer-dependency.rule.ts
1126
+ function getDirectoryRole(dirPath, config) {
1127
+ const folderName = import_path12.default.basename(dirPath);
1128
+ if (folderName.startsWith("_")) {
1129
+ return getRoleFromFolderName(folderName, config);
1130
+ }
1131
+ const contents = readDirCached(dirPath);
1132
+ for (const file of contents.files) {
1133
+ if (file.startsWith(folderName + ".")) {
1134
+ const role = getFileRole(file, config);
1135
+ if (role) return role;
1136
+ }
1137
+ }
1138
+ return null;
1139
+ }
1140
+ function getRoleFromFolderName(name, config) {
1141
+ const catConfig = config.categories?.find((c) => c.name === name);
1142
+ return catConfig ? catConfig.role ?? null : null;
1143
+ }
1144
+ function getRootNodeIndex(relPath, rfConfig, lcaPath) {
1145
+ const relToLca = import_path12.default.relative(lcaPath, relPath).replace(/\\/g, "/");
1146
+ for (let i = 0; i < rfConfig.rootNodes.length; i++) {
1147
+ const group = rfConfig.rootNodes[i];
1148
+ if (group && group.includes(relToLca)) {
1149
+ return i;
1150
+ }
1151
+ }
1152
+ return -1;
1153
+ }
1154
+ function isInsidePrivateCategoryOfLca(relPathB, lcaPath, config) {
1155
+ let current = relPathB;
1156
+ while (current && current !== "." && current !== "/" && current !== lcaPath) {
1157
+ const parent = import_path12.default.dirname(current).replace(/\\/g, "/");
1158
+ if (parent === current) {
1159
+ break;
1160
+ }
1161
+ const currentType = classifyFolder(current, config);
1162
+ const parentType = classifyFolder(parent, config);
1163
+ if (currentType === "category" && (parentType === "fragment" || parentType === "root-fragment") && parent === lcaPath) {
1164
+ return true;
1165
+ }
1166
+ current = parent;
1167
+ }
1168
+ return false;
1169
+ }
1170
+ var rule6 = {
1171
+ create(context) {
1172
+ const absPath = context.filename;
1173
+ const relPath = toRelativePath(absPath);
1174
+ const settings = context.settings;
1175
+ if (!settings.faf) {
1176
+ return {};
1177
+ }
1178
+ const config = findTreeConfigIncludingExcluded(relPath, settings.faf);
1179
+ if (!config) {
1180
+ return {};
1181
+ }
1182
+ const currentDir = import_path12.default.dirname(relPath);
1183
+ const isCurrentFileExcluded = config.excludes?.some(
1184
+ (exc) => relPath === exc || relPath.startsWith(exc + "/")
1185
+ ) ?? false;
1186
+ function checkImport(node, importValue) {
1187
+ const resolvedRelPath = resolveImportPath(
1188
+ importValue,
1189
+ relPath,
1190
+ settings.faf
1191
+ );
1192
+ if (!resolvedRelPath) {
1193
+ return;
1194
+ }
1195
+ const isImportedFileExcluded = config?.excludes?.some(
1196
+ (exc) => resolvedRelPath === exc || resolvedRelPath.startsWith(exc + "/")
1197
+ ) ?? false;
1198
+ if (!isCurrentFileExcluded && isImportedFileExcluded) {
1199
+ context.report({
1200
+ message: `Importing from Foreign Domain "${resolvedRelPath}" is forbidden for Logical Nodes.`,
1201
+ node
1202
+ });
1203
+ return;
1204
+ }
1205
+ if (isCurrentFileExcluded && !isImportedFileExcluded) {
1206
+ context.report({
1207
+ message: `Foreign Domain files cannot import Logical Nodes from FAF tree ("${resolvedRelPath}").`,
1208
+ node
1209
+ });
1210
+ return;
1211
+ }
1212
+ if (isCurrentFileExcluded || isImportedFileExcluded) {
1213
+ return;
1214
+ }
1215
+ const importedDir = import_path12.default.dirname(resolvedRelPath);
1216
+ if (currentDir === importedDir) {
1217
+ const fileA = import_path12.default.basename(relPath);
1218
+ const fileB = import_path12.default.basename(resolvedRelPath);
1219
+ const roleA = getFileRole(fileA, config);
1220
+ const roleB = getFileRole(fileB, config);
1221
+ if (roleA && roleB) {
1222
+ const idxA = getRoleHierarchyIndex(roleA, config);
1223
+ const idxB = getRoleHierarchyIndex(roleB, config);
1224
+ if (idxA !== -1 && idxB !== -1) {
1225
+ if (idxB >= idxA) {
1226
+ context.report({
1227
+ message: `Sibling import violation: "${fileA}" (role "${roleA}", level ${idxA}) cannot import from "${fileB}" (role "${roleB}", level ${idxB}). Imports must flow from lower to higher levels.`,
1228
+ node
1229
+ });
1230
+ }
1231
+ }
1232
+ }
1233
+ return;
1234
+ }
1235
+ const lcaInfo = getLcaAndSubBranches(currentDir, importedDir);
1236
+ if (!lcaInfo) {
1237
+ return;
1238
+ }
1239
+ const { lca, subA, subB } = lcaInfo;
1240
+ if (subB.startsWith("_") && subB.includes("@shared")) {
1241
+ return;
1242
+ }
1243
+ if (isInsidePrivateCategoryOfLca(importedDir, lca, config)) {
1244
+ return;
1245
+ }
1246
+ if (subB === "" && classifyFolder(lca, config) === "fragment") {
1247
+ return;
1248
+ }
1249
+ const rfConfig = getRootFragmentConfig(lca, config);
1250
+ if (rfConfig) {
1251
+ const idxRootA = getRootNodeIndex(relPath, rfConfig, lca);
1252
+ const idxRootB = getRootNodeIndex(resolvedRelPath, rfConfig, lca);
1253
+ if (idxRootA !== -1 && idxRootB !== -1) {
1254
+ if (idxRootB >= idxRootA) {
1255
+ context.report({
1256
+ message: `Root Node import violation: "${import_path12.default.basename(relPath)}" cannot import from "${import_path12.default.basename(resolvedRelPath)}" under Root Fragment "${lca}".`,
1257
+ node
1258
+ });
1259
+ }
1260
+ return;
1261
+ }
1262
+ }
1263
+ const { allowed, defined: hasDefinedHierarchy } = resolveHorizontalHierarchy(lca, subA, subB, config);
1264
+ if (hasDefinedHierarchy) {
1265
+ if (!allowed) {
1266
+ context.report({
1267
+ message: `Horizontal hierarchy violation: "${subA}" cannot import from "${subB}" under parent "${lca}".`,
1268
+ node
1269
+ });
1270
+ }
1271
+ } else {
1272
+ const roleA = getDirectoryRole(import_path12.default.posix.join(lca, subA), config);
1273
+ const roleB = getDirectoryRole(import_path12.default.posix.join(lca, subB), config);
1274
+ const idxA = roleA ? getRoleHierarchyIndex(roleA, config) : -1;
1275
+ const idxB = roleB ? getRoleHierarchyIndex(roleB, config) : -1;
1276
+ if (idxA !== -1 && idxB !== -1) {
1277
+ if (idxB >= idxA) {
1278
+ context.report({
1279
+ message: `Horizontal hierarchy violation (fallback to roles): sibling directory "${subA}" (mapped to role "${roleA}", level ${idxA}) cannot import from "${subB}" (mapped to role "${roleB}", level ${idxB}) under parent "${lca}".`,
1280
+ node
1281
+ });
1282
+ }
1283
+ } else {
1284
+ context.report({
1285
+ message: `Peer separation violation: sibling directories "${subA}" and "${subB}" cannot import each other because no horizontal hierarchy is defined under parent "${lca}".`,
1286
+ node
1287
+ });
1288
+ }
1289
+ }
1290
+ }
1291
+ return {
1292
+ ExportAllDeclaration(node) {
1293
+ checkImport(node, node.source.value);
1294
+ },
1295
+ ExportNamedDeclaration(node) {
1296
+ if (node.source) {
1297
+ checkImport(node, node.source.value);
1298
+ }
1299
+ },
1300
+ ImportDeclaration(node) {
1301
+ checkImport(node, node.source.value);
1302
+ }
1303
+ };
1304
+ },
1305
+ meta: {
1306
+ docs: {
1307
+ description: "Enforce the Law of Separation between Peers (no-peer-dependency)"
1308
+ },
1309
+ schema: [],
1310
+ type: "problem"
1311
+ }
1312
+ };
1313
+ var no_peer_dependency_rule_default = rule6;
1314
+
1315
+ // src/_rules/no-private-category-leak/no-private-category-leak.rule.ts
1316
+ var import_path13 = __toESM(require("path"), 1);
1317
+ var rule7 = {
1318
+ create(context) {
1319
+ const absPath = context.filename;
1320
+ const relPath = toRelativePath(absPath);
1321
+ const settings = context.settings;
1322
+ if (!settings.faf) {
1323
+ return {};
1324
+ }
1325
+ const config = findTreeConfig(relPath, settings.faf);
1326
+ if (!config) {
1327
+ return {};
1328
+ }
1329
+ function checkImport(node, importValue) {
1330
+ const resolvedRelPath = resolveImportPath(
1331
+ importValue,
1332
+ relPath,
1333
+ settings.faf
1334
+ );
1335
+ if (!resolvedRelPath) {
1336
+ return;
1337
+ }
1338
+ const importedDir = import_path13.default.dirname(resolvedRelPath);
1339
+ const owner = getPrivateCategoryOwner(importedDir, config);
1340
+ if (owner) {
1341
+ const isDescendant = relPath === owner || relPath.startsWith(owner + "/");
1342
+ if (!isDescendant) {
1343
+ context.report({
1344
+ message: `Importing from Private Category is forbidden. The imported resource is private to "${owner}".`,
1345
+ node
1346
+ });
1347
+ }
1348
+ }
1349
+ }
1350
+ return {
1351
+ ExportAllDeclaration(node) {
1352
+ checkImport(node, node.source.value);
1353
+ },
1354
+ ExportNamedDeclaration(node) {
1355
+ if (node.source) {
1356
+ checkImport(node, node.source.value);
1357
+ }
1358
+ },
1359
+ ImportDeclaration(node) {
1360
+ checkImport(node, node.source.value);
1361
+ }
1362
+ };
1363
+ },
1364
+ meta: {
1365
+ docs: {
1366
+ description: "Prevent leakage and unauthorized imports of Private Category internals"
1367
+ },
1368
+ schema: [],
1369
+ type: "problem"
1370
+ }
1371
+ };
1372
+ var no_private_category_leak_rule_default = rule7;
1373
+
1374
+ // src/rules.ts
1375
+ var rules = {
1376
+ "category-mutually-exclusive": category_mutually_exclusive_rule_default,
1377
+ "enforce-access-node": enforce_access_node_rule_default,
1378
+ "naming-conventions": naming_conventions_rule_default,
1379
+ "no-direct-fragment-import": no_direct_fragment_import_rule_default,
1380
+ "no-fractal-branch-leak": no_fractal_branch_leak_rule_default,
1381
+ "no-peer-dependency": no_peer_dependency_rule_default,
1382
+ "no-private-category-leak": no_private_category_leak_rule_default
1383
+ };
1384
+
1385
+ // src/configs.ts
1386
+ var configs = {
1387
+ recommended: {
1388
+ plugins: {
1389
+ faf: {
1390
+ rules
1391
+ }
1392
+ },
1393
+ rules: {
1394
+ "faf/category-mutually-exclusive": "error",
1395
+ "faf/enforce-access-node": "error",
1396
+ "faf/naming-conventions": "error",
1397
+ "faf/no-direct-fragment-import": "error",
1398
+ "faf/no-fractal-branch-leak": "error",
1399
+ "faf/no-peer-dependency": "error",
1400
+ "faf/no-private-category-leak": "error"
1401
+ }
1402
+ }
1403
+ };
1404
+
1405
+ // src/main.ts
1406
+ var plugin = {
1407
+ configs,
1408
+ meta: {
1409
+ name: "@morriconeluca/eslint-plugin-faf",
1410
+ version: package_default.version
1411
+ },
1412
+ rules
1413
+ };
1414
+ var main_default = plugin;
1415
+ // Annotate the CommonJS export names for ESM import in node:
1416
+ 0 && (module.exports = {
1417
+ configs,
1418
+ rules
1419
+ });
1420
+ //# sourceMappingURL=main.cjs.map