eslint-plugin-smarthr 6.10.0 → 6.10.2
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 +14 -0
- package/package.json +2 -2
- package/rules/require-barrel-import/index.js +198 -118
- package/test/prohibit-export-array-type.js +2 -1
- package/test/require-barrel-import.js +407 -0
- package/tsconfig.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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.10.2](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.10.1...eslint-plugin-smarthr-v6.10.2) (2026-04-09)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **test:** typescript-eslint v8のparserに更新 ([#1217](https://github.com/kufu/tamatebako/issues/1217)) ([e44ac27](https://github.com/kufu/tamatebako/commit/e44ac27e26afc9cbe26244afd38e62bb65aecaa4))
|
|
11
|
+
|
|
12
|
+
## [6.10.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.10.0...eslint-plugin-smarthr-v6.10.1) (2026-04-08)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **require-barrel-import:** next.jsの特殊文字パスでのバグ修正とリファクタリング ([#1210](https://github.com/kufu/tamatebako/issues/1210)) ([458cfe9](https://github.com/kufu/tamatebako/commit/458cfe90084cbcee936bcb375216f5807880edd7))
|
|
18
|
+
|
|
5
19
|
## [6.10.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.9.1...eslint-plugin-smarthr-v6.10.0) (2026-04-08)
|
|
6
20
|
|
|
7
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "6.10.
|
|
3
|
+
"version": "6.10.2",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"eslintplugin",
|
|
38
38
|
"smarthr"
|
|
39
39
|
],
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "662f8876643b981d82a4a5aaf5d9a96d7d7ed58b"
|
|
41
41
|
}
|
|
@@ -31,62 +31,172 @@ const SCHEMA = [
|
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
const entriedReplacePaths = Object.entries(replacePaths)
|
|
35
34
|
const CWD = process.cwd()
|
|
36
35
|
const REGEX_UNNECESSARY_SLASH = /(\/)+/g
|
|
37
36
|
const REGEX_ROOT_PATH = new RegExp(`^${rootPath}/index\.`)
|
|
38
37
|
const REGEX_INDEX_FILE = /\/index\.(ts|js)x?$/
|
|
39
38
|
const TARGET_EXTS = ['ts', 'tsx', 'js', 'jsx']
|
|
40
39
|
|
|
41
|
-
//
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (source[0] === '/') {
|
|
51
|
-
return source
|
|
40
|
+
// Path aliasの情報を事前計算してキャッシュ
|
|
41
|
+
const REPLACE_PATHS_INFO = Object.entries(replacePaths).map(([key, values]) => {
|
|
42
|
+
const resolvedPaths = values.map(v => path.resolve(`${CWD}/${v.replace(/\/\*$/, '')}`))
|
|
43
|
+
return {
|
|
44
|
+
key,
|
|
45
|
+
values,
|
|
46
|
+
keyRegex: new RegExp(`^${key}(.+)$`),
|
|
47
|
+
resolvedPaths,
|
|
48
|
+
valueRegexes: resolvedPaths.map(p => new RegExp(`^${p}(.+)$`))
|
|
52
49
|
}
|
|
50
|
+
})
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
// @/ と ~/ のパスのみをrootとする(READMEの仕様通り)
|
|
53
|
+
const ALL_ROOT_PATHS = (() => {
|
|
54
|
+
const rootKeys = ['@/', '~/']
|
|
55
|
+
return REPLACE_PATHS_INFO
|
|
56
|
+
.filter(info => rootKeys.includes(info.key))
|
|
57
|
+
.flatMap(info => info.resolvedPaths)
|
|
58
|
+
})()
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Path aliasを絶対パスに変換する
|
|
62
|
+
* @param {string} importPath - import文のパス(例: '@/components/Button')
|
|
63
|
+
* @returns {string} 絶対パス(例: '/path/to/src/components/Button')
|
|
64
|
+
*/
|
|
65
|
+
const resolvePathAlias = (importPath) => {
|
|
66
|
+
if (importPath[0] === '/') {
|
|
67
|
+
return importPath
|
|
68
|
+
}
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
for (const { keyRegex, resolvedPaths } of REPLACE_PATHS_INFO) {
|
|
71
|
+
if (keyRegex.test(importPath)) {
|
|
72
|
+
return importPath.replace(keyRegex, `${resolvedPaths[0]}/$1`)
|
|
63
73
|
}
|
|
74
|
+
}
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
}, source)
|
|
76
|
+
return importPath
|
|
67
77
|
}
|
|
68
|
-
const calculateReplacedImportPath = (source) => {
|
|
69
|
-
return entriedReplacePathsWithRegex.reduce((prev, [key, values, keyRegex, valueRegexes]) => {
|
|
70
|
-
if (source === prev) {
|
|
71
|
-
return values.reduce((p, v, index) => {
|
|
72
|
-
if (prev === p) {
|
|
73
|
-
const regexp = valueRegexes[index]
|
|
74
|
-
|
|
75
|
-
if (regexp.test(prev)) {
|
|
76
|
-
return p.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
/**
|
|
80
|
+
* 絶対パスをPath aliasに変換する
|
|
81
|
+
* @param {string} absolutePath - 絶対パス(例: '/path/to/src/components/Button')
|
|
82
|
+
* @returns {string} Path alias(例: '@/components/Button')
|
|
83
|
+
*/
|
|
84
|
+
const convertToPathAlias = (absolutePath) => {
|
|
85
|
+
for (const { key, valueRegexes } of REPLACE_PATHS_INFO) {
|
|
86
|
+
for (const regexp of valueRegexes) {
|
|
87
|
+
if (regexp.test(absolutePath)) {
|
|
88
|
+
return absolutePath.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
|
|
89
|
+
}
|
|
82
90
|
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return absolutePath
|
|
94
|
+
}
|
|
95
|
+
|
|
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
|
+
/**
|
|
108
|
+
* allowedImportsオプションに基づいて、特定のimportが許可されているかチェックする
|
|
109
|
+
* @param {object} node - ImportDeclaration node
|
|
110
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
111
|
+
* @param {Array} targetAllowedImports - 適用されるallowedImportsのキー配列
|
|
112
|
+
* @param {object} allowedImportsConfig - allowedImportsの設定
|
|
113
|
+
* @returns {{ shouldSkip: boolean, deniedModules: Array }} チェック結果
|
|
114
|
+
*/
|
|
115
|
+
const checkAllowedImports = (node, importerDir, targetAllowedImports, allowedImportsConfig) => {
|
|
116
|
+
let isDenyPath = false
|
|
117
|
+
let deniedModules = []
|
|
118
|
+
|
|
119
|
+
for (const allowedKey of targetAllowedImports) {
|
|
120
|
+
const allowedOption = allowedImportsConfig[allowedKey]
|
|
121
|
+
|
|
122
|
+
for (const targetModule in allowedOption) {
|
|
123
|
+
const actualTarget = targetModule[0] !== '.'
|
|
124
|
+
? targetModule
|
|
125
|
+
: path.resolve(`${CWD}/${targetModule}`)
|
|
126
|
+
|
|
127
|
+
let importSource = node.source.value
|
|
128
|
+
|
|
129
|
+
// 絶対パスの場合は、import元ディレクトリを基準に解決
|
|
130
|
+
if (actualTarget[0] === '/') {
|
|
131
|
+
importSource = path.resolve(`${importerDir}/${importSource}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (actualTarget !== importSource) {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const allowedModules = allowedOption[targetModule] || true
|
|
139
|
+
|
|
140
|
+
if (!Array.isArray(allowedModules)) {
|
|
141
|
+
isDenyPath = true
|
|
142
|
+
deniedModules.push(true)
|
|
143
|
+
} else {
|
|
144
|
+
const importedNames = node.specifiers.map(s => s.imported?.name)
|
|
145
|
+
const notAllowedModules = importedNames.filter(name => !allowedModules.includes(name))
|
|
146
|
+
deniedModules.push(notAllowedModules)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 完全に許可されている場合はスキップ
|
|
152
|
+
const shouldSkip =
|
|
153
|
+
(isDenyPath && deniedModules[0] === true) ||
|
|
154
|
+
(!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0)
|
|
83
155
|
|
|
84
|
-
|
|
85
|
-
}, source)
|
|
156
|
+
return { shouldSkip, deniedModules }
|
|
86
157
|
}
|
|
87
158
|
|
|
88
|
-
|
|
89
|
-
|
|
159
|
+
/**
|
|
160
|
+
* import先のパスから親方向に barrel ファイルを探索する
|
|
161
|
+
* @param {string} importedPath - import先の絶対パス
|
|
162
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
163
|
+
* @returns {string|undefined} 見つかったbarrelファイルのパス
|
|
164
|
+
*/
|
|
165
|
+
const findBarrelFile = (importedPath, importerDir) => {
|
|
166
|
+
const pathSegments = importedPath.split('/')
|
|
167
|
+
let currentPath = importedPath
|
|
168
|
+
let barrel = undefined
|
|
169
|
+
|
|
170
|
+
// ディレクトリ指定の場合、そのindex.tsを指していることは自明なので一階層上から探索
|
|
171
|
+
if (fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
|
|
172
|
+
pathSegments.pop()
|
|
173
|
+
currentPath = pathSegments.join('/')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
while (pathSegments.length > 0) {
|
|
177
|
+
// 以下の場合は探索終了
|
|
178
|
+
// 1. いずれかのreplacePathsのルートに到達した場合
|
|
179
|
+
// 2. import先がimport元の内部にある場合(同階層・サブディレクトリからのimport)
|
|
180
|
+
if (ALL_ROOT_PATHS.includes(currentPath) || isImportedInsideImporter(importerDir, currentPath)) {
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 現在のパスにbarrelファイルがあるかチェック
|
|
185
|
+
const foundBarrel = TARGET_EXTS
|
|
186
|
+
.map(ext => `${currentPath}/index.${ext}`)
|
|
187
|
+
.find(filePath => fs.existsSync(filePath))
|
|
188
|
+
|
|
189
|
+
if (foundBarrel) {
|
|
190
|
+
barrel = foundBarrel
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 一階層上に移動
|
|
194
|
+
pathSegments.pop()
|
|
195
|
+
currentPath = pathSegments.join('/')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return barrel
|
|
199
|
+
}
|
|
90
200
|
|
|
91
201
|
/**
|
|
92
202
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
@@ -99,108 +209,78 @@ module.exports = {
|
|
|
99
209
|
create(context) {
|
|
100
210
|
const option = context.options[0] || {}
|
|
101
211
|
|
|
102
|
-
|
|
103
|
-
|
|
212
|
+
// ignoresオプションでスキップ対象のファイルかチェック
|
|
213
|
+
if (option.ignores) {
|
|
214
|
+
const isIgnored = option.ignores.some(pattern =>
|
|
215
|
+
new RegExp(pattern).test(context.filename)
|
|
216
|
+
)
|
|
217
|
+
if (isIgnored) {
|
|
218
|
+
return {}
|
|
219
|
+
}
|
|
104
220
|
}
|
|
105
221
|
|
|
106
|
-
const
|
|
222
|
+
const importerDir = getParentDir(context.filename)
|
|
223
|
+
|
|
224
|
+
// このファイルに適用されるallowedImportsのキーを収集
|
|
107
225
|
const targetAllowedImports = []
|
|
108
226
|
if (option?.allowedImports) {
|
|
109
|
-
for (const
|
|
110
|
-
if (
|
|
111
|
-
targetAllowedImports.push(
|
|
227
|
+
for (const pattern in option.allowedImports) {
|
|
228
|
+
if (new RegExp(pattern).test(context.filename)) {
|
|
229
|
+
targetAllowedImports.push(pattern)
|
|
112
230
|
}
|
|
113
231
|
}
|
|
114
232
|
}
|
|
115
233
|
|
|
116
234
|
return {
|
|
117
235
|
ImportDeclaration: (node) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let sourceValue = node.source.value
|
|
127
|
-
|
|
128
|
-
if (actualTarget[0] === '/') {
|
|
129
|
-
sourceValue = path.resolve(`${dir}/${sourceValue}`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (actualTarget !== sourceValue) {
|
|
133
|
-
continue
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let allowedModules = allowedOption[targetModule] || true
|
|
137
|
-
|
|
138
|
-
if (!Array.isArray(allowedModules)) {
|
|
139
|
-
isDenyPath = true
|
|
140
|
-
deniedModules.push(true)
|
|
141
|
-
} else {
|
|
142
|
-
deniedModules.push(node.specifiers.map(pickImportedName).filter(i => allowedModules.indexOf(i) == -1))
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
isDenyPath && deniedModules[0] === true ||
|
|
149
|
-
!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0
|
|
150
|
-
) {
|
|
236
|
+
// allowedImportsチェック
|
|
237
|
+
const { shouldSkip, deniedModules } = checkAllowedImports(
|
|
238
|
+
node,
|
|
239
|
+
importerDir,
|
|
240
|
+
targetAllowedImports,
|
|
241
|
+
option.allowedImports || {}
|
|
242
|
+
)
|
|
243
|
+
if (shouldSkip) {
|
|
151
244
|
return
|
|
152
245
|
}
|
|
153
246
|
|
|
154
|
-
|
|
247
|
+
// import先のパスを絶対パスに変換
|
|
248
|
+
let importedPath = node.source.value
|
|
155
249
|
|
|
156
|
-
|
|
157
|
-
|
|
250
|
+
// 相対パスの場合、絶対パスに変換
|
|
251
|
+
if (importedPath[0] === '.') {
|
|
252
|
+
importedPath = path.resolve(`${importerDir}/${importedPath}`)
|
|
158
253
|
}
|
|
159
254
|
|
|
160
|
-
|
|
255
|
+
// Path alias(@/, ~/など)を絶対パスに変換
|
|
256
|
+
importedPath = resolvePathAlias(importedPath)
|
|
161
257
|
|
|
162
|
-
|
|
258
|
+
// 絶対パスでない場合(node_modulesなど)はスキップ
|
|
259
|
+
if (importedPath[0] !== '/') {
|
|
163
260
|
return
|
|
164
261
|
}
|
|
165
262
|
|
|
166
|
-
|
|
263
|
+
// barrel ファイルを探索
|
|
264
|
+
const barrelPath = findBarrelFile(importedPath, importerDir)
|
|
167
265
|
|
|
168
|
-
//
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
sourceValue = sources.join('/')
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
let barrel = undefined
|
|
175
|
-
|
|
176
|
-
while (sources.length > 0) {
|
|
177
|
-
// HINT: 以下の場合は即終了
|
|
178
|
-
// - import元以下のimportだった場合
|
|
179
|
-
// - rootまで捜索した場合
|
|
180
|
-
if (
|
|
181
|
-
dir === rootPath ||
|
|
182
|
-
dir.match(new RegExp(`^${sourceValue}`))
|
|
183
|
-
) {
|
|
184
|
-
break
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
barrel = TARGET_EXTS.map((e) => `${sourceValue}/index.${e}`).find(findExistsSync) || barrel
|
|
188
|
-
|
|
189
|
-
sources.pop()
|
|
190
|
-
sourceValue = sources.join('/')
|
|
266
|
+
// barrel が見つからない、またはroot pathのindex.tsの場合はスキップ
|
|
267
|
+
if (!barrelPath || REGEX_ROOT_PATH.test(barrelPath)) {
|
|
268
|
+
return
|
|
191
269
|
}
|
|
192
270
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
271
|
+
// barrel パスをPath aliasに変換
|
|
272
|
+
const barrelWithAlias = convertToPathAlias(barrelPath)
|
|
273
|
+
const barrelDirWithAlias = barrelWithAlias.replace(REGEX_INDEX_FILE, '')
|
|
274
|
+
const uniqueDeniedModules = [...new Set(deniedModules.flat())]
|
|
275
|
+
|
|
276
|
+
// エラーを報告
|
|
277
|
+
context.report({
|
|
278
|
+
node,
|
|
279
|
+
message: uniqueDeniedModules.length
|
|
280
|
+
? `${uniqueDeniedModules.join(', ')} は ${barrelDirWithAlias} からimportしてください`
|
|
281
|
+
: `${barrelDirWithAlias} からimportするか、${barrelWithAlias} のbarrelファイルを削除して直接import可能にしてください`
|
|
282
|
+
+ '\n - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import',
|
|
283
|
+
})
|
|
204
284
|
},
|
|
205
285
|
}
|
|
206
286
|
},
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const rule = require('../rules/prohibit-export-array-type')
|
|
2
2
|
const RuleTester = require('eslint').RuleTester
|
|
3
|
+
const tseslint = require('typescript-eslint')
|
|
3
4
|
|
|
4
5
|
const ruleTester = new RuleTester({
|
|
5
6
|
languageOptions: {
|
|
6
|
-
parser:
|
|
7
|
+
parser: tseslint.parser,
|
|
7
8
|
parserOptions: {
|
|
8
9
|
ecmaFeatures: {
|
|
9
10
|
jsx: true,
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const rule = require('../rules/require-barrel-import')
|
|
4
|
+
const RuleTester = require('eslint').RuleTester
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parserOptions: {
|
|
9
|
+
ecmaFeatures: {
|
|
10
|
+
jsx: true,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// テストフィクスチャのルートディレクトリ
|
|
17
|
+
const fixturesRoot = path.join(__dirname, '..', 'test-fixtures')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* テスト用のファイル構造を作成するヘルパー
|
|
21
|
+
* @param {string} testName - テスト名(ディレクトリ名として使用)
|
|
22
|
+
* @param {Object} structure - ファイル構造定義
|
|
23
|
+
* @returns {string} 作成したディレクトリのパス
|
|
24
|
+
*/
|
|
25
|
+
function createFixture(testName, structure) {
|
|
26
|
+
const fixtureDir = path.join(fixturesRoot, testName)
|
|
27
|
+
|
|
28
|
+
// ディレクトリが既に存在する場合は削除
|
|
29
|
+
if (fs.existsSync(fixtureDir)) {
|
|
30
|
+
fs.rmSync(fixtureDir, { recursive: true, force: true })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ディレクトリとファイルを再帰的に作成
|
|
34
|
+
function createStructure(dir, struct) {
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
36
|
+
|
|
37
|
+
for (const [name, content] of Object.entries(struct)) {
|
|
38
|
+
const fullPath = path.join(dir, name)
|
|
39
|
+
|
|
40
|
+
if (typeof content === 'object' && content !== null) {
|
|
41
|
+
// ディレクトリ
|
|
42
|
+
createStructure(fullPath, content)
|
|
43
|
+
} else {
|
|
44
|
+
// ファイル
|
|
45
|
+
fs.writeFileSync(fullPath, content || '')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
createStructure(fixtureDir, structure)
|
|
51
|
+
return fixtureDir
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* テスト終了後のクリーンアップ
|
|
56
|
+
*/
|
|
57
|
+
function cleanupFixtures() {
|
|
58
|
+
if (fs.existsSync(fixturesRoot)) {
|
|
59
|
+
const entries = fs.readdirSync(fixturesRoot)
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const fullPath = path.join(fixturesRoot, entry)
|
|
62
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
63
|
+
fs.rmSync(fullPath, { recursive: true, force: true })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// テスト終了後にクリーンアップ
|
|
70
|
+
afterAll(() => {
|
|
71
|
+
cleanupFixtures()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
ruleTester.run('require-barrel-import', rule, {
|
|
75
|
+
valid: [
|
|
76
|
+
// 同階層・サブディレクトリからのimport(エラーにならない)
|
|
77
|
+
{
|
|
78
|
+
code: `import { useMenu } from './hooks/useMenu'`,
|
|
79
|
+
filename: (() => {
|
|
80
|
+
createFixture('same-level-import', {
|
|
81
|
+
'Menu': {
|
|
82
|
+
'MenuItem.tsx': '',
|
|
83
|
+
'index.tsx': 'export {}',
|
|
84
|
+
'hooks': {
|
|
85
|
+
'useMenu.ts': '',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
return `${fixturesRoot}/same-level-import/Menu/MenuItem.tsx`
|
|
90
|
+
})(),
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Next.js App Router特殊文字パス - 同階層import
|
|
94
|
+
{
|
|
95
|
+
code: `import { useUsers } from './hooks/useUsers'`,
|
|
96
|
+
filename: (() => {
|
|
97
|
+
createFixture('nextjs-special-chars', {
|
|
98
|
+
'app': {
|
|
99
|
+
'(private)': {
|
|
100
|
+
'settings': {
|
|
101
|
+
'user_roles': {
|
|
102
|
+
'_components': {
|
|
103
|
+
'index.tsx': 'export {}',
|
|
104
|
+
'AddUserRoleDialog': {
|
|
105
|
+
'index.tsx': 'export {}',
|
|
106
|
+
'AddUserRoleDialog.tsx': '',
|
|
107
|
+
'hooks': {
|
|
108
|
+
'useUsers.ts': '',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
return `${fixturesRoot}/nextjs-special-chars/app/(private)/settings/user_roles/_components/AddUserRoleDialog/AddUserRoleDialog.tsx`
|
|
118
|
+
})(),
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Dynamic Routes - [id]パス(同階層)
|
|
122
|
+
{
|
|
123
|
+
code: `import { useDetail } from './hooks/useDetail'`,
|
|
124
|
+
filename: (() => {
|
|
125
|
+
createFixture('nextjs-dynamic-route', {
|
|
126
|
+
'app': {
|
|
127
|
+
'items': {
|
|
128
|
+
'[id]': {
|
|
129
|
+
'index.tsx': 'export {}',
|
|
130
|
+
'DetailPage.tsx': '',
|
|
131
|
+
'hooks': {
|
|
132
|
+
'useDetail.ts': '',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
return `${fixturesRoot}/nextjs-dynamic-route/app/items/[id]/DetailPage.tsx`
|
|
139
|
+
})(),
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// barrel が存在しない場合(同階層サブディレクトリ)
|
|
143
|
+
{
|
|
144
|
+
code: `import { helper } from './utils/helper'`,
|
|
145
|
+
filename: (() => {
|
|
146
|
+
createFixture('no-barrel', {
|
|
147
|
+
'components': {
|
|
148
|
+
'Button': {
|
|
149
|
+
'Button.tsx': '',
|
|
150
|
+
'utils': {
|
|
151
|
+
'helper.ts': '',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
return `${fixturesRoot}/no-barrel/components/Button/Button.tsx`
|
|
157
|
+
})(),
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// 親階層からのimport + barrelなし(エラーにならない)
|
|
161
|
+
{
|
|
162
|
+
code: `import { helper } from '../utils/helper'`,
|
|
163
|
+
filename: (() => {
|
|
164
|
+
createFixture('parent-import-no-barrel', {
|
|
165
|
+
// index.tsx なし(barrelなし)
|
|
166
|
+
'Button': {
|
|
167
|
+
'Button.tsx': '',
|
|
168
|
+
},
|
|
169
|
+
'utils': {
|
|
170
|
+
'helper.ts': '',
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
return `${fixturesRoot}/parent-import-no-barrel/Button/Button.tsx`
|
|
174
|
+
})(),
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// Next.js特殊文字パス - 親階層からのimport + barrelなし
|
|
178
|
+
{
|
|
179
|
+
code: `import { createUserRole } from '../hooks/createUserRoleAction'`,
|
|
180
|
+
filename: (() => {
|
|
181
|
+
createFixture('nextjs-parent-no-barrel', {
|
|
182
|
+
// index.tsx なし(barrelなし)
|
|
183
|
+
'AddUserRoleDialog': {
|
|
184
|
+
'AddUserRoleDialog.tsx': '',
|
|
185
|
+
},
|
|
186
|
+
'hooks': {
|
|
187
|
+
'createUserRoleAction.ts': '',
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
return `${fixturesRoot}/nextjs-parent-no-barrel/AddUserRoleDialog/AddUserRoleDialog.tsx`
|
|
191
|
+
})(),
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
// Dynamic Routes - 親階層からのimport + barrelなし
|
|
195
|
+
{
|
|
196
|
+
code: `import { api } from '../api/client'`,
|
|
197
|
+
filename: (() => {
|
|
198
|
+
createFixture('dynamic-route-parent-no-barrel', {
|
|
199
|
+
// index.tsx なし(barrelなし)
|
|
200
|
+
'[id]': {
|
|
201
|
+
'DetailPage.tsx': '',
|
|
202
|
+
},
|
|
203
|
+
'api': {
|
|
204
|
+
'client.ts': '',
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
return `${fixturesRoot}/dynamic-route-parent-no-barrel/[id]/DetailPage.tsx`
|
|
208
|
+
})(),
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// ============================================================
|
|
212
|
+
// Path alias - 同階層からのimport(エラーにならない)
|
|
213
|
+
// ============================================================
|
|
214
|
+
{
|
|
215
|
+
code: `import { useMenu } from '~/path-alias-same-level/Menu/hooks/useMenu'`,
|
|
216
|
+
filename: (() => {
|
|
217
|
+
createFixture('path-alias-same-level', {
|
|
218
|
+
'Menu': {
|
|
219
|
+
'MenuItem.tsx': '',
|
|
220
|
+
'index.tsx': 'export {}',
|
|
221
|
+
'hooks': {
|
|
222
|
+
'useMenu.ts': '',
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
return `${fixturesRoot}/path-alias-same-level/Menu/MenuItem.tsx`
|
|
227
|
+
})(),
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// Path alias - 同階層サブディレクトリ + barrelなし
|
|
231
|
+
{
|
|
232
|
+
code: `import { helper } from '~/path-alias-no-barrel/components/Button/utils/helper'`,
|
|
233
|
+
filename: (() => {
|
|
234
|
+
createFixture('path-alias-no-barrel', {
|
|
235
|
+
'components': {
|
|
236
|
+
'Button': {
|
|
237
|
+
'Button.tsx': '',
|
|
238
|
+
'utils': {
|
|
239
|
+
'helper.ts': '',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
return `${fixturesRoot}/path-alias-no-barrel/components/Button/Button.tsx`
|
|
245
|
+
})(),
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Path alias - 親階層からのimport + barrelなし
|
|
249
|
+
{
|
|
250
|
+
code: `import { helper } from '~/path-alias-parent-no-barrel/utils/helper'`,
|
|
251
|
+
filename: (() => {
|
|
252
|
+
createFixture('path-alias-parent-no-barrel', {
|
|
253
|
+
// index.tsx なし(barrelなし)
|
|
254
|
+
'Button': {
|
|
255
|
+
'Button.tsx': '',
|
|
256
|
+
},
|
|
257
|
+
'utils': {
|
|
258
|
+
'helper.ts': '',
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
return `${fixturesRoot}/path-alias-parent-no-barrel/Button/Button.tsx`
|
|
262
|
+
})(),
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
|
|
266
|
+
invalid: [
|
|
267
|
+
// 親階層からのimport(barrelが存在する場合)
|
|
268
|
+
{
|
|
269
|
+
code: `import { createUserRole } from '../hooks/createUserRoleAction'`,
|
|
270
|
+
filename: (() => {
|
|
271
|
+
createFixture('parent-import-with-barrel', {
|
|
272
|
+
'components': {
|
|
273
|
+
'index.tsx': 'export {}',
|
|
274
|
+
'AddDialog': {
|
|
275
|
+
'AddDialog.tsx': '',
|
|
276
|
+
},
|
|
277
|
+
'hooks': {
|
|
278
|
+
'createUserRoleAction.ts': '',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
return `${fixturesRoot}/parent-import-with-barrel/components/AddDialog/AddDialog.tsx`
|
|
283
|
+
})(),
|
|
284
|
+
errors: [
|
|
285
|
+
{
|
|
286
|
+
message: /からimportするか、.*のbarrelファイルを削除して直接import可能にしてください/,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// Next.js特殊文字パス - 親階層からのimport
|
|
292
|
+
{
|
|
293
|
+
code: `import { createUserRole } from '../hooks/createUserRoleAction'`,
|
|
294
|
+
filename: (() => {
|
|
295
|
+
createFixture('nextjs-parent-import', {
|
|
296
|
+
'app': {
|
|
297
|
+
'(private)': {
|
|
298
|
+
'settings': {
|
|
299
|
+
'user_roles': {
|
|
300
|
+
'_components': {
|
|
301
|
+
'index.tsx': 'export {}',
|
|
302
|
+
'AddUserRoleDialog': {
|
|
303
|
+
'AddUserRoleDialog.tsx': '',
|
|
304
|
+
},
|
|
305
|
+
'hooks': {
|
|
306
|
+
'createUserRoleAction.ts': '',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
return `${fixturesRoot}/nextjs-parent-import/app/(private)/settings/user_roles/_components/AddUserRoleDialog/AddUserRoleDialog.tsx`
|
|
315
|
+
})(),
|
|
316
|
+
errors: [
|
|
317
|
+
{
|
|
318
|
+
message: /からimportするか、.*のbarrelファイルを削除して直接import可能にしてください/,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// [id] Dynamic Routes - 親階層からのimport
|
|
324
|
+
{
|
|
325
|
+
code: `import { api } from '../api/client'`,
|
|
326
|
+
filename: (() => {
|
|
327
|
+
createFixture('dynamic-route-parent-import', {
|
|
328
|
+
'app': {
|
|
329
|
+
'items': {
|
|
330
|
+
'index.tsx': 'export {}',
|
|
331
|
+
'[id]': {
|
|
332
|
+
'DetailPage.tsx': '',
|
|
333
|
+
},
|
|
334
|
+
'api': {
|
|
335
|
+
'client.ts': '',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
return `${fixturesRoot}/dynamic-route-parent-import/app/items/[id]/DetailPage.tsx`
|
|
341
|
+
})(),
|
|
342
|
+
errors: [
|
|
343
|
+
{
|
|
344
|
+
message: /からimportするか、.*のbarrelファイルを削除して直接import可能にしてください/,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// ============================================================
|
|
350
|
+
// Path alias - 親階層からのimport(barrelあり)
|
|
351
|
+
// ============================================================
|
|
352
|
+
{
|
|
353
|
+
code: `import { createUserRole } from '~/path-alias-parent-import-with-barrel/components/hooks/createUserRoleAction'`,
|
|
354
|
+
filename: (() => {
|
|
355
|
+
createFixture('path-alias-parent-import-with-barrel', {
|
|
356
|
+
'components': {
|
|
357
|
+
'index.tsx': 'export {}',
|
|
358
|
+
'AddDialog': {
|
|
359
|
+
'AddDialog.tsx': '',
|
|
360
|
+
},
|
|
361
|
+
'hooks': {
|
|
362
|
+
'createUserRoleAction.ts': '',
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
return `${fixturesRoot}/path-alias-parent-import-with-barrel/components/AddDialog/AddDialog.tsx`
|
|
367
|
+
})(),
|
|
368
|
+
errors: [
|
|
369
|
+
{
|
|
370
|
+
message: /からimportするか、.*のbarrelファイルを削除して直接import可能にしてください/,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// Path alias + Next.js特殊文字パス - 親階層からのimport
|
|
376
|
+
{
|
|
377
|
+
code: `import { createUserRole } from '~/path-alias-nextjs-parent/app/(private)/settings/user_roles/_components/hooks/createUserRoleAction'`,
|
|
378
|
+
filename: (() => {
|
|
379
|
+
createFixture('path-alias-nextjs-parent', {
|
|
380
|
+
'app': {
|
|
381
|
+
'(private)': {
|
|
382
|
+
'settings': {
|
|
383
|
+
'user_roles': {
|
|
384
|
+
'_components': {
|
|
385
|
+
'index.tsx': 'export {}',
|
|
386
|
+
'AddUserRoleDialog': {
|
|
387
|
+
'AddUserRoleDialog.tsx': '',
|
|
388
|
+
},
|
|
389
|
+
'hooks': {
|
|
390
|
+
'createUserRoleAction.ts': '',
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
})
|
|
398
|
+
return `${fixturesRoot}/path-alias-nextjs-parent/app/(private)/settings/user_roles/_components/AddUserRoleDialog/AddUserRoleDialog.tsx`
|
|
399
|
+
})(),
|
|
400
|
+
errors: [
|
|
401
|
+
{
|
|
402
|
+
message: /からimportするか、.*のbarrelファイルを削除して直接import可能にしてください/,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
})
|