eslint-plugin-barrel-rules 1.1.0 → 1.1.3

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.0-blue.svg" alt="Version"/>
6
+ <img src="https://img.shields.io/badge/version-1.1.3-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
  ## 지원 환경
@@ -48,11 +52,44 @@ JavaScript/TypeScript 프로젝트에서 Barrel Pattern(배럴 패턴)을 강제
48
52
  (예: `import ... from "../domains/foo"`는 허용,
49
53
  `import ... from "../domains/foo/components/Bar"`는 차단)
50
54
 
55
+ - **Isolation Barrel Module**
56
+ 지정한 barrel path 외부의 모듈이 내부 파일을 직접 import하지 못하도록 막을 수 있습니다.
57
+ `isolated: true` 옵션을 사용하면 같은 barrel path 내부에서는 자유롭게 import가 가능하고,
58
+ 외부에서는 해당 barrel path로의 import가 모두 차단됩니다. (barrel(index) 파일을 통한 접근도 불가)
59
+ 만약 특정 공유 import 경로만 허용하고 싶다면 `allowedImportPaths` 옵션을 사용할 수 있습니다.
60
+ 이를 통해 각 모듈의 경계를 엄격하게 보호하고, 모듈의 독립성을 유지할 수 있습니다.
61
+
62
+ - **와일드카드 import/export 방지**
63
+ `import * as foo from "module"` 또는 `export * from "./module"`과 같은 와일드카드(네임스페이스) import/export를 금지합니다.
64
+ 명시적인 이름 기반 import/export만 허용하여 트리쉐이킹과 코드 명확성을 높입니다.
65
+
51
66
  - **고성능 glob 매칭**
52
67
  `src/domains/*`처럼 glob 패턴으로 여러 디렉토리 지정 가능
53
68
 
54
69
  ---
55
70
 
71
+ ## 규칙(Rules)
72
+
73
+ 1. **enforce-barrel-pattern**
74
+ 모듈 import 시 barrel 패턴을 강제합니다.
75
+ 지정한 barrel 파일(예: index.ts)로만 import를 허용하고, 내부 모듈에 대한 직접 접근을 차단합니다.
76
+ `isolated: true` 옵션을 사용하면 같은 barrel path 내부 파일끼리만 import가 가능하며, 외부에서의 import는 barrel 파일을 통한 접근도 모두 차단됩니다.
77
+ `allowedImportPaths` 옵션을 사용하면 특정 공유 import 경로만 예외적으로 허용할 수 있습니다.
78
+
79
+ - **옵션:**
80
+ - `paths`: barrel 패턴을 적용할 디렉토리 목록(`baseDir` 기준 상대경로)
81
+ - `baseDir` (선택): `paths` 기준이 되는 베이스 디렉토리 (기본값: ESLint 실행 디렉토리)
82
+ - `isolated` (선택): `true`일 경우, barrel path 외부에서의 모든 import를 차단합니다(barrel 파일 통한 접근 포함). 같은 barrel path 내부 또는 `allowedImportPaths`만 허용.
83
+ - `allowedImportPaths` (선택): isolation 모드에서도 직접 import를 허용할 경로 배열
84
+
85
+ 2. **no-wildcard**
86
+ `import * as foo from "module"` 또는 `export * from "./module"`과 같은 와일드카드(네임스페이스) import/export를 금지합니다.
87
+ `enforce-barrel-pattern` 룰과 함께 사용하는 것을 적극 추천합니다.
88
+ 두 룰을 함께 적용하면 모듈 경계를 엄격하게 지킬 수 있을 뿐만 아니라,
89
+ 트리쉐이킹을 통한 성능 향상과 코드 추적 및 유지보수의 용이성까지 모두 얻을 수 있습니다.
90
+
91
+ ---
92
+
56
93
  ## 설치
57
94
 
58
95
  ```bash
@@ -84,8 +121,15 @@ module.exports = {
84
121
  // (옵션) 설정하지 않으면 기본값은 ESLint를 실행한 위치(작업 디렉토리)입니다.
85
122
  // 예: `npx eslint .`처럼 실행하면, 실행 시점의 현재 디렉토리가 기본값이 됩니다.
86
123
  baseDir: __dirname,
124
+ // isolation 모드 활성화: barrel path 외부에서의 모든 import를 차단합니다.
125
+ isolated: true,
126
+ // "shared" 디렉토리만 직접 import를 허용합니다.
127
+ // 필요에 따라 이 배열에 "node_modules/*" 등 원하는 경로를 자유롭게 추가할 수 있습니다.
128
+ allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
87
129
  },
88
130
  ],
131
+ // import * 또는 export * 금지
132
+ "barrel-rules/no-wildcard": ["error"],
89
133
  },
90
134
  };
91
135
  ```
@@ -131,8 +175,15 @@ export default tseslint.config([
131
175
  // (옵션) 설정하지 않으면 기본값은 ESLint를 실행한 위치(작업 디렉토리)입니다.
132
176
  // 예: `npx eslint .`처럼 실행하면, 실행 시점의 현재 디렉토리가 기본값이 됩니다.
133
177
  baseDir: __dirname,
178
+ // isolation 모드 활성화: barrel path 외부에서의 모든 import를 차단합니다.
179
+ isolated: true,
180
+ // "shared" 디렉토리만 직접 import를 허용합니다.
181
+ // 필요에 따라 이 배열에 "node_modules/*" 등 원하는 경로를 자유롭게 추가할 수 있습니다.
182
+ allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
134
183
  },
135
184
  ],
185
+ // import * 또는 export * 금지
186
+ "barrel-rules/no-wildcard": ["error"],
136
187
  },
137
188
  },
138
189
  ]);
@@ -154,12 +205,14 @@ import { Test } from "../domains/foo";
154
205
 
155
206
  ## 앞으로의 계획
156
207
 
157
- - 더 다양한 모듈 경계/추상화 관련 룰 추가 예정
158
- - Alias/tsconfig 지원: TypeScript의 paths, Vite의 resolve.alias, 기타 커스텀 경로 매핑을 완벽하게 지원
208
+ - 더 다양한 모듈 경계/추상화 관련 룰 추가 예정 (~Ing)
209
+ - Alias/tsconfig 지원: TypeScript의 paths, Vite의 resolve.alias, 기타 커스텀 경로 매핑을 완벽하게 지원 (~Ing)
159
210
  - **CJS 지원** (OK)
160
211
  - **ESLint 8 지원** (OK)
161
212
  - **번들 플러그인(플러그인 내 모든 기능 통합)** (OK)
162
213
  - **잘못된 경로 설정 검증 기능** (OK)
214
+ - **와일드카드 import/export 제한 규칙** (OK)
215
+ - **지정한 Barrel 경로 격리** (OK)
163
216
 
164
217
  ---
165
218
 
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.0-blue.svg" alt="Version"/>
6
+ <img src="https://img.shields.io/badge/version-1.1.3-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
@@ -51,11 +55,44 @@ Direct imports from internal files are blocked, maximizing
51
55
  (e.g., `import ... from "../domains/foo"` is allowed,
52
56
  but `import ... from "../domains/foo/components/Bar"` is blocked)
53
57
 
58
+ - **Isolation Barrel Module**
59
+ You can prevent modules outside the specified barrel path from directly importing internal files.
60
+ By enabling `isolated: true`, only files within the same barrel path can freely import each other.
61
+ 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 `allowedImportPaths` option.
63
+ This helps you strictly protect your module boundaries and keep each module truly independent.
64
+
65
+ - **Prevent Wildcard Import/Export**
66
+ Disallows wildcard (namespace) imports and exports such as `import * as foo from "module"` or `export * from "./module"`.
67
+ This enforces the use of named imports/exports for better tree-shaking and code clarity.
68
+
54
69
  - **High-performance glob matching**
55
70
  Specify multiple directories using glob patterns like `src/domains/*`
56
71
 
57
72
  ---
58
73
 
74
+ ## Rules
75
+
76
+ 1. **enforce-barrel-pattern**
77
+ Enforces the barrel pattern for module imports.
78
+ 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
+
82
+ - **Options:**
83
+ - `paths`: The directories to be protected by the barrel pattern (relative to `baseDir`).
84
+ - `baseDir` (optional): The base directory for resolving `paths`. Defaults to the ESLint execution directory.
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.
87
+
88
+ 2. **no-wildcard**
89
+ Disallows wildcard (namespace) imports such as `import * as foo from "module"` or `export * from "./module"`.
90
+ We highly recommend enabling this rule together with the `enforce-barrel-pattern` rule.
91
+ Using both rules together not only enforces strict module boundaries,
92
+ but also improves performance through better tree-shaking and makes code tracing and maintenance much easier.
93
+
94
+ ---
95
+
59
96
  ## Install
60
97
 
61
98
  ```bash
@@ -87,8 +124,15 @@ module.exports = {
87
124
  // Optional config. The default value is the directory where ESLint is executed.
88
125
  // For example, if you run `npx eslint .`, the default will be the current working directory at the time of execution.
89
126
  baseDir: __dirname,
127
+ // Enable isolation mode: block all imports from outside the barrel path
128
+ isolated: true,
129
+ // Allow direct imports only from the "shared" directory.
130
+ // You can customize this array as needed, e.g., add "node_modules/..." or any other path you want to allow.
131
+ allowedImportPaths: ["src/typescript/shared", "node_modules/*"],
90
132
  },
91
133
  ],
134
+ // Disallow wildcard (namespace) import/export.
135
+ "barrel-rules/no-wildcard": ["error"],
92
136
  },
93
137
  };
94
138
  ```
@@ -134,8 +178,15 @@ export default tseslint.config([
134
178
  // Optional config. The default value is the directory where ESLint is executed.
135
179
  // For example, if you run `npx eslint .`, the default will be the current working directory at the time of execution.
136
180
  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/*"],
137
186
  },
138
187
  ],
188
+ // Disallow wildcard (namespace) import/export.
189
+ "barrel-rules/no-wildcard": ["error"],
139
190
  },
140
191
  },
141
192
  ]);
@@ -157,16 +208,18 @@ import { Test } from "../domains/foo";
157
208
 
158
209
  ## Future Work
159
210
 
160
- - More rules for module boundaries and abstraction
211
+ - More rules for module boundaries and abstraction (~Ing)
161
212
 
162
213
  - **Alias/tsconfig Support**
163
- Fully supports TypeScript `paths`, Vite `resolve.alias`, and other custom path mappings
214
+ Fully supports TypeScript `paths`, Vite `resolve.alias`, and other custom path mappings (~Ing)
164
215
 
165
216
  - **CJS Support** (OK)
166
217
  - **Eslint8 Support** (OK)
167
218
  - **Bundle Plugin(capsure any features in plugin)**
168
219
  (OK)
169
220
  - **Wrong Path Setup Validator** (OK)
221
+ - **Wildcard Import/Export Protection Rule** (OK)
222
+ - **Isolation Barrel Module** (OK)
170
223
 
171
224
  ---
172
225
 
package/dist/index.cjs CHANGED
@@ -38,7 +38,45 @@ module.exports = __toCommonJS(index_exports);
38
38
  var import_utils = require("@typescript-eslint/utils");
39
39
  var import_path = __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(path2, baseDir) {
46
+ const globResult = import_fast_glob.default.sync(path2, {
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: ${path2}, any directory was not found`
54
+ );
55
+ }
56
+ return globResult;
57
+ }
58
+ };
59
+
60
+ // src/rules/enforce-barrel-pattern.ts
61
+ var BARREL_ENTRY_POINT_FILE_NAMES = [
62
+ "index.ts",
63
+ "index.tsx",
64
+ "index.js",
65
+ "index.jsx",
66
+ "index.cjs",
67
+ "index.mjs",
68
+ "index.d.ts"
69
+ ];
70
+ var RESOLVE_EXTENSIONS = [
71
+ ".ts",
72
+ ".tsx",
73
+ ".js",
74
+ ".jsx",
75
+ ".json",
76
+ ".d.ts",
77
+ ".mjs",
78
+ ".cjs"
79
+ ];
42
80
  var enforceBarrelPattern = {
43
81
  meta: {
44
82
  type: "problem",
@@ -50,33 +88,36 @@ var enforceBarrelPattern = {
50
88
  type: "object",
51
89
  properties: {
52
90
  paths: { type: "array", items: { type: "string" } },
53
- baseDir: { type: "string" }
91
+ baseDir: { type: "string" },
92
+ isolated: { type: "boolean" },
93
+ allowedImportPaths: { type: "array", items: { type: "string" } }
54
94
  },
55
95
  required: ["paths"],
56
96
  additionalProperties: false
57
97
  }
58
98
  ],
59
99
  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."
100
+ 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
+ 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
102
  }
62
103
  },
63
104
  //default options(baseDir is current working directory. almost user execute eslint in project root)
64
- defaultOptions: [{ paths: [], baseDir: process.cwd() }],
105
+ defaultOptions: [
106
+ {
107
+ paths: [],
108
+ baseDir: process.cwd(),
109
+ isolated: false,
110
+ allowedImportPaths: []
111
+ }
112
+ ],
65
113
  create(context) {
66
114
  const option = context.options[0];
67
115
  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;
116
+ const absoluteTargetPaths = option.paths.flatMap((_path) => {
117
+ return Glob.resolvePath(_path, baseDir);
118
+ });
119
+ const allowedImportPaths = option.allowedImportPaths.flatMap((_path) => {
120
+ return Glob.resolvePath(_path, baseDir);
80
121
  });
81
122
  return {
82
123
  //check only import declaration(ESM)
@@ -87,47 +128,108 @@ var enforceBarrelPattern = {
87
128
  try {
88
129
  absoluteImportPath = import_resolve.default.sync(rawImportPath, {
89
130
  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
- ]
131
+ extensions: RESOLVE_EXTENSIONS
100
132
  });
101
133
  } catch (e) {
102
134
  return;
103
135
  }
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;
136
+ {
137
+ let matchedLatestTargetPath = null;
138
+ const invalidDirectedImport = absoluteTargetPaths.some(
139
+ (absoluteTargetPath) => {
140
+ const targetPathEntryPoints = BARREL_ENTRY_POINT_FILE_NAMES.map(
141
+ (entry) => import_path.default.resolve(absoluteTargetPath, entry)
142
+ );
143
+ const closedTargetPath = absoluteTargetPath + "/";
144
+ const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
145
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
146
+ const currentFileInEnforceBarrel = absoluteCurrentFilePath.startsWith(closedTargetPath);
147
+ const importedOutsideOfTargetPath = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
148
+ const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
149
+ if (invalidImported) {
150
+ matchedLatestTargetPath = absoluteTargetPath;
151
+ }
152
+ return invalidImported;
153
+ }
154
+ );
155
+ if (invalidDirectedImport) {
156
+ context.report({
157
+ node,
158
+ messageId: "DirectImportDisallowed",
159
+ data: {
160
+ rawImportPath,
161
+ matchedTargetPath: matchedLatestTargetPath
162
+ }
163
+ });
121
164
  }
122
- return invalidImported;
123
- });
124
- if (invalidDirectedImport) {
165
+ }
166
+ {
167
+ if (option.isolated) {
168
+ const currentFileInEnforceBarrel = absoluteTargetPaths.some(
169
+ (absoluteTargetPath) => {
170
+ const closedTargetPath = absoluteTargetPath + "/";
171
+ return absoluteCurrentFilePath.startsWith(closedTargetPath);
172
+ }
173
+ );
174
+ const sameBarrel = absoluteTargetPaths.some(
175
+ (absoluteTargetPath) => {
176
+ const closedTargetPath = absoluteTargetPath + "/";
177
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
178
+ const currentFileInEnforceBarrel2 = absoluteCurrentFilePath.startsWith(closedTargetPath);
179
+ return importedEnforceBarrelFile && currentFileInEnforceBarrel2;
180
+ }
181
+ );
182
+ const allowedImportPath = allowedImportPaths.some(
183
+ (allowedImportPath2) => {
184
+ return absoluteImportPath.startsWith(allowedImportPath2);
185
+ }
186
+ );
187
+ if (!allowedImportPath && !sameBarrel && currentFileInEnforceBarrel) {
188
+ context.report({
189
+ node,
190
+ messageId: "IsolatedBarrelImportDisallowed"
191
+ });
192
+ }
193
+ }
194
+ }
195
+ }
196
+ };
197
+ }
198
+ };
199
+
200
+ // src/rules/no-wildcard.ts
201
+ var import_utils2 = require("@typescript-eslint/utils");
202
+ var noWildcard = {
203
+ meta: {
204
+ type: "problem",
205
+ docs: {
206
+ description: "Wildcard (namespace) import is not allowed."
207
+ },
208
+ schema: [],
209
+ messages: {
210
+ NoWildcardImport: "Wildcard import (`import * as ... from ...`) is not allowed. Please use named imports instead.",
211
+ NoExportAll: "Export all (`export * from ...`) is not allowed. Please use named exports instead."
212
+ }
213
+ },
214
+ defaultOptions: [],
215
+ create(context) {
216
+ return {
217
+ ImportDeclaration(node) {
218
+ const hasNamespaceImport = node.specifiers.some(
219
+ (specifier) => specifier.type === "ImportNamespaceSpecifier"
220
+ );
221
+ if (hasNamespaceImport) {
125
222
  context.report({
126
223
  node,
127
- messageId: "DirectImportDisallowed",
128
- data: { rawImportPath, matchedTargetPath: matchedLatestTargetPath }
224
+ messageId: "NoWildcardImport"
129
225
  });
130
226
  }
227
+ },
228
+ ExportAllDeclaration(node) {
229
+ context.report({
230
+ node,
231
+ messageId: "NoExportAll"
232
+ });
131
233
  }
132
234
  };
133
235
  }
@@ -135,7 +237,8 @@ var enforceBarrelPattern = {
135
237
 
136
238
  // src/index.ts
137
239
  var rules = {
138
- "enforce-barrel-pattern": enforceBarrelPattern
240
+ "enforce-barrel-pattern": enforceBarrelPattern,
241
+ "no-wildcard": noWildcard
139
242
  };
140
243
  var index_default = { rules };
141
244
  module.exports = { rules };
package/dist/index.d.cts CHANGED
@@ -2,10 +2,13 @@ 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>;
11
+ "no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
9
12
  };
10
13
  };
11
14
 
package/dist/index.d.ts CHANGED
@@ -2,10 +2,13 @@ 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>;
11
+ "no-wildcard": _typescript_eslint_utils_ts_eslint.RuleModule<"NoWildcardImport" | "NoExportAll", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
9
12
  };
10
13
  };
11
14
 
package/dist/index.js CHANGED
@@ -2,7 +2,45 @@
2
2
  import "@typescript-eslint/utils";
3
3
  import path 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(path2, baseDir) {
10
+ const globResult = FastGlob.sync(path2, {
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: ${path2}, any directory was not found`
18
+ );
19
+ }
20
+ return globResult;
21
+ }
22
+ };
23
+
24
+ // src/rules/enforce-barrel-pattern.ts
25
+ var BARREL_ENTRY_POINT_FILE_NAMES = [
26
+ "index.ts",
27
+ "index.tsx",
28
+ "index.js",
29
+ "index.jsx",
30
+ "index.cjs",
31
+ "index.mjs",
32
+ "index.d.ts"
33
+ ];
34
+ var RESOLVE_EXTENSIONS = [
35
+ ".ts",
36
+ ".tsx",
37
+ ".js",
38
+ ".jsx",
39
+ ".json",
40
+ ".d.ts",
41
+ ".mjs",
42
+ ".cjs"
43
+ ];
6
44
  var enforceBarrelPattern = {
7
45
  meta: {
8
46
  type: "problem",
@@ -14,33 +52,36 @@ var enforceBarrelPattern = {
14
52
  type: "object",
15
53
  properties: {
16
54
  paths: { type: "array", items: { type: "string" } },
17
- baseDir: { type: "string" }
55
+ baseDir: { type: "string" },
56
+ isolated: { type: "boolean" },
57
+ allowedImportPaths: { type: "array", items: { type: "string" } }
18
58
  },
19
59
  required: ["paths"],
20
60
  additionalProperties: false
21
61
  }
22
62
  ],
23
63
  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."
64
+ 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
+ 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
66
  }
26
67
  },
27
68
  //default options(baseDir is current working directory. almost user execute eslint in project root)
28
- defaultOptions: [{ paths: [], baseDir: process.cwd() }],
69
+ defaultOptions: [
70
+ {
71
+ paths: [],
72
+ baseDir: process.cwd(),
73
+ isolated: false,
74
+ allowedImportPaths: []
75
+ }
76
+ ],
29
77
  create(context) {
30
78
  const option = context.options[0];
31
79
  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;
80
+ const absoluteTargetPaths = option.paths.flatMap((_path) => {
81
+ return Glob.resolvePath(_path, baseDir);
82
+ });
83
+ const allowedImportPaths = option.allowedImportPaths.flatMap((_path) => {
84
+ return Glob.resolvePath(_path, baseDir);
44
85
  });
45
86
  return {
46
87
  //check only import declaration(ESM)
@@ -51,47 +92,108 @@ var enforceBarrelPattern = {
51
92
  try {
52
93
  absoluteImportPath = resolve.sync(rawImportPath, {
53
94
  basedir: path.dirname(absoluteCurrentFilePath),
54
- extensions: [
55
- ".ts",
56
- ".tsx",
57
- ".js",
58
- ".jsx",
59
- ".json",
60
- ".d.ts",
61
- ".mjs",
62
- ".cjs"
63
- ]
95
+ extensions: RESOLVE_EXTENSIONS
64
96
  });
65
97
  } catch (e) {
66
98
  return;
67
99
  }
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;
100
+ {
101
+ let matchedLatestTargetPath = null;
102
+ const invalidDirectedImport = absoluteTargetPaths.some(
103
+ (absoluteTargetPath) => {
104
+ const targetPathEntryPoints = BARREL_ENTRY_POINT_FILE_NAMES.map(
105
+ (entry) => path.resolve(absoluteTargetPath, entry)
106
+ );
107
+ const closedTargetPath = absoluteTargetPath + "/";
108
+ const targetPathEntryPointed = targetPathEntryPoints.includes(absoluteImportPath);
109
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
110
+ const currentFileInEnforceBarrel = absoluteCurrentFilePath.startsWith(closedTargetPath);
111
+ const importedOutsideOfTargetPath = !currentFileInEnforceBarrel && importedEnforceBarrelFile;
112
+ const invalidImported = !targetPathEntryPointed && importedOutsideOfTargetPath;
113
+ if (invalidImported) {
114
+ matchedLatestTargetPath = absoluteTargetPath;
115
+ }
116
+ return invalidImported;
117
+ }
118
+ );
119
+ if (invalidDirectedImport) {
120
+ context.report({
121
+ node,
122
+ messageId: "DirectImportDisallowed",
123
+ data: {
124
+ rawImportPath,
125
+ matchedTargetPath: matchedLatestTargetPath
126
+ }
127
+ });
85
128
  }
86
- return invalidImported;
87
- });
88
- if (invalidDirectedImport) {
129
+ }
130
+ {
131
+ if (option.isolated) {
132
+ const currentFileInEnforceBarrel = absoluteTargetPaths.some(
133
+ (absoluteTargetPath) => {
134
+ const closedTargetPath = absoluteTargetPath + "/";
135
+ return absoluteCurrentFilePath.startsWith(closedTargetPath);
136
+ }
137
+ );
138
+ const sameBarrel = absoluteTargetPaths.some(
139
+ (absoluteTargetPath) => {
140
+ const closedTargetPath = absoluteTargetPath + "/";
141
+ const importedEnforceBarrelFile = absoluteImportPath.startsWith(closedTargetPath);
142
+ const currentFileInEnforceBarrel2 = absoluteCurrentFilePath.startsWith(closedTargetPath);
143
+ return importedEnforceBarrelFile && currentFileInEnforceBarrel2;
144
+ }
145
+ );
146
+ const allowedImportPath = allowedImportPaths.some(
147
+ (allowedImportPath2) => {
148
+ return absoluteImportPath.startsWith(allowedImportPath2);
149
+ }
150
+ );
151
+ if (!allowedImportPath && !sameBarrel && currentFileInEnforceBarrel) {
152
+ context.report({
153
+ node,
154
+ messageId: "IsolatedBarrelImportDisallowed"
155
+ });
156
+ }
157
+ }
158
+ }
159
+ }
160
+ };
161
+ }
162
+ };
163
+
164
+ // src/rules/no-wildcard.ts
165
+ import "@typescript-eslint/utils";
166
+ var noWildcard = {
167
+ meta: {
168
+ type: "problem",
169
+ docs: {
170
+ description: "Wildcard (namespace) import is not allowed."
171
+ },
172
+ schema: [],
173
+ messages: {
174
+ NoWildcardImport: "Wildcard import (`import * as ... from ...`) is not allowed. Please use named imports instead.",
175
+ NoExportAll: "Export all (`export * from ...`) is not allowed. Please use named exports instead."
176
+ }
177
+ },
178
+ defaultOptions: [],
179
+ create(context) {
180
+ return {
181
+ ImportDeclaration(node) {
182
+ const hasNamespaceImport = node.specifiers.some(
183
+ (specifier) => specifier.type === "ImportNamespaceSpecifier"
184
+ );
185
+ if (hasNamespaceImport) {
89
186
  context.report({
90
187
  node,
91
- messageId: "DirectImportDisallowed",
92
- data: { rawImportPath, matchedTargetPath: matchedLatestTargetPath }
188
+ messageId: "NoWildcardImport"
93
189
  });
94
190
  }
191
+ },
192
+ ExportAllDeclaration(node) {
193
+ context.report({
194
+ node,
195
+ messageId: "NoExportAll"
196
+ });
95
197
  }
96
198
  };
97
199
  }
@@ -99,7 +201,8 @@ var enforceBarrelPattern = {
99
201
 
100
202
  // src/index.ts
101
203
  var rules = {
102
- "enforce-barrel-pattern": enforceBarrelPattern
204
+ "enforce-barrel-pattern": enforceBarrelPattern,
205
+ "no-wildcard": noWildcard
103
206
  };
104
207
  var index_default = { rules };
105
208
  module.exports = { rules };
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.0",
28
+ "version": "1.1.3",
27
29
  "type": "module",
28
30
  "main": "dist/index.cjs",
29
31
  "module": "dist/index.js",