11import { ESLintUtils , TSESTree } from '@typescript-eslint/utils'
2+
3+ /**
4+ * 문자열을 파스칼 케이스로 변환합니다.
5+ * @param str 변환할 문자열
6+ * @returns 파스칼 케이스로 변환된 문자열
7+ */
28function 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+ / [ \\ / ] u t i l s [ \\ / ] / ,
22+ / [ \\ / ] ( _ _ ) ? t e s t s ? ( _ _ ) ? [ \\ / ] / ,
23+ / \. ( t e s t | c s s | s t o r i e s ) \. [ j t ] s x $ / ,
24+ ]
25+
26+ // 검사 대상 파일 패턴
27+ const INCLUDE_PATTERNS = [
28+ / ( s r c [ / \\ ] ) ? ( a p p [ / \\ ] (? ! .* [ \\ / ] ? ( p a g e | l a y o u t | 4 0 4 ) \. [ j t ] s x $ ) | c o m p o n e n t s [ / \\ ] ) .* \. [ j t ] s x $ / ,
29+ ]
30+
1331export 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- ! / ( s r c [ / \\ ] ) ? ( a p p [ / \\ ] (? ! .* [ \\ / ] ? ( p a g e | l a y o u t | 4 0 4 ) \. [ j t ] s x $ ) | c o m p o n e n t s [ / \\ ] ) .* \. [ j t ] s x $ / . test (
36- filename ,
37- ) ||
38- / [ \\ / ] u t i l s [ \\ / ] | [ \\ / ] ( _ _ ) ? t e s t s ? ( _ _ ) ? [ \\ / ] | \. ( t e s t | c s s | s t o r i e s ) \. [ j t ] s x $ / . 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 = / ( [ ^ / \\ ] + ) [ / \\ ] ( [ ^ / \\ ] + ) \. [ j t ] s x $ / 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