@@ -72,6 +72,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
72
72
( checkJS && filename . endsWith ( ".js" ) ) ;
73
73
if ( ! shouldScan ) return { } ;
74
74
75
+ const allowExportNamesSet = allowExportNames
76
+ ? new Set ( allowExportNames )
77
+ : undefined ;
78
+
75
79
return {
76
80
Program ( program ) {
77
81
let hasExports = false ;
@@ -98,7 +102,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
98
102
nonComponentExports . push ( identifierNode ) ;
99
103
return ;
100
104
}
101
- if ( allowExportNames ?. includes ( identifierNode . name ) ) return ;
105
+ if ( allowExportNamesSet ?. has ( identifierNode . name ) ) return ;
102
106
if (
103
107
allowConstantExport &&
104
108
init &&
@@ -109,6 +113,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
109
113
) {
110
114
return ;
111
115
}
116
+
112
117
if ( isFunction ) {
113
118
if ( possibleReactExportRE . test ( identifierNode . name ) ) {
114
119
mayHaveReactExport = true ;
@@ -119,7 +124,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
119
124
if (
120
125
init &&
121
126
// Switch to allowList?
122
- notReactComponentExpression . includes ( init . type )
127
+ notReactComponentExpression . has ( init . type )
123
128
) {
124
129
nonComponentExports . push ( identifierNode ) ;
125
130
return ;
@@ -153,12 +158,23 @@ export const onlyExportComponents: TSESLint.RuleModule<
153
158
}
154
159
} else if ( node . type === "CallExpression" ) {
155
160
if (
156
- node . callee . type === "Identifier" &&
157
- reactHOCs . includes ( node . callee . name ) &&
161
+ node . callee . type !== "Identifier" ||
162
+ ! reactHOCs . has ( node . callee . name )
163
+ ) {
164
+ // we rule out non HoC first
165
+ context . report ( { messageId : "anonymousExport" , node } ) ;
166
+ } else if (
158
167
node . arguments [ 0 ] ?. type === "FunctionExpression" &&
159
168
node . arguments [ 0 ] . id
160
169
) {
170
+ // export default memo(function Foo() {})
161
171
handleExportIdentifier ( node . arguments [ 0 ] . id , true ) ;
172
+ } else if ( node . arguments [ 0 ] ?. type === "Identifier" ) {
173
+ // const Foo = () => {}; export default memo(Foo);
174
+ // No need to check further, the identifier has necessarily a named,
175
+ // and it would throw at runtime if it's not a React component.
176
+ // We have React exports since we are exporting return value of HoC
177
+ mayHaveReactExport = true ;
162
178
} else {
163
179
context . report ( { messageId : "anonymousExport" , node } ) ;
164
180
}
@@ -234,18 +250,20 @@ export const onlyExportComponents: TSESLint.RuleModule<
234
250
} ,
235
251
} ;
236
252
237
- const reactHOCs = [ "memo" , "forwardRef" ] ;
253
+ const reactHOCs = new Set ( [ "memo" , "forwardRef" ] ) ;
238
254
const canBeReactFunctionComponent = ( init : TSESTree . Expression | null ) => {
239
255
if ( ! init ) return false ;
240
256
if ( init . type === "ArrowFunctionExpression" ) return true ;
241
257
if ( init . type === "CallExpression" && init . callee . type === "Identifier" ) {
242
- return reactHOCs . includes ( init . callee . name ) ;
258
+ return reactHOCs . has ( init . callee . name ) ;
243
259
}
244
260
return false ;
245
261
} ;
246
262
247
263
type ToString < T > = T extends `${infer V } ` ? V : never ;
248
- const notReactComponentExpression : ToString < TSESTree . Expression [ "type" ] > [ ] = [
264
+ const notReactComponentExpression = new Set <
265
+ ToString < TSESTree . Expression [ "type" ] >
266
+ > ( [
249
267
"ArrayExpression" ,
250
268
"AwaitExpression" ,
251
269
"BinaryExpression" ,
@@ -258,4 +276,4 @@ const notReactComponentExpression: ToString<TSESTree.Expression["type"]>[] = [
258
276
"ThisExpression" ,
259
277
"UnaryExpression" ,
260
278
"UpdateExpression" ,
261
- ] ;
279
+ ] ) ;
0 commit comments