Skip to content

Commit c0568ab

Browse files
committed
Allow class
1 parent d36ceaf commit c0568ab

File tree

2 files changed

+70
-23
lines changed

2 files changed

+70
-23
lines changed

src/rules/component/__tests__/index.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ describe('component rule', () => {
1515
})
1616
ruleTester.run('component rule', component, {
1717
valid: [
18+
{
19+
code: 'export default function IndexPage(){return <Row><Col></Col></Row>}',
20+
filename: 'src/app/page',
21+
},
1822
{
1923
code: 'export default function IndexPage(){return <Row><Col></Col></Row>}',
2024
filename: 'src/app/page.tsx',

src/rules/component/index.ts

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'
2+
3+
/**
4+
* 문자열을 파스칼 케이스로 변환합니다.
5+
* @param str 변환할 문자열
6+
* @returns 파스칼 케이스로 변환된 문자열
7+
*/
28
function toPascal(str: string) {
39
return str
410
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
@@ -10,6 +16,18 @@ const createRule = ESLintUtils.RuleCreator(
1016
`https://github.com/dev-five-git/devup/tree/main/packages/eslint-plugin/src/rules/${name}`,
1117
)
1218

19+
// 검사 제외 파일 패턴
20+
const EXCLUDE_PATTERNS = [
21+
/[\\/]utils[\\/]/,
22+
/[\\/](__)?tests?(__)?[\\/]/,
23+
/\.(test|css|stories)\.[jt]sx$/,
24+
]
25+
26+
// 검사 대상 파일 패턴
27+
const INCLUDE_PATTERNS = [
28+
/(src[/\\])?(app[/\\](?!.*[\\/]?(page|layout|404)\.[jt]sx$)|components[/\\]).*\.[jt]sx$/,
29+
]
30+
1331
export const component = createRule({
1432
name: 'component',
1533
defaultOptions: [],
@@ -31,67 +49,88 @@ export const component = createRule({
3149
create(context) {
3250
const filename = context.physicalFilename
3351

34-
if (
35-
!/(src[/\\])?(app[/\\](?!.*[\\/]?(page|layout|404)\.[jt]sx$)|components[/\\]).*\.[jt]sx$/.test(
36-
filename,
37-
) ||
38-
/[\\/]utils[\\/]|[\\/](__)?tests?(__)?[\\/]|\.(test|css|stories)\.[jt]sx$/.test(
39-
filename,
40-
)
41-
)
52+
// 검사 대상이 아닌 파일은 빈 객체 반환
53+
const isIncluded = INCLUDE_PATTERNS.some(pattern => pattern.test(filename))
54+
const isExcluded = EXCLUDE_PATTERNS.some(pattern => pattern.test(filename))
55+
56+
if (!isIncluded || isExcluded) {
4257
return {}
58+
}
59+
60+
// 파일 경로에서 컴포넌트 이름 추출
4361
const targetComponentRegex = /([^/\\]+)[/\\]([^/\\]+)\.[jt]sx$/i.exec(
4462
filename,
4563
)!
46-
let targetComponentName
64+
4765
const isIndex = targetComponentRegex[2].startsWith('index')
48-
49-
if (isIndex) targetComponentName = toPascal(targetComponentRegex[1])
50-
else targetComponentName = toPascal(targetComponentRegex[2])
66+
const targetComponentName = isIndex
67+
? toPascal(targetComponentRegex[1])
68+
: toPascal(targetComponentRegex[2])
69+
5170
const exportFunc: TSESTree.Node[] = []
5271
let ok = false
5372

73+
/**
74+
* 선언이 타겟 컴포넌트 이름과 일치하는지 확인
75+
* @param name 선언된 이름
76+
* @returns 일치 여부
77+
*/
78+
const isTargetComponent = (name: string) => name === targetComponentName
79+
5480
return {
5581
ExportNamedDeclaration(namedExport) {
5682
if (ok) return
83+
84+
// index 파일에서 export 구문이 있는 경우 검사 통과
5785
if (namedExport.specifiers.length && isIndex) {
58-
// export 용
5986
ok = true
6087
return
6188
}
89+
6290
const declaration = namedExport.declaration
6391
if (!declaration) return
64-
if (declaration.type === 'FunctionDeclaration') {
65-
// export 아래기 때문에 반드시 id가 있습니다.
66-
if (targetComponentName === declaration.id!.name) {
92+
93+
// 함수 선언 검사
94+
if (declaration.type === 'FunctionDeclaration' && declaration.id) {
95+
if (isTargetComponent(declaration.id.name)) {
6796
ok = true
6897
return
6998
}
70-
exportFunc.push(declaration.id!)
99+
exportFunc.push(declaration.id)
71100
}
72-
if (declaration.type === 'ClassDeclaration') {
73-
if (declaration.id!.name === targetComponentName) {
101+
102+
// 클래스 선언 검사
103+
if (declaration.type === 'ClassDeclaration' && declaration.id) {
104+
if (isTargetComponent(declaration.id.name)) {
74105
ok = true
75106
return
76107
}
77108
}
109+
110+
// 변수 선언 검사 (화살표 함수, 함수 표현식 등)
78111
if (declaration.type === 'VariableDeclaration') {
79112
for (const el of declaration.declarations) {
80113
if (el.id.type !== 'Identifier') continue
81-
if (el.id.name === targetComponentName) {
114+
115+
if (isTargetComponent(el.id.name)) {
82116
ok = true
83117
return
84118
}
85-
if (
119+
120+
const isComponentFunction =
86121
el.init?.type === 'ArrowFunctionExpression' ||
87122
el.init?.type === 'FunctionExpression'
88-
)
123+
124+
if (isComponentFunction) {
89125
exportFunc.push(el.id)
126+
}
90127
}
91128
}
92129
},
93130
'Program:exit'(program) {
94131
if (ok) return
132+
133+
// 컴포넌트 이름이 일치하지 않는 경우 수정 제안
95134
if (exportFunc.length) {
96135
for (const exported of exportFunc) {
97136
context.report({
@@ -104,13 +143,17 @@ export const component = createRule({
104143
}
105144
return
106145
}
146+
147+
// 컴포넌트를 내보내지 않는 경우 기본 컴포넌트 추가 제안
107148
context.report({
108149
node: program,
109150
messageId: 'componentFileShouldExportComponent',
110151
fix(fixer) {
152+
const hasContent = context.sourceCode.text.trim().length > 0
153+
const newline = hasContent ? '\n' : ''
111154
return fixer.insertTextAfter(
112155
context.sourceCode.ast,
113-
`${context.sourceCode.text.trim().length ? '\n' : ''}export function ${targetComponentName}(){return <></>}`,
156+
`${newline}export function ${targetComponentName}(){return <></>}`,
114157
)
115158
},
116159
})

0 commit comments

Comments
 (0)