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 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.0",
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": "9f682d10a15702c061e23d7c0922e0d3f69149d5"
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 entriedReplacePathsWithRegex = entriedReplacePaths.map(([key, values]) => [
43
- key,
44
- values,
45
- new RegExp(`^${key}(.+)$`),
46
- values.map(v => new RegExp(`^${path.resolve(`${CWD}/${v}`)}(.+)$`))
47
- ])
48
-
49
- const calculateAbsoluteImportPath = (source) => {
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
- return entriedReplacePathsWithRegex.reduce((prev, [key, values, keyRegex]) => {
55
- if (source === prev) {
56
- return values.reduce((p, v) => {
57
- if (prev === p && keyRegex.test(keyRegex)) {
58
- return p.replace(keyRegex, `${path.resolve(`${CWD}/${v}`)}/$1`)
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
- return p
62
- }, prev)
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
- return prev
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
- return p
81
- }, prev)
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
- return prev
85
- }, source)
156
+ return { shouldSkip, deniedModules }
86
157
  }
87
158
 
88
- const pickImportedName = (s) => s.imported?.name
89
- const findExistsSync = (p) => fs.existsSync(p)
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
- if (option.ignores && option.ignores.some((i) => (new RegExp(i)).test(context.filename))) {
103
- return {}
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 dir = getParentDir(context.filename)
222
+ const importerDir = getParentDir(context.filename)
223
+
224
+ // このファイルに適用されるallowedImportsのキーを収集
107
225
  const targetAllowedImports = []
108
226
  if (option?.allowedImports) {
109
- for (const regex in option.allowedImports) {
110
- if ((new RegExp(regex)).test(context.filename)) {
111
- targetAllowedImports.push(regex)
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
- let isDenyPath = false
119
- let deniedModules = []
120
-
121
- for (const allowedKey of targetAllowedImports) {
122
- const allowedOption = option.allowedImports[allowedKey]
123
-
124
- for (const targetModule in allowedOption) {
125
- const actualTarget = targetModule[0] !== '.' ? targetModule : path.resolve(`${CWD}/${targetModule}`)
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
- let sourceValue = node.source.value
247
+ // import先のパスを絶対パスに変換
248
+ let importedPath = node.source.value
155
249
 
156
- if (sourceValue[0] === '.') {
157
- sourceValue = path.resolve(`${dir}/${sourceValue}`)
250
+ // 相対パスの場合、絶対パスに変換
251
+ if (importedPath[0] === '.') {
252
+ importedPath = path.resolve(`${importerDir}/${importedPath}`)
158
253
  }
159
254
 
160
- sourceValue = calculateAbsoluteImportPath(sourceValue)
255
+ // Path alias(@/, ~/など)を絶対パスに変換
256
+ importedPath = resolvePathAlias(importedPath)
161
257
 
162
- if (sourceValue[0] !== '/') {
258
+ // 絶対パスでない場合(node_modulesなど)はスキップ
259
+ if (importedPath[0] !== '/') {
163
260
  return
164
261
  }
165
262
 
166
- const sources = sourceValue.split('/')
263
+ // barrel ファイルを探索
264
+ const barrelPath = findBarrelFile(importedPath, importerDir)
167
265
 
168
- // HINT: directoryの場合、indexファイルからimportしていることは自明であるため、一階層上からチェックする
169
- if (fs.existsSync(sourceValue) && fs.statSync(sourceValue).isDirectory()) {
170
- sources.pop()
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
- if (barrel && !barrel.match(REGEX_ROOT_PATH)) {
194
- barrel = calculateReplacedImportPath(barrel)
195
- const noExt = barrel.replace(REGEX_INDEX_FILE, '')
196
- deniedModules = [...new Set(deniedModules.flat())]
197
-
198
- context.report({
199
- node,
200
- message: `${deniedModules.length ? `${deniedModules.join(', ')} は ${noExt} からimportしてください` : `${noExt} からimportするか、${barrel} のbarrelファイルを削除して直接import可能にしてください`}
201
- - 詳細: https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-barrel-import`,
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: require('@typescript-eslint/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
+ })
package/tsconfig.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "paths": {
4
+ // ルール実装で使用
4
5
  "@/*": ["./src/*"],
5
- "~/*": ["./src/*"]
6
+ // テスト用
7
+ "~/*": ["./test-fixtures/*"]
6
8
  }
7
9
  }
8
10
  }