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 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.2-blue.svg" alt="Version"/>
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.2-blue.svg" alt="Version"/>
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(path4, baseDir) {
46
- const globResult = import_fast_glob.default.sync(path4, {
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 === "success") {
65
- for (const [pattern, targets] of Object.entries(configResult.paths)) {
66
- const origin = targets[0];
67
- if (pattern.includes("*")) {
68
- const patternRegex = new RegExp(
69
- `^${pattern.replace("*", "(.*)")}$`
70
- );
71
- const match = rawPath.match(patternRegex);
72
- if (match) {
73
- const [, matchedPath] = match;
74
- const extendedOrigin = origin.replace("*", matchedPath);
75
- const absolutePath = import_path.default.resolve(
76
- `${configResult.absoluteBaseUrl}/${extendedOrigin}`
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
- absolutePath: rawPath,
98
- type: "fail"
99
- };
100
- } catch (e) {
101
- return {
102
- absolutePath: rawPath,
103
- type: "fail"
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
- absoluteImportPath = import_resolve.default.sync(rawImportPath, {
233
- basedir: import_path2.default.dirname(absoluteCurrentFilePath),
234
- extensions: RESOLVE_EXTENSIONS
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 importedOutsideOfTargetPath = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
256
- const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
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((path4) => {
329
- return Glob.resolvePath(path4, baseDir);
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((path4) => {
334
- return Glob.resolvePath(path4, baseDir);
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 isolationIndex = absoluteIsolations.findIndex((isolation) => {
378
- const absoluteIsolationPath = isolation.isolationPath;
379
- const closedIsolationPath = absoluteIsolationPath + "/";
380
- return absoluteCurrentFilePath.startsWith(closedIsolationPath);
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 allowedImport = isAllowedImport || isGlobalAllowedImport;
401
- if (!allowedImport) {
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
- // "no-cycle": noCycle,
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(path4, baseDir) {
10
- const globResult = FastGlob.sync(path4, {
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 === "success") {
29
- for (const [pattern, targets] of Object.entries(configResult.paths)) {
30
- const origin = targets[0];
31
- if (pattern.includes("*")) {
32
- const patternRegex = new RegExp(
33
- `^${pattern.replace("*", "(.*)")}$`
34
- );
35
- const match = rawPath.match(patternRegex);
36
- if (match) {
37
- const [, matchedPath] = match;
38
- const extendedOrigin = origin.replace("*", matchedPath);
39
- const absolutePath = path.resolve(
40
- `${configResult.absoluteBaseUrl}/${extendedOrigin}`
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
- absolutePath: rawPath,
62
- type: "fail"
63
- };
64
- } catch (e) {
65
- return {
66
- absolutePath: rawPath,
67
- type: "fail"
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
- absoluteImportPath = resolve.sync(rawImportPath, {
197
- basedir: path2.dirname(absoluteCurrentFilePath),
198
- extensions: RESOLVE_EXTENSIONS
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 importedOutsideOfTargetPath = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
220
- const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
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((path4) => {
293
- return Glob.resolvePath(path4, baseDir);
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((path4) => {
298
- return Glob.resolvePath(path4, baseDir);
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 isolationIndex = absoluteIsolations.findIndex((isolation) => {
342
- const absoluteIsolationPath = isolation.isolationPath;
343
- const closedIsolationPath = absoluteIsolationPath + "/";
344
- return absoluteCurrentFilePath.startsWith(closedIsolationPath);
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 isGlobalAllowedImport = absoluteGlobalAllowPaths.some(
357
- (allowedPath) => {
358
- const same = absoluteImportPath === allowedPath;
359
- const closedAllowedPath = allowedPath + "/";
360
- const sub = absoluteImportPath.startsWith(closedAllowedPath);
361
- return same || sub;
362
- }
363
- );
364
- const allowedImport = isAllowedImport || isGlobalAllowedImport;
365
- if (!allowedImport) {
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
- // "no-cycle": noCycle,
710
+ "no-wildcard": noWildcard,
711
+ "no-cycle": noCycle
420
712
  };
421
713
  export {
422
714
  rules
package/package.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "isolated barrel module",
26
26
  "no-wildcard"
27
27
  ],
28
- "version": "1.4.2",
28
+ "version": "1.4.4",
29
29
  "type": "module",
30
30
  "main": "dist/index.cjs",
31
31
  "module": "dist/index.js",