@@ -72,6 +72,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
7272 ( checkJS && filename . endsWith ( ".js" ) ) ;
7373 if ( ! shouldScan ) return { } ;
7474
75+ const allowExportNamesSet = allowExportNames
76+ ? new Set ( allowExportNames )
77+ : undefined ;
78+
7579 return {
7680 Program ( program ) {
7781 let hasExports = false ;
@@ -98,7 +102,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
98102 nonComponentExports . push ( identifierNode ) ;
99103 return ;
100104 }
101- if ( allowExportNames ?. includes ( identifierNode . name ) ) return ;
105+ if ( allowExportNamesSet ?. has ( identifierNode . name ) ) return ;
102106 if (
103107 allowConstantExport &&
104108 init &&
@@ -109,6 +113,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
109113 ) {
110114 return ;
111115 }
116+
112117 if ( isFunction ) {
113118 if ( possibleReactExportRE . test ( identifierNode . name ) ) {
114119 mayHaveReactExport = true ;
@@ -119,7 +124,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
119124 if (
120125 init &&
121126 // Switch to allowList?
122- notReactComponentExpression . includes ( init . type )
127+ notReactComponentExpression . has ( init . type )
123128 ) {
124129 nonComponentExports . push ( identifierNode ) ;
125130 return ;
@@ -153,12 +158,23 @@ export const onlyExportComponents: TSESLint.RuleModule<
153158 }
154159 } else if ( node . type === "CallExpression" ) {
155160 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 (
158167 node . arguments [ 0 ] ?. type === "FunctionExpression" &&
159168 node . arguments [ 0 ] . id
160169 ) {
170+ // export default memo(function Foo() {})
161171 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 ;
162178 } else {
163179 context . report ( { messageId : "anonymousExport" , node } ) ;
164180 }
@@ -234,18 +250,20 @@ export const onlyExportComponents: TSESLint.RuleModule<
234250 } ,
235251} ;
236252
237- const reactHOCs = [ "memo" , "forwardRef" ] ;
253+ const reactHOCs = new Set ( [ "memo" , "forwardRef" ] ) ;
238254const canBeReactFunctionComponent = ( init : TSESTree . Expression | null ) => {
239255 if ( ! init ) return false ;
240256 if ( init . type === "ArrowFunctionExpression" ) return true ;
241257 if ( init . type === "CallExpression" && init . callee . type === "Identifier" ) {
242- return reactHOCs . includes ( init . callee . name ) ;
258+ return reactHOCs . has ( init . callee . name ) ;
243259 }
244260 return false ;
245261} ;
246262
247263type 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+ > ( [
249267 "ArrayExpression" ,
250268 "AwaitExpression" ,
251269 "BinaryExpression" ,
@@ -258,4 +276,4 @@ const notReactComponentExpression: ToString<TSESTree.Expression["type"]>[] = [
258276 "ThisExpression" ,
259277 "UnaryExpression" ,
260278 "UpdateExpression" ,
261- ] ;
279+ ] ) ;
0 commit comments