eslint-plugin-barrel-rules 1.4.2 → 1.4.4
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.ko.md +1 -1
- package/README.md +1 -1
- package/dist/index.cjs +374 -82
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +374 -82
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# **Advanced Barrel Pattern Enforcement for JavaScript/TypeScript Projects**
|
|
4
4
|
|
|
5
5
|
<div align="center">
|
|
6
|
-
<img src="https://img.shields.io/badge/version-1.4.
|
|
6
|
+
<img src="https://img.shields.io/badge/version-1.4.4-blue.svg" alt="Version"/>
|
|
7
7
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License"/>
|
|
8
8
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"/>
|
|
9
9
|
</div>
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# **Advanced Barrel Pattern Enforcement for JavaScript/TypeScript Projects**
|
|
4
4
|
|
|
5
5
|
<div align="center">
|
|
6
|
-
<img src="https://img.shields.io/badge/version-1.4.
|
|
6
|
+
<img src="https://img.shields.io/badge/version-1.4.4-blue.svg" alt="Version"/>
|
|
7
7
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License"/>
|
|
8
8
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"/>
|
|
9
9
|
</div>
|
package/dist/index.cjs
CHANGED
|
@@ -42,8 +42,8 @@ var import_resolve = __toESM(require("resolve"), 1);
|
|
|
42
42
|
// src/utils/glob.ts
|
|
43
43
|
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
44
44
|
var Glob = class {
|
|
45
|
-
static resolvePath(
|
|
46
|
-
const globResult = import_fast_glob.default.sync(
|
|
45
|
+
static resolvePath(path5, baseDir) {
|
|
46
|
+
const globResult = import_fast_glob.default.sync(path5, {
|
|
47
47
|
cwd: baseDir,
|
|
48
48
|
onlyDirectories: true,
|
|
49
49
|
absolute: true
|
|
@@ -55,55 +55,125 @@ var Glob = class {
|
|
|
55
55
|
// src/utils/alias.ts
|
|
56
56
|
var import_tsconfig_paths = require("tsconfig-paths");
|
|
57
57
|
var import_path = __toESM(require("path"), 1);
|
|
58
|
-
var Alias = class {
|
|
58
|
+
var Alias = class _Alias {
|
|
59
59
|
constructor() {
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolves an alias path to an absolute file path
|
|
63
|
+
*
|
|
64
|
+
* @param rawPath - The alias path to resolve (e.g., "@entities/user", "@utils")
|
|
65
|
+
* @param currentFileDir - The directory of the current file (used to find tsconfig.json)
|
|
66
|
+
* @returns Result object with absolutePath and type ("success" or "fail")
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // With wildcard alias: "@entities/*" -> "src/entities/*"
|
|
70
|
+
* Alias.resolvePath("@entities/user", "/project/src/features")
|
|
71
|
+
* // Returns: { absolutePath: "/project/src/entities/user", type: "success" }
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Without wildcard: "@utils" -> "src/utils/index"
|
|
75
|
+
* Alias.resolvePath("@utils", "/project/src/features")
|
|
76
|
+
* // Returns: { absolutePath: "/project/src/utils/index", type: "success" }
|
|
77
|
+
*/
|
|
61
78
|
static resolvePath(rawPath, currentFileDir) {
|
|
62
79
|
try {
|
|
63
80
|
const configResult = (0, import_tsconfig_paths.loadConfig)(currentFileDir);
|
|
64
|
-
if (configResult.resultType
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
absolutePath,
|
|
80
|
-
type: "success"
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
if (rawPath === pattern) {
|
|
85
|
-
const absolutePath = import_path.default.resolve(
|
|
86
|
-
`${configResult.absoluteBaseUrl}/${origin}`
|
|
87
|
-
);
|
|
88
|
-
return {
|
|
89
|
-
absolutePath,
|
|
90
|
-
type: "success"
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
81
|
+
if (configResult.resultType !== "success") {
|
|
82
|
+
return this.createFailResult(rawPath);
|
|
83
|
+
}
|
|
84
|
+
const { paths, absoluteBaseUrl } = configResult;
|
|
85
|
+
for (const [aliasPattern, targetPaths] of Object.entries(paths)) {
|
|
86
|
+
const targetPath = targetPaths[0];
|
|
87
|
+
const matchResult = this.tryMatchPattern(
|
|
88
|
+
rawPath,
|
|
89
|
+
aliasPattern,
|
|
90
|
+
targetPath,
|
|
91
|
+
absoluteBaseUrl
|
|
92
|
+
);
|
|
93
|
+
if (matchResult) {
|
|
94
|
+
return matchResult;
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
return _Alias.createFailResult(rawPath);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return _Alias.createFailResult(rawPath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Attempts to match a raw path against an alias pattern
|
|
104
|
+
*
|
|
105
|
+
* @param rawPath - The path to match (e.g., "@entities/user")
|
|
106
|
+
* @param aliasPattern - The alias pattern from tsconfig (e.g., "@entities/*")
|
|
107
|
+
* @param targetPath - The target path from tsconfig (e.g., "src/entities/*")
|
|
108
|
+
* @param baseUrl - The absolute base URL from tsconfig
|
|
109
|
+
* @returns Success result if matched, null otherwise
|
|
110
|
+
*/
|
|
111
|
+
static tryMatchPattern(rawPath, aliasPattern, targetPath, baseUrl) {
|
|
112
|
+
const hasWildcard = aliasPattern.includes("*");
|
|
113
|
+
if (hasWildcard) {
|
|
114
|
+
return this.matchWildcardPattern(
|
|
115
|
+
rawPath,
|
|
116
|
+
aliasPattern,
|
|
117
|
+
targetPath,
|
|
118
|
+
baseUrl
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
return this.matchExactPattern(rawPath, aliasPattern, targetPath, baseUrl);
|
|
105
122
|
}
|
|
106
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Matches a wildcard alias pattern (e.g., "@entities/*")
|
|
126
|
+
*
|
|
127
|
+
* Pattern: "@entities/*" -> Target: "src/entities/*"
|
|
128
|
+
* Input: "@entities/user" -> Matches: "user" -> Output: "src/entities/user"
|
|
129
|
+
*
|
|
130
|
+
* Note: The original implementation uses simple string replacement for regex,
|
|
131
|
+
* which works because alias patterns typically don't contain special regex chars.
|
|
132
|
+
* We maintain this behavior for compatibility.
|
|
133
|
+
*/
|
|
134
|
+
static matchWildcardPattern(rawPath, aliasPattern, targetPath, baseUrl) {
|
|
135
|
+
const regexPattern = `^${aliasPattern.replace(/\*/g, "(.*)")}$`;
|
|
136
|
+
const regex = new RegExp(regexPattern);
|
|
137
|
+
const match = rawPath.match(regex);
|
|
138
|
+
if (!match) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const capturedPath = match[1];
|
|
142
|
+
const resolvedTargetPath = targetPath.replace(/\*/g, capturedPath);
|
|
143
|
+
const absolutePath = import_path.default.resolve(`${baseUrl}/${resolvedTargetPath}`);
|
|
144
|
+
return _Alias.createSuccessResult(absolutePath);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Matches an exact alias pattern (no wildcard)
|
|
148
|
+
*
|
|
149
|
+
* Pattern: "@utils" -> Target: "src/utils/index"
|
|
150
|
+
* Input: "@utils" -> Output: "src/utils/index"
|
|
151
|
+
*/
|
|
152
|
+
static matchExactPattern(rawPath, aliasPattern, targetPath, baseUrl) {
|
|
153
|
+
if (rawPath !== aliasPattern) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const absolutePath = import_path.default.resolve(`${baseUrl}/${targetPath}`);
|
|
157
|
+
return _Alias.createSuccessResult(absolutePath);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Creates a success result
|
|
161
|
+
*/
|
|
162
|
+
static createSuccessResult(absolutePath) {
|
|
163
|
+
return {
|
|
164
|
+
absolutePath,
|
|
165
|
+
type: "success"
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Creates a fail result
|
|
170
|
+
*/
|
|
171
|
+
static createFailResult(originalPath) {
|
|
172
|
+
return {
|
|
173
|
+
absolutePath: originalPath,
|
|
174
|
+
type: "fail"
|
|
175
|
+
};
|
|
176
|
+
}
|
|
107
177
|
};
|
|
108
178
|
|
|
109
179
|
// src/utils/constants.ts
|
|
@@ -229,16 +299,16 @@ var enforceBarrelPattern = {
|
|
|
229
299
|
if (!rawImportPath.startsWith(".") && !rawImportPath.startsWith("/") || rawImportPath.includes("/node_modules/")) {
|
|
230
300
|
return;
|
|
231
301
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
302
|
+
try {
|
|
303
|
+
absoluteImportPath = import_resolve.default.sync(rawImportPath, {
|
|
304
|
+
basedir: import_path2.default.dirname(absoluteCurrentFilePath),
|
|
305
|
+
extensions: RESOLVE_EXTENSIONS
|
|
306
|
+
});
|
|
307
|
+
} catch (e) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
236
310
|
}
|
|
237
311
|
} catch (e) {
|
|
238
|
-
context.report({
|
|
239
|
-
node,
|
|
240
|
-
messageId: "TransformedAliasResolveFailed"
|
|
241
|
-
});
|
|
242
312
|
return;
|
|
243
313
|
}
|
|
244
314
|
{
|
|
@@ -252,8 +322,8 @@ var enforceBarrelPattern = {
|
|
|
252
322
|
const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
|
|
253
323
|
const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
|
|
254
324
|
const currentFileInEnforceBarrel = absoluteCurrentFilePath.startsWith(closedTargetPath);
|
|
255
|
-
const
|
|
256
|
-
const invalidImported = !targetPathEntryPointed &&
|
|
325
|
+
const importedOutsideOfBarrel = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
|
|
326
|
+
const invalidImported = !targetPathEntryPointed && importedOutsideOfBarrel;
|
|
257
327
|
if (invalidImported) {
|
|
258
328
|
matchedLatestTargetPath = absoluteTargetPath;
|
|
259
329
|
}
|
|
@@ -325,13 +395,13 @@ var isolateBarrelFile = {
|
|
|
325
395
|
create(context) {
|
|
326
396
|
const option = context.options[0];
|
|
327
397
|
const baseDir = option.baseDir;
|
|
328
|
-
const absoluteGlobalAllowPaths = option.globalAllowPaths.flatMap((
|
|
329
|
-
return Glob.resolvePath(
|
|
398
|
+
const absoluteGlobalAllowPaths = option.globalAllowPaths.flatMap((path5) => {
|
|
399
|
+
return Glob.resolvePath(path5, baseDir);
|
|
330
400
|
});
|
|
331
401
|
const absoluteIsolations = option.isolations.flatMap((isolation) => {
|
|
332
402
|
const isolationPaths = Glob.resolvePath(isolation.path, baseDir);
|
|
333
|
-
const allowedPaths = isolation.allowedPaths.flatMap((
|
|
334
|
-
return Glob.resolvePath(
|
|
403
|
+
const allowedPaths = isolation.allowedPaths.flatMap((path5) => {
|
|
404
|
+
return Glob.resolvePath(path5, baseDir);
|
|
335
405
|
});
|
|
336
406
|
return isolationPaths.map((isolationPath) => ({
|
|
337
407
|
isolationPath,
|
|
@@ -359,7 +429,7 @@ var isolateBarrelFile = {
|
|
|
359
429
|
return;
|
|
360
430
|
}
|
|
361
431
|
} else {
|
|
362
|
-
if (!rawImportPath.startsWith(".") && !rawImportPath.startsWith("/")) {
|
|
432
|
+
if (!rawImportPath.startsWith(".") && !rawImportPath.startsWith("/") || rawImportPath.includes("/node_modules/")) {
|
|
363
433
|
return;
|
|
364
434
|
}
|
|
365
435
|
absoluteImportPath = import_resolve2.default.sync(rawImportPath, {
|
|
@@ -374,31 +444,26 @@ var isolateBarrelFile = {
|
|
|
374
444
|
});
|
|
375
445
|
return;
|
|
376
446
|
}
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
});
|
|
382
|
-
const matchedIsolation = absoluteIsolations[isolationIndex];
|
|
383
|
-
if (!matchedIsolation) return;
|
|
384
|
-
const isAllowedImport = matchedIsolation.allowedPaths.some(
|
|
385
|
-
(allowedPath) => {
|
|
386
|
-
const same = absoluteImportPath === allowedPath;
|
|
387
|
-
const closedAllowedPath = allowedPath + "/";
|
|
388
|
-
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
389
|
-
return same || sub;
|
|
390
|
-
}
|
|
391
|
-
);
|
|
392
|
-
const isGlobalAllowedImport = absoluteGlobalAllowPaths.some(
|
|
393
|
-
(allowedPath) => {
|
|
394
|
-
const same = absoluteImportPath === allowedPath;
|
|
395
|
-
const closedAllowedPath = allowedPath + "/";
|
|
396
|
-
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
397
|
-
return same || sub;
|
|
447
|
+
const matchedIsolationIndex = absoluteIsolations.findIndex(
|
|
448
|
+
(isolation) => {
|
|
449
|
+
const closedIsolationPath = isolation.isolationPath + "/";
|
|
450
|
+
return absoluteCurrentFilePath.startsWith(closedIsolationPath);
|
|
398
451
|
}
|
|
399
452
|
);
|
|
400
|
-
const
|
|
401
|
-
if (!
|
|
453
|
+
const matchedIsolation = absoluteIsolations[matchedIsolationIndex];
|
|
454
|
+
if (!matchedIsolation) return;
|
|
455
|
+
const isAllowedImport = [
|
|
456
|
+
//global allowed import paths
|
|
457
|
+
...absoluteGlobalAllowPaths,
|
|
458
|
+
//allowed import paths in the matched isolation
|
|
459
|
+
...matchedIsolation.allowedPaths
|
|
460
|
+
].some((allowedPath) => {
|
|
461
|
+
const same = absoluteImportPath === allowedPath;
|
|
462
|
+
const closedAllowedPath = allowedPath + "/";
|
|
463
|
+
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
464
|
+
return same || sub;
|
|
465
|
+
});
|
|
466
|
+
if (!isAllowedImport) {
|
|
402
467
|
context.report({
|
|
403
468
|
node,
|
|
404
469
|
messageId: "IsolatedBarrelImportDisallowed"
|
|
@@ -447,12 +512,239 @@ var noWildcard = {
|
|
|
447
512
|
}
|
|
448
513
|
};
|
|
449
514
|
|
|
515
|
+
// src/rules/no-cycle.ts
|
|
516
|
+
var import_types4 = require("@typescript-eslint/types");
|
|
517
|
+
var import_path4 = __toESM(require("path"), 1);
|
|
518
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
519
|
+
var import_resolve3 = __toESM(require("resolve"), 1);
|
|
520
|
+
var importGraph = /* @__PURE__ */ new Map();
|
|
521
|
+
var barrelExportsCache = /* @__PURE__ */ new Map();
|
|
522
|
+
function isBarrelFile(filePath) {
|
|
523
|
+
const fileName = import_path4.default.basename(filePath);
|
|
524
|
+
return BARREL_ENTRY_POINT_FILE_NAMES.includes(
|
|
525
|
+
fileName
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
function isExternalImport(rawPath) {
|
|
529
|
+
if (!rawPath.startsWith(".") && !rawPath.startsWith("/")) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
if (rawPath.includes("/node_modules/")) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
function resolveImportPath(rawImportPath, currentFileDir) {
|
|
538
|
+
try {
|
|
539
|
+
const aliasResult = Alias.resolvePath(rawImportPath, currentFileDir);
|
|
540
|
+
if (aliasResult.type === "success") {
|
|
541
|
+
return resolveAliasPath(aliasResult.absolutePath, currentFileDir);
|
|
542
|
+
}
|
|
543
|
+
if (isExternalImport(rawImportPath)) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
return import_resolve3.default.sync(rawImportPath, {
|
|
547
|
+
basedir: currentFileDir,
|
|
548
|
+
extensions: RESOLVE_EXTENSIONS
|
|
549
|
+
});
|
|
550
|
+
} catch (e) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function resolveAliasPath(resolvedPath, currentFileDir) {
|
|
555
|
+
try {
|
|
556
|
+
const stats = import_fs.default.statSync(resolvedPath);
|
|
557
|
+
if (stats.isDirectory()) {
|
|
558
|
+
return import_resolve3.default.sync("index", {
|
|
559
|
+
basedir: resolvedPath,
|
|
560
|
+
extensions: RESOLVE_EXTENSIONS
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return resolvedPath;
|
|
564
|
+
} catch (e) {
|
|
565
|
+
try {
|
|
566
|
+
return import_resolve3.default.sync(resolvedPath, {
|
|
567
|
+
basedir: currentFileDir,
|
|
568
|
+
extensions: RESOLVE_EXTENSIONS
|
|
569
|
+
});
|
|
570
|
+
} catch (e2) {
|
|
571
|
+
return resolvedPath;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function getBarrelExports(barrelFileDir, ast) {
|
|
576
|
+
const exports2 = [];
|
|
577
|
+
for (const statement of ast.body) {
|
|
578
|
+
if ((statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") && statement.source) {
|
|
579
|
+
const exportPath = statement.source.value;
|
|
580
|
+
const resolved = tryResolve(exportPath, barrelFileDir);
|
|
581
|
+
if (resolved && !exports2.includes(resolved)) {
|
|
582
|
+
exports2.push(resolved);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (statement.type === "ImportDeclaration" && statement.source) {
|
|
586
|
+
const importPath = statement.source.value;
|
|
587
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
588
|
+
const resolved = tryResolve(importPath, barrelFileDir);
|
|
589
|
+
if (resolved && !exports2.includes(resolved)) {
|
|
590
|
+
exports2.push(resolved);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return exports2;
|
|
596
|
+
}
|
|
597
|
+
function tryResolve(importPath, basedir) {
|
|
598
|
+
try {
|
|
599
|
+
return import_resolve3.default.sync(importPath, {
|
|
600
|
+
basedir,
|
|
601
|
+
extensions: RESOLVE_EXTENSIONS
|
|
602
|
+
});
|
|
603
|
+
} catch (e) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function detectCycle(startFile, visited, recStack, currentPath) {
|
|
608
|
+
visited.add(startFile);
|
|
609
|
+
recStack.add(startFile);
|
|
610
|
+
currentPath.push(startFile);
|
|
611
|
+
const imports = importGraph.get(startFile) || /* @__PURE__ */ new Set();
|
|
612
|
+
for (const importedFile of imports) {
|
|
613
|
+
if (!visited.has(importedFile)) {
|
|
614
|
+
const cycle = detectCycle(importedFile, visited, recStack, [
|
|
615
|
+
...currentPath
|
|
616
|
+
]);
|
|
617
|
+
if (cycle) return cycle;
|
|
618
|
+
} else if (recStack.has(importedFile)) {
|
|
619
|
+
const cycleStart = currentPath.indexOf(importedFile);
|
|
620
|
+
return [...currentPath.slice(cycleStart), importedFile];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
recStack.delete(startFile);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
function hasBidirectionalCycle(fileA, fileB) {
|
|
627
|
+
var _a;
|
|
628
|
+
const bImports = importGraph.get(fileB);
|
|
629
|
+
return (_a = bImports == null ? void 0 : bImports.has(fileA)) != null ? _a : false;
|
|
630
|
+
}
|
|
631
|
+
function hasCycleThroughBarrel(currentFile, exportedModules) {
|
|
632
|
+
for (const exportedModule of exportedModules) {
|
|
633
|
+
if (exportedModule === currentFile) continue;
|
|
634
|
+
const moduleImports = importGraph.get(exportedModule);
|
|
635
|
+
if (moduleImports == null ? void 0 : moduleImports.has(currentFile)) {
|
|
636
|
+
return exportedModule;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
var noCycle = {
|
|
642
|
+
meta: {
|
|
643
|
+
type: "problem",
|
|
644
|
+
docs: {
|
|
645
|
+
description: "Detect circular dependencies and enforce relative imports in barrel files."
|
|
646
|
+
},
|
|
647
|
+
schema: [],
|
|
648
|
+
messages: {
|
|
649
|
+
CircularDependency: "Circular dependency detected: {{cyclePath}}. This creates a dependency cycle that can cause runtime errors and make code harder to maintain.",
|
|
650
|
+
BarrelInternalImportDisallowed: "Barrel files (index.ts) must use relative imports (./ or ../) for internal modules. Importing via barrel file or absolute path is not allowed. Use relative path: '{{relativePath}}'",
|
|
651
|
+
TransformedAliasResolveFailed: "Transformed alias resolve failed. please check the alias config."
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
defaultOptions: [],
|
|
655
|
+
create(context) {
|
|
656
|
+
const currentFile = context.getFilename();
|
|
657
|
+
const currentDir = import_path4.default.dirname(currentFile);
|
|
658
|
+
if (!importGraph.has(currentFile)) {
|
|
659
|
+
importGraph.set(currentFile, /* @__PURE__ */ new Set());
|
|
660
|
+
}
|
|
661
|
+
importGraph.get(currentFile).clear();
|
|
662
|
+
if (isBarrelFile(currentFile)) {
|
|
663
|
+
const ast = context.getSourceCode().ast;
|
|
664
|
+
const exports2 = getBarrelExports(currentDir, ast);
|
|
665
|
+
barrelExportsCache.set(currentFile, exports2);
|
|
666
|
+
}
|
|
667
|
+
function checkImport(node) {
|
|
668
|
+
if (!node.source) return;
|
|
669
|
+
const rawPath = node.source.value;
|
|
670
|
+
const absolutePath = resolveImportPath(rawPath, currentDir);
|
|
671
|
+
if (!absolutePath) return;
|
|
672
|
+
if (isBarrelFile(currentFile)) {
|
|
673
|
+
const error = checkBarrelInternalImport(rawPath, absolutePath);
|
|
674
|
+
if (error) {
|
|
675
|
+
context.report({
|
|
676
|
+
node,
|
|
677
|
+
messageId: "BarrelInternalImportDisallowed",
|
|
678
|
+
data: { relativePath: error }
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const isBarrelInternal = isBarrelFile(currentFile) && absolutePath.startsWith(currentDir + import_path4.default.sep);
|
|
684
|
+
addToGraph(absolutePath);
|
|
685
|
+
if (isBarrelInternal) return;
|
|
686
|
+
const cycleError = checkForCycles(absolutePath);
|
|
687
|
+
if (cycleError) {
|
|
688
|
+
context.report({
|
|
689
|
+
node,
|
|
690
|
+
messageId: "CircularDependency",
|
|
691
|
+
data: { cyclePath: cycleError }
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function checkBarrelInternalImport(rawPath, absolutePath) {
|
|
696
|
+
const isInternal = absolutePath.startsWith(currentDir + import_path4.default.sep) || absolutePath === currentDir;
|
|
697
|
+
if (!isInternal) return null;
|
|
698
|
+
if (rawPath.startsWith("./") || rawPath.startsWith("../")) {
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
const relative = import_path4.default.relative(currentDir, absolutePath);
|
|
702
|
+
const suggested = relative.startsWith(".") ? relative : `./${relative}`;
|
|
703
|
+
return suggested.replace(/\\/g, "/");
|
|
704
|
+
}
|
|
705
|
+
function addToGraph(absolutePath) {
|
|
706
|
+
const imports = importGraph.get(currentFile);
|
|
707
|
+
imports.add(absolutePath);
|
|
708
|
+
if (isBarrelFile(absolutePath)) {
|
|
709
|
+
const exports2 = barrelExportsCache.get(absolutePath) || [];
|
|
710
|
+
for (const exp of exports2) {
|
|
711
|
+
if (exp !== currentFile) {
|
|
712
|
+
imports.add(exp);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function checkForCycles(absolutePath) {
|
|
718
|
+
if (isBarrelFile(absolutePath)) {
|
|
719
|
+
const exports2 = barrelExportsCache.get(absolutePath) || [];
|
|
720
|
+
const cycleModule = hasCycleThroughBarrel(currentFile, exports2);
|
|
721
|
+
if (cycleModule) {
|
|
722
|
+
return `${currentFile} \u2192 ${absolutePath} \u2192 ${cycleModule} \u2192 ${absolutePath} \u2192 ${currentFile}`;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (hasBidirectionalCycle(currentFile, absolutePath)) {
|
|
726
|
+
return `${currentFile} \u2192 ${absolutePath} \u2192 ${currentFile}`;
|
|
727
|
+
}
|
|
728
|
+
const cycle = detectCycle(currentFile, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), []);
|
|
729
|
+
if (cycle && cycle.length > 0) {
|
|
730
|
+
return cycle.join(" \u2192 ");
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
ImportDeclaration: checkImport,
|
|
736
|
+
ExportNamedDeclaration: checkImport,
|
|
737
|
+
ExportAllDeclaration: checkImport
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
450
742
|
// src/index.ts
|
|
451
743
|
var rules = {
|
|
452
744
|
"enforce-barrel-pattern": enforceBarrelPattern,
|
|
453
745
|
"isolate-barrel-file": isolateBarrelFile,
|
|
454
|
-
"no-wildcard": noWildcard
|
|
455
|
-
|
|
746
|
+
"no-wildcard": noWildcard,
|
|
747
|
+
"no-cycle": noCycle
|
|
456
748
|
};
|
|
457
749
|
// Annotate the CommonJS export names for ESM import in node:
|
|
458
750
|
0 && (module.exports = {
|
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,7 @@ declare const rules: {
|
|
|
14
14
|
globalAllowPaths: string[];
|
|
15
15
|
}[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
16
16
|
"no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
17
|
+
"no-cycle": _typescript_eslint_utils_ts_eslint.RuleModule<"TransformedAliasResolveFailed" | "CircularDependency" | "BarrelInternalImportDisallowed", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export { rules };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ declare const rules: {
|
|
|
14
14
|
globalAllowPaths: string[];
|
|
15
15
|
}[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
16
16
|
"no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
17
|
+
"no-cycle": _typescript_eslint_utils_ts_eslint.RuleModule<"TransformedAliasResolveFailed" | "CircularDependency" | "BarrelInternalImportDisallowed", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export { rules };
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import resolve from "resolve";
|
|
|
6
6
|
// src/utils/glob.ts
|
|
7
7
|
import FastGlob from "fast-glob";
|
|
8
8
|
var Glob = class {
|
|
9
|
-
static resolvePath(
|
|
10
|
-
const globResult = FastGlob.sync(
|
|
9
|
+
static resolvePath(path5, baseDir) {
|
|
10
|
+
const globResult = FastGlob.sync(path5, {
|
|
11
11
|
cwd: baseDir,
|
|
12
12
|
onlyDirectories: true,
|
|
13
13
|
absolute: true
|
|
@@ -19,55 +19,125 @@ var Glob = class {
|
|
|
19
19
|
// src/utils/alias.ts
|
|
20
20
|
import { loadConfig } from "tsconfig-paths";
|
|
21
21
|
import path from "path";
|
|
22
|
-
var Alias = class {
|
|
22
|
+
var Alias = class _Alias {
|
|
23
23
|
constructor() {
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolves an alias path to an absolute file path
|
|
27
|
+
*
|
|
28
|
+
* @param rawPath - The alias path to resolve (e.g., "@entities/user", "@utils")
|
|
29
|
+
* @param currentFileDir - The directory of the current file (used to find tsconfig.json)
|
|
30
|
+
* @returns Result object with absolutePath and type ("success" or "fail")
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // With wildcard alias: "@entities/*" -> "src/entities/*"
|
|
34
|
+
* Alias.resolvePath("@entities/user", "/project/src/features")
|
|
35
|
+
* // Returns: { absolutePath: "/project/src/entities/user", type: "success" }
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Without wildcard: "@utils" -> "src/utils/index"
|
|
39
|
+
* Alias.resolvePath("@utils", "/project/src/features")
|
|
40
|
+
* // Returns: { absolutePath: "/project/src/utils/index", type: "success" }
|
|
41
|
+
*/
|
|
25
42
|
static resolvePath(rawPath, currentFileDir) {
|
|
26
43
|
try {
|
|
27
44
|
const configResult = loadConfig(currentFileDir);
|
|
28
|
-
if (configResult.resultType
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
absolutePath,
|
|
44
|
-
type: "success"
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
if (rawPath === pattern) {
|
|
49
|
-
const absolutePath = path.resolve(
|
|
50
|
-
`${configResult.absoluteBaseUrl}/${origin}`
|
|
51
|
-
);
|
|
52
|
-
return {
|
|
53
|
-
absolutePath,
|
|
54
|
-
type: "success"
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
45
|
+
if (configResult.resultType !== "success") {
|
|
46
|
+
return this.createFailResult(rawPath);
|
|
47
|
+
}
|
|
48
|
+
const { paths, absoluteBaseUrl } = configResult;
|
|
49
|
+
for (const [aliasPattern, targetPaths] of Object.entries(paths)) {
|
|
50
|
+
const targetPath = targetPaths[0];
|
|
51
|
+
const matchResult = this.tryMatchPattern(
|
|
52
|
+
rawPath,
|
|
53
|
+
aliasPattern,
|
|
54
|
+
targetPath,
|
|
55
|
+
absoluteBaseUrl
|
|
56
|
+
);
|
|
57
|
+
if (matchResult) {
|
|
58
|
+
return matchResult;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
return _Alias.createFailResult(rawPath);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return _Alias.createFailResult(rawPath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Attempts to match a raw path against an alias pattern
|
|
68
|
+
*
|
|
69
|
+
* @param rawPath - The path to match (e.g., "@entities/user")
|
|
70
|
+
* @param aliasPattern - The alias pattern from tsconfig (e.g., "@entities/*")
|
|
71
|
+
* @param targetPath - The target path from tsconfig (e.g., "src/entities/*")
|
|
72
|
+
* @param baseUrl - The absolute base URL from tsconfig
|
|
73
|
+
* @returns Success result if matched, null otherwise
|
|
74
|
+
*/
|
|
75
|
+
static tryMatchPattern(rawPath, aliasPattern, targetPath, baseUrl) {
|
|
76
|
+
const hasWildcard = aliasPattern.includes("*");
|
|
77
|
+
if (hasWildcard) {
|
|
78
|
+
return this.matchWildcardPattern(
|
|
79
|
+
rawPath,
|
|
80
|
+
aliasPattern,
|
|
81
|
+
targetPath,
|
|
82
|
+
baseUrl
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
return this.matchExactPattern(rawPath, aliasPattern, targetPath, baseUrl);
|
|
69
86
|
}
|
|
70
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Matches a wildcard alias pattern (e.g., "@entities/*")
|
|
90
|
+
*
|
|
91
|
+
* Pattern: "@entities/*" -> Target: "src/entities/*"
|
|
92
|
+
* Input: "@entities/user" -> Matches: "user" -> Output: "src/entities/user"
|
|
93
|
+
*
|
|
94
|
+
* Note: The original implementation uses simple string replacement for regex,
|
|
95
|
+
* which works because alias patterns typically don't contain special regex chars.
|
|
96
|
+
* We maintain this behavior for compatibility.
|
|
97
|
+
*/
|
|
98
|
+
static matchWildcardPattern(rawPath, aliasPattern, targetPath, baseUrl) {
|
|
99
|
+
const regexPattern = `^${aliasPattern.replace(/\*/g, "(.*)")}$`;
|
|
100
|
+
const regex = new RegExp(regexPattern);
|
|
101
|
+
const match = rawPath.match(regex);
|
|
102
|
+
if (!match) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const capturedPath = match[1];
|
|
106
|
+
const resolvedTargetPath = targetPath.replace(/\*/g, capturedPath);
|
|
107
|
+
const absolutePath = path.resolve(`${baseUrl}/${resolvedTargetPath}`);
|
|
108
|
+
return _Alias.createSuccessResult(absolutePath);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Matches an exact alias pattern (no wildcard)
|
|
112
|
+
*
|
|
113
|
+
* Pattern: "@utils" -> Target: "src/utils/index"
|
|
114
|
+
* Input: "@utils" -> Output: "src/utils/index"
|
|
115
|
+
*/
|
|
116
|
+
static matchExactPattern(rawPath, aliasPattern, targetPath, baseUrl) {
|
|
117
|
+
if (rawPath !== aliasPattern) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const absolutePath = path.resolve(`${baseUrl}/${targetPath}`);
|
|
121
|
+
return _Alias.createSuccessResult(absolutePath);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Creates a success result
|
|
125
|
+
*/
|
|
126
|
+
static createSuccessResult(absolutePath) {
|
|
127
|
+
return {
|
|
128
|
+
absolutePath,
|
|
129
|
+
type: "success"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Creates a fail result
|
|
134
|
+
*/
|
|
135
|
+
static createFailResult(originalPath) {
|
|
136
|
+
return {
|
|
137
|
+
absolutePath: originalPath,
|
|
138
|
+
type: "fail"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
71
141
|
};
|
|
72
142
|
|
|
73
143
|
// src/utils/constants.ts
|
|
@@ -193,16 +263,16 @@ var enforceBarrelPattern = {
|
|
|
193
263
|
if (!rawImportPath.startsWith(".") && !rawImportPath.startsWith("/") || rawImportPath.includes("/node_modules/")) {
|
|
194
264
|
return;
|
|
195
265
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
266
|
+
try {
|
|
267
|
+
absoluteImportPath = resolve.sync(rawImportPath, {
|
|
268
|
+
basedir: path2.dirname(absoluteCurrentFilePath),
|
|
269
|
+
extensions: RESOLVE_EXTENSIONS
|
|
270
|
+
});
|
|
271
|
+
} catch (e) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
200
274
|
}
|
|
201
275
|
} catch (e) {
|
|
202
|
-
context.report({
|
|
203
|
-
node,
|
|
204
|
-
messageId: "TransformedAliasResolveFailed"
|
|
205
|
-
});
|
|
206
276
|
return;
|
|
207
277
|
}
|
|
208
278
|
{
|
|
@@ -216,8 +286,8 @@ var enforceBarrelPattern = {
|
|
|
216
286
|
const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
|
|
217
287
|
const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
|
|
218
288
|
const currentFileInEnforceBarrel = absoluteCurrentFilePath.startsWith(closedTargetPath);
|
|
219
|
-
const
|
|
220
|
-
const invalidImported = !targetPathEntryPointed &&
|
|
289
|
+
const importedOutsideOfBarrel = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
|
|
290
|
+
const invalidImported = !targetPathEntryPointed && importedOutsideOfBarrel;
|
|
221
291
|
if (invalidImported) {
|
|
222
292
|
matchedLatestTargetPath = absoluteTargetPath;
|
|
223
293
|
}
|
|
@@ -289,13 +359,13 @@ var isolateBarrelFile = {
|
|
|
289
359
|
create(context) {
|
|
290
360
|
const option = context.options[0];
|
|
291
361
|
const baseDir = option.baseDir;
|
|
292
|
-
const absoluteGlobalAllowPaths = option.globalAllowPaths.flatMap((
|
|
293
|
-
return Glob.resolvePath(
|
|
362
|
+
const absoluteGlobalAllowPaths = option.globalAllowPaths.flatMap((path5) => {
|
|
363
|
+
return Glob.resolvePath(path5, baseDir);
|
|
294
364
|
});
|
|
295
365
|
const absoluteIsolations = option.isolations.flatMap((isolation) => {
|
|
296
366
|
const isolationPaths = Glob.resolvePath(isolation.path, baseDir);
|
|
297
|
-
const allowedPaths = isolation.allowedPaths.flatMap((
|
|
298
|
-
return Glob.resolvePath(
|
|
367
|
+
const allowedPaths = isolation.allowedPaths.flatMap((path5) => {
|
|
368
|
+
return Glob.resolvePath(path5, baseDir);
|
|
299
369
|
});
|
|
300
370
|
return isolationPaths.map((isolationPath) => ({
|
|
301
371
|
isolationPath,
|
|
@@ -323,7 +393,7 @@ var isolateBarrelFile = {
|
|
|
323
393
|
return;
|
|
324
394
|
}
|
|
325
395
|
} else {
|
|
326
|
-
if (!rawImportPath.startsWith(".") && !rawImportPath.startsWith("/")) {
|
|
396
|
+
if (!rawImportPath.startsWith(".") && !rawImportPath.startsWith("/") || rawImportPath.includes("/node_modules/")) {
|
|
327
397
|
return;
|
|
328
398
|
}
|
|
329
399
|
absoluteImportPath = resolve2.sync(rawImportPath, {
|
|
@@ -338,31 +408,26 @@ var isolateBarrelFile = {
|
|
|
338
408
|
});
|
|
339
409
|
return;
|
|
340
410
|
}
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
});
|
|
346
|
-
const matchedIsolation = absoluteIsolations[isolationIndex];
|
|
347
|
-
if (!matchedIsolation) return;
|
|
348
|
-
const isAllowedImport = matchedIsolation.allowedPaths.some(
|
|
349
|
-
(allowedPath) => {
|
|
350
|
-
const same = absoluteImportPath === allowedPath;
|
|
351
|
-
const closedAllowedPath = allowedPath + "/";
|
|
352
|
-
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
353
|
-
return same || sub;
|
|
411
|
+
const matchedIsolationIndex = absoluteIsolations.findIndex(
|
|
412
|
+
(isolation) => {
|
|
413
|
+
const closedIsolationPath = isolation.isolationPath + "/";
|
|
414
|
+
return absoluteCurrentFilePath.startsWith(closedIsolationPath);
|
|
354
415
|
}
|
|
355
416
|
);
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
|
|
417
|
+
const matchedIsolation = absoluteIsolations[matchedIsolationIndex];
|
|
418
|
+
if (!matchedIsolation) return;
|
|
419
|
+
const isAllowedImport = [
|
|
420
|
+
//global allowed import paths
|
|
421
|
+
...absoluteGlobalAllowPaths,
|
|
422
|
+
//allowed import paths in the matched isolation
|
|
423
|
+
...matchedIsolation.allowedPaths
|
|
424
|
+
].some((allowedPath) => {
|
|
425
|
+
const same = absoluteImportPath === allowedPath;
|
|
426
|
+
const closedAllowedPath = allowedPath + "/";
|
|
427
|
+
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
428
|
+
return same || sub;
|
|
429
|
+
});
|
|
430
|
+
if (!isAllowedImport) {
|
|
366
431
|
context.report({
|
|
367
432
|
node,
|
|
368
433
|
messageId: "IsolatedBarrelImportDisallowed"
|
|
@@ -411,12 +476,239 @@ var noWildcard = {
|
|
|
411
476
|
}
|
|
412
477
|
};
|
|
413
478
|
|
|
479
|
+
// src/rules/no-cycle.ts
|
|
480
|
+
import "@typescript-eslint/types";
|
|
481
|
+
import path4 from "path";
|
|
482
|
+
import fs from "fs";
|
|
483
|
+
import resolve3 from "resolve";
|
|
484
|
+
var importGraph = /* @__PURE__ */ new Map();
|
|
485
|
+
var barrelExportsCache = /* @__PURE__ */ new Map();
|
|
486
|
+
function isBarrelFile(filePath) {
|
|
487
|
+
const fileName = path4.basename(filePath);
|
|
488
|
+
return BARREL_ENTRY_POINT_FILE_NAMES.includes(
|
|
489
|
+
fileName
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
function isExternalImport(rawPath) {
|
|
493
|
+
if (!rawPath.startsWith(".") && !rawPath.startsWith("/")) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
if (rawPath.includes("/node_modules/")) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
function resolveImportPath(rawImportPath, currentFileDir) {
|
|
502
|
+
try {
|
|
503
|
+
const aliasResult = Alias.resolvePath(rawImportPath, currentFileDir);
|
|
504
|
+
if (aliasResult.type === "success") {
|
|
505
|
+
return resolveAliasPath(aliasResult.absolutePath, currentFileDir);
|
|
506
|
+
}
|
|
507
|
+
if (isExternalImport(rawImportPath)) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
return resolve3.sync(rawImportPath, {
|
|
511
|
+
basedir: currentFileDir,
|
|
512
|
+
extensions: RESOLVE_EXTENSIONS
|
|
513
|
+
});
|
|
514
|
+
} catch (e) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function resolveAliasPath(resolvedPath, currentFileDir) {
|
|
519
|
+
try {
|
|
520
|
+
const stats = fs.statSync(resolvedPath);
|
|
521
|
+
if (stats.isDirectory()) {
|
|
522
|
+
return resolve3.sync("index", {
|
|
523
|
+
basedir: resolvedPath,
|
|
524
|
+
extensions: RESOLVE_EXTENSIONS
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return resolvedPath;
|
|
528
|
+
} catch (e) {
|
|
529
|
+
try {
|
|
530
|
+
return resolve3.sync(resolvedPath, {
|
|
531
|
+
basedir: currentFileDir,
|
|
532
|
+
extensions: RESOLVE_EXTENSIONS
|
|
533
|
+
});
|
|
534
|
+
} catch (e2) {
|
|
535
|
+
return resolvedPath;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function getBarrelExports(barrelFileDir, ast) {
|
|
540
|
+
const exports = [];
|
|
541
|
+
for (const statement of ast.body) {
|
|
542
|
+
if ((statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") && statement.source) {
|
|
543
|
+
const exportPath = statement.source.value;
|
|
544
|
+
const resolved = tryResolve(exportPath, barrelFileDir);
|
|
545
|
+
if (resolved && !exports.includes(resolved)) {
|
|
546
|
+
exports.push(resolved);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (statement.type === "ImportDeclaration" && statement.source) {
|
|
550
|
+
const importPath = statement.source.value;
|
|
551
|
+
if (importPath.startsWith(".") || importPath.startsWith("/")) {
|
|
552
|
+
const resolved = tryResolve(importPath, barrelFileDir);
|
|
553
|
+
if (resolved && !exports.includes(resolved)) {
|
|
554
|
+
exports.push(resolved);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return exports;
|
|
560
|
+
}
|
|
561
|
+
function tryResolve(importPath, basedir) {
|
|
562
|
+
try {
|
|
563
|
+
return resolve3.sync(importPath, {
|
|
564
|
+
basedir,
|
|
565
|
+
extensions: RESOLVE_EXTENSIONS
|
|
566
|
+
});
|
|
567
|
+
} catch (e) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function detectCycle(startFile, visited, recStack, currentPath) {
|
|
572
|
+
visited.add(startFile);
|
|
573
|
+
recStack.add(startFile);
|
|
574
|
+
currentPath.push(startFile);
|
|
575
|
+
const imports = importGraph.get(startFile) || /* @__PURE__ */ new Set();
|
|
576
|
+
for (const importedFile of imports) {
|
|
577
|
+
if (!visited.has(importedFile)) {
|
|
578
|
+
const cycle = detectCycle(importedFile, visited, recStack, [
|
|
579
|
+
...currentPath
|
|
580
|
+
]);
|
|
581
|
+
if (cycle) return cycle;
|
|
582
|
+
} else if (recStack.has(importedFile)) {
|
|
583
|
+
const cycleStart = currentPath.indexOf(importedFile);
|
|
584
|
+
return [...currentPath.slice(cycleStart), importedFile];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
recStack.delete(startFile);
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
function hasBidirectionalCycle(fileA, fileB) {
|
|
591
|
+
var _a;
|
|
592
|
+
const bImports = importGraph.get(fileB);
|
|
593
|
+
return (_a = bImports == null ? void 0 : bImports.has(fileA)) != null ? _a : false;
|
|
594
|
+
}
|
|
595
|
+
function hasCycleThroughBarrel(currentFile, exportedModules) {
|
|
596
|
+
for (const exportedModule of exportedModules) {
|
|
597
|
+
if (exportedModule === currentFile) continue;
|
|
598
|
+
const moduleImports = importGraph.get(exportedModule);
|
|
599
|
+
if (moduleImports == null ? void 0 : moduleImports.has(currentFile)) {
|
|
600
|
+
return exportedModule;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
var noCycle = {
|
|
606
|
+
meta: {
|
|
607
|
+
type: "problem",
|
|
608
|
+
docs: {
|
|
609
|
+
description: "Detect circular dependencies and enforce relative imports in barrel files."
|
|
610
|
+
},
|
|
611
|
+
schema: [],
|
|
612
|
+
messages: {
|
|
613
|
+
CircularDependency: "Circular dependency detected: {{cyclePath}}. This creates a dependency cycle that can cause runtime errors and make code harder to maintain.",
|
|
614
|
+
BarrelInternalImportDisallowed: "Barrel files (index.ts) must use relative imports (./ or ../) for internal modules. Importing via barrel file or absolute path is not allowed. Use relative path: '{{relativePath}}'",
|
|
615
|
+
TransformedAliasResolveFailed: "Transformed alias resolve failed. please check the alias config."
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
defaultOptions: [],
|
|
619
|
+
create(context) {
|
|
620
|
+
const currentFile = context.getFilename();
|
|
621
|
+
const currentDir = path4.dirname(currentFile);
|
|
622
|
+
if (!importGraph.has(currentFile)) {
|
|
623
|
+
importGraph.set(currentFile, /* @__PURE__ */ new Set());
|
|
624
|
+
}
|
|
625
|
+
importGraph.get(currentFile).clear();
|
|
626
|
+
if (isBarrelFile(currentFile)) {
|
|
627
|
+
const ast = context.getSourceCode().ast;
|
|
628
|
+
const exports = getBarrelExports(currentDir, ast);
|
|
629
|
+
barrelExportsCache.set(currentFile, exports);
|
|
630
|
+
}
|
|
631
|
+
function checkImport(node) {
|
|
632
|
+
if (!node.source) return;
|
|
633
|
+
const rawPath = node.source.value;
|
|
634
|
+
const absolutePath = resolveImportPath(rawPath, currentDir);
|
|
635
|
+
if (!absolutePath) return;
|
|
636
|
+
if (isBarrelFile(currentFile)) {
|
|
637
|
+
const error = checkBarrelInternalImport(rawPath, absolutePath);
|
|
638
|
+
if (error) {
|
|
639
|
+
context.report({
|
|
640
|
+
node,
|
|
641
|
+
messageId: "BarrelInternalImportDisallowed",
|
|
642
|
+
data: { relativePath: error }
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const isBarrelInternal = isBarrelFile(currentFile) && absolutePath.startsWith(currentDir + path4.sep);
|
|
648
|
+
addToGraph(absolutePath);
|
|
649
|
+
if (isBarrelInternal) return;
|
|
650
|
+
const cycleError = checkForCycles(absolutePath);
|
|
651
|
+
if (cycleError) {
|
|
652
|
+
context.report({
|
|
653
|
+
node,
|
|
654
|
+
messageId: "CircularDependency",
|
|
655
|
+
data: { cyclePath: cycleError }
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function checkBarrelInternalImport(rawPath, absolutePath) {
|
|
660
|
+
const isInternal = absolutePath.startsWith(currentDir + path4.sep) || absolutePath === currentDir;
|
|
661
|
+
if (!isInternal) return null;
|
|
662
|
+
if (rawPath.startsWith("./") || rawPath.startsWith("../")) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
const relative = path4.relative(currentDir, absolutePath);
|
|
666
|
+
const suggested = relative.startsWith(".") ? relative : `./${relative}`;
|
|
667
|
+
return suggested.replace(/\\/g, "/");
|
|
668
|
+
}
|
|
669
|
+
function addToGraph(absolutePath) {
|
|
670
|
+
const imports = importGraph.get(currentFile);
|
|
671
|
+
imports.add(absolutePath);
|
|
672
|
+
if (isBarrelFile(absolutePath)) {
|
|
673
|
+
const exports = barrelExportsCache.get(absolutePath) || [];
|
|
674
|
+
for (const exp of exports) {
|
|
675
|
+
if (exp !== currentFile) {
|
|
676
|
+
imports.add(exp);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function checkForCycles(absolutePath) {
|
|
682
|
+
if (isBarrelFile(absolutePath)) {
|
|
683
|
+
const exports = barrelExportsCache.get(absolutePath) || [];
|
|
684
|
+
const cycleModule = hasCycleThroughBarrel(currentFile, exports);
|
|
685
|
+
if (cycleModule) {
|
|
686
|
+
return `${currentFile} \u2192 ${absolutePath} \u2192 ${cycleModule} \u2192 ${absolutePath} \u2192 ${currentFile}`;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (hasBidirectionalCycle(currentFile, absolutePath)) {
|
|
690
|
+
return `${currentFile} \u2192 ${absolutePath} \u2192 ${currentFile}`;
|
|
691
|
+
}
|
|
692
|
+
const cycle = detectCycle(currentFile, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), []);
|
|
693
|
+
if (cycle && cycle.length > 0) {
|
|
694
|
+
return cycle.join(" \u2192 ");
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
ImportDeclaration: checkImport,
|
|
700
|
+
ExportNamedDeclaration: checkImport,
|
|
701
|
+
ExportAllDeclaration: checkImport
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
414
706
|
// src/index.ts
|
|
415
707
|
var rules = {
|
|
416
708
|
"enforce-barrel-pattern": enforceBarrelPattern,
|
|
417
709
|
"isolate-barrel-file": isolateBarrelFile,
|
|
418
|
-
"no-wildcard": noWildcard
|
|
419
|
-
|
|
710
|
+
"no-wildcard": noWildcard,
|
|
711
|
+
"no-cycle": noCycle
|
|
420
712
|
};
|
|
421
713
|
export {
|
|
422
714
|
rules
|