eslint-plugin-barrel-rules 1.1.1 → 1.2.0

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.1.1-blue.svg" alt="Version"/>
6
+ <img src="https://img.shields.io/badge/version-1.2.0-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>
@@ -28,6 +28,10 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
28
28
  내부 파일을 직접 import하는 것을 차단하여
29
29
  **모듈화, 추상화, 유지보수성, 확장성**을 극대화합니다.
30
30
 
31
+ > 💡 Tip:
32
+ > 코드 품질을 더욱 강화하고 싶다면, 이 플러그인과 함께 [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import)의 `no-cycle` 룰을 사용하는 것을 추천합니다.
33
+ > 이를 통해 프로젝트 내의 순환 참조(Import Cycle)도 효과적으로 감지하고 방지할 수 있습니다.
34
+
31
35
  ---
32
36
 
33
37
  ## 지원 환경
@@ -35,7 +39,12 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
35
39
  - ESLint 9
36
40
  > Flat config(eslint.config.js), TypeScript 지원 시 "typescript-eslint" config 사용 필요
37
41
  - ESLint 8
42
+
38
43
  > Legacy config(.eslintrc.js), TypeScript 지원 시 "@typescript-eslint/parser"를 parser로 지정하고, "@typescript-eslint"를 plugin에 추가해야 함
44
+
45
+ - TypeScript Alias Import 지원
46
+ > Import 구문에서 TypeScript 경로 별칭(예: `@ts/barrel/inner`)을 `tsconfig.json` 기반으로 자동 해석합니다.
47
+ > 단, ESLint 플러그인 설정에서는 alias 사용 불가능 - 상대경로나 절대경로만 사용하세요.
39
48
  - Node.js (ES2015 이상)
40
49
  - ES 모듈, CommonJS 모듈 모두 지원
41
50
 
@@ -48,6 +57,17 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
48
57
  (예: `import ... from "../domains/foo"`는 허용,
49
58
  `import ... from "../domains/foo/components/Bar"`는 차단)
50
59
 
60
+ - **Isolation Barrel Module**
61
+ 지정한 barrel path 외부의 모듈이 내부 파일을 직접 import하지 못하도록 막을 수 있습니다.
62
+ `isolated: true` 옵션을 사용하면 같은 barrel path 내부에서는 자유롭게 import가 가능하고,
63
+ 외부에서는 해당 barrel path로의 import가 모두 차단됩니다. (barrel(index) 파일을 통한 접근도 불가)
64
+ 만약 특정 공유 import 경로만 허용하고 싶다면 `allowedImportPaths` 옵션을 사용할 수 있습니다.
65
+ 이를 통해 각 모듈의 경계를 엄격하게 보호하고, 모듈의 독립성을 유지할 수 있습니다.
66
+
67
+ - **와일드카드 import/export 방지**
68
+ `import * as foo from "module"` 또는 `export * from "./module"`과 같은 와일드카드(네임스페이스) import/export를 금지합니다.
69
+ 명시적인 이름 기반 import/export만 허용하여 트리쉐이킹과 코드 명확성을 높입니다.
70
+
51
71
  - **고성능 glob 매칭**
52
72
  `src/domains/*`처럼 glob 패턴으로 여러 디렉토리 지정 가능
53
73
 
@@ -56,16 +76,22 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
56
76
  ## 규칙(Rules)
57
77
 
58
78
  1. **enforce-barrel-pattern**
59
- 모듈 임포트배럴 패턴(Barrel Pattern)을 강제합니다.
60
- 지정한 배럴 파일을 통해서만 임포트가 가능하며, 내부 모듈에 직접 접근하는 것을 방지합니다.
79
+ 모듈 importbarrel 패턴을 강제합니다.
80
+ 지정한 barrel 파일(예: index.ts)로만 import를 허용하고, 내부 모듈에 대한 직접 접근을 차단합니다.
81
+ `isolated: true` 옵션을 사용하면 같은 barrel path 내부 파일끼리만 import가 가능하며, 외부에서의 import는 barrel 파일을 통한 접근도 모두 차단됩니다.
82
+ `allowedImportPaths` 옵션을 사용하면 특정 공유 import 경로만 예외적으로 허용할 수 있습니다.
61
83
 
62
84
  - **옵션:**
63
- - `paths`: 배럴 패턴으로 보호할 디렉토리 경로(`baseDir` 기준 상대경로)
64
- - `baseDir` (선택): `paths` 해석 기준이 되는 디렉토리. 기본값은 ESLint 실행 위치입니다.
85
+ - `paths`: barrel 패턴을 적용할 디렉토리 목록(`baseDir` 기준 상대경로)
86
+ - `baseDir` (선택): `paths` 기준이 되는 베이스 디렉토리 (기본값: ESLint 실행 디렉토리)
87
+ - `isolated` (선택): `true`일 경우, barrel path 외부에서의 모든 import를 차단합니다(barrel 파일 통한 접근 포함). 같은 barrel path 내부 또는 `allowedImportPaths`만 허용.
88
+ - `allowedImportPaths` (선택): isolation 모드에서도 직접 import를 허용할 경로 배열
65
89
 
66
90
  2. **no-wildcard**
67
- `import * as foo from "module"`, `export * from "./module"`과 같은 와일드카드(네임스페이스) import/export를 금지합니다.
68
- 트리쉐이킹 코드 명확성을 위해 반드시 개별(named) import/export만 허용합니다.
91
+ `import * as foo from "module"` 또는 `export * from "./module"`과 같은 와일드카드(네임스페이스) import/export를 금지합니다.
92
+ `enforce-barrel-pattern` 룰과 함께 사용하는 것을 적극 추천합니다.
93
+ 두 룰을 함께 적용하면 모듈 경계를 엄격하게 지킬 수 있을 뿐만 아니라,
94
+ 트리쉐이킹을 통한 성능 향상과 코드 추적 및 유지보수의 용이성까지 모두 얻을 수 있습니다.
69
95
 
70
96
  ---
71
97
 
@@ -100,6 +126,11 @@ module.exports = {
100
126
  // (옵션) 설정하지 않으면 기본값은 ESLint를 실행한 위치(작업 디렉토리)입니다.
101
127
  // 예: `npx eslint .`처럼 실행하면, 실행 시점의 현재 디렉토리가 기본값이 됩니다.
102
128
  baseDir: __dirname,
129
+ // isolation 모드 활성화: barrel path 외부에서의 모든 import를 차단합니다.
130
+ isolated: true,
131
+ // "shared" 디렉토리만 직접 import를 허용합니다.
132
+ // 필요에 따라 이 배열에 "node_modules/*" 등 원하는 경로를 자유롭게 추가할 수 있습니다.
133
+ allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
103
134
  },
104
135
  ],
105
136
  // import * 또는 export * 금지
@@ -149,6 +180,11 @@ export default tseslint.config([
149
180
  // (옵션) 설정하지 않으면 기본값은 ESLint를 실행한 위치(작업 디렉토리)입니다.
150
181
  // 예: `npx eslint .`처럼 실행하면, 실행 시점의 현재 디렉토리가 기본값이 됩니다.
151
182
  baseDir: __dirname,
183
+ // isolation 모드 활성화: barrel path 외부에서의 모든 import를 차단합니다.
184
+ isolated: true,
185
+ // "shared" 디렉토리만 직접 import를 허용합니다.
186
+ // 필요에 따라 이 배열에 "node_modules/*" 등 원하는 경로를 자유롭게 추가할 수 있습니다.
187
+ allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
152
188
  },
153
189
  ],
154
190
  // import * 또는 export * 금지
@@ -162,25 +198,49 @@ export default tseslint.config([
162
198
 
163
199
  ## 예시
164
200
 
201
+ ### 1. 배럴 내부파일 직접 접근
202
+
165
203
  ```ts
166
- // 내부 파일을 직접 import하면 차단됩니다.
204
+ file(src / index.ts);
205
+
206
+ // ❌ 내부 파일 직접 import 차단
167
207
  import { Test } from "../domains/foo/components/Test";
168
208
 
169
- // ✅ 반드시 배럴(index) 파일을 통해 import해야 합니다.
209
+ // ✅ barrel(index) 파일을 통한 import 허용
170
210
  import { Test } from "../domains/foo";
171
211
  ```
172
212
 
213
+ ### 2. 격리된 모듈 접근 (TypeScript Alias 지원)
214
+
215
+ ```ts
216
+ file(src / domains / foo / index.ts);
217
+
218
+ // ❌ 격리된 barrel로의 외부 import 차단 (alias 사용해도 차단)
219
+ // barrel 외부에서 접근 (bar의 경로는 src/domains/bar/)
220
+ import { Test } from "@domains/bar/components/Test";
221
+ // 또는
222
+ import { Test } from "../domains/bar";
223
+
224
+ // ✅ 같은 barrel 내부에서의 import는 허용 (alias 지원)
225
+ import { Hook } from "@domains/foo/hooks/useTest"; // 같은 barrel 내부에서
226
+ import { Utils } from "./utils/helper"; // 같은 barrel 내부에서
227
+
228
+ // ✅ 허용된 import 경로는 사용 가능 (alias 지원)
229
+ import { SharedUtil } from "@shared/utils"; // allowedImportPaths에 "src/shared/*"가 있는 경우
230
+ ```
231
+
173
232
  ---
174
233
 
175
234
  ## 앞으로의 계획
176
235
 
177
236
  - 더 다양한 모듈 경계/추상화 관련 룰 추가 예정 (~Ing)
178
- - Alias/tsconfig 지원: TypeScript의 paths, Vite의 resolve.alias, 기타 커스텀 경로 매핑을 완벽하게 지원 (~Ing)
237
+ - **Alias/tsconfig 지원: TypeScript의 paths 맵핑 완벽하게 지원** (OK)
179
238
  - **CJS 지원** (OK)
180
239
  - **ESLint 8 지원** (OK)
181
240
  - **번들 플러그인(플러그인 내 모든 기능 통합)** (OK)
182
241
  - **잘못된 경로 설정 검증 기능** (OK)
183
242
  - **와일드카드 import/export 제한 규칙** (OK)
243
+ - **지정한 Barrel 경로 격리** (OK)
184
244
 
185
245
  ---
186
246
 
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.1.1-blue.svg" alt="Version"/>
6
+ <img src="https://img.shields.io/badge/version-1.2.0-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>
@@ -31,6 +31,10 @@ internal implementation details must only be accessed via the directory’s **in
31
31
  Direct imports from internal files are blocked, maximizing
32
32
  **modularity, abstraction, maintainability, and scalability**.
33
33
 
34
+ > 💡 Tip:
35
+ > For even stronger code quality, we recommend using the `no-cycle` rule from [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) together with this plugin.
36
+ > This allows you to detect and prevent circular dependencies (import cycles) in your project.
37
+
34
38
  ---
35
39
 
36
40
  ## Supports
@@ -39,6 +43,9 @@ Direct imports from internal files are blocked, maximizing
39
43
  > Flat config(eslint.config.js), for TypeScript support, use the "typescript-eslint" config
40
44
  - ESLint 8
41
45
  > Legacy config(.eslintrc.js), for TypeScript support, set "@typescript-eslint/parser" as the parser and add "@typescript-eslint" as a plugin
46
+ - TypeScript Alias Import Support
47
+ > Automatically resolves TypeScript path aliases (e.g., `@ts/barrel/inner`) in import statements based on your `tsconfig.json`.
48
+ > Note: ESLint plugin configuration does not support aliases - use relative or absolute paths only.
42
49
  - Node.js (ES2015+)
43
50
  - Supports both ES Modules and CommonJS
44
51
 
@@ -51,6 +58,17 @@ Direct imports from internal files are blocked, maximizing
51
58
  (e.g., `import ... from "../domains/foo"` is allowed,
52
59
  but `import ... from "../domains/foo/components/Bar"` is blocked)
53
60
 
61
+ - **Isolation Barrel Module**
62
+ You can prevent modules outside the specified barrel path from directly importing internal files.
63
+ By enabling `isolated: true`, only files within the same barrel path can freely import each other.
64
+ Any import from outside the enforced barrel path is completely blocked, even if it tries to import via the barrel (index) file.
65
+ If you want to allow specific shared imports, you can use the `allowedImportPaths` option.
66
+ This helps you strictly protect your module boundaries and keep each module truly independent.
67
+
68
+ - **Prevent Wildcard Import/Export**
69
+ Disallows wildcard (namespace) imports and exports such as `import * as foo from "module"` or `export * from "./module"`.
70
+ This enforces the use of named imports/exports for better tree-shaking and code clarity.
71
+
54
72
  - **High-performance glob matching**
55
73
  Specify multiple directories using glob patterns like `src/domains/*`
56
74
 
@@ -61,14 +79,20 @@ Direct imports from internal files are blocked, maximizing
61
79
  1. **enforce-barrel-pattern**
62
80
  Enforces the barrel pattern for module imports.
63
81
  Only allows imports from designated barrel files and prevents direct access to internal modules.
82
+ When `isolated: true` is set, only files within the same barrel path can import each other, and any import from outside the barrel path is completely blocked (even via the barrel file).
83
+ You can allow specific shared import paths by using the `allowedImportPaths` option.
64
84
 
65
85
  - **Options:**
66
86
  - `paths`: The directories to be protected by the barrel pattern (relative to `baseDir`).
67
87
  - `baseDir` (optional): The base directory for resolving `paths`. Defaults to the ESLint execution directory.
88
+ - `isolated` (optional): If `true`, blocks all imports from outside the barrel path, even via the barrel file. Only allows imports within the same barrel path or from `allowedImportPaths`.
89
+ - `allowedImportPaths` (optional): Array of paths that are allowed to be imported directly, even in isolation mode.
68
90
 
69
91
  2. **no-wildcard**
70
- Disallows wildcard (namespace) imports such as `import * as foo from "module"`, `export * from "./module"`
71
- Forces you to use named imports for better tree-shaking and code clarity.
92
+ Disallows wildcard (namespace) imports such as `import * as foo from "module"` or `export * from "./module"`.
93
+ We highly recommend enabling this rule together with the `enforce-barrel-pattern` rule.
94
+ Using both rules together not only enforces strict module boundaries,
95
+ but also improves performance through better tree-shaking and makes code tracing and maintenance much easier.
72
96
 
73
97
  ---
74
98
 
@@ -103,6 +127,11 @@ module.exports = {
103
127
  // Optional config. The default value is the directory where ESLint is executed.
104
128
  // For example, if you run `npx eslint .`, the default will be the current working directory at the time of execution.
105
129
  baseDir: __dirname,
130
+ // Enable isolation mode: block all imports from outside the barrel path
131
+ isolated: true,
132
+ // Allow direct imports only from the "shared" directory.
133
+ // You can customize this array as needed, e.g., add "node_modules/..." or any other path you want to allow.
134
+ allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
106
135
  },
107
136
  ],
108
137
  // Disallow wildcard (namespace) import/export.
@@ -152,6 +181,11 @@ export default tseslint.config([
152
181
  // Optional config. The default value is the directory where ESLint is executed.
153
182
  // For example, if you run `npx eslint .`, the default will be the current working directory at the time of execution.
154
183
  baseDir: __dirname,
184
+ // Enable isolation mode: block all imports from outside the barrel path
185
+ isolated: true,
186
+ // Allow direct imports only from the "shared" directory.
187
+ // You can customize this array as needed, e.g., add "node_modules/*" or any other path you want to allow.
188
+ allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
155
189
  },
156
190
  ],
157
191
  // Disallow wildcard (namespace) import/export.
@@ -163,9 +197,13 @@ export default tseslint.config([
163
197
 
164
198
  ---
165
199
 
166
- ## Example
200
+ ## Examples
201
+
202
+ ### 1. Direct Access
167
203
 
168
204
  ```ts
205
+ file(src / index.ts);
206
+
169
207
  // ❌ Direct import from internal file is blocked
170
208
  import { Test } from "../domains/foo/components/Test";
171
209
 
@@ -173,6 +211,25 @@ import { Test } from "../domains/foo/components/Test";
173
211
  import { Test } from "../domains/foo";
174
212
  ```
175
213
 
214
+ ### 2. Isolated Module Access (with TypeScript Alias Support)
215
+
216
+ ```ts
217
+ file(src / domains / foo / index.ts);
218
+
219
+ // ❌ External import to isolated barrel is blocked (even with alias)
220
+ // from outside barrel (bar's path is src/domains/bar/)
221
+ import { Test } from "@domains/bar/components/Test";
222
+ // or
223
+ import { Test } from "../domains/bar";
224
+
225
+ // ✅ Internal imports within same barrel are allowed (alias supported)
226
+ import { Hook } from "@domains/foo/hooks/useTest"; // from inside same barrel
227
+ import { Utils } from "./utils/helper"; // from inside same barrel
228
+
229
+ // ✅ Allowed import paths are permitted (alias supported)
230
+ import { SharedUtil } from "@shㅇㅇared/utils"; // if "src/shared/*" is in allowedImportPaths
231
+ ```
232
+
176
233
  ---
177
234
 
178
235
  ## Future Work
@@ -180,14 +237,14 @@ import { Test } from "../domains/foo";
180
237
  - More rules for module boundaries and abstraction (~Ing)
181
238
 
182
239
  - **Alias/tsconfig Support**
183
- Fully supports TypeScript `paths`, Vite `resolve.alias`, and other custom path mappings (~Ing)
184
-
240
+ Fully supports TypeScript `paths` (OK)
185
241
  - **CJS Support** (OK)
186
242
  - **Eslint8 Support** (OK)
187
243
  - **Bundle Plugin(capsure any features in plugin)**
188
244
  (OK)
189
245
  - **Wrong Path Setup Validator** (OK)
190
246
  - **Wildcard Import/Export Protection Rule** (OK)
247
+ - **Isolation Barrel Module** (OK)
191
248
 
192
249
  ---
193
250
 
package/dist/index.cjs CHANGED
@@ -36,9 +36,101 @@ module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // src/rules/enforce-barrel-pattern.ts
38
38
  var import_utils = require("@typescript-eslint/utils");
39
- var import_path = __toESM(require("path"), 1);
39
+ var import_path2 = __toESM(require("path"), 1);
40
40
  var import_resolve = __toESM(require("resolve"), 1);
41
+
42
+ // src/utils/glob.ts
41
43
  var import_fast_glob = __toESM(require("fast-glob"), 1);
44
+ var Glob = class {
45
+ static resolvePath(path3, baseDir) {
46
+ const globResult = import_fast_glob.default.sync(path3, {
47
+ cwd: baseDir,
48
+ onlyDirectories: true,
49
+ absolute: true
50
+ });
51
+ if (globResult.length === 0) {
52
+ throw new Error(
53
+ `[Glob] In baseDir: ${baseDir}, path: ${path3}, any directory was not found`
54
+ );
55
+ }
56
+ return globResult;
57
+ }
58
+ };
59
+
60
+ // src/utils/alias.ts
61
+ var import_tsconfig_paths = require("tsconfig-paths");
62
+ var import_path = __toESM(require("path"), 1);
63
+ var Alias = class {
64
+ constructor() {
65
+ }
66
+ static resolvePath(rawPath, currentFileDir) {
67
+ try {
68
+ const configResult = (0, import_tsconfig_paths.loadConfig)(currentFileDir);
69
+ if (configResult.resultType === "success") {
70
+ for (const [pattern, targets] of Object.entries(configResult.paths)) {
71
+ const origin = targets[0];
72
+ if (pattern.includes("*")) {
73
+ const patternRegex = new RegExp(
74
+ `^${pattern.replace("*", "(.*)")}$`
75
+ );
76
+ const match = rawPath.match(patternRegex);
77
+ if (match) {
78
+ const [, matchedPath] = match;
79
+ const extendedOrigin = origin.replace("*", matchedPath);
80
+ const absolutePath = import_path.default.resolve(
81
+ `${configResult.absoluteBaseUrl}/${extendedOrigin}`
82
+ );
83
+ return {
84
+ absolutePath,
85
+ type: "success"
86
+ };
87
+ }
88
+ } else {
89
+ if (rawPath === pattern) {
90
+ const absolutePath = import_path.default.resolve(
91
+ `${configResult.absoluteBaseUrl}/${origin}`
92
+ );
93
+ return {
94
+ absolutePath,
95
+ type: "success"
96
+ };
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return {
102
+ absolutePath: rawPath,
103
+ type: "fail"
104
+ };
105
+ } catch (e) {
106
+ return {
107
+ absolutePath: rawPath,
108
+ type: "fail"
109
+ };
110
+ }
111
+ }
112
+ };
113
+
114
+ // src/rules/enforce-barrel-pattern.ts
115
+ var BARREL_ENTRY_POINT_FILE_NAMES = [
116
+ "index.ts",
117
+ "index.tsx",
118
+ "index.js",
119
+ "index.jsx",
120
+ "index.cjs",
121
+ "index.mjs",
122
+ "index.d.ts"
123
+ ];
124
+ var RESOLVE_EXTENSIONS = [
125
+ ".ts",
126
+ ".tsx",
127
+ ".js",
128
+ ".jsx",
129
+ ".json",
130
+ ".d.ts",
131
+ ".mjs",
132
+ ".cjs"
133
+ ];
42
134
  var enforceBarrelPattern = {
43
135
  meta: {
44
136
  type: "problem",
@@ -50,33 +142,36 @@ var enforceBarrelPattern = {
50
142
  type: "object",
51
143
  properties: {
52
144
  paths: { type: "array", items: { type: "string" } },
53
- baseDir: { type: "string" }
145
+ baseDir: { type: "string" },
146
+ isolated: { type: "boolean" },
147
+ allowedImportPaths: { type: "array", items: { type: "string" } }
54
148
  },
55
149
  required: ["paths"],
56
150
  additionalProperties: false
57
151
  }
58
152
  ],
59
153
  messages: {
60
- DirectImportDisallowed: "Please import from '{{matchedTargetPath}}'. Direct access to '{{rawImportPath}}' is not allowed. You must use the barrel pattern and only consume APIs exposed externally. This is to ensure encapsulation of internal logic and maintain module boundaries."
154
+ DirectImportDisallowed: "Please import from '{{matchedTargetPath}}'. Direct access to '{{rawImportPath}}' is not allowed. You must use the barrel pattern and only consume APIs exposed externally. This is to ensure encapsulation of internal logic and maintain module boundaries.",
155
+ IsolatedBarrelImportDisallowed: "This barrel file is isolated. external import is not allowed. if you want to import outside of the barrel file, please add 'allowedImportPaths' to the plugin options."
61
156
  }
62
157
  },
63
158
  //default options(baseDir is current working directory. almost user execute eslint in project root)
64
- defaultOptions: [{ paths: [], baseDir: process.cwd() }],
159
+ defaultOptions: [
160
+ {
161
+ paths: [],
162
+ baseDir: process.cwd(),
163
+ isolated: false,
164
+ allowedImportPaths: []
165
+ }
166
+ ],
65
167
  create(context) {
66
168
  const option = context.options[0];
67
169
  const baseDir = option.baseDir;
68
- const targetPaths = option.paths.flatMap((_path) => {
69
- const globResult = import_fast_glob.default.sync(_path, {
70
- cwd: baseDir,
71
- onlyDirectories: true,
72
- absolute: true
73
- });
74
- if (globResult.length === 0) {
75
- throw new Error(
76
- `[enforce-barrel-pattern] In baseDir: ${baseDir}, path: ${_path}, any directory was not found`
77
- );
78
- }
79
- return globResult;
170
+ const absoluteTargetPaths = option.paths.flatMap((_path) => {
171
+ return Glob.resolvePath(_path, baseDir);
172
+ });
173
+ const allowedImportPaths = option.allowedImportPaths.flatMap((_path) => {
174
+ return Glob.resolvePath(_path, baseDir);
80
175
  });
81
176
  return {
82
177
  //check only import declaration(ESM)
@@ -85,48 +180,79 @@ var enforceBarrelPattern = {
85
180
  const absoluteCurrentFilePath = context.getFilename();
86
181
  let absoluteImportPath = null;
87
182
  try {
88
- absoluteImportPath = import_resolve.default.sync(rawImportPath, {
89
- basedir: import_path.default.dirname(absoluteCurrentFilePath),
90
- extensions: [
91
- ".ts",
92
- ".tsx",
93
- ".js",
94
- ".jsx",
95
- ".json",
96
- ".d.ts",
97
- ".mjs",
98
- ".cjs"
99
- ]
100
- });
183
+ const aliasResult = Alias.resolvePath(
184
+ rawImportPath,
185
+ import_path2.default.dirname(absoluteCurrentFilePath)
186
+ );
187
+ if (aliasResult.type === "success") {
188
+ absoluteImportPath = aliasResult.absolutePath;
189
+ } else {
190
+ absoluteImportPath = import_resolve.default.sync(rawImportPath, {
191
+ basedir: import_path2.default.dirname(absoluteCurrentFilePath),
192
+ extensions: RESOLVE_EXTENSIONS
193
+ });
194
+ }
101
195
  } catch (e) {
102
196
  return;
103
197
  }
104
- let matchedLatestTargetPath = null;
105
- const invalidDirectedImport = targetPaths.some((targetPath) => {
106
- const targetPathEntryPoints = [
107
- "index.ts",
108
- "index.tsx",
109
- "index.js",
110
- "index.jsx",
111
- "index.cjs",
112
- "index.mjs",
113
- "index.d.ts"
114
- ].map((entry) => import_path.default.resolve(targetPath, entry));
115
- const closedTargetPath = targetPath + "/";
116
- const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
117
- const importedOutsideOfTargetPath = !absoluteCurrentFilePath.startsWith(closedTargetPath) && absoluteImportPath.startsWith(closedTargetPath);
118
- const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
119
- if (invalidImported) {
120
- matchedLatestTargetPath = targetPath;
198
+ {
199
+ let matchedLatestTargetPath = null;
200
+ const invalidDirectedImport = absoluteTargetPaths.some(
201
+ (absoluteTargetPath) => {
202
+ const targetPathEntryPoints = BARREL_ENTRY_POINT_FILE_NAMES.map(
203
+ (entry) => import_path2.default.resolve(absoluteTargetPath, entry)
204
+ );
205
+ const closedTargetPath = absoluteTargetPath + "/";
206
+ const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
207
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
208
+ const currentFileInEnforceBarrel = absoluteCurrentFilePath.startsWith(closedTargetPath);
209
+ const importedOutsideOfTargetPath = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
210
+ const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
211
+ if (invalidImported) {
212
+ matchedLatestTargetPath = absoluteTargetPath;
213
+ }
214
+ return invalidImported;
215
+ }
216
+ );
217
+ if (invalidDirectedImport) {
218
+ context.report({
219
+ node,
220
+ messageId: "DirectImportDisallowed",
221
+ data: {
222
+ rawImportPath,
223
+ matchedTargetPath: matchedLatestTargetPath
224
+ }
225
+ });
226
+ }
227
+ }
228
+ {
229
+ if (option.isolated) {
230
+ const currentFileInEnforceBarrel = absoluteTargetPaths.some(
231
+ (absoluteTargetPath) => {
232
+ const closedTargetPath = absoluteTargetPath + "/";
233
+ return absoluteCurrentFilePath.startsWith(closedTargetPath);
234
+ }
235
+ );
236
+ const sameBarrel = absoluteTargetPaths.some(
237
+ (absoluteTargetPath) => {
238
+ const closedTargetPath = absoluteTargetPath + "/";
239
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
240
+ const currentFileInEnforceBarrel2 = absoluteCurrentFilePath.startsWith(closedTargetPath);
241
+ return importedEnforceBarrelFile && currentFileInEnforceBarrel2;
242
+ }
243
+ );
244
+ const allowedImportPath = allowedImportPaths.some(
245
+ (allowedImportPath2) => {
246
+ return absoluteImportPath.startsWith(allowedImportPath2);
247
+ }
248
+ );
249
+ if (!allowedImportPath && !sameBarrel && currentFileInEnforceBarrel) {
250
+ context.report({
251
+ node,
252
+ messageId: "IsolatedBarrelImportDisallowed"
253
+ });
254
+ }
121
255
  }
122
- return invalidImported;
123
- });
124
- if (invalidDirectedImport) {
125
- context.report({
126
- node,
127
- messageId: "DirectImportDisallowed",
128
- data: { rawImportPath, matchedTargetPath: matchedLatestTargetPath }
129
- });
130
256
  }
131
257
  }
132
258
  };
package/dist/index.d.cts CHANGED
@@ -2,9 +2,11 @@ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts
2
2
 
3
3
  declare const _default: {
4
4
  rules: {
5
- "enforce-barrel-pattern": _typescript_eslint_utils_ts_eslint.RuleModule<"DirectImportDisallowed", {
5
+ "enforce-barrel-pattern": _typescript_eslint_utils_ts_eslint.RuleModule<"DirectImportDisallowed" | "IsolatedBarrelImportDisallowed", {
6
6
  paths: string[];
7
7
  baseDir: string;
8
+ isolated: boolean;
9
+ allowedImportPaths: string[];
8
10
  }[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
9
11
  "no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
10
12
  };
package/dist/index.d.ts CHANGED
@@ -2,9 +2,11 @@ import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts
2
2
 
3
3
  declare const _default: {
4
4
  rules: {
5
- "enforce-barrel-pattern": _typescript_eslint_utils_ts_eslint.RuleModule<"DirectImportDisallowed", {
5
+ "enforce-barrel-pattern": _typescript_eslint_utils_ts_eslint.RuleModule<"DirectImportDisallowed" | "IsolatedBarrelImportDisallowed", {
6
6
  paths: string[];
7
7
  baseDir: string;
8
+ isolated: boolean;
9
+ allowedImportPaths: string[];
8
10
  }[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
9
11
  "no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
10
12
  };
package/dist/index.js CHANGED
@@ -1,8 +1,100 @@
1
1
  // src/rules/enforce-barrel-pattern.ts
2
2
  import "@typescript-eslint/utils";
3
- import path from "path";
3
+ import path2 from "path";
4
4
  import resolve from "resolve";
5
- import fastGlob from "fast-glob";
5
+
6
+ // src/utils/glob.ts
7
+ import FastGlob from "fast-glob";
8
+ var Glob = class {
9
+ static resolvePath(path3, baseDir) {
10
+ const globResult = FastGlob.sync(path3, {
11
+ cwd: baseDir,
12
+ onlyDirectories: true,
13
+ absolute: true
14
+ });
15
+ if (globResult.length === 0) {
16
+ throw new Error(
17
+ `[Glob] In baseDir: ${baseDir}, path: ${path3}, any directory was not found`
18
+ );
19
+ }
20
+ return globResult;
21
+ }
22
+ };
23
+
24
+ // src/utils/alias.ts
25
+ import { loadConfig } from "tsconfig-paths";
26
+ import path from "path";
27
+ var Alias = class {
28
+ constructor() {
29
+ }
30
+ static resolvePath(rawPath, currentFileDir) {
31
+ try {
32
+ const configResult = loadConfig(currentFileDir);
33
+ if (configResult.resultType === "success") {
34
+ for (const [pattern, targets] of Object.entries(configResult.paths)) {
35
+ const origin = targets[0];
36
+ if (pattern.includes("*")) {
37
+ const patternRegex = new RegExp(
38
+ `^${pattern.replace("*", "(.*)")}$`
39
+ );
40
+ const match = rawPath.match(patternRegex);
41
+ if (match) {
42
+ const [, matchedPath] = match;
43
+ const extendedOrigin = origin.replace("*", matchedPath);
44
+ const absolutePath = path.resolve(
45
+ `${configResult.absoluteBaseUrl}/${extendedOrigin}`
46
+ );
47
+ return {
48
+ absolutePath,
49
+ type: "success"
50
+ };
51
+ }
52
+ } else {
53
+ if (rawPath === pattern) {
54
+ const absolutePath = path.resolve(
55
+ `${configResult.absoluteBaseUrl}/${origin}`
56
+ );
57
+ return {
58
+ absolutePath,
59
+ type: "success"
60
+ };
61
+ }
62
+ }
63
+ }
64
+ }
65
+ return {
66
+ absolutePath: rawPath,
67
+ type: "fail"
68
+ };
69
+ } catch (e) {
70
+ return {
71
+ absolutePath: rawPath,
72
+ type: "fail"
73
+ };
74
+ }
75
+ }
76
+ };
77
+
78
+ // src/rules/enforce-barrel-pattern.ts
79
+ var BARREL_ENTRY_POINT_FILE_NAMES = [
80
+ "index.ts",
81
+ "index.tsx",
82
+ "index.js",
83
+ "index.jsx",
84
+ "index.cjs",
85
+ "index.mjs",
86
+ "index.d.ts"
87
+ ];
88
+ var RESOLVE_EXTENSIONS = [
89
+ ".ts",
90
+ ".tsx",
91
+ ".js",
92
+ ".jsx",
93
+ ".json",
94
+ ".d.ts",
95
+ ".mjs",
96
+ ".cjs"
97
+ ];
6
98
  var enforceBarrelPattern = {
7
99
  meta: {
8
100
  type: "problem",
@@ -14,33 +106,36 @@ var enforceBarrelPattern = {
14
106
  type: "object",
15
107
  properties: {
16
108
  paths: { type: "array", items: { type: "string" } },
17
- baseDir: { type: "string" }
109
+ baseDir: { type: "string" },
110
+ isolated: { type: "boolean" },
111
+ allowedImportPaths: { type: "array", items: { type: "string" } }
18
112
  },
19
113
  required: ["paths"],
20
114
  additionalProperties: false
21
115
  }
22
116
  ],
23
117
  messages: {
24
- DirectImportDisallowed: "Please import from '{{matchedTargetPath}}'. Direct access to '{{rawImportPath}}' is not allowed. You must use the barrel pattern and only consume APIs exposed externally. This is to ensure encapsulation of internal logic and maintain module boundaries."
118
+ DirectImportDisallowed: "Please import from '{{matchedTargetPath}}'. Direct access to '{{rawImportPath}}' is not allowed. You must use the barrel pattern and only consume APIs exposed externally. This is to ensure encapsulation of internal logic and maintain module boundaries.",
119
+ IsolatedBarrelImportDisallowed: "This barrel file is isolated. external import is not allowed. if you want to import outside of the barrel file, please add 'allowedImportPaths' to the plugin options."
25
120
  }
26
121
  },
27
122
  //default options(baseDir is current working directory. almost user execute eslint in project root)
28
- defaultOptions: [{ paths: [], baseDir: process.cwd() }],
123
+ defaultOptions: [
124
+ {
125
+ paths: [],
126
+ baseDir: process.cwd(),
127
+ isolated: false,
128
+ allowedImportPaths: []
129
+ }
130
+ ],
29
131
  create(context) {
30
132
  const option = context.options[0];
31
133
  const baseDir = option.baseDir;
32
- const targetPaths = option.paths.flatMap((_path) => {
33
- const globResult = fastGlob.sync(_path, {
34
- cwd: baseDir,
35
- onlyDirectories: true,
36
- absolute: true
37
- });
38
- if (globResult.length === 0) {
39
- throw new Error(
40
- `[enforce-barrel-pattern] In baseDir: ${baseDir}, path: ${_path}, any directory was not found`
41
- );
42
- }
43
- return globResult;
134
+ const absoluteTargetPaths = option.paths.flatMap((_path) => {
135
+ return Glob.resolvePath(_path, baseDir);
136
+ });
137
+ const allowedImportPaths = option.allowedImportPaths.flatMap((_path) => {
138
+ return Glob.resolvePath(_path, baseDir);
44
139
  });
45
140
  return {
46
141
  //check only import declaration(ESM)
@@ -49,48 +144,79 @@ var enforceBarrelPattern = {
49
144
  const absoluteCurrentFilePath = context.getFilename();
50
145
  let absoluteImportPath = null;
51
146
  try {
52
- absoluteImportPath = resolve.sync(rawImportPath, {
53
- basedir: path.dirname(absoluteCurrentFilePath),
54
- extensions: [
55
- ".ts",
56
- ".tsx",
57
- ".js",
58
- ".jsx",
59
- ".json",
60
- ".d.ts",
61
- ".mjs",
62
- ".cjs"
63
- ]
64
- });
147
+ const aliasResult = Alias.resolvePath(
148
+ rawImportPath,
149
+ path2.dirname(absoluteCurrentFilePath)
150
+ );
151
+ if (aliasResult.type === "success") {
152
+ absoluteImportPath = aliasResult.absolutePath;
153
+ } else {
154
+ absoluteImportPath = resolve.sync(rawImportPath, {
155
+ basedir: path2.dirname(absoluteCurrentFilePath),
156
+ extensions: RESOLVE_EXTENSIONS
157
+ });
158
+ }
65
159
  } catch (e) {
66
160
  return;
67
161
  }
68
- let matchedLatestTargetPath = null;
69
- const invalidDirectedImport = targetPaths.some((targetPath) => {
70
- const targetPathEntryPoints = [
71
- "index.ts",
72
- "index.tsx",
73
- "index.js",
74
- "index.jsx",
75
- "index.cjs",
76
- "index.mjs",
77
- "index.d.ts"
78
- ].map((entry) => path.resolve(targetPath, entry));
79
- const closedTargetPath = targetPath + "/";
80
- const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
81
- const importedOutsideOfTargetPath = !absoluteCurrentFilePath.startsWith(closedTargetPath) && absoluteImportPath.startsWith(closedTargetPath);
82
- const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
83
- if (invalidImported) {
84
- matchedLatestTargetPath = targetPath;
162
+ {
163
+ let matchedLatestTargetPath = null;
164
+ const invalidDirectedImport = absoluteTargetPaths.some(
165
+ (absoluteTargetPath) => {
166
+ const targetPathEntryPoints = BARREL_ENTRY_POINT_FILE_NAMES.map(
167
+ (entry) => path2.resolve(absoluteTargetPath, entry)
168
+ );
169
+ const closedTargetPath = absoluteTargetPath + "/";
170
+ const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
171
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
172
+ const currentFileInEnforceBarrel = absoluteCurrentFilePath.startsWith(closedTargetPath);
173
+ const importedOutsideOfTargetPath = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
174
+ const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
175
+ if (invalidImported) {
176
+ matchedLatestTargetPath = absoluteTargetPath;
177
+ }
178
+ return invalidImported;
179
+ }
180
+ );
181
+ if (invalidDirectedImport) {
182
+ context.report({
183
+ node,
184
+ messageId: "DirectImportDisallowed",
185
+ data: {
186
+ rawImportPath,
187
+ matchedTargetPath: matchedLatestTargetPath
188
+ }
189
+ });
190
+ }
191
+ }
192
+ {
193
+ if (option.isolated) {
194
+ const currentFileInEnforceBarrel = absoluteTargetPaths.some(
195
+ (absoluteTargetPath) => {
196
+ const closedTargetPath = absoluteTargetPath + "/";
197
+ return absoluteCurrentFilePath.startsWith(closedTargetPath);
198
+ }
199
+ );
200
+ const sameBarrel = absoluteTargetPaths.some(
201
+ (absoluteTargetPath) => {
202
+ const closedTargetPath = absoluteTargetPath + "/";
203
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
204
+ const currentFileInEnforceBarrel2 = absoluteCurrentFilePath.startsWith(closedTargetPath);
205
+ return importedEnforceBarrelFile && currentFileInEnforceBarrel2;
206
+ }
207
+ );
208
+ const allowedImportPath = allowedImportPaths.some(
209
+ (allowedImportPath2) => {
210
+ return absoluteImportPath.startsWith(allowedImportPath2);
211
+ }
212
+ );
213
+ if (!allowedImportPath && !sameBarrel && currentFileInEnforceBarrel) {
214
+ context.report({
215
+ node,
216
+ messageId: "IsolatedBarrelImportDisallowed"
217
+ });
218
+ }
85
219
  }
86
- return invalidImported;
87
- });
88
- if (invalidDirectedImport) {
89
- context.report({
90
- node,
91
- messageId: "DirectImportDisallowed",
92
- data: { rawImportPath, matchedTargetPath: matchedLatestTargetPath }
93
- });
94
220
  }
95
221
  }
96
222
  };
package/package.json CHANGED
@@ -21,9 +21,11 @@
21
21
  "barrel-module",
22
22
  "barrel pattern",
23
23
  "encapsulation directory",
24
- "enforce barrel pattern"
24
+ "enforce barrel pattern",
25
+ "isolated barrel module",
26
+ "no-wildcard"
25
27
  ],
26
- "version": "1.1.1",
28
+ "version": "1.2.0",
27
29
  "type": "module",
28
30
  "main": "dist/index.cjs",
29
31
  "module": "dist/index.js",