eslint-plugin-smarthr 6.11.0 → 6.12.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [6.12.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.11.0...eslint-plugin-smarthr-v6.12.0) (2026-04-15)
6
+
7
+
8
+ ### Features
9
+
10
+ * **require-barrel-import:** additionalBarrelFileNamesオプション追加と同階層・子階層からのバレルimport禁止 ([#1244](https://github.com/kufu/tamatebako/issues/1244)) ([b38a143](https://github.com/kufu/tamatebako/commit/b38a1432a92bd3ee4e52f3a450177e59f65512f0))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **format-import-path:** dirCount関数でmatch結果がnullの場合のエラーを修正 ([#1246](https://github.com/kufu/tamatebako/issues/1246)) ([fc50fba](https://github.com/kufu/tamatebako/commit/fc50fbadc3da3b046a73bbab664c281fc7be847f))
16
+
5
17
  ## [6.11.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.10.4...eslint-plugin-smarthr-v6.11.0) (2026-04-14)
6
18
 
7
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-smarthr",
3
- "version": "6.11.0",
3
+ "version": "6.12.0",
4
4
  "author": "SmartHR",
5
5
  "license": "MIT",
6
6
  "description": "A sharable ESLint plugin for SmartHR",
@@ -34,5 +34,5 @@
34
34
  "eslintplugin",
35
35
  "smarthr"
36
36
  ],
37
- "gitHead": "17b3e279b5cf8ffeef90d1eccce77101c7b68cba"
37
+ "gitHead": "ae65de908cf0b81ad201a18e376bb500a9bd7842"
38
38
  }
@@ -28,7 +28,11 @@ const DIR_SEPARATE_REGEX = /\//g
28
28
  const MULTIPLE_DIR_SEPARATE_REGEX =/(\/)+/g
29
29
  const TRAILING_SLASH_REGEX = /^(.+?)\/$/
30
30
 
31
- const dirCount = (dir) => dir.match(DIR_SEPARATE_REGEX).length
31
+ const dirCount = (dir) => {
32
+ if (!dir) return 0
33
+ const matches = dir.match(DIR_SEPARATE_REGEX)
34
+ return matches ? matches.length : 0
35
+ }
32
36
 
33
37
  const convertType = (calcContext, calcDomainNode) => {
34
38
  const { option: { format: { all, outside, globalModule, module, domain, lower } } } = calcContext
@@ -24,6 +24,49 @@ barrelを経由することで、内部のファイル構成を変更しても
24
24
 
25
25
  例えば、`Page/parts/Menu/Item` を `Page/parts/Menu` から importすることで、import文がより簡潔で読みやすくなります。
26
26
 
27
+ ## 同階層・子階層からのバレルimportの禁止
28
+
29
+ バレルファイル(index.ts、client.ts等)と同じディレクトリまたは子ディレクトリ内からバレルファイルをimportすることを禁止します。
30
+
31
+ ### なぜ禁止する必要があるのか
32
+
33
+ バレルファイルは**ディレクトリ外部**へのエクスポート専用として設計されています。同じディレクトリ内や子ディレクトリからバレルファイルをimportすると、以下の問題が発生します:
34
+
35
+ 1. **循環参照のリスク**: バレルファイルが内部のファイルをexportし、その内部ファイルがバレルファイルをimportする循環が発生しやすい
36
+ 2. **カプセル化の崩壊**: バレルファイルは外部向けAPIであり、内部からimportすると内部実装の境界が曖昧になる
37
+ 3. **不要な依存**: 直接相対パスでimportできるものをバレル経由でimportすることは冗長
38
+
39
+ ### ❌ 検出されるエラーケース
40
+
41
+ ```typescript
42
+ // src/components/Button/Button.tsx から同じディレクトリのバレルをimport
43
+ import { ButtonProps } from '.' // NG
44
+ import { ButtonProps } from './index' // NG
45
+ import { ButtonProps } from '@/components/Button' // NG
46
+
47
+ // src/components/Button/_utils/helper.ts から親のバレルをimport
48
+ import { Button } from '..' // NG
49
+ import { Button } from '../index' // NG
50
+ import { Button } from '@/components/Button' // NG
51
+
52
+ // client.ts を使用している場合
53
+ import { ButtonPresentation } from './client' // NG
54
+ ```
55
+
56
+ ### ✅ 正しいimport方法
57
+
58
+ ```typescript
59
+ // 同じディレクトリ内では直接相対パスを使用
60
+ import { ButtonProps } from './types' // OK
61
+
62
+ // 親ディレクトリのファイルも直接相対パスを使用
63
+ import { buttonUtils } from '../utils' // OK
64
+
65
+ // バレルファイルは外部ディレクトリからのみimport
66
+ // src/pages/HomePage.tsx から
67
+ import { Button } from '@/components/Button' // OK
68
+ ```
69
+
27
70
  ## config
28
71
 
29
72
  ### 必須設定
@@ -43,6 +86,95 @@ tsconfig.json の compilerOptions.pathsに `@/*` もしくは `~/*` としてroo
43
86
 
44
87
  特定のファイルから特定のimportを許可する設定を記述できます。
45
88
 
89
+ ### additionalBarrelFileNames
90
+
91
+ `index` 以外にbarrelファイルとして扱うファイル名を配列で指定します(拡張子なし)。
92
+
93
+ Next.jsなどで使用される `client.ts` をbarrelファイルとして扱いたい場合に使用します。
94
+
95
+ - デフォルト: `[]`(`index.*` のみがbarrelファイル)
96
+ - 例: `['client']` を指定すると、`client.ts`, `client.tsx` などもbarrelファイルとして扱われます
97
+ - 複数指定も可能: `['client', 'server']` を指定すると、`server.ts`, `server.tsx` なども追加されます
98
+
99
+ #### 優先順位とチェックルール
100
+
101
+ **1. 同じディレクトリ内に複数のbarrelがある場合**
102
+ - `client.ts` と `index.ts` が両方ある場合、どちらからのimportも許容されます
103
+ - 例: `import { Foo } from './api'` も `import { Foo } from './api/client'` もOK
104
+
105
+ **2. 同じファイル名の場合は親を優先**
106
+
107
+ 探索により同じファイル名のbarrelが複数見つかった場合(例: `client.ts`同士、`index.ts`同士)、より親のbarrelを推奨します。
108
+
109
+ ```typescript
110
+ // 例: 同じファイル名の場合(index同士)
111
+ route/
112
+ index.ts ← より親を推奨
113
+ edit/
114
+ index.ts
115
+
116
+ // import { Foo } from './route/edit/Foo'
117
+ // → route/index.ts を推奨(より親のbarrel)
118
+ ```
119
+
120
+ **index.ts経由のre-export対応:**
121
+
122
+ 子で`index.ts`を見つけた場合でも、親方向に`client.ts`があれば、そちらを優先します。
123
+
124
+ ```typescript
125
+ // 子が index.ts、親が client.ts のパターン
126
+ route/
127
+ client.ts ← これを推奨
128
+ edit/
129
+ index.ts
130
+
131
+ // import { Foo } from './route/edit/Foo'
132
+ // → route/client.ts を推奨(親のclient.tsが見つかった)
133
+
134
+ // この場合、route/client.ts で edit/index.ts をre-exportする想定:
135
+ // route/client.ts
136
+ export * from './edit'
137
+ ```
138
+
139
+ **3. エラーメッセージの表示**
140
+
141
+ `additionalBarrelFileNames`が設定されている場合、エラーメッセージには存在しないbarrelファイルも含めて全ての選択肢が表示されます。
142
+
143
+ ```typescript
144
+ // 例: additionalBarrelFileNames: ['client'] 設定時、index.tsのみ存在
145
+ 検出されたバレル: @/components/api/index.ts
146
+ 現在のimport: import { fetchUser } from '@/components/api/user'
147
+ 推奨されるimport(以下のいずれか):
148
+ - import { fetchUser } from '@/components/api' // index.ts
149
+ - import { fetchUser } from '@/components/api/client' // client.ts (作成が必要)
150
+
151
+ ※ 存在しないバレルファイルは必要に応じて作成してください。
152
+ ```
153
+
154
+ - `index.ts`が優先的に表示されます(常に最初)
155
+ - 存在しないファイルには `(作成が必要)` マークが表示されます
156
+ - 存在しないファイルがある場合、注意メッセージが追加されます
157
+ - 「検出されたバレル」には実際に存在するファイルのみが表示されます
158
+
159
+ **4. 階層の一貫性チェック**
160
+
161
+ 子ディレクトリで`client.ts`を使用している場合、親ディレクトリにも同名のbarrelを作成することを促します。これにより、プロジェクト全体でbarrel構造の一貫性を保ちます。
162
+
163
+ ```typescript
164
+ // 例: client.tsパターンの一貫性チェック
165
+ route/
166
+ index.ts ← client.tsがない
167
+ edit/
168
+ client.ts ← client.tsを使用
169
+
170
+ // import { Foo } from './route/edit/client'
171
+ // → エラー: route/client.ts を作成して、edit/client のexportをまとめてください
172
+
173
+ // この場合、以下のようにroute/client.tsを作成する必要があります:
174
+ // route/client.ts
175
+ export * from './edit/client'
176
+ ```
177
+
46
178
  ## rules
47
179
 
48
180
  ```js
@@ -50,13 +182,16 @@ tsconfig.json の compilerOptions.pathsに `@/*` もしくは `~/*` としてroo
50
182
  rules: {
51
183
  'smarthr/require-barrel-import': [
52
184
  'error',
53
- // ignores: ['\\/test\\/'], // 除外したいファイルの正規表現
54
- // allowedImports: {
55
- // '/any/path/': { // 正規表現でチェックするファイルを指定
56
- // // import制御するファイル (相対パスを指定する場合、.eslintrc.js を基準とする)
57
- // '@/hoge/fuga': true // ['abc', 'def'] と指定すると個別に指定
58
- // }
59
- // },
185
+ {
186
+ // ignores: ['\\/test\\/'], // 除外したいファイルの正規表現
187
+ // allowedImports: {
188
+ // '/any/path/': { // 正規表現でチェックするファイルを指定
189
+ // // import制御するファイル (相対パスを指定する場合、.eslintrc.js を基準とする)
190
+ // '@/hoge/fuga': true // ['abc', 'def'] と指定すると個別に指定
191
+ // }
192
+ // },
193
+ // additionalBarrelFileNames: ['client'], // Next.jsなどでclient.tsをbarrelとして扱う
194
+ }
60
195
  ],
61
196
  },
62
197
  }
@@ -26,6 +26,7 @@ const SCHEMA = [
26
26
  additionalProperties: true,
27
27
  },
28
28
  ignores: { type: 'array', items: { type: 'string' }, default: [] },
29
+ additionalBarrelFileNames: { type: 'array', items: { type: 'string' }, default: [] },
29
30
  },
30
31
  additionalProperties: false,
31
32
  }
@@ -35,6 +36,7 @@ const CWD = process.cwd()
35
36
  const REGEX_UNNECESSARY_SLASH = /(\/)+/g
36
37
  const REGEX_ROOT_PATH = new RegExp(`^${rootPath}/index\.`)
37
38
  const REGEX_INDEX_FILE = /\/index\.(ts|js)x?$/
39
+ const REGEX_BARREL_FILE_EXT = /\.(ts|js)x?$/
38
40
  const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
39
41
 
40
42
  // Path aliasの情報を事前計算してキャッシュ
@@ -93,17 +95,6 @@ const convertToPathAlias = (absolutePath) => {
93
95
  return absolutePath
94
96
  }
95
97
 
96
- /**
97
- * import先がimport元の内部にあるかチェック(同階層・サブディレクトリからのimport)
98
- * (Next.js App Routerの特殊文字パスにも対応)
99
- * @param {string} importerDir - import元のディレクトリ
100
- * @param {string} importedPath - import先のパス
101
- * @returns {boolean}
102
- */
103
- const isImportedInsideImporter = (importerDir, importedPath) => {
104
- return importedPath === importerDir || importedPath.startsWith(importerDir + '/')
105
- }
106
-
107
98
  /**
108
99
  * 2つのパスの共通の親ディレクトリを見つける
109
100
  * @param {string} path1 - パス1
@@ -122,6 +113,58 @@ const findCommonParent = (path1, path2) => {
122
113
  return segments1.slice(0, i).join('/')
123
114
  }
124
115
 
116
+ /**
117
+ * 指定されたbarrelファイル名の候補パスを生成する
118
+ * @param {string} dir - ディレクトリパス
119
+ * @param {Array<string>} fileNames - barrelファイル名の配列
120
+ * @returns {Array<string>} パス候補の配列
121
+ */
122
+ const generateBarrelFilePaths = (dir, fileNames) => {
123
+ return fileNames.flatMap(name => TARGET_EXTS.map(ext => `${dir}/${name}.${ext}`))
124
+ }
125
+
126
+ /**
127
+ * パスからファイル名(拡張子なし)を抽出する
128
+ * @param {string} filePath - ファイルパス
129
+ * @returns {string} ファイル名(拡張子なし)
130
+ */
131
+ const extractFileName = (filePath) => {
132
+ return filePath.split('/').pop().replace(REGEX_BARREL_FILE_EXT, '')
133
+ }
134
+
135
+ /**
136
+ * import先が直接バレルファイルを指しているか検出する
137
+ * @param {string} importedPath - import先の絶対パス
138
+ * @param {Array<string>} barrelFileNames - バレルファイル名のリスト(index, client, server等)
139
+ * @returns {string|undefined} バレルファイルのパス、またはundefined
140
+ */
141
+ const detectDirectBarrelImport = (importedPath, barrelFileNames) => {
142
+ // ディレクトリ指定の場合('.'、'..'、'@/components/Button'など)
143
+ if (fs.existsSync(importedPath) && fs.statSync(importedPath).isDirectory()) {
144
+ const indexFile = TARGET_EXTS
145
+ .map(ext => `${importedPath}/index.${ext}`)
146
+ .find(f => fs.existsSync(f))
147
+ return indexFile
148
+ }
149
+
150
+ // ファイル指定の場合('./index'、'./client'、'@/components/Button/client'など)
151
+ const fileWithExt = TARGET_EXTS
152
+ .map(ext => `${importedPath}.${ext}`)
153
+ .find(f => fs.existsSync(f))
154
+
155
+ if (!fileWithExt) {
156
+ return undefined
157
+ }
158
+
159
+ // バレルファイル名のパターンにマッチするかチェック
160
+ const barrelPattern = `\\/(${barrelFileNames.join('|')})\\.(ts|tsx|js|jsx)$`
161
+ if (new RegExp(barrelPattern).test(fileWithExt)) {
162
+ return fileWithExt
163
+ }
164
+
165
+ return undefined
166
+ }
167
+
125
168
  /**
126
169
  * allowedImportsオプションに基づいて、特定のimportが許可されているかチェックする
127
170
  * @param {object} node - ImportDeclaration node
@@ -178,39 +221,86 @@ const checkAllowedImports = (node, importerDir, targetAllowedImports, allowedImp
178
221
  * import先のパスから親方向に barrel ファイルを探索する
179
222
  * @param {string} importedPath - import先の絶対パス
180
223
  * @param {string} importerDir - import元のディレクトリ
181
- * @returns {string|undefined} 見つかったbarrelファイルのパス
224
+ * @param {Array<string>} additionalBarrelFileNames - 追加でbarrelファイルとして扱うファイル名(拡張子なし、例: ['client', 'server'])
225
+ * @returns {{ barrelPath: string|undefined, missingBarrel: { fileName: string, parentDir: string }|null }} 見つかったbarrelファイルのパスと、作成すべきbarrelファイル情報
182
226
  */
183
- const findBarrelFile = (importedPath, importerDir) => {
227
+ const findBarrelFile = (importedPath, importerDir, additionalBarrelFileNames = []) => {
184
228
  const pathSegments = importedPath.split('/')
185
229
  let currentPath = importedPath
186
- let barrel = undefined
230
+ let barrel
231
+ let missingBarrel = null
232
+
233
+ // 優先順位: 追加指定されたファイル名 > index
234
+ const barrelFileNames = [...additionalBarrelFileNames, 'index']
187
235
 
188
236
  // import元とimport先の共通の親ディレクトリを見つける
189
237
  // 共通の親のbarrelファイルは除外する(同じディレクトリツリー内の相対importには適用されない)
190
238
  const commonParent = findCommonParent(importerDir, importedPath)
191
239
 
192
- // ディレクトリ指定の場合、そのindex.tsを指していることは自明なので一階層上から探索
193
- if (fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
240
+ // ディレクトリ指定の場合、またはファイルが存在しない場合は親ディレクトリから探索
241
+ if (!fs.existsSync(currentPath) || fs.statSync(currentPath).isDirectory()) {
194
242
  pathSegments.pop()
195
243
  currentPath = pathSegments.join('/')
196
244
  }
197
245
 
246
+ // 見つかったbarrelのファイル名を記録
247
+ // additionalBarrelFileNames(client, server等)が見つかった場合のみ、親方向には同じファイル名のみ探索
248
+ // index.tsが見つかった場合は、親方向にもadditionalBarrelFileNamesを探し続ける
249
+ let foundBarrelFileName = null
250
+
198
251
  while (pathSegments.length > 0) {
199
252
  // 以下の場合は探索終了
200
253
  // 1. 共通の親ディレクトリに到達した場合(commonParent自体のbarrelは除外)
201
254
  // 2. いずれかのreplacePathsのルートに到達した場合
202
- // 3. import先がimport元の内部にある場合(同階層・サブディレクトリからのimport)
203
- if (currentPath === commonParent || ALL_ROOT_PATHS.includes(currentPath) || isImportedInsideImporter(importerDir, currentPath)) {
255
+ if (currentPath === commonParent || ALL_ROOT_PATHS.includes(currentPath)) {
204
256
  break
205
257
  }
206
258
 
259
+ // 追加指定されたbarrelファイル(client.ts, server.ts等)が見つかっているか
260
+ const isAdditionalBarrelFound = foundBarrelFileName && additionalBarrelFileNames.includes(foundBarrelFileName)
261
+
262
+ // 探索するファイル名を決定
263
+ // - まだ見つかっていない、またはindexが見つかった → 全てのbarrelFileNamesを探す
264
+ // - additionalBarrelFileNames(client, server)が見つかった → 同じファイル名のみ探す
265
+ const searchFileNames = isAdditionalBarrelFound
266
+ ? [foundBarrelFileName]
267
+ : barrelFileNames
268
+
207
269
  // 現在のパスにbarrelファイルがあるかチェック
208
- const foundBarrel = TARGET_EXTS
209
- .map(ext => `${currentPath}/index.${ext}`)
270
+ const foundBarrel = generateBarrelFilePaths(currentPath, searchFileNames)
210
271
  .find(filePath => fs.existsSync(filePath))
211
272
 
212
273
  if (foundBarrel) {
213
- barrel = foundBarrel
274
+ const fileName = extractFileName(foundBarrel)
275
+
276
+ if (!foundBarrelFileName) {
277
+ // 最初に見つかったbarrel
278
+ barrel = foundBarrel
279
+ foundBarrelFileName = fileName
280
+ } else if (fileName === foundBarrelFileName) {
281
+ // 同じファイル名の場合は、より親を優先(上書き)
282
+ barrel = foundBarrel
283
+ } else if (isAdditionalBarrelFound && fileName === 'index') {
284
+ // client.tsを探していたが、親でindex.tsしか見つからなかった場合
285
+ // client.tsを作成してexportをまとめるよう促す
286
+ missingBarrel = {
287
+ fileName: foundBarrelFileName,
288
+ parentDir: currentPath,
289
+ }
290
+ }
291
+ // 異なるファイル名の場合は上書きしない(最も近いbarrelを維持)
292
+ } else if (isAdditionalBarrelFound) {
293
+ // client.tsを探していたが見つからなかった場合、index.tsがあるかチェック
294
+ const indexBarrel = generateBarrelFilePaths(currentPath, ['index'])
295
+ .find(filePath => fs.existsSync(filePath))
296
+
297
+ if (indexBarrel && !missingBarrel) {
298
+ // index.tsは見つかったが、client.tsがない
299
+ missingBarrel = {
300
+ fileName: foundBarrelFileName,
301
+ parentDir: currentPath,
302
+ }
303
+ }
214
304
  }
215
305
 
216
306
  // 一階層上に移動
@@ -218,7 +308,7 @@ const findBarrelFile = (importedPath, importerDir) => {
218
308
  currentPath = pathSegments.join('/')
219
309
  }
220
310
 
221
- return barrel
311
+ return { barrelPath: barrel, missingBarrel }
222
312
  }
223
313
 
224
314
  /**
@@ -234,9 +324,8 @@ module.exports = {
234
324
 
235
325
  // ignoresオプションでスキップ対象のファイルかチェック
236
326
  if (option.ignores) {
237
- const isIgnored = option.ignores.some(pattern =>
238
- new RegExp(pattern).test(context.filename)
239
- )
327
+ const ignorePatterns = option.ignores.map(pattern => new RegExp(pattern))
328
+ const isIgnored = ignorePatterns.some(regex => regex.test(context.filename))
240
329
  if (isIgnored) {
241
330
  return {}
242
331
  }
@@ -283,24 +372,139 @@ module.exports = {
283
372
  return
284
373
  }
285
374
 
286
- // barrel ファイルを探索
287
- const barrelPath = findBarrelFile(importedPath, importerDir)
375
+ // バレルファイル名のリストを作成(index + 追加指定されたファイル名)
376
+ const additionalBarrelFileNames = option.additionalBarrelFileNames || []
377
+ const barrelFileNames = [...additionalBarrelFileNames, 'index']
378
+
379
+ // ========================================
380
+ // 同階層・子階層からのバレルimportチェック
381
+ // ========================================
382
+ // import文が直接バレルファイル(index.ts、client.ts等)を指している場合、
383
+ // import元の位置関係をチェックする
384
+ const directBarrelPath = detectDirectBarrelImport(importedPath, barrelFileNames)
385
+
386
+ if (directBarrelPath) {
387
+ const barrelDir = path.dirname(directBarrelPath)
388
+
389
+ // import元がバレルと同階層、またはバレルディレクトリ以下にある場合はNG
390
+ const isSameLevelOrChild = importerDir === barrelDir || importerDir.startsWith(barrelDir + '/')
391
+
392
+ if (isSameLevelOrChild) {
393
+ const barrelWithAlias = convertToPathAlias(directBarrelPath)
394
+
395
+ context.report({
396
+ node,
397
+ message: `バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています。
398
+
399
+ 検出されたバレル: ${barrelWithAlias}
400
+ 現在のimport: import from '${node.source.value}'
401
+
402
+ バレルファイルはディレクトリ外部へのエクスポートにのみ使用してください。
403
+ 同じディレクトリまたは子ディレクトリ内では、直接相対パスでimportしてください。
404
+
405
+ 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import`,
406
+ })
407
+ return
408
+ }
409
+ }
410
+
411
+ // ========================================
412
+ // バレルファイルを探索
413
+ // ========================================
414
+ // import先のパスから親方向にバレルファイルを探索
415
+ const { barrelPath, missingBarrel } = findBarrelFile(importedPath, importerDir, additionalBarrelFileNames)
288
416
 
289
417
  // barrel が見つからない、またはroot pathのindex.tsの場合はスキップ
290
418
  if (!barrelPath || REGEX_ROOT_PATH.test(barrelPath)) {
291
419
  return
292
420
  }
293
421
 
422
+ // 親階層でclient.ts/server.tsが見つからず、index.tsのみ見つかった場合
423
+ // barrelファイル自体からのimportでも一貫性チェックは実行
424
+ if (missingBarrel) {
425
+ const missingBarrelWithAlias = convertToPathAlias(`${missingBarrel.parentDir}/${missingBarrel.fileName}`)
426
+ const existingBarrelWithAlias = convertToPathAlias(barrelPath).replace(REGEX_BARREL_FILE_EXT, '')
427
+
428
+ context.report({
429
+ node,
430
+ message: `${missingBarrelWithAlias}.ts を作成して、${existingBarrelWithAlias} のexportをまとめてください
431
+
432
+ 親ディレクトリに ${missingBarrel.fileName}.ts が存在しないため、一貫性のあるbarrel構造を保つために作成が必要です。
433
+
434
+ 作成例:
435
+ // ${missingBarrelWithAlias}.ts
436
+ export * from '${existingBarrelWithAlias}'
437
+
438
+ 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import`,
439
+ })
440
+ return
441
+ }
442
+
443
+ // barrelファイル自体、または同じディレクトリの他のbarrelファイルからimportしている場合はスキップ
444
+ // 同じディレクトリに index.ts と client.ts がある場合、どちらからのimportも許容する
445
+ const barrelDir = barrelPath.substring(0, barrelPath.lastIndexOf('/'))
446
+ const allBarrelsInSameDir = generateBarrelFilePaths(barrelDir, barrelFileNames)
447
+ .filter(filePath => fs.existsSync(filePath))
448
+
449
+ const importedPathWithExts = TARGET_EXTS.map(ext => `${importedPath}.${ext}`)
450
+ const isImportingFromBarrel = importedPathWithExts.some(p => allBarrelsInSameDir.includes(p))
451
+ if (isImportingFromBarrel) {
452
+ return
453
+ }
454
+
294
455
  // barrel パスをPath aliasに変換
295
456
  const barrelWithAlias = convertToPathAlias(barrelPath)
296
- const barrelDirWithAlias = barrelWithAlias.replace(REGEX_INDEX_FILE, '')
457
+ // barrelファイルの拡張子を除去(index.ts ディレクトリパス、client.ts → client)
458
+ const barrelDirWithAlias = REGEX_INDEX_FILE.test(barrelWithAlias)
459
+ ? barrelWithAlias.replace(REGEX_INDEX_FILE, '')
460
+ : barrelWithAlias.replace(REGEX_BARREL_FILE_EXT, '')
297
461
  const uniqueDeniedModules = [...new Set(deniedModules.flat())]
298
462
 
463
+ // ========================================
464
+ // エラーメッセージ生成の準備
465
+ // ========================================
299
466
  // importしているモジュール名を取得
300
- const importedModules = node.specifiers
301
- .map(s => s.imported?.name || s.local?.name)
302
- .filter(Boolean)
303
- .join(', ')
467
+ const importedModules = node.specifiers.reduce((acc, s) => {
468
+ const name = s.imported?.name || s.local?.name
469
+ return name ? (acc ? `${acc}, ${name}` : name) : acc
470
+ }, '')
471
+
472
+ // additionalBarrelFileNamesが設定されている場合は、
473
+ // 存在しないファイルも含めて全ての選択肢を表示する
474
+ const hasAdditionalBarrels = option?.additionalBarrelFileNames?.length > 0
475
+
476
+ // エラーメッセージに表示するbarrelファイルのリストを作成
477
+ const barrelFilesToShow = hasAdditionalBarrels
478
+ ? barrelFileNames.map(name => {
479
+ // 各barrelファイル名について、存在する拡張子を優先、なければ.tsを使用
480
+ const candidates = TARGET_EXTS.map(ext => `${barrelDir}/${name}.${ext}`)
481
+ return candidates.find(filePath => fs.existsSync(filePath)) || `${barrelDir}/${name}.ts`
482
+ })
483
+ : allBarrelsInSameDir
484
+
485
+ // barrelファイルをエラーメッセージ用に変換
486
+ // (path alias変換、import path生成、存在チェック)
487
+ const barrelSuggestions = barrelFilesToShow.map(filePath => {
488
+ const pathWithAlias = convertToPathAlias(filePath)
489
+ const dirWithAlias = REGEX_INDEX_FILE.test(pathWithAlias)
490
+ ? pathWithAlias.replace(REGEX_INDEX_FILE, '')
491
+ : pathWithAlias.replace(REGEX_BARREL_FILE_EXT, '')
492
+
493
+ // 元のimport記法(相対パス or path alias)に合わせたimportパスを生成
494
+ let importPath = dirWithAlias
495
+ if (node.source.value[0] === '.') {
496
+ const dirAbsolute = resolvePathAlias(dirWithAlias)
497
+ const relativePath = path.relative(importerDir, dirAbsolute)
498
+ importPath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`
499
+ }
500
+
501
+ return {
502
+ barrelFile: pathWithAlias,
503
+ importPath,
504
+ fileName: path.basename(filePath),
505
+ exists: fs.existsSync(filePath)
506
+ }
507
+ })
304
508
 
305
509
  // 推奨されるimportパスを生成(元の記法に合わせる)
306
510
  let suggestedImportPath = barrelDirWithAlias
@@ -311,19 +515,58 @@ module.exports = {
311
515
  suggestedImportPath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`
312
516
  }
313
517
 
518
+ // ========================================
519
+ // エラーメッセージを生成
520
+ // ========================================
521
+ // barrelファイルが複数ある、またはadditionalBarrelFileNamesが設定されている場合は
522
+ // 複数選択肢形式で表示(存在しないファイルも含む)
523
+ const shouldShowAllSuggestions = barrelSuggestions.length > 1 || hasAdditionalBarrels
524
+
525
+ let suggestionsMessage = ''
526
+ if (shouldShowAllSuggestions) {
527
+ // index.ts を優先的に表示するためにソート
528
+ const sortedSuggestions = [...barrelSuggestions].sort((a, b) => {
529
+ const isAIndex = a.fileName.startsWith('index.')
530
+ const isBIndex = b.fileName.startsWith('index.')
531
+ if (isAIndex && !isBIndex) return -1
532
+ if (!isAIndex && isBIndex) return 1
533
+ return 0
534
+ })
535
+
536
+ suggestionsMessage = '\n推奨されるimport(以下のいずれか):\n' +
537
+ sortedSuggestions.map(({ importPath, fileName, exists }) =>
538
+ ` - import { ${importedModules} } from '${importPath}' // ${fileName}${exists ? '' : ' (作成が必要)'}`
539
+ ).join('\n')
540
+
541
+ // 存在しないファイルがある場合は注意メッセージを追加
542
+ const missingBarrels = barrelSuggestions.filter(({ exists }) => !exists)
543
+ if (missingBarrels.length > 0) {
544
+ suggestionsMessage += '\n\n※ 存在しないバレルファイルは必要に応じて作成してください。'
545
+ }
546
+ } else {
547
+ suggestionsMessage = `\n推奨されるimport: import { ${importedModules} } from '${suggestedImportPath}'`
548
+ }
549
+
550
+ // 「検出されたバレル」には実際に存在するファイルのみを表示
551
+ const existingBarrels = barrelSuggestions.filter(({ exists }) => exists)
552
+ const barrelFilesInfo = existingBarrels.length > 1
553
+ ? existingBarrels.map(({ barrelFile }) => barrelFile).join(', ')
554
+ : barrelWithAlias
555
+
556
+ // ========================================
314
557
  // エラーを報告
558
+ // ========================================
315
559
  context.report({
316
560
  node,
317
561
  message: uniqueDeniedModules.length
318
562
  ? `${uniqueDeniedModules.join(', ')} は ${barrelDirWithAlias} からimportしてください`
319
563
  : `バレルファイルを経由してimportしてください
320
564
 
321
- 検出されたバレル: ${barrelWithAlias}
322
- 現在のimport: import { ${importedModules} } from '${node.source.value}'
323
- 推奨されるimport: import { ${importedModules} } from '${suggestedImportPath}'
565
+ 検出されたバレル: ${barrelFilesInfo}
566
+ 現在のimport: import { ${importedModules} } from '${node.source.value}'${suggestionsMessage}
324
567
 
325
568
  注意: バレルファイルに ${importedModules} のexportが必要です。
326
- 存在しない場合は ${path.basename(barrelPath)} に追加してください。
569
+ 存在しない場合は対象のファイルに追加してください。
327
570
 
328
571
  詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import`,
329
572
  })
@@ -90,6 +90,89 @@ ruleTester.run('require-barrel-import', rule, {
90
90
  })(),
91
91
  },
92
92
 
93
+ // additionalBarrelFileNames - client.tsからimport(エラーにならない)
94
+ {
95
+ code: `import { fetchUser } from './api/client'`,
96
+ filename: (() => {
97
+ createFixture('barrel-file-names-valid', {
98
+ 'components': {
99
+ 'Page.tsx': '',
100
+ 'api': {
101
+ 'client.ts': 'export {}',
102
+ 'user.ts': '',
103
+ },
104
+ },
105
+ })
106
+ return `${fixturesRoot}/barrel-file-names-valid/components/Page.tsx`
107
+ })(),
108
+ options: [
109
+ {
110
+ additionalBarrelFileNames: ['client', 'server'],
111
+ },
112
+ ],
113
+ },
114
+
115
+ // additionalBarrelFileNames - 同じディレクトリにindex.tsとclient.tsがある場合、index.tsからimportもOK
116
+ {
117
+ code: `import { fetchUser } from './api'`,
118
+ filename: (() => {
119
+ createFixture('barrel-file-names-both-index', {
120
+ 'components': {
121
+ 'Page.tsx': '',
122
+ 'api': {
123
+ 'index.ts': 'export {}',
124
+ 'client.ts': 'export {}',
125
+ 'user.ts': '',
126
+ },
127
+ },
128
+ })
129
+ return `${fixturesRoot}/barrel-file-names-both-index/components/Page.tsx`
130
+ })(),
131
+ options: [
132
+ {
133
+ additionalBarrelFileNames: ['client', 'server'],
134
+ },
135
+ ],
136
+ },
137
+
138
+ // additionalBarrelFileNames - 同じディレクトリにindex.tsとclient.tsがある場合、client.tsからimportもOK
139
+ {
140
+ code: `import { fetchUser } from './api/client'`,
141
+ filename: (() => {
142
+ createFixture('barrel-file-names-both-client', {
143
+ 'components': {
144
+ 'Page.tsx': '',
145
+ 'api': {
146
+ 'index.ts': 'export {}',
147
+ 'client.ts': 'export {}',
148
+ 'user.ts': '',
149
+ },
150
+ },
151
+ })
152
+ return `${fixturesRoot}/barrel-file-names-both-client/components/Page.tsx`
153
+ })(),
154
+ options: [
155
+ {
156
+ additionalBarrelFileNames: ['client', 'server'],
157
+ },
158
+ ],
159
+ },
160
+
161
+ // 同じディレクトリで非バレルファイルをimport(エラーにならない)
162
+ {
163
+ code: `import { ButtonProps } from './types'`,
164
+ filename: (() => {
165
+ createFixture('same-dir-non-barrel', {
166
+ 'Button': {
167
+ 'index.tsx': 'export {}',
168
+ 'Button.tsx': '',
169
+ 'types.ts': '',
170
+ },
171
+ })
172
+ return `${fixturesRoot}/same-dir-non-barrel/Button/Button.tsx`
173
+ })(),
174
+ },
175
+
93
176
  // Next.js App Router特殊文字パス - 同階層import
94
177
  {
95
178
  code: `import { useUsers } from './hooks/useUsers'`,
@@ -391,5 +474,529 @@ ruleTester.run('require-barrel-import', rule, {
391
474
  },
392
475
  ],
393
476
  },
477
+
478
+ // additionalBarrelFileNames - index.tsとclient.tsが両方ある場合、複数の選択肢を表示
479
+ {
480
+ code: `import { fetchUser } from './api/user'`,
481
+ filename: (() => {
482
+ createFixture('barrel-file-names-multiple-options', {
483
+ 'components': {
484
+ 'Page.tsx': '',
485
+ 'api': {
486
+ 'index.ts': 'export {}',
487
+ 'client.ts': 'export {}',
488
+ 'user.ts': '',
489
+ },
490
+ },
491
+ })
492
+ return `${fixturesRoot}/barrel-file-names-multiple-options/components/Page.tsx`
493
+ })(),
494
+ options: [
495
+ {
496
+ additionalBarrelFileNames: ['client'],
497
+ },
498
+ ],
499
+ errors: [
500
+ {
501
+ message: /推奨されるimport(以下のいずれか)[\s\S]*index\.ts[\s\S]*client\.ts/,
502
+ },
503
+ ],
504
+ },
505
+
506
+ // additionalBarrelFileNames - index.tsのみ存在する場合、存在しないclient.tsも選択肢に表示
507
+ {
508
+ code: `import { fetchUser } from './api/user'`,
509
+ filename: (() => {
510
+ createFixture('barrel-file-names-with-missing-client', {
511
+ 'components': {
512
+ 'Page.tsx': '',
513
+ 'api': {
514
+ 'index.ts': 'export {}',
515
+ 'user.ts': '',
516
+ },
517
+ },
518
+ })
519
+ return `${fixturesRoot}/barrel-file-names-with-missing-client/components/Page.tsx`
520
+ })(),
521
+ options: [
522
+ {
523
+ additionalBarrelFileNames: ['client'],
524
+ },
525
+ ],
526
+ errors: [
527
+ {
528
+ message: /推奨されるimport(以下のいずれか)[\s\S]*index\.ts[\s\S]*client\.ts \(作成が必要\)[\s\S]*※ 存在しないバレルファイルは必要に応じて作成してください。/,
529
+ },
530
+ ],
531
+ },
532
+
533
+ // additionalBarrelFileNames - client.tsのみ存在する場合、存在しないindex.tsも選択肢に表示
534
+ {
535
+ code: `import { fetchUser } from './api/user'`,
536
+ filename: (() => {
537
+ createFixture('barrel-file-names-with-missing-index', {
538
+ 'components': {
539
+ 'Page.tsx': '',
540
+ 'api': {
541
+ 'client.ts': 'export {}',
542
+ 'user.ts': '',
543
+ },
544
+ },
545
+ })
546
+ return `${fixturesRoot}/barrel-file-names-with-missing-index/components/Page.tsx`
547
+ })(),
548
+ options: [
549
+ {
550
+ additionalBarrelFileNames: ['client'],
551
+ },
552
+ ],
553
+ errors: [
554
+ {
555
+ message: /推奨されるimport(以下のいずれか)[\s\S]*index\.ts \(作成が必要\)[\s\S]*client\.ts[\s\S]*※ 存在しないバレルファイルは必要に応じて作成してください。/,
556
+ },
557
+ ],
558
+ },
559
+
560
+ // additionalBarrelFileNames - client.tsをbarrelとして扱う
561
+ {
562
+ code: `import { fetchUser } from './api/user'`,
563
+ filename: (() => {
564
+ createFixture('barrel-file-names-client', {
565
+ 'components': {
566
+ 'Page.tsx': '',
567
+ 'api': {
568
+ 'client.ts': 'export {}', // client.tsがbarrel
569
+ 'user.ts': '',
570
+ },
571
+ },
572
+ })
573
+ return `${fixturesRoot}/barrel-file-names-client/components/Page.tsx`
574
+ })(),
575
+ options: [
576
+ {
577
+ additionalBarrelFileNames: ['client', 'server'],
578
+ },
579
+ ],
580
+ errors: [
581
+ {
582
+ message: /バレルファイルを経由してimportしてください/,
583
+ },
584
+ ],
585
+ },
586
+
587
+ // additionalBarrelFileNames - server.tsをbarrelとして扱う
588
+ {
589
+ code: `import { getServerData } from './server-api/data'`,
590
+ filename: (() => {
591
+ createFixture('barrel-file-names-server', {
592
+ 'lib': {
593
+ 'App.tsx': '',
594
+ 'server-api': {
595
+ 'server.ts': 'export {}', // server.tsがbarrel
596
+ 'data.ts': '',
597
+ },
598
+ },
599
+ })
600
+ return `${fixturesRoot}/barrel-file-names-server/lib/App.tsx`
601
+ })(),
602
+ options: [
603
+ {
604
+ additionalBarrelFileNames: ['client', 'server'],
605
+ },
606
+ ],
607
+ errors: [
608
+ {
609
+ message: /バレルファイルを経由してimportしてください/,
610
+ },
611
+ ],
612
+ },
613
+
614
+ // additionalBarrelFileNames - client.tsがindexより優先される(同じディレクトリ内)
615
+ {
616
+ code: `import { fetchUser } from './api/user'`,
617
+ filename: (() => {
618
+ createFixture('barrel-file-names-priority', {
619
+ 'components': {
620
+ 'Page.tsx': '',
621
+ 'api': {
622
+ 'client.ts': 'export {}', // client.tsが優先
623
+ 'index.ts': 'export {}',
624
+ 'user.ts': '',
625
+ },
626
+ },
627
+ })
628
+ return `${fixturesRoot}/barrel-file-names-priority/components/Page.tsx`
629
+ })(),
630
+ options: [
631
+ {
632
+ additionalBarrelFileNames: ['client'],
633
+ },
634
+ ],
635
+ errors: [
636
+ {
637
+ message: /client\.ts/, // client.tsが検出される
638
+ },
639
+ ],
640
+ },
641
+
642
+ // additionalBarrelFileNames - 異なるファイル名の場合は最も近いbarrelを優先
643
+ {
644
+ code: `import { useFormContext } from './route/edit/_hooks/useFormContext'`,
645
+ filename: (() => {
646
+ createFixture('barrel-file-names-nearest-priority', {
647
+ 'Page.tsx': '', // importer
648
+ 'route': {
649
+ 'client.ts': 'export {}', // 親のclient.ts
650
+ 'edit': {
651
+ 'index.ts': 'export {}', // より近いindex.tsが優先される
652
+ '_hooks': {
653
+ 'useFormContext.ts': '',
654
+ },
655
+ },
656
+ },
657
+ })
658
+ return `${fixturesRoot}/barrel-file-names-nearest-priority/Page.tsx`
659
+ })(),
660
+ options: [
661
+ {
662
+ additionalBarrelFileNames: ['client', 'server'],
663
+ },
664
+ ],
665
+ errors: [
666
+ {
667
+ message: /route\/edit/, // 最も近いindex.ts(route/edit)が検出される
668
+ },
669
+ ],
670
+ },
671
+
672
+ // additionalBarrelFileNames - より親のclient.tsを優先
673
+ {
674
+ code: `import { useFormContext } from './route/edit/_hooks/useFormContext'`,
675
+ filename: (() => {
676
+ createFixture('barrel-file-names-parent-priority', {
677
+ 'Page.tsx': '', // importer
678
+ 'route': {
679
+ 'client.ts': 'export {}', // 親のclient.tsが優先される
680
+ 'edit': {
681
+ 'client.ts': 'export {}', // こちらではなく親が検出される
682
+ '_hooks': {
683
+ 'useFormContext.ts': '',
684
+ },
685
+ },
686
+ },
687
+ })
688
+ return `${fixturesRoot}/barrel-file-names-parent-priority/Page.tsx`
689
+ })(),
690
+ options: [
691
+ {
692
+ additionalBarrelFileNames: ['client', 'server'],
693
+ },
694
+ ],
695
+ errors: [
696
+ {
697
+ message: /route\/client\.ts/, // より親のclient.tsが検出される
698
+ },
699
+ ],
700
+ },
701
+
702
+ // additionalBarrelFileNames - 親にclient.tsがなくindex.tsのみの場合、client.ts作成を促す
703
+ {
704
+ code: `import { useFormContext } from './route/edit/_hooks/useFormContext'`,
705
+ filename: (() => {
706
+ createFixture('barrel-file-names-missing-client', {
707
+ 'Page.tsx': '', // importer
708
+ 'route': {
709
+ 'index.ts': 'export {}', // index.tsのみ
710
+ // client.ts なし
711
+ 'edit': {
712
+ 'client.ts': 'export {}', // 子にはclient.tsがある
713
+ '_hooks': {
714
+ 'useFormContext.ts': '',
715
+ },
716
+ },
717
+ },
718
+ })
719
+ return `${fixturesRoot}/barrel-file-names-missing-client/Page.tsx`
720
+ })(),
721
+ options: [
722
+ {
723
+ additionalBarrelFileNames: ['client', 'server'],
724
+ },
725
+ ],
726
+ errors: [
727
+ {
728
+ message: /route\/client\.ts を作成して.*edit\/client のexportをまとめてください/,
729
+ },
730
+ ],
731
+ },
732
+
733
+ // additionalBarrelFileNames - 複雑なネスト: 子=index, 中間=client, 親=index
734
+ {
735
+ code: `import { Component } from './route/edit/components/Component'`,
736
+ filename: (() => {
737
+ createFixture('barrel-file-names-nested-mixed', {
738
+ 'Page.tsx': '', // importer
739
+ 'route': {
740
+ 'index.ts': 'export {}', // 親 (index)
741
+ 'edit': {
742
+ 'client.ts': 'export {}', // 中間 (client)
743
+ 'components': {
744
+ 'index.ts': 'export {}', // 子 (index) - 最も近い
745
+ 'Component.tsx': '',
746
+ },
747
+ },
748
+ },
749
+ })
750
+ return `${fixturesRoot}/barrel-file-names-nested-mixed/Page.tsx`
751
+ })(),
752
+ options: [
753
+ {
754
+ additionalBarrelFileNames: ['client', 'server'],
755
+ },
756
+ ],
757
+ errors: [
758
+ {
759
+ message: /route\/edit\/components/, // 最も近いindex.ts(components)が検出される
760
+ },
761
+ ],
762
+ },
763
+
764
+ // additionalBarrelFileNames - 複雑なネスト: 全てclient.ts
765
+ {
766
+ code: `import { Component } from './route/edit/components/Component'`,
767
+ filename: (() => {
768
+ createFixture('barrel-file-names-nested-all-client', {
769
+ 'Page.tsx': '', // importer
770
+ 'route': {
771
+ 'client.ts': 'export {}', // 親 (client)
772
+ 'edit': {
773
+ 'client.ts': 'export {}', // 中間 (client)
774
+ 'components': {
775
+ 'client.ts': 'export {}', // 子 (client) - 同名なので探索を続ける
776
+ 'Component.tsx': '',
777
+ },
778
+ },
779
+ },
780
+ })
781
+ return `${fixturesRoot}/barrel-file-names-nested-all-client/Page.tsx`
782
+ })(),
783
+ options: [
784
+ {
785
+ additionalBarrelFileNames: ['client', 'server'],
786
+ },
787
+ ],
788
+ errors: [
789
+ {
790
+ message: /route\/client/, // より親のclient.tsが検出される
791
+ },
792
+ ],
793
+ },
794
+
795
+ // additionalBarrelFileNames - 複雑なネスト: 子=client, 中間=index, 親=なし
796
+ {
797
+ code: `import { Component } from './route/edit/components/Component'`,
798
+ filename: (() => {
799
+ createFixture('barrel-file-names-nested-reverse', {
800
+ 'Page.tsx': '', // importer
801
+ 'route': {
802
+ // barrelなし
803
+ 'edit': {
804
+ 'index.ts': 'export {}', // 中間 (index)
805
+ 'components': {
806
+ 'client.ts': 'export {}', // 子 (client) - 最も近い
807
+ 'Component.tsx': '',
808
+ },
809
+ },
810
+ },
811
+ })
812
+ return `${fixturesRoot}/barrel-file-names-nested-reverse/Page.tsx`
813
+ })(),
814
+ options: [
815
+ {
816
+ additionalBarrelFileNames: ['client', 'server'],
817
+ },
818
+ ],
819
+ errors: [
820
+ {
821
+ message: /route\/edit\/components\/client/, // 最も近いclient.tsが検出される
822
+ },
823
+ ],
824
+ },
825
+
826
+ // ============================================================
827
+ // 【新規】同じディレクトリまたは子階層からバレルファイルを経由するimport
828
+ // ============================================================
829
+
830
+ // 1. 同じディレクトリでバレルファイルを経由(from '.')
831
+ {
832
+ code: `import { Button } from '.'`,
833
+ filename: (() => {
834
+ createFixture('same-dir-barrel-dot', {
835
+ 'Button': {
836
+ 'index.tsx': 'export { Button } from "./Button"',
837
+ 'Button.tsx': '',
838
+ },
839
+ })
840
+ return `${fixturesRoot}/same-dir-barrel-dot/Button/Button.tsx`
841
+ })(),
842
+ errors: [
843
+ {
844
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
845
+ },
846
+ ],
847
+ },
848
+
849
+ // 2. 同じディレクトリでバレルファイルを経由(from './index')
850
+ {
851
+ code: `import { Button } from './index'`,
852
+ filename: (() => {
853
+ createFixture('same-dir-barrel-index', {
854
+ 'Button': {
855
+ 'index.tsx': 'export { Button } from "./Button"',
856
+ 'Button.tsx': '',
857
+ },
858
+ })
859
+ return `${fixturesRoot}/same-dir-barrel-index/Button/Button.tsx`
860
+ })(),
861
+ errors: [
862
+ {
863
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
864
+ },
865
+ ],
866
+ },
867
+
868
+ // 3. 同じディレクトリでclient.tsを経由(from './client')
869
+ {
870
+ code: `import { ButtonPresentation } from './client'`,
871
+ filename: (() => {
872
+ createFixture('same-dir-barrel-client', {
873
+ 'Button': {
874
+ 'Button.container.tsx': '',
875
+ 'Button.presentation.tsx': '',
876
+ 'client.ts': 'export { ButtonPresentation } from "./Button.presentation"',
877
+ },
878
+ })
879
+ return `${fixturesRoot}/same-dir-barrel-client/Button/Button.container.tsx`
880
+ })(),
881
+ options: [
882
+ {
883
+ additionalBarrelFileNames: ['client'],
884
+ },
885
+ ],
886
+ errors: [
887
+ {
888
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
889
+ },
890
+ ],
891
+ },
892
+
893
+ // 4. 親ディレクトリのバレルを子階層から経由(from '..')
894
+ {
895
+ code: `import { Button } from '..'`,
896
+ filename: (() => {
897
+ createFixture('child-dir-parent-barrel-dot', {
898
+ 'Button': {
899
+ 'index.tsx': 'export { Button } from "./Button"',
900
+ 'Button.tsx': '',
901
+ '_utils': {
902
+ 'helper.ts': '',
903
+ },
904
+ },
905
+ })
906
+ return `${fixturesRoot}/child-dir-parent-barrel-dot/Button/_utils/helper.ts`
907
+ })(),
908
+ errors: [
909
+ {
910
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
911
+ },
912
+ ],
913
+ },
914
+
915
+ // 5. 親ディレクトリのバレルを子階層から経由(from '../index')
916
+ {
917
+ code: `import { Button } from '../index'`,
918
+ filename: (() => {
919
+ createFixture('child-dir-parent-barrel-index', {
920
+ 'Button': {
921
+ 'index.tsx': 'export { Button } from "./Button"',
922
+ 'Button.tsx': '',
923
+ '_utils': {
924
+ 'helper.ts': '',
925
+ },
926
+ },
927
+ })
928
+ return `${fixturesRoot}/child-dir-parent-barrel-index/Button/_utils/helper.ts`
929
+ })(),
930
+ errors: [
931
+ {
932
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
933
+ },
934
+ ],
935
+ },
936
+
937
+ // 6. path aliasで同じディレクトリのバレルを経由
938
+ {
939
+ code: `import { Button } from '@/same-dir-path-alias-barrel/Button'`,
940
+ filename: (() => {
941
+ createFixture('same-dir-path-alias-barrel', {
942
+ 'Button': {
943
+ 'index.tsx': 'export { Button } from "./Button"',
944
+ 'Button.tsx': '',
945
+ },
946
+ })
947
+ return `${fixturesRoot}/same-dir-path-alias-barrel/Button/Button.tsx`
948
+ })(),
949
+ errors: [
950
+ {
951
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
952
+ },
953
+ ],
954
+ },
955
+
956
+ // 7. path aliasでバレルディレクトリの子階層から経由
957
+ {
958
+ code: `import { Button } from '@/child-dir-path-alias-barrel/Button'`,
959
+ filename: (() => {
960
+ createFixture('child-dir-path-alias-barrel', {
961
+ 'Button': {
962
+ 'index.tsx': 'export { Button } from "./Button"',
963
+ 'Button.tsx': '',
964
+ '_utils': {
965
+ 'helper.ts': '',
966
+ },
967
+ },
968
+ })
969
+ return `${fixturesRoot}/child-dir-path-alias-barrel/Button/_utils/helper.ts`
970
+ })(),
971
+ errors: [
972
+ {
973
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
974
+ },
975
+ ],
976
+ },
977
+
978
+ // 8. 孫ディレクトリからバレルを経由(from '../../index')
979
+ {
980
+ code: `import { Button } from '../../index'`,
981
+ filename: (() => {
982
+ createFixture('grandchild-dir-barrel', {
983
+ 'Button': {
984
+ 'index.tsx': 'export { Button } from "./Button"',
985
+ 'Button.tsx': '',
986
+ '_utils': {
987
+ '_helpers': {
988
+ 'deep.ts': '',
989
+ },
990
+ },
991
+ },
992
+ })
993
+ return `${fixturesRoot}/grandchild-dir-barrel/Button/_utils/_helpers/deep.ts`
994
+ })(),
995
+ errors: [
996
+ {
997
+ message: /バレルファイルからのimportは、そのディレクトリ外部からのみ許可されています/,
998
+ },
999
+ ],
1000
+ },
394
1001
  ],
395
1002
  })