eslint-plugin-barrel-rules 1.1.3 → 1.3.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 +107 -42
- package/README.md +108 -40
- package/dist/index.cjs +219 -49
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +219 -49
- package/package.json +11 -2
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.
|
|
6
|
+
<img src="https://img.shields.io/badge/version-1.3.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>
|
|
@@ -39,7 +39,12 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
|
|
|
39
39
|
- ESLint 9
|
|
40
40
|
> Flat config(eslint.config.js), TypeScript 지원 시 "typescript-eslint" config 사용 필요
|
|
41
41
|
- ESLint 8
|
|
42
|
+
|
|
42
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 사용 불가능 - 상대경로나 절대경로만 사용하세요.
|
|
43
48
|
- Node.js (ES2015 이상)
|
|
44
49
|
- ES 모듈, CommonJS 모듈 모두 지원
|
|
45
50
|
|
|
@@ -53,11 +58,7 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
|
|
|
53
58
|
`import ... from "../domains/foo/components/Bar"`는 차단)
|
|
54
59
|
|
|
55
60
|
- **Isolation Barrel Module**
|
|
56
|
-
지정한 barrel path 외부의 모듈이 내부 파일을 직접 import하지 못하도록 막을 수 있습니다.
|
|
57
|
-
`isolated: true` 옵션을 사용하면 같은 barrel path 내부에서는 자유롭게 import가 가능하고,
|
|
58
|
-
외부에서는 해당 barrel path로의 import가 모두 차단됩니다. (barrel(index) 파일을 통한 접근도 불가)
|
|
59
|
-
만약 특정 공유 import 경로만 허용하고 싶다면 `allowedImportPaths` 옵션을 사용할 수 있습니다.
|
|
60
|
-
이를 통해 각 모듈의 경계를 엄격하게 보호하고, 모듈의 독립성을 유지할 수 있습니다.
|
|
61
|
+
지정한 barrel path 외부의 모듈이 내부 파일을 직접 import하지 못하도록 막을 수 있습니다.
|
|
61
62
|
|
|
62
63
|
- **와일드카드 import/export 방지**
|
|
63
64
|
`import * as foo from "module"` 또는 `export * from "./module"`과 같은 와일드카드(네임스페이스) import/export를 금지합니다.
|
|
@@ -70,17 +71,13 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
|
|
|
70
71
|
|
|
71
72
|
## 규칙(Rules)
|
|
72
73
|
|
|
73
|
-
1. **enforce-barrel-pattern**
|
|
74
|
+
1. **enforce-barrel-pattern** (isolate 옵션이 제거되었습니다.)
|
|
74
75
|
모듈 import 시 barrel 패턴을 강제합니다.
|
|
75
76
|
지정한 barrel 파일(예: index.ts)로만 import를 허용하고, 내부 모듈에 대한 직접 접근을 차단합니다.
|
|
76
|
-
`isolated: true` 옵션을 사용하면 같은 barrel path 내부 파일끼리만 import가 가능하며, 외부에서의 import는 barrel 파일을 통한 접근도 모두 차단됩니다.
|
|
77
|
-
`allowedImportPaths` 옵션을 사용하면 특정 공유 import 경로만 예외적으로 허용할 수 있습니다.
|
|
78
77
|
|
|
79
78
|
- **옵션:**
|
|
80
79
|
- `paths`: barrel 패턴을 적용할 디렉토리 목록(`baseDir` 기준 상대경로)
|
|
81
|
-
- `baseDir
|
|
82
|
-
- `isolated` (선택): `true`일 경우, barrel path 외부에서의 모든 import를 차단합니다(barrel 파일 통한 접근 포함). 같은 barrel path 내부 또는 `allowedImportPaths`만 허용.
|
|
83
|
-
- `allowedImportPaths` (선택): isolation 모드에서도 직접 import를 허용할 경로 배열
|
|
80
|
+
- `baseDir`: `paths` 기준이 되는 베이스 디렉토리 (기본값: ESLint 실행 디렉토리)
|
|
84
81
|
|
|
85
82
|
2. **no-wildcard**
|
|
86
83
|
`import * as foo from "module"` 또는 `export * from "./module"`과 같은 와일드카드(네임스페이스) import/export를 금지합니다.
|
|
@@ -88,6 +85,14 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
|
|
|
88
85
|
두 룰을 함께 적용하면 모듈 경계를 엄격하게 지킬 수 있을 뿐만 아니라,
|
|
89
86
|
트리쉐이킹을 통한 성능 향상과 코드 추적 및 유지보수의 용이성까지 모두 얻을 수 있습니다.
|
|
90
87
|
|
|
88
|
+
3. **isolate-barrel-file** (isolated 기능을 새로운 룰로 제작했습니다.)
|
|
89
|
+
모듈 import 시 barrel 패턴을 강제합니다.
|
|
90
|
+
지정한 barrel 파일(예: index.ts)로만 import를 허용하고, 내부 모듈에 대한 직접 접근을 차단합니다.
|
|
91
|
+
- **옵션:**
|
|
92
|
+
- `isolations(Array<{ path: string, allowedPaths: string[] }>)`: `path`와 `allowedPaths`로 구성된 isolation을 추가할 수 있습니다.
|
|
93
|
+
- `baseDir`: `paths` 기준이 되는 베이스 디렉토리 (기본값: ESLint 실행 디렉토리)
|
|
94
|
+
- `globalAllowedPaths` : 모든 isolations에 공통적으로 허용할 경로를 지정합니다(node_modules ... etc)
|
|
95
|
+
|
|
91
96
|
---
|
|
92
97
|
|
|
93
98
|
## 설치
|
|
@@ -113,23 +118,43 @@ module.exports = {
|
|
|
113
118
|
parser: "@typescript-eslint/parser",
|
|
114
119
|
plugins: ["@typescript-eslint", "barrel-rules"],
|
|
115
120
|
rules: {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
121
|
+
|
|
122
|
+
//barrel-file 캡슐화
|
|
123
|
+
"barrel-rules/enforce-barrel-pattern": [
|
|
124
|
+
"error",
|
|
125
|
+
{
|
|
126
|
+
// encapsulation barrel file
|
|
127
|
+
paths: ["src/pages/*", "src/features/*", "src/entities/*"],
|
|
128
|
+
baseDir: __dirname,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
|
|
132
|
+
//barrel file내부에서 외부 모듈 사용 제한
|
|
133
|
+
"barrel-rules/isolate-barrel-file": [
|
|
134
|
+
"error",
|
|
135
|
+
{
|
|
136
|
+
//isolation options
|
|
137
|
+
isolations: [
|
|
138
|
+
{
|
|
139
|
+
path: "src/pages/*",
|
|
140
|
+
allowedPaths: ["src/features/*", "src/entities/*"],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
path: "src/features/*",
|
|
144
|
+
allowedPaths: ["src/entities/*"],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
path: "src/entities/*",
|
|
148
|
+
allowedPaths: [],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
baseDir: __dirname,
|
|
152
|
+
globalAllowPaths: ["src/shares/*", "node_modules/*"],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
|
|
156
|
+
// "*"를 사용한 불 분명한 import/export 방지
|
|
157
|
+
"barrel-rules/no-wildcard": ["error"],
|
|
133
158
|
},
|
|
134
159
|
};
|
|
135
160
|
```
|
|
@@ -167,22 +192,41 @@ export default tseslint.config([
|
|
|
167
192
|
},
|
|
168
193
|
// barrel-rules에 대한 설정만 추가하면 됩니다.
|
|
169
194
|
rules: {
|
|
195
|
+
//barrel-file 캡슐화
|
|
170
196
|
"barrel-rules/enforce-barrel-pattern": [
|
|
171
197
|
"error",
|
|
172
198
|
{
|
|
173
|
-
//
|
|
174
|
-
paths: ["src/
|
|
175
|
-
// (옵션) 설정하지 않으면 기본값은 ESLint를 실행한 위치(작업 디렉토리)입니다.
|
|
176
|
-
// 예: `npx eslint .`처럼 실행하면, 실행 시점의 현재 디렉토리가 기본값이 됩니다.
|
|
199
|
+
// encapsulation barrel file
|
|
200
|
+
paths: ["src/pages/*", "src/features/*", "src/entities/*"],
|
|
177
201
|
baseDir: __dirname,
|
|
178
|
-
// isolation 모드 활성화: barrel path 외부에서의 모든 import를 차단합니다.
|
|
179
|
-
isolated: true,
|
|
180
|
-
// "shared" 디렉토리만 직접 import를 허용합니다.
|
|
181
|
-
// 필요에 따라 이 배열에 "node_modules/*" 등 원하는 경로를 자유롭게 추가할 수 있습니다.
|
|
182
|
-
allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
|
|
183
202
|
},
|
|
184
203
|
],
|
|
185
|
-
|
|
204
|
+
|
|
205
|
+
//barrel file내부에서 외부 모듈 사용 제한
|
|
206
|
+
"barrel-rules/isolate-barrel-file": [
|
|
207
|
+
"error",
|
|
208
|
+
{
|
|
209
|
+
//isolation options
|
|
210
|
+
isolations: [
|
|
211
|
+
{
|
|
212
|
+
path: "src/pages/*",
|
|
213
|
+
allowedPaths: ["src/features/*", "src/entities/*"],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
path: "src/features/*",
|
|
217
|
+
allowedPaths: ["src/entities/*"],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
path: "src/entities/*",
|
|
221
|
+
allowedPaths: [],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
baseDir: __dirname,
|
|
225
|
+
globalAllowPaths: ["src/shares/*", "node_modules/*"],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
|
|
229
|
+
// "*"를 사용한 불 분명한 import/export 방지
|
|
186
230
|
"barrel-rules/no-wildcard": ["error"],
|
|
187
231
|
},
|
|
188
232
|
},
|
|
@@ -193,20 +237,41 @@ export default tseslint.config([
|
|
|
193
237
|
|
|
194
238
|
## 예시
|
|
195
239
|
|
|
240
|
+
### 1. 배럴 내부파일 직접 접근
|
|
241
|
+
|
|
196
242
|
```ts
|
|
197
|
-
|
|
243
|
+
file(src / index.ts);
|
|
244
|
+
|
|
245
|
+
// ❌ 내부 파일 직접 import 차단
|
|
198
246
|
import { Test } from "../domains/foo/components/Test";
|
|
199
247
|
|
|
200
|
-
// ✅
|
|
248
|
+
// ✅ barrel(index) 파일을 통한 import만 허용
|
|
201
249
|
import { Test } from "../domains/foo";
|
|
202
250
|
```
|
|
203
251
|
|
|
252
|
+
### 2. 격리된 모듈 접근 (TypeScript Alias 지원)
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
file(src / domains / foo / index.ts);
|
|
256
|
+
|
|
257
|
+
// ❌ 격리된 barrel로의 외부 import 차단 (alias 사용해도 차단)
|
|
258
|
+
// barrel 외부에서 접근 (bar의 경로는 src/domains/bar/)
|
|
259
|
+
import { Test } from "@domains/bar/components/Test";
|
|
260
|
+
|
|
261
|
+
// ✅ 같은 barrel 내부에서의 import는 허용 (alias 지원)
|
|
262
|
+
import { Hook } from "@domains/foo/hooks/useTest"; // 같은 barrel 내부에서
|
|
263
|
+
import { Utils } from "./utils/helper"; // 같은 barrel 내부에서
|
|
264
|
+
|
|
265
|
+
// ✅ 허용된 import 경로는 사용 가능 (alias 지원)
|
|
266
|
+
import { SharedUtil } from "@shared/utils"; // allowedImportPaths에 "src/shared/*"가 있는 경우
|
|
267
|
+
```
|
|
268
|
+
|
|
204
269
|
---
|
|
205
270
|
|
|
206
271
|
## 앞으로의 계획
|
|
207
272
|
|
|
208
273
|
- 더 다양한 모듈 경계/추상화 관련 룰 추가 예정 (~Ing)
|
|
209
|
-
- Alias/tsconfig 지원: TypeScript의 paths
|
|
274
|
+
- **Alias/tsconfig 지원: TypeScript의 paths 맵핑 완벽하게 지원** (OK)
|
|
210
275
|
- **CJS 지원** (OK)
|
|
211
276
|
- **ESLint 8 지원** (OK)
|
|
212
277
|
- **번들 플러그인(플러그인 내 모든 기능 통합)** (OK)
|
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.
|
|
6
|
+
<img src="https://img.shields.io/badge/version-1.3.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>
|
|
@@ -43,6 +43,9 @@ Direct imports from internal files are blocked, maximizing
|
|
|
43
43
|
> Flat config(eslint.config.js), for TypeScript support, use the "typescript-eslint" config
|
|
44
44
|
- ESLint 8
|
|
45
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.
|
|
46
49
|
- Node.js (ES2015+)
|
|
47
50
|
- Supports both ES Modules and CommonJS
|
|
48
51
|
|
|
@@ -57,10 +60,9 @@ Direct imports from internal files are blocked, maximizing
|
|
|
57
60
|
|
|
58
61
|
- **Isolation Barrel Module**
|
|
59
62
|
You can prevent modules outside the specified barrel path from directly importing internal files.
|
|
60
|
-
By enabling `
|
|
63
|
+
By enabling `isolate-barrel-file`, only files within the same barrel path can freely import each other.
|
|
61
64
|
Any import from outside the enforced barrel path is completely blocked, even if it tries to import via the barrel (index) file.
|
|
62
|
-
If you want to allow specific shared imports, you can use the `
|
|
63
|
-
This helps you strictly protect your module boundaries and keep each module truly independent.
|
|
65
|
+
If you want to allow specific shared imports, you can use the `allowedPaths` or `globalAllowedPaths` option.
|
|
64
66
|
|
|
65
67
|
- **Prevent Wildcard Import/Export**
|
|
66
68
|
Disallows wildcard (namespace) imports and exports such as `import * as foo from "module"` or `export * from "./module"`.
|
|
@@ -73,17 +75,13 @@ Direct imports from internal files are blocked, maximizing
|
|
|
73
75
|
|
|
74
76
|
## Rules
|
|
75
77
|
|
|
76
|
-
1. **enforce-barrel-pattern**
|
|
78
|
+
1. **enforce-barrel-pattern** (Isolation is exracted as new rule :))
|
|
77
79
|
Enforces the barrel pattern for module imports.
|
|
78
80
|
Only allows imports from designated barrel files and prevents direct access to internal modules.
|
|
79
|
-
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).
|
|
80
|
-
You can allow specific shared import paths by using the `allowedImportPaths` option.
|
|
81
81
|
|
|
82
82
|
- **Options:**
|
|
83
83
|
- `paths`: The directories to be protected by the barrel pattern (relative to `baseDir`).
|
|
84
|
-
- `baseDir
|
|
85
|
-
- `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`.
|
|
86
|
-
- `allowedImportPaths` (optional): Array of paths that are allowed to be imported directly, even in isolation mode.
|
|
84
|
+
- `baseDir`: The base directory for resolving `paths`. Defaults to the ESLint execution directory.
|
|
87
85
|
|
|
88
86
|
2. **no-wildcard**
|
|
89
87
|
Disallows wildcard (namespace) imports such as `import * as foo from "module"` or `export * from "./module"`.
|
|
@@ -91,6 +89,14 @@ Direct imports from internal files are blocked, maximizing
|
|
|
91
89
|
Using both rules together not only enforces strict module boundaries,
|
|
92
90
|
but also improves performance through better tree-shaking and makes code tracing and maintenance much easier.
|
|
93
91
|
|
|
92
|
+
3. **isolate-barrel-file** (New Rules!!!)
|
|
93
|
+
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).
|
|
94
|
+
You can allow specific shared import paths by using the `allowedPaths` option.
|
|
95
|
+
- **Options:**
|
|
96
|
+
- `isolations(Array<{ path: string, allowedPaths: string[] }>)`: If you set isolation path, blocks all imports from outside the barrel path, even via the barrel file. Only allows imports within the same barrel path or from `allowedPaths` or `globalAllowedPaths`.
|
|
97
|
+
- `baseDir`: The base directory for resolving `paths`. Defaults to the ESLint execution directory.
|
|
98
|
+
- `globalAllowedPaths` : Array of paths that are allowed to be imported directly, even in isolation mode.
|
|
99
|
+
|
|
94
100
|
---
|
|
95
101
|
|
|
96
102
|
## Install
|
|
@@ -116,23 +122,44 @@ module.exports = {
|
|
|
116
122
|
parser: "@typescript-eslint/parser",
|
|
117
123
|
plugins: ["@typescript-eslint", "barrel-rules"],
|
|
118
124
|
rules: {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
|
|
126
|
+
//enforce barrel capsuling
|
|
127
|
+
"barrel-rules/enforce-barrel-pattern": [
|
|
128
|
+
"error",
|
|
129
|
+
{
|
|
130
|
+
// encapsulation barrel file
|
|
131
|
+
paths: ["src/pages/*", "src/features/*", "src/entities/*"],
|
|
132
|
+
baseDir: __dirname,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
|
|
136
|
+
//protect barrel file from outside module
|
|
137
|
+
"barrel-rules/isolate-barrel-file": [
|
|
138
|
+
"error",
|
|
139
|
+
{
|
|
140
|
+
//isolation options
|
|
141
|
+
isolations: [
|
|
142
|
+
{
|
|
143
|
+
path: "src/pages/*",
|
|
144
|
+
allowedPaths: ["src/features/*", "src/entities/*"],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
path: "src/features/*",
|
|
148
|
+
allowedPaths: ["src/entities/*"],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
path: "src/entities/*",
|
|
152
|
+
allowedPaths: [],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
baseDir: __dirname,
|
|
156
|
+
globalAllowPaths: ["src/shares/*", "node_modules/*"],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
|
|
160
|
+
// protect wildcard import/export
|
|
161
|
+
"barrel-rules/no-wildcard": ["error"],
|
|
162
|
+
|
|
136
163
|
},
|
|
137
164
|
};
|
|
138
165
|
```
|
|
@@ -170,23 +197,44 @@ export default tseslint.config([
|
|
|
170
197
|
},
|
|
171
198
|
//just set your setting for barrel-rules
|
|
172
199
|
rules: {
|
|
200
|
+
|
|
201
|
+
//enforce barrel capsuling
|
|
173
202
|
"barrel-rules/enforce-barrel-pattern": [
|
|
174
203
|
"error",
|
|
175
204
|
{
|
|
176
|
-
//
|
|
177
|
-
paths: ["src/
|
|
178
|
-
// Optional config. The default value is the directory where ESLint is executed.
|
|
179
|
-
// For example, if you run `npx eslint .`, the default will be the current working directory at the time of execution.
|
|
205
|
+
// encapsulation barrel file
|
|
206
|
+
paths: ["src/pages/*", "src/features/*", "src/entities/*"],
|
|
180
207
|
baseDir: __dirname,
|
|
181
|
-
// Enable isolation mode: block all imports from outside the barrel path
|
|
182
|
-
isolated: true,
|
|
183
|
-
// Allow direct imports only from the "shared" directory.
|
|
184
|
-
// You can customize this array as needed, e.g., add "node_modules/*" or any other path you want to allow.
|
|
185
|
-
allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
|
|
186
208
|
},
|
|
187
209
|
],
|
|
188
|
-
|
|
210
|
+
|
|
211
|
+
//protect barrel file from outside module
|
|
212
|
+
"barrel-rules/isolate-barrel-file": [
|
|
213
|
+
"error",
|
|
214
|
+
{
|
|
215
|
+
//isolation options
|
|
216
|
+
isolations: [
|
|
217
|
+
{
|
|
218
|
+
path: "src/pages/*",
|
|
219
|
+
allowedPaths: ["src/features/*", "src/entities/*"],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
path: "src/features/*",
|
|
223
|
+
allowedPaths: ["src/entities/*"],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
path: "src/entities/*",
|
|
227
|
+
allowedPaths: [],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
baseDir: __dirname,
|
|
231
|
+
globalAllowPaths: ["src/shares/*", "node_modules/*"],
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
|
|
235
|
+
// protect wildcard import/export
|
|
189
236
|
"barrel-rules/no-wildcard": ["error"],
|
|
237
|
+
|
|
190
238
|
},
|
|
191
239
|
},
|
|
192
240
|
]);
|
|
@@ -194,9 +242,13 @@ export default tseslint.config([
|
|
|
194
242
|
|
|
195
243
|
---
|
|
196
244
|
|
|
197
|
-
##
|
|
245
|
+
## Examples
|
|
246
|
+
|
|
247
|
+
### 1. Direct Access
|
|
198
248
|
|
|
199
249
|
```ts
|
|
250
|
+
file(src / index.ts);
|
|
251
|
+
|
|
200
252
|
// ❌ Direct import from internal file is blocked
|
|
201
253
|
import { Test } from "../domains/foo/components/Test";
|
|
202
254
|
|
|
@@ -204,6 +256,23 @@ import { Test } from "../domains/foo/components/Test";
|
|
|
204
256
|
import { Test } from "../domains/foo";
|
|
205
257
|
```
|
|
206
258
|
|
|
259
|
+
### 2. Isolated Module Access (with TypeScript Alias Support)
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
file(src / domains / foo / index.ts);
|
|
263
|
+
|
|
264
|
+
// ❌ External import to isolated barrel is blocked (even with alias)
|
|
265
|
+
// from outside barrel (bar's path is src/domains/bar/)
|
|
266
|
+
import { Test } from "@domains/bar/components/Test";
|
|
267
|
+
|
|
268
|
+
// ✅ Internal imports within same barrel are allowed (alias supported)
|
|
269
|
+
import { Hook } from "@domains/foo/hooks/useTest"; // from inside same barrel
|
|
270
|
+
import { Utils } from "./utils/helper"; // from inside same barrel
|
|
271
|
+
|
|
272
|
+
// ✅ Allowed import paths are permitted (alias supported)
|
|
273
|
+
import { SharedUtil } from "@shared/utils"; // if "src/shared/*" is in allowedPaths or globalAllowedPaths
|
|
274
|
+
```
|
|
275
|
+
|
|
207
276
|
---
|
|
208
277
|
|
|
209
278
|
## Future Work
|
|
@@ -211,8 +280,7 @@ import { Test } from "../domains/foo";
|
|
|
211
280
|
- More rules for module boundaries and abstraction (~Ing)
|
|
212
281
|
|
|
213
282
|
- **Alias/tsconfig Support**
|
|
214
|
-
Fully supports TypeScript `paths
|
|
215
|
-
|
|
283
|
+
Fully supports TypeScript `paths` (OK)
|
|
216
284
|
- **CJS Support** (OK)
|
|
217
285
|
- **Eslint8 Support** (OK)
|
|
218
286
|
- **Bundle Plugin(capsure any features in plugin)**
|
package/dist/index.cjs
CHANGED
|
@@ -35,28 +35,82 @@ __export(index_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
37
|
// src/rules/enforce-barrel-pattern.ts
|
|
38
|
-
var
|
|
39
|
-
var
|
|
38
|
+
var import_types = require("@typescript-eslint/types");
|
|
39
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
40
40
|
var import_resolve = __toESM(require("resolve"), 1);
|
|
41
41
|
|
|
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(path4, baseDir) {
|
|
46
|
+
const globResult = import_fast_glob.default.sync(path4, {
|
|
47
47
|
cwd: baseDir,
|
|
48
48
|
onlyDirectories: true,
|
|
49
49
|
absolute: true
|
|
50
50
|
});
|
|
51
51
|
if (globResult.length === 0) {
|
|
52
52
|
throw new Error(
|
|
53
|
-
`[Glob] In baseDir: ${baseDir}, path: ${
|
|
53
|
+
`[Glob] In baseDir: ${baseDir}, path: ${path4}, any directory was not found`
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
return globResult;
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
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
|
+
|
|
60
114
|
// src/rules/enforce-barrel-pattern.ts
|
|
61
115
|
var BARREL_ENTRY_POINT_FILE_NAMES = [
|
|
62
116
|
"index.ts",
|
|
@@ -88,37 +142,47 @@ var enforceBarrelPattern = {
|
|
|
88
142
|
type: "object",
|
|
89
143
|
properties: {
|
|
90
144
|
paths: { type: "array", items: { type: "string" } },
|
|
91
|
-
baseDir: { type: "string" }
|
|
92
|
-
isolated: { type: "boolean" },
|
|
93
|
-
allowedImportPaths: { type: "array", items: { type: "string" } }
|
|
145
|
+
baseDir: { type: "string" }
|
|
94
146
|
},
|
|
95
|
-
required: ["paths"],
|
|
147
|
+
required: ["paths", "baseDir"],
|
|
96
148
|
additionalProperties: false
|
|
97
149
|
}
|
|
98
150
|
],
|
|
99
151
|
messages: {
|
|
152
|
+
TransformedAliasResolveFailed: "Transformed alias resolve failed. please check the alias config.",
|
|
100
153
|
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.",
|
|
101
|
-
|
|
154
|
+
EmptyEslintConfig: "Please set the eslint config '{{property}}' to the eslint config. if you want to use this rule, please set the eslint config."
|
|
102
155
|
}
|
|
103
156
|
},
|
|
104
157
|
//default options(baseDir is current working directory. almost user execute eslint in project root)
|
|
105
158
|
defaultOptions: [
|
|
106
159
|
{
|
|
107
160
|
paths: [],
|
|
108
|
-
baseDir: process.cwd()
|
|
109
|
-
isolated: false,
|
|
110
|
-
allowedImportPaths: []
|
|
161
|
+
baseDir: process.cwd()
|
|
111
162
|
}
|
|
112
163
|
],
|
|
113
164
|
create(context) {
|
|
114
165
|
const option = context.options[0];
|
|
166
|
+
if (!option) {
|
|
167
|
+
return {
|
|
168
|
+
Program(node) {
|
|
169
|
+
const option2 = context.options[0];
|
|
170
|
+
if (!option2) {
|
|
171
|
+
return context.report({
|
|
172
|
+
node,
|
|
173
|
+
messageId: "EmptyEslintConfig",
|
|
174
|
+
data: {
|
|
175
|
+
property: "{ path: Array<string>, baseDir: string }"
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
115
182
|
const baseDir = option.baseDir;
|
|
116
183
|
const absoluteTargetPaths = option.paths.flatMap((_path) => {
|
|
117
184
|
return Glob.resolvePath(_path, baseDir);
|
|
118
185
|
});
|
|
119
|
-
const allowedImportPaths = option.allowedImportPaths.flatMap((_path) => {
|
|
120
|
-
return Glob.resolvePath(_path, baseDir);
|
|
121
|
-
});
|
|
122
186
|
return {
|
|
123
187
|
//check only import declaration(ESM)
|
|
124
188
|
ImportDeclaration(node) {
|
|
@@ -126,11 +190,23 @@ var enforceBarrelPattern = {
|
|
|
126
190
|
const absoluteCurrentFilePath = context.getFilename();
|
|
127
191
|
let absoluteImportPath = null;
|
|
128
192
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
193
|
+
const aliasResult = Alias.resolvePath(
|
|
194
|
+
rawImportPath,
|
|
195
|
+
import_path2.default.dirname(absoluteCurrentFilePath)
|
|
196
|
+
);
|
|
197
|
+
if (aliasResult.type === "success") {
|
|
198
|
+
absoluteImportPath = aliasResult.absolutePath;
|
|
199
|
+
} else {
|
|
200
|
+
absoluteImportPath = import_resolve.default.sync(rawImportPath, {
|
|
201
|
+
basedir: import_path2.default.dirname(absoluteCurrentFilePath),
|
|
202
|
+
extensions: RESOLVE_EXTENSIONS
|
|
203
|
+
});
|
|
204
|
+
}
|
|
133
205
|
} catch (e) {
|
|
206
|
+
context.report({
|
|
207
|
+
node,
|
|
208
|
+
messageId: "TransformedAliasResolveFailed"
|
|
209
|
+
});
|
|
134
210
|
return;
|
|
135
211
|
}
|
|
136
212
|
{
|
|
@@ -138,7 +214,7 @@ var enforceBarrelPattern = {
|
|
|
138
214
|
const invalidDirectedImport = absoluteTargetPaths.some(
|
|
139
215
|
(absoluteTargetPath) => {
|
|
140
216
|
const targetPathEntryPoints = BARREL_ENTRY_POINT_FILE_NAMES.map(
|
|
141
|
-
(entry) =>
|
|
217
|
+
(entry) => import_path2.default.resolve(absoluteTargetPath, entry)
|
|
142
218
|
);
|
|
143
219
|
const closedTargetPath = absoluteTargetPath + "/";
|
|
144
220
|
const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
|
|
@@ -163,34 +239,127 @@ var enforceBarrelPattern = {
|
|
|
163
239
|
});
|
|
164
240
|
}
|
|
165
241
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/rules/isolate-barrel-file.ts
|
|
248
|
+
var import_types2 = require("@typescript-eslint/types");
|
|
249
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
250
|
+
var import_resolve2 = __toESM(require("resolve"), 1);
|
|
251
|
+
var RESOLVE_EXTENSIONS2 = [
|
|
252
|
+
".ts",
|
|
253
|
+
".tsx",
|
|
254
|
+
".js",
|
|
255
|
+
".jsx",
|
|
256
|
+
".d.ts",
|
|
257
|
+
".mjs",
|
|
258
|
+
".cjs"
|
|
259
|
+
];
|
|
260
|
+
var isolateBarrelFile = {
|
|
261
|
+
meta: {
|
|
262
|
+
type: "problem",
|
|
263
|
+
docs: {
|
|
264
|
+
description: "Isolate barrel file is not allowed to import outside of the barrel file"
|
|
265
|
+
},
|
|
266
|
+
schema: [
|
|
267
|
+
{
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
isolations: { type: "array", items: { type: "object" } },
|
|
271
|
+
baseDir: { type: "string" },
|
|
272
|
+
globalAllowPaths: { type: "array", items: { type: "string" } }
|
|
273
|
+
},
|
|
274
|
+
required: ["isolations", "baseDir", "globalAllowPaths"],
|
|
275
|
+
additionalProperties: false
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
messages: {
|
|
279
|
+
TransformedAliasResolveFailed: "Transformed alias resolve failed. please check the alias config.",
|
|
280
|
+
IsolatedBarrelImportDisallowed: "This barrel file is isolated. external import is not allowed. if you want to import outside of the barrel file, please add 'allowedPath' to the plugin options."
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
//default options(baseDir is current working directory. almost user execute eslint in project root)
|
|
284
|
+
defaultOptions: [
|
|
285
|
+
{
|
|
286
|
+
isolations: [],
|
|
287
|
+
baseDir: process.cwd(),
|
|
288
|
+
globalAllowPaths: []
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
create(context) {
|
|
292
|
+
const option = context.options[0];
|
|
293
|
+
const baseDir = option.baseDir;
|
|
294
|
+
const absoluteGlobalAllowPaths = option.globalAllowPaths.flatMap((path4) => {
|
|
295
|
+
return Glob.resolvePath(path4, baseDir);
|
|
296
|
+
});
|
|
297
|
+
const absoluteIsolations = option.isolations.flatMap((isolation) => {
|
|
298
|
+
const isolationPaths = Glob.resolvePath(isolation.path, baseDir);
|
|
299
|
+
const allowedPaths = isolation.allowedPaths.flatMap((path4) => {
|
|
300
|
+
return Glob.resolvePath(path4, baseDir);
|
|
301
|
+
});
|
|
302
|
+
return isolationPaths.map((isolationPath) => ({
|
|
303
|
+
isolationPath,
|
|
304
|
+
//self isolatedPath also allowed to be imported
|
|
305
|
+
allowedPaths: [...allowedPaths, isolationPath]
|
|
306
|
+
}));
|
|
307
|
+
});
|
|
308
|
+
return {
|
|
309
|
+
//check only import declaration(ESM)
|
|
310
|
+
ImportDeclaration(node) {
|
|
311
|
+
const rawImportPath = node.source.value;
|
|
312
|
+
const absoluteCurrentFilePath = context.getFilename();
|
|
313
|
+
let absoluteImportPath = null;
|
|
314
|
+
try {
|
|
315
|
+
const aliasResult = Alias.resolvePath(
|
|
316
|
+
rawImportPath,
|
|
317
|
+
import_path3.default.dirname(absoluteCurrentFilePath)
|
|
318
|
+
);
|
|
319
|
+
if (aliasResult.type === "success") {
|
|
320
|
+
absoluteImportPath = aliasResult.absolutePath;
|
|
321
|
+
} else {
|
|
322
|
+
absoluteImportPath = import_resolve2.default.sync(rawImportPath, {
|
|
323
|
+
basedir: import_path3.default.dirname(absoluteCurrentFilePath),
|
|
324
|
+
extensions: RESOLVE_EXTENSIONS2
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
} catch (e) {
|
|
328
|
+
context.report({
|
|
329
|
+
node,
|
|
330
|
+
messageId: "TransformedAliasResolveFailed"
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const isolationIndex = absoluteIsolations.findIndex((isolation) => {
|
|
335
|
+
const absoluteIsolationPath = isolation.isolationPath;
|
|
336
|
+
const closedIsolationPath = absoluteIsolationPath + "/";
|
|
337
|
+
return absoluteCurrentFilePath.startsWith(closedIsolationPath);
|
|
338
|
+
});
|
|
339
|
+
const matchedIsolation = absoluteIsolations[isolationIndex];
|
|
340
|
+
if (!matchedIsolation) return;
|
|
341
|
+
const isAllowedImport = matchedIsolation.allowedPaths.some(
|
|
342
|
+
(allowedPath) => {
|
|
343
|
+
const same = absoluteImportPath === allowedPath;
|
|
344
|
+
const closedAllowedPath = allowedPath + "/";
|
|
345
|
+
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
346
|
+
return same || sub;
|
|
193
347
|
}
|
|
348
|
+
);
|
|
349
|
+
const isGlobalAllowedImport = absoluteGlobalAllowPaths.some(
|
|
350
|
+
(allowedPath) => {
|
|
351
|
+
const same = absoluteImportPath === allowedPath;
|
|
352
|
+
const closedAllowedPath = allowedPath + "/";
|
|
353
|
+
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
354
|
+
return same || sub;
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
const allowedImport = isAllowedImport || isGlobalAllowedImport;
|
|
358
|
+
if (!allowedImport) {
|
|
359
|
+
context.report({
|
|
360
|
+
node,
|
|
361
|
+
messageId: "IsolatedBarrelImportDisallowed"
|
|
362
|
+
});
|
|
194
363
|
}
|
|
195
364
|
}
|
|
196
365
|
};
|
|
@@ -198,7 +367,7 @@ var enforceBarrelPattern = {
|
|
|
198
367
|
};
|
|
199
368
|
|
|
200
369
|
// src/rules/no-wildcard.ts
|
|
201
|
-
var
|
|
370
|
+
var import_types3 = require("@typescript-eslint/types");
|
|
202
371
|
var noWildcard = {
|
|
203
372
|
meta: {
|
|
204
373
|
type: "problem",
|
|
@@ -238,6 +407,7 @@ var noWildcard = {
|
|
|
238
407
|
// src/index.ts
|
|
239
408
|
var rules = {
|
|
240
409
|
"enforce-barrel-pattern": enforceBarrelPattern,
|
|
410
|
+
"isolate-barrel-file": isolateBarrelFile,
|
|
241
411
|
"no-wildcard": noWildcard
|
|
242
412
|
};
|
|
243
413
|
var index_default = { rules };
|
package/dist/index.d.cts
CHANGED
|
@@ -2,11 +2,17 @@ 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" | "TransformedAliasResolveFailed" | "EmptyEslintConfig", {
|
|
6
6
|
paths: string[];
|
|
7
7
|
baseDir: string;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
}[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
"isolate-barrel-file": _typescript_eslint_utils_ts_eslint.RuleModule<"TransformedAliasResolveFailed" | "IsolatedBarrelImportDisallowed", {
|
|
10
|
+
isolations: {
|
|
11
|
+
path: string;
|
|
12
|
+
allowedPaths: string[];
|
|
13
|
+
}[];
|
|
14
|
+
baseDir: string;
|
|
15
|
+
globalAllowPaths: string[];
|
|
10
16
|
}[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
11
17
|
"no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
12
18
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,11 +2,17 @@ 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" | "TransformedAliasResolveFailed" | "EmptyEslintConfig", {
|
|
6
6
|
paths: string[];
|
|
7
7
|
baseDir: string;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
}[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
"isolate-barrel-file": _typescript_eslint_utils_ts_eslint.RuleModule<"TransformedAliasResolveFailed" | "IsolatedBarrelImportDisallowed", {
|
|
10
|
+
isolations: {
|
|
11
|
+
path: string;
|
|
12
|
+
allowedPaths: string[];
|
|
13
|
+
}[];
|
|
14
|
+
baseDir: string;
|
|
15
|
+
globalAllowPaths: string[];
|
|
10
16
|
}[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
11
17
|
"no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
12
18
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,80 @@
|
|
|
1
1
|
// src/rules/enforce-barrel-pattern.ts
|
|
2
|
-
import "@typescript-eslint/
|
|
3
|
-
import
|
|
2
|
+
import "@typescript-eslint/types";
|
|
3
|
+
import path2 from "path";
|
|
4
4
|
import resolve from "resolve";
|
|
5
5
|
|
|
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(path4, baseDir) {
|
|
10
|
+
const globResult = FastGlob.sync(path4, {
|
|
11
11
|
cwd: baseDir,
|
|
12
12
|
onlyDirectories: true,
|
|
13
13
|
absolute: true
|
|
14
14
|
});
|
|
15
15
|
if (globResult.length === 0) {
|
|
16
16
|
throw new Error(
|
|
17
|
-
`[Glob] In baseDir: ${baseDir}, path: ${
|
|
17
|
+
`[Glob] In baseDir: ${baseDir}, path: ${path4}, any directory was not found`
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
return globResult;
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
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
|
+
|
|
24
78
|
// src/rules/enforce-barrel-pattern.ts
|
|
25
79
|
var BARREL_ENTRY_POINT_FILE_NAMES = [
|
|
26
80
|
"index.ts",
|
|
@@ -52,37 +106,47 @@ var enforceBarrelPattern = {
|
|
|
52
106
|
type: "object",
|
|
53
107
|
properties: {
|
|
54
108
|
paths: { type: "array", items: { type: "string" } },
|
|
55
|
-
baseDir: { type: "string" }
|
|
56
|
-
isolated: { type: "boolean" },
|
|
57
|
-
allowedImportPaths: { type: "array", items: { type: "string" } }
|
|
109
|
+
baseDir: { type: "string" }
|
|
58
110
|
},
|
|
59
|
-
required: ["paths"],
|
|
111
|
+
required: ["paths", "baseDir"],
|
|
60
112
|
additionalProperties: false
|
|
61
113
|
}
|
|
62
114
|
],
|
|
63
115
|
messages: {
|
|
116
|
+
TransformedAliasResolveFailed: "Transformed alias resolve failed. please check the alias config.",
|
|
64
117
|
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.",
|
|
65
|
-
|
|
118
|
+
EmptyEslintConfig: "Please set the eslint config '{{property}}' to the eslint config. if you want to use this rule, please set the eslint config."
|
|
66
119
|
}
|
|
67
120
|
},
|
|
68
121
|
//default options(baseDir is current working directory. almost user execute eslint in project root)
|
|
69
122
|
defaultOptions: [
|
|
70
123
|
{
|
|
71
124
|
paths: [],
|
|
72
|
-
baseDir: process.cwd()
|
|
73
|
-
isolated: false,
|
|
74
|
-
allowedImportPaths: []
|
|
125
|
+
baseDir: process.cwd()
|
|
75
126
|
}
|
|
76
127
|
],
|
|
77
128
|
create(context) {
|
|
78
129
|
const option = context.options[0];
|
|
130
|
+
if (!option) {
|
|
131
|
+
return {
|
|
132
|
+
Program(node) {
|
|
133
|
+
const option2 = context.options[0];
|
|
134
|
+
if (!option2) {
|
|
135
|
+
return context.report({
|
|
136
|
+
node,
|
|
137
|
+
messageId: "EmptyEslintConfig",
|
|
138
|
+
data: {
|
|
139
|
+
property: "{ path: Array<string>, baseDir: string }"
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
79
146
|
const baseDir = option.baseDir;
|
|
80
147
|
const absoluteTargetPaths = option.paths.flatMap((_path) => {
|
|
81
148
|
return Glob.resolvePath(_path, baseDir);
|
|
82
149
|
});
|
|
83
|
-
const allowedImportPaths = option.allowedImportPaths.flatMap((_path) => {
|
|
84
|
-
return Glob.resolvePath(_path, baseDir);
|
|
85
|
-
});
|
|
86
150
|
return {
|
|
87
151
|
//check only import declaration(ESM)
|
|
88
152
|
ImportDeclaration(node) {
|
|
@@ -90,11 +154,23 @@ var enforceBarrelPattern = {
|
|
|
90
154
|
const absoluteCurrentFilePath = context.getFilename();
|
|
91
155
|
let absoluteImportPath = null;
|
|
92
156
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
157
|
+
const aliasResult = Alias.resolvePath(
|
|
158
|
+
rawImportPath,
|
|
159
|
+
path2.dirname(absoluteCurrentFilePath)
|
|
160
|
+
);
|
|
161
|
+
if (aliasResult.type === "success") {
|
|
162
|
+
absoluteImportPath = aliasResult.absolutePath;
|
|
163
|
+
} else {
|
|
164
|
+
absoluteImportPath = resolve.sync(rawImportPath, {
|
|
165
|
+
basedir: path2.dirname(absoluteCurrentFilePath),
|
|
166
|
+
extensions: RESOLVE_EXTENSIONS
|
|
167
|
+
});
|
|
168
|
+
}
|
|
97
169
|
} catch (e) {
|
|
170
|
+
context.report({
|
|
171
|
+
node,
|
|
172
|
+
messageId: "TransformedAliasResolveFailed"
|
|
173
|
+
});
|
|
98
174
|
return;
|
|
99
175
|
}
|
|
100
176
|
{
|
|
@@ -102,7 +178,7 @@ var enforceBarrelPattern = {
|
|
|
102
178
|
const invalidDirectedImport = absoluteTargetPaths.some(
|
|
103
179
|
(absoluteTargetPath) => {
|
|
104
180
|
const targetPathEntryPoints = BARREL_ENTRY_POINT_FILE_NAMES.map(
|
|
105
|
-
(entry) =>
|
|
181
|
+
(entry) => path2.resolve(absoluteTargetPath, entry)
|
|
106
182
|
);
|
|
107
183
|
const closedTargetPath = absoluteTargetPath + "/";
|
|
108
184
|
const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
|
|
@@ -127,34 +203,127 @@ var enforceBarrelPattern = {
|
|
|
127
203
|
});
|
|
128
204
|
}
|
|
129
205
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/rules/isolate-barrel-file.ts
|
|
212
|
+
import "@typescript-eslint/types";
|
|
213
|
+
import path3 from "path";
|
|
214
|
+
import resolve2 from "resolve";
|
|
215
|
+
var RESOLVE_EXTENSIONS2 = [
|
|
216
|
+
".ts",
|
|
217
|
+
".tsx",
|
|
218
|
+
".js",
|
|
219
|
+
".jsx",
|
|
220
|
+
".d.ts",
|
|
221
|
+
".mjs",
|
|
222
|
+
".cjs"
|
|
223
|
+
];
|
|
224
|
+
var isolateBarrelFile = {
|
|
225
|
+
meta: {
|
|
226
|
+
type: "problem",
|
|
227
|
+
docs: {
|
|
228
|
+
description: "Isolate barrel file is not allowed to import outside of the barrel file"
|
|
229
|
+
},
|
|
230
|
+
schema: [
|
|
231
|
+
{
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
isolations: { type: "array", items: { type: "object" } },
|
|
235
|
+
baseDir: { type: "string" },
|
|
236
|
+
globalAllowPaths: { type: "array", items: { type: "string" } }
|
|
237
|
+
},
|
|
238
|
+
required: ["isolations", "baseDir", "globalAllowPaths"],
|
|
239
|
+
additionalProperties: false
|
|
240
|
+
}
|
|
241
|
+
],
|
|
242
|
+
messages: {
|
|
243
|
+
TransformedAliasResolveFailed: "Transformed alias resolve failed. please check the alias config.",
|
|
244
|
+
IsolatedBarrelImportDisallowed: "This barrel file is isolated. external import is not allowed. if you want to import outside of the barrel file, please add 'allowedPath' to the plugin options."
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
//default options(baseDir is current working directory. almost user execute eslint in project root)
|
|
248
|
+
defaultOptions: [
|
|
249
|
+
{
|
|
250
|
+
isolations: [],
|
|
251
|
+
baseDir: process.cwd(),
|
|
252
|
+
globalAllowPaths: []
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
create(context) {
|
|
256
|
+
const option = context.options[0];
|
|
257
|
+
const baseDir = option.baseDir;
|
|
258
|
+
const absoluteGlobalAllowPaths = option.globalAllowPaths.flatMap((path4) => {
|
|
259
|
+
return Glob.resolvePath(path4, baseDir);
|
|
260
|
+
});
|
|
261
|
+
const absoluteIsolations = option.isolations.flatMap((isolation) => {
|
|
262
|
+
const isolationPaths = Glob.resolvePath(isolation.path, baseDir);
|
|
263
|
+
const allowedPaths = isolation.allowedPaths.flatMap((path4) => {
|
|
264
|
+
return Glob.resolvePath(path4, baseDir);
|
|
265
|
+
});
|
|
266
|
+
return isolationPaths.map((isolationPath) => ({
|
|
267
|
+
isolationPath,
|
|
268
|
+
//self isolatedPath also allowed to be imported
|
|
269
|
+
allowedPaths: [...allowedPaths, isolationPath]
|
|
270
|
+
}));
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
//check only import declaration(ESM)
|
|
274
|
+
ImportDeclaration(node) {
|
|
275
|
+
const rawImportPath = node.source.value;
|
|
276
|
+
const absoluteCurrentFilePath = context.getFilename();
|
|
277
|
+
let absoluteImportPath = null;
|
|
278
|
+
try {
|
|
279
|
+
const aliasResult = Alias.resolvePath(
|
|
280
|
+
rawImportPath,
|
|
281
|
+
path3.dirname(absoluteCurrentFilePath)
|
|
282
|
+
);
|
|
283
|
+
if (aliasResult.type === "success") {
|
|
284
|
+
absoluteImportPath = aliasResult.absolutePath;
|
|
285
|
+
} else {
|
|
286
|
+
absoluteImportPath = resolve2.sync(rawImportPath, {
|
|
287
|
+
basedir: path3.dirname(absoluteCurrentFilePath),
|
|
288
|
+
extensions: RESOLVE_EXTENSIONS2
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
} catch (e) {
|
|
292
|
+
context.report({
|
|
293
|
+
node,
|
|
294
|
+
messageId: "TransformedAliasResolveFailed"
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const isolationIndex = absoluteIsolations.findIndex((isolation) => {
|
|
299
|
+
const absoluteIsolationPath = isolation.isolationPath;
|
|
300
|
+
const closedIsolationPath = absoluteIsolationPath + "/";
|
|
301
|
+
return absoluteCurrentFilePath.startsWith(closedIsolationPath);
|
|
302
|
+
});
|
|
303
|
+
const matchedIsolation = absoluteIsolations[isolationIndex];
|
|
304
|
+
if (!matchedIsolation) return;
|
|
305
|
+
const isAllowedImport = matchedIsolation.allowedPaths.some(
|
|
306
|
+
(allowedPath) => {
|
|
307
|
+
const same = absoluteImportPath === allowedPath;
|
|
308
|
+
const closedAllowedPath = allowedPath + "/";
|
|
309
|
+
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
310
|
+
return same || sub;
|
|
157
311
|
}
|
|
312
|
+
);
|
|
313
|
+
const isGlobalAllowedImport = absoluteGlobalAllowPaths.some(
|
|
314
|
+
(allowedPath) => {
|
|
315
|
+
const same = absoluteImportPath === allowedPath;
|
|
316
|
+
const closedAllowedPath = allowedPath + "/";
|
|
317
|
+
const sub = absoluteImportPath.startsWith(closedAllowedPath);
|
|
318
|
+
return same || sub;
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
const allowedImport = isAllowedImport || isGlobalAllowedImport;
|
|
322
|
+
if (!allowedImport) {
|
|
323
|
+
context.report({
|
|
324
|
+
node,
|
|
325
|
+
messageId: "IsolatedBarrelImportDisallowed"
|
|
326
|
+
});
|
|
158
327
|
}
|
|
159
328
|
}
|
|
160
329
|
};
|
|
@@ -162,7 +331,7 @@ var enforceBarrelPattern = {
|
|
|
162
331
|
};
|
|
163
332
|
|
|
164
333
|
// src/rules/no-wildcard.ts
|
|
165
|
-
import "@typescript-eslint/
|
|
334
|
+
import "@typescript-eslint/types";
|
|
166
335
|
var noWildcard = {
|
|
167
336
|
meta: {
|
|
168
337
|
type: "problem",
|
|
@@ -202,6 +371,7 @@ var noWildcard = {
|
|
|
202
371
|
// src/index.ts
|
|
203
372
|
var rules = {
|
|
204
373
|
"enforce-barrel-pattern": enforceBarrelPattern,
|
|
374
|
+
"isolate-barrel-file": isolateBarrelFile,
|
|
205
375
|
"no-wildcard": noWildcard
|
|
206
376
|
};
|
|
207
377
|
var index_default = { rules };
|
package/package.json
CHANGED
|
@@ -25,15 +25,21 @@
|
|
|
25
25
|
"isolated barrel module",
|
|
26
26
|
"no-wildcard"
|
|
27
27
|
],
|
|
28
|
-
"version": "1.
|
|
28
|
+
"version": "1.3.0",
|
|
29
29
|
"type": "module",
|
|
30
30
|
"main": "dist/index.cjs",
|
|
31
31
|
"module": "dist/index.js",
|
|
32
32
|
"types": "dist/index.d.ts",
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@types/jest": "^30.0.0",
|
|
35
|
+
"@typescript-eslint/parser": "^8.46.2",
|
|
36
|
+
"@typescript-eslint/rule-tester": "^8.46.2",
|
|
37
|
+
"@typescript-eslint/types": "^8.46.2",
|
|
34
38
|
"@typescript-eslint/utils": "^8.36.0",
|
|
35
39
|
"fast-glob": "^3.3.3",
|
|
40
|
+
"jest": "^30.2.0",
|
|
36
41
|
"resolve": "^1.22.10",
|
|
42
|
+
"ts-jest": "^29.4.5",
|
|
37
43
|
"tsconfig-paths": "^4.2.0"
|
|
38
44
|
},
|
|
39
45
|
"devDependencies": {
|
|
@@ -43,8 +49,11 @@
|
|
|
43
49
|
"typescript": "~5.8.3"
|
|
44
50
|
},
|
|
45
51
|
"scripts": {
|
|
46
|
-
"build": "tsup src/index.ts --dts --format cjs,esm",
|
|
52
|
+
"build": "pnpm run test && tsup src/index.ts --dts --format cjs,esm",
|
|
47
53
|
"type-check": "tsc --noEmit",
|
|
54
|
+
"test": "jest",
|
|
55
|
+
"test:watch": "jest --watch",
|
|
56
|
+
"test:coverage": "jest --coverage",
|
|
48
57
|
"release": "pnpm run build && pnpm publish --access=public"
|
|
49
58
|
}
|
|
50
59
|
}
|