11import { TSESLint } from "@typescript-eslint/utils" ;
22import { TSESTree } from "@typescript-eslint/types" ;
33
4+ const possibleReactExportRE = / ^ [ A - Z ] [ a - z A - Z ] * $ / ;
5+ // Only letters, starts with uppercase and at least one lowercase
6+ // This can lead to some false positive (ex: `const CMS = () => <></>`)
7+ // But allow to catch `export const CONSTANT = 3`
8+ const strictReactExportRE = / ^ [ A - Z ] [ a - z A - Z ] * [ a - z ] + [ a - z A - Z ] * $ / ;
9+
410export const onlyExportComponents : TSESLint . RuleModule <
5- "exportAll" | "namedExport" | "anonymousExport"
11+ | "exportAll"
12+ | "namedExport"
13+ | "anonymousExport"
14+ | "noExport"
15+ | "localComponents"
616> = {
717 meta : {
818 messages : {
@@ -12,6 +22,10 @@ export const onlyExportComponents: TSESLint.RuleModule<
1222 "Fast refresh only works when a file only export components. Use a new file to share constant or functions between components." ,
1323 anonymousExport :
1424 "Fast refresh can't handle anonymous component. Add a name to your export." ,
25+ localComponents :
26+ "Fast refresh only works when a file only export components. Move your component(s) to a separate file." ,
27+ noExport :
28+ "Fast refresh only works when a file has exports. Move your component(s) to a separate file." ,
1529 } ,
1630 type : "problem" ,
1731 schema : [ ] ,
@@ -22,67 +36,102 @@ export const onlyExportComponents: TSESLint.RuleModule<
2236
2337 return {
2438 Program ( program ) {
39+ let hasExports = false ;
2540 let mayHaveReactExport = false ;
41+ const localComponents : TSESTree . Identifier [ ] = [ ] ;
2642 const nonComponentExport : TSESTree . BindingName [ ] = [ ] ;
2743
28- const handleIdentifier = ( identifierNode : TSESTree . BindingName ) => {
44+ const handleLocalIdentifier = (
45+ identifierNode : TSESTree . BindingName ,
46+ ) => {
47+ if ( identifierNode . type !== "Identifier" ) return ;
48+ if ( possibleReactExportRE . test ( identifierNode . name ) ) {
49+ localComponents . push ( identifierNode ) ;
50+ }
51+ } ;
52+
53+ const handleExportIdentifier = (
54+ identifierNode : TSESTree . BindingName ,
55+ ) => {
2956 if ( identifierNode . type !== "Identifier" ) {
3057 nonComponentExport . push ( identifierNode ) ;
3158 return ;
3259 }
33- if ( / ^ [ A - Z ] [ a - z A - Z ] * $ / . test ( identifierNode . name ) ) {
60+ if (
61+ ! mayHaveReactExport &&
62+ possibleReactExportRE . test ( identifierNode . name )
63+ ) {
3464 mayHaveReactExport = true ;
3565 }
36- // Only letters, starts with uppercase and at least one lowercase
37- // This can lead to some false positive (ex: `const CMS = () => <></>`)
38- // But allow to catch `export const CONSTANT = 3`
39- if ( ! / ^ [ A - Z ] [ a - z A - Z ] * [ a - z ] + [ a - z A - Z ] * $ / . test ( identifierNode . name ) ) {
66+ if ( ! strictReactExportRE . test ( identifierNode . name ) ) {
4067 nonComponentExport . push ( identifierNode ) ;
4168 }
4269 } ;
4370
4471 const handleExportDeclaration = ( node : TSESTree . ExportDeclaration ) => {
4572 if ( node . type === "VariableDeclaration" ) {
4673 for ( const variable of node . declarations ) {
47- handleIdentifier ( variable . id ) ;
74+ handleExportIdentifier ( variable . id ) ;
4875 }
4976 } else if ( node . type === "FunctionDeclaration" ) {
5077 if ( node . id === null ) {
5178 context . report ( { messageId : "anonymousExport" , node } ) ;
5279 } else {
53- handleIdentifier ( node . id ) ;
80+ handleExportIdentifier ( node . id ) ;
5481 }
5582 }
5683 } ;
5784
5885 for ( const node of program . body ) {
5986 if ( node . type === "ExportAllDeclaration" ) {
87+ hasExports = true ;
6088 context . report ( { messageId : "exportAll" , node } ) ;
6189 } else if ( node . type === "ExportDefaultDeclaration" ) {
90+ hasExports = true ;
6291 if (
6392 node . declaration . type === "VariableDeclaration" ||
6493 node . declaration . type === "FunctionDeclaration"
6594 ) {
6695 handleExportDeclaration ( node . declaration ) ;
6796 }
97+ if ( node . declaration . type === "Identifier" ) {
98+ handleExportIdentifier ( node . declaration ) ;
99+ }
68100 if (
69101 node . declaration . type === "ArrowFunctionExpression" &&
70102 ! node . declaration . id
71103 ) {
72104 context . report ( { messageId : "anonymousExport" , node } ) ;
73105 }
74106 } else if ( node . type === "ExportNamedDeclaration" ) {
107+ hasExports = true ;
75108 if ( node . declaration ) handleExportDeclaration ( node . declaration ) ;
76109 for ( const specifier of node . specifiers ) {
77- handleIdentifier ( specifier . exported ) ;
110+ handleExportIdentifier ( specifier . exported ) ;
111+ }
112+ } else if ( node . type === "VariableDeclaration" ) {
113+ for ( const variable of node . declarations ) {
114+ handleLocalIdentifier ( variable . id ) ;
78115 }
116+ } else if ( node . type === "FunctionDeclaration" ) {
117+ handleLocalIdentifier ( node . id ) ;
79118 }
80119 }
81120
82- if ( ! mayHaveReactExport ) return ;
83-
84- for ( const node of nonComponentExport ) {
85- context . report ( { messageId : "namedExport" , node } ) ;
121+ if ( hasExports ) {
122+ if ( mayHaveReactExport ) {
123+ for ( const node of nonComponentExport ) {
124+ context . report ( { messageId : "namedExport" , node } ) ;
125+ }
126+ } else if ( localComponents . length ) {
127+ for ( const node of localComponents ) {
128+ context . report ( { messageId : "localComponents" , node } ) ;
129+ }
130+ }
131+ } else if ( localComponents . length ) {
132+ for ( const node of localComponents ) {
133+ context . report ( { messageId : "noExport" , node } ) ;
134+ }
86135 }
87136 } ,
88137 } ;
0 commit comments