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.
|
|
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": "
|
|
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) =>
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
//
|
|
193
|
-
if (fs.existsSync(currentPath)
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
238
|
-
|
|
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
|
-
//
|
|
287
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
検出されたバレル: ${
|
|
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
|
-
|
|
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
|
})
|