eslint-plugin-smarthr 6.10.0 → 6.10.1
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 +7 -0
- package/package.json +2 -2
- package/rules/require-barrel-import/index.js +187 -110
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.10.0...eslint-plugin-smarthr-v6.10.1) (2026-04-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **require-barrel-import:** next.jsの特殊文字パスでのバグ修正とリファクタリング ([#1210](https://github.com/kufu/tamatebako/issues/1210)) ([458cfe9](https://github.com/kufu/tamatebako/commit/458cfe90084cbcee936bcb375216f5807880edd7))
|
|
11
|
+
|
|
5
12
|
## [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
13
|
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "6.10.
|
|
3
|
+
"version": "6.10.1",
|
|
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": "e2e55eea3e72809fab55082ad6ef4da0c6025ae8"
|
|
41
41
|
}
|
|
@@ -31,62 +31,169 @@ 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 entriedReplacePathsWithRegex =
|
|
40
|
+
// Path aliasの正規表現を事前生成してキャッシュ
|
|
41
|
+
const entriedReplacePathsWithRegex = Object.entries(replacePaths).map(([key, values]) => [
|
|
43
42
|
key,
|
|
44
43
|
values,
|
|
45
44
|
new RegExp(`^${key}(.+)$`),
|
|
46
45
|
values.map(v => new RegExp(`^${path.resolve(`${CWD}/${v}`)}(.+)$`))
|
|
47
46
|
])
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Path aliasを絶対パスに変換する
|
|
50
|
+
* @param {string} importPath - import文のパス(例: '@/components/Button')
|
|
51
|
+
* @returns {string} 絶対パス(例: '/path/to/src/components/Button')
|
|
52
|
+
*/
|
|
53
|
+
const resolvePathAlias = (importPath) => {
|
|
54
|
+
if (importPath[0] === '/') {
|
|
55
|
+
return importPath
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
return entriedReplacePathsWithRegex.reduce((
|
|
55
|
-
if (
|
|
56
|
-
return values.reduce((
|
|
57
|
-
if (
|
|
58
|
-
return
|
|
58
|
+
return entriedReplacePathsWithRegex.reduce((result, [key, values, keyRegex]) => {
|
|
59
|
+
if (result === importPath) {
|
|
60
|
+
return values.reduce((resolved, value) => {
|
|
61
|
+
if (resolved === result && keyRegex.test(result)) {
|
|
62
|
+
return resolved.replace(keyRegex, `${path.resolve(`${CWD}/${value}`)}/$1`)
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}, prev)
|
|
64
|
+
return resolved
|
|
65
|
+
}, result)
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}, source)
|
|
67
|
+
return result
|
|
68
|
+
}, importPath)
|
|
67
69
|
}
|
|
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
70
|
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
/**
|
|
72
|
+
* 絶対パスをPath aliasに変換する
|
|
73
|
+
* @param {string} absolutePath - 絶対パス(例: '/path/to/src/components/Button')
|
|
74
|
+
* @returns {string} Path alias(例: '@/components/Button')
|
|
75
|
+
*/
|
|
76
|
+
const convertToPathAlias = (absolutePath) => {
|
|
77
|
+
return entriedReplacePathsWithRegex.reduce((result, [key, values, keyRegex, valueRegexes]) => {
|
|
78
|
+
if (result === absolutePath) {
|
|
79
|
+
return values.reduce((converted, value, index) => {
|
|
80
|
+
if (converted === result) {
|
|
81
|
+
const regexp = valueRegexes[index]
|
|
82
|
+
if (regexp.test(converted)) {
|
|
83
|
+
return converted.replace(regexp, `${key}/$1`).replace(REGEX_UNNECESSARY_SLASH, '/')
|
|
77
84
|
}
|
|
78
85
|
}
|
|
86
|
+
return converted
|
|
87
|
+
}, result)
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
}, absolutePath)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* import先がimport元の内部にあるかチェック(同階層・サブディレクトリからのimport)
|
|
95
|
+
* (Next.js App Routerの特殊文字パスにも対応)
|
|
96
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
97
|
+
* @param {string} importedPath - import先のパス
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
const isImportedInsideImporter = (importerDir, importedPath) => {
|
|
101
|
+
return importedPath === importerDir || importedPath.startsWith(importerDir + '/')
|
|
102
|
+
}
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
/**
|
|
105
|
+
* allowedImportsオプションに基づいて、特定のimportが許可されているかチェックする
|
|
106
|
+
* @param {object} node - ImportDeclaration node
|
|
107
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
108
|
+
* @param {Array} targetAllowedImports - 適用されるallowedImportsのキー配列
|
|
109
|
+
* @param {object} allowedImportsConfig - allowedImportsの設定
|
|
110
|
+
* @returns {{ shouldSkip: boolean, deniedModules: Array }} チェック結果
|
|
111
|
+
*/
|
|
112
|
+
const checkAllowedImports = (node, importerDir, targetAllowedImports, allowedImportsConfig) => {
|
|
113
|
+
let isDenyPath = false
|
|
114
|
+
let deniedModules = []
|
|
115
|
+
|
|
116
|
+
for (const allowedKey of targetAllowedImports) {
|
|
117
|
+
const allowedOption = allowedImportsConfig[allowedKey]
|
|
118
|
+
|
|
119
|
+
for (const targetModule in allowedOption) {
|
|
120
|
+
const actualTarget = targetModule[0] !== '.'
|
|
121
|
+
? targetModule
|
|
122
|
+
: path.resolve(`${CWD}/${targetModule}`)
|
|
123
|
+
|
|
124
|
+
let importSource = node.source.value
|
|
125
|
+
|
|
126
|
+
// 絶対パスの場合は、import元ディレクトリを基準に解決
|
|
127
|
+
if (actualTarget[0] === '/') {
|
|
128
|
+
importSource = path.resolve(`${importerDir}/${importSource}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (actualTarget !== importSource) {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const allowedModules = allowedOption[targetModule] || true
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(allowedModules)) {
|
|
138
|
+
isDenyPath = true
|
|
139
|
+
deniedModules.push(true)
|
|
140
|
+
} else {
|
|
141
|
+
const importedNames = node.specifiers.map(s => s.imported?.name)
|
|
142
|
+
const notAllowedModules = importedNames.filter(name => !allowedModules.includes(name))
|
|
143
|
+
deniedModules.push(notAllowedModules)
|
|
144
|
+
}
|
|
82
145
|
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 完全に許可されている場合はスキップ
|
|
149
|
+
const shouldSkip =
|
|
150
|
+
(isDenyPath && deniedModules[0] === true) ||
|
|
151
|
+
(!isDenyPath && deniedModules.length === 1 && deniedModules[0].length === 0)
|
|
83
152
|
|
|
84
|
-
|
|
85
|
-
}, source)
|
|
153
|
+
return { shouldSkip, deniedModules }
|
|
86
154
|
}
|
|
87
155
|
|
|
88
|
-
|
|
89
|
-
|
|
156
|
+
/**
|
|
157
|
+
* import先のパスから親方向に barrel ファイルを探索する
|
|
158
|
+
* @param {string} importedPath - import先の絶対パス
|
|
159
|
+
* @param {string} importerDir - import元のディレクトリ
|
|
160
|
+
* @returns {string|undefined} 見つかったbarrelファイルのパス
|
|
161
|
+
*/
|
|
162
|
+
const findBarrelFile = (importedPath, importerDir) => {
|
|
163
|
+
const pathSegments = importedPath.split('/')
|
|
164
|
+
let currentPath = importedPath
|
|
165
|
+
let barrel = undefined
|
|
166
|
+
|
|
167
|
+
// ディレクトリ指定の場合、そのindex.tsを指していることは自明なので一階層上から探索
|
|
168
|
+
if (fs.existsSync(currentPath) && fs.statSync(currentPath).isDirectory()) {
|
|
169
|
+
pathSegments.pop()
|
|
170
|
+
currentPath = pathSegments.join('/')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
while (pathSegments.length > 0) {
|
|
174
|
+
// 以下の場合は探索終了
|
|
175
|
+
// 1. root pathに到達した場合
|
|
176
|
+
// 2. import先がimport元の内部にある場合(同階層・サブディレクトリからのimport)
|
|
177
|
+
if (importerDir === rootPath || isImportedInsideImporter(importerDir, currentPath)) {
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 現在のパスにbarrelファイルがあるかチェック
|
|
182
|
+
const foundBarrel = TARGET_EXTS
|
|
183
|
+
.map(ext => `${currentPath}/index.${ext}`)
|
|
184
|
+
.find(filePath => fs.existsSync(filePath))
|
|
185
|
+
|
|
186
|
+
if (foundBarrel) {
|
|
187
|
+
barrel = foundBarrel
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 一階層上に移動
|
|
191
|
+
pathSegments.pop()
|
|
192
|
+
currentPath = pathSegments.join('/')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return barrel
|
|
196
|
+
}
|
|
90
197
|
|
|
91
198
|
/**
|
|
92
199
|
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<''>}
|
|
@@ -99,108 +206,78 @@ module.exports = {
|
|
|
99
206
|
create(context) {
|
|
100
207
|
const option = context.options[0] || {}
|
|
101
208
|
|
|
102
|
-
|
|
103
|
-
|
|
209
|
+
// ignoresオプションでスキップ対象のファイルかチェック
|
|
210
|
+
if (option.ignores) {
|
|
211
|
+
const isIgnored = option.ignores.some(pattern =>
|
|
212
|
+
new RegExp(pattern).test(context.filename)
|
|
213
|
+
)
|
|
214
|
+
if (isIgnored) {
|
|
215
|
+
return {}
|
|
216
|
+
}
|
|
104
217
|
}
|
|
105
218
|
|
|
106
|
-
const
|
|
219
|
+
const importerDir = getParentDir(context.filename)
|
|
220
|
+
|
|
221
|
+
// このファイルに適用されるallowedImportsのキーを収集
|
|
107
222
|
const targetAllowedImports = []
|
|
108
223
|
if (option?.allowedImports) {
|
|
109
|
-
for (const
|
|
110
|
-
if (
|
|
111
|
-
targetAllowedImports.push(
|
|
224
|
+
for (const pattern in option.allowedImports) {
|
|
225
|
+
if (new RegExp(pattern).test(context.filename)) {
|
|
226
|
+
targetAllowedImports.push(pattern)
|
|
112
227
|
}
|
|
113
228
|
}
|
|
114
229
|
}
|
|
115
230
|
|
|
116
231
|
return {
|
|
117
232
|
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
|
-
) {
|
|
233
|
+
// allowedImportsチェック
|
|
234
|
+
const { shouldSkip, deniedModules } = checkAllowedImports(
|
|
235
|
+
node,
|
|
236
|
+
importerDir,
|
|
237
|
+
targetAllowedImports,
|
|
238
|
+
option.allowedImports || {}
|
|
239
|
+
)
|
|
240
|
+
if (shouldSkip) {
|
|
151
241
|
return
|
|
152
242
|
}
|
|
153
243
|
|
|
154
|
-
|
|
244
|
+
// import先のパスを絶対パスに変換
|
|
245
|
+
let importedPath = node.source.value
|
|
155
246
|
|
|
156
|
-
|
|
157
|
-
|
|
247
|
+
// 相対パスの場合、絶対パスに変換
|
|
248
|
+
if (importedPath[0] === '.') {
|
|
249
|
+
importedPath = path.resolve(`${importerDir}/${importedPath}`)
|
|
158
250
|
}
|
|
159
251
|
|
|
160
|
-
|
|
252
|
+
// Path alias(@/, ~/など)を絶対パスに変換
|
|
253
|
+
importedPath = resolvePathAlias(importedPath)
|
|
161
254
|
|
|
162
|
-
|
|
255
|
+
// 絶対パスでない場合(node_modulesなど)はスキップ
|
|
256
|
+
if (importedPath[0] !== '/') {
|
|
163
257
|
return
|
|
164
258
|
}
|
|
165
259
|
|
|
166
|
-
|
|
260
|
+
// barrel ファイルを探索
|
|
261
|
+
const barrelPath = findBarrelFile(importedPath, importerDir)
|
|
167
262
|
|
|
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('/')
|
|
263
|
+
// barrel が見つからない、またはroot pathのindex.tsの場合はスキップ
|
|
264
|
+
if (!barrelPath || REGEX_ROOT_PATH.test(barrelPath)) {
|
|
265
|
+
return
|
|
191
266
|
}
|
|
192
267
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
268
|
+
// barrel パスをPath aliasに変換
|
|
269
|
+
const barrelWithAlias = convertToPathAlias(barrelPath)
|
|
270
|
+
const barrelDirWithAlias = barrelWithAlias.replace(REGEX_INDEX_FILE, '')
|
|
271
|
+
const uniqueDeniedModules = [...new Set(deniedModules.flat())]
|
|
272
|
+
|
|
273
|
+
// エラーを報告
|
|
274
|
+
context.report({
|
|
275
|
+
node,
|
|
276
|
+
message: uniqueDeniedModules.length
|
|
277
|
+
? `${uniqueDeniedModules.join(', ')} は ${barrelDirWithAlias} からimportしてください`
|
|
278
|
+
: `${barrelDirWithAlias} からimportするか、${barrelWithAlias} のbarrelファイルを削除して直接import可能にしてください`
|
|
279
|
+
+ '\n - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import',
|
|
280
|
+
})
|
|
204
281
|
},
|
|
205
282
|
}
|
|
206
283
|
},
|