1
1
import type { TSESLint } from "@typescript-eslint/utils" ;
2
2
import type { TSESTree } from "@typescript-eslint/types" ;
3
3
4
- const possibleReactExportRE = / ^ [ A - Z ] [ a - z A - Z 0 - 9 ] * $ / u;
5
- // Starts with uppercase and at least one lowercase
6
- // This can lead to some false positive (ex: `const CMS = () => <></>; export default CMS`)
7
- // But allow to catch `export const CONSTANT = 3`
8
- // and the false positive can be avoided with direct name export
9
- const strictReactExportRE = / ^ [ A - Z ] [ a - z A - Z 0 - 9 ] * [ a - z ] + [ a - z A - Z 0 - 9 ] * $ / u;
4
+ const reactComponentNameRE = / ^ [ A - Z ] [ a - z A - Z 0 - 9 ] * $ / u;
10
5
11
6
export const onlyExportComponents : TSESLint . RuleModule <
12
7
| "exportAll"
@@ -18,10 +13,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
18
13
| [ ]
19
14
| [
20
15
{
21
- allowConstantExport ?: boolean ;
22
- checkJS ?: boolean ;
23
16
allowExportNames ?: string [ ] ;
17
+ allowConstantExport ?: boolean ;
24
18
customHOCs ?: string [ ] ;
19
+ checkJS ?: boolean ;
25
20
} ,
26
21
]
27
22
> = {
@@ -45,10 +40,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
45
40
{
46
41
type : "object" ,
47
42
properties : {
48
- allowConstantExport : { type : "boolean" } ,
49
- checkJS : { type : "boolean" } ,
50
43
allowExportNames : { type : "array" , items : { type : "string" } } ,
44
+ allowConstantExport : { type : "boolean" } ,
51
45
customHOCs : { type : "array" , items : { type : "string" } } ,
46
+ checkJS : { type : "boolean" } ,
52
47
} ,
53
48
additionalProperties : false ,
54
49
} ,
@@ -57,10 +52,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
57
52
defaultOptions : [ ] ,
58
53
create : ( context ) => {
59
54
const {
60
- allowConstantExport = false ,
61
- checkJS = false ,
62
55
allowExportNames,
56
+ allowConstantExport = false ,
63
57
customHOCs = [ ] ,
58
+ checkJS = false ,
64
59
} = context . options [ 0 ] ?? { } ;
65
60
const filename = context . filename ;
66
61
// Skip tests & stories files
@@ -82,20 +77,20 @@ export const onlyExportComponents: TSESLint.RuleModule<
82
77
? new Set ( allowExportNames )
83
78
: undefined ;
84
79
85
- const reactHOCs = new Set ( [ "memo" , "forwardRef" , ...customHOCs ] ) ;
80
+ const reactHOCs = [ "memo" , "forwardRef" , ...customHOCs ] ;
86
81
const canBeReactFunctionComponent = ( init : TSESTree . Expression | null ) => {
87
82
if ( ! init ) return false ;
88
83
if ( init . type === "ArrowFunctionExpression" ) return true ;
89
84
if ( init . type === "CallExpression" && init . callee . type === "Identifier" ) {
90
- return reactHOCs . has ( init . callee . name ) ;
85
+ return reactHOCs . includes ( init . callee . name ) ;
91
86
}
92
87
return false ;
93
88
} ;
94
89
95
90
return {
96
91
Program ( program ) {
97
92
let hasExports = false ;
98
- let mayHaveReactExport = false ;
93
+ let hasReactExport = false ;
99
94
let reactIsInScope = false ;
100
95
const localComponents : TSESTree . Identifier [ ] = [ ] ;
101
96
const nonComponentExports : (
@@ -108,7 +103,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
108
103
identifierNode : TSESTree . BindingName ,
109
104
) => {
110
105
if ( identifierNode . type !== "Identifier" ) return ;
111
- if ( possibleReactExportRE . test ( identifierNode . name ) ) {
106
+ if ( reactComponentNameRE . test ( identifierNode . name ) ) {
112
107
localComponents . push ( identifierNode ) ;
113
108
}
114
109
} ;
@@ -135,8 +130,8 @@ export const onlyExportComponents: TSESLint.RuleModule<
135
130
}
136
131
137
132
if ( isFunction ) {
138
- if ( possibleReactExportRE . test ( identifierNode . name ) ) {
139
- mayHaveReactExport = true ;
133
+ if ( reactComponentNameRE . test ( identifierNode . name ) ) {
134
+ hasReactExport = true ;
140
135
} else {
141
136
nonComponentExports . push ( identifierNode ) ;
142
137
}
@@ -162,13 +157,9 @@ export const onlyExportComponents: TSESLint.RuleModule<
162
157
nonComponentExports . push ( identifierNode ) ;
163
158
return ;
164
159
}
165
- if (
166
- ! mayHaveReactExport &&
167
- possibleReactExportRE . test ( identifierNode . name )
168
- ) {
169
- mayHaveReactExport = true ;
170
- }
171
- if ( ! strictReactExportRE . test ( identifierNode . name ) ) {
160
+ if ( reactComponentNameRE . test ( identifierNode . name ) ) {
161
+ hasReactExport = true ;
162
+ } else {
172
163
nonComponentExports . push ( identifierNode ) ;
173
164
}
174
165
}
@@ -197,21 +188,21 @@ export const onlyExportComponents: TSESLint.RuleModule<
197
188
) {
198
189
// support for react-redux
199
190
// export default connect(mapStateToProps, mapDispatchToProps)(Comp)
200
- mayHaveReactExport = true ;
191
+ hasReactExport = true ;
201
192
} else if ( node . callee . type !== "Identifier" ) {
202
193
// we rule out non HoC first
203
194
// export default React.memo(function Foo() {})
204
195
// export default Preact.memo(function Foo() {})
205
196
if (
206
197
node . callee . type === "MemberExpression" &&
207
198
node . callee . property . type === "Identifier" &&
208
- reactHOCs . has ( node . callee . property . name )
199
+ reactHOCs . includes ( node . callee . property . name )
209
200
) {
210
- mayHaveReactExport = true ;
201
+ hasReactExport = true ;
211
202
} else {
212
203
context . report ( { messageId : "anonymousExport" , node } ) ;
213
204
}
214
- } else if ( ! reactHOCs . has ( node . callee . name ) ) {
205
+ } else if ( ! reactHOCs . includes ( node . callee . name ) ) {
215
206
// we rule out non HoC first
216
207
context . report ( { messageId : "anonymousExport" , node } ) ;
217
208
} else if (
@@ -225,7 +216,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
225
216
// No need to check further, the identifier has necessarily a named,
226
217
// and it would throw at runtime if it's not a React component.
227
218
// We have React exports since we are exporting return value of HoC
228
- mayHaveReactExport = true ;
219
+ hasReactExport = true ;
229
220
} else {
230
221
context . report ( { messageId : "anonymousExport" , node } ) ;
231
222
}
@@ -289,7 +280,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
289
280
290
281
if ( hasExports ) {
291
282
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
292
- if ( mayHaveReactExport ) {
283
+ if ( hasReactExport ) {
293
284
for ( const node of nonComponentExports ) {
294
285
context . report ( { messageId : "namedExport" , node } ) ;
295
286
}
0 commit comments