diff --git a/README.md b/README.md index defb494..e056cf3 100644 --- a/README.md +++ b/README.md @@ -175,3 +175,16 @@ If your using JSX inside `.js` files (which I don't recommend because it forces "react-refresh/only-export-components": ["error", { "checkJS": true }] } ``` + +### customHOCs (v0.4.15) + +If you're exporting a component wrapped in a custom HOC, you can use this option to avoid false positives. + +```json +{ + "react-refresh/only-export-components": [ + "error", + { "customHOCs": ["observer", "withAuth"] } + ] +} +``` diff --git a/src/only-export-components.test.ts b/src/only-export-components.test.ts index b5accd9..1bfc26a 100755 --- a/src/only-export-components.test.ts +++ b/src/only-export-components.test.ts @@ -189,6 +189,11 @@ const valid = [ name: "Only React context", code: "export const MyContext = createContext('test');", }, + { + name: "Custom HOCs like mobx's observer", + code: "const MyComponent = () => {}; export default observer(MyComponent);", + options: [{ customHOCs: ["observer"] }], + }, ]; const invalid = [ @@ -295,6 +300,11 @@ const invalid = [ code: "export const MyComponent = () => {}; export const MyContext = React.createContext('test');", errorId: "reactContext", }, + { + name: "should be invalid when custom HOC is used without adding it to the rule configuration", + code: "const MyComponent = () => {}; export default observer(MyComponent);", + errorId: ["localComponents", "anonymousExport"], + }, ]; const it = (name: string, cases: Parameters[2]) => { @@ -322,7 +332,9 @@ for (const { name, code, errorId, filename, options = [] } of invalid) { { filename: filename ?? "Test.jsx", code, - errors: [{ messageId: errorId }], + errors: Array.isArray(errorId) + ? errorId.map((messageId) => ({ messageId })) + : [{ messageId: errorId }], options, }, ], diff --git a/src/only-export-components.ts b/src/only-export-components.ts index b74a285..e85948c 100644 --- a/src/only-export-components.ts +++ b/src/only-export-components.ts @@ -21,6 +21,7 @@ export const onlyExportComponents: TSESLint.RuleModule< allowConstantExport?: boolean; checkJS?: boolean; allowExportNames?: string[]; + customHOCs?: string[]; }, ] > = { @@ -47,6 +48,7 @@ export const onlyExportComponents: TSESLint.RuleModule< allowConstantExport: { type: "boolean" }, checkJS: { type: "boolean" }, allowExportNames: { type: "array", items: { type: "string" } }, + customHOCs: { type: "array", items: { type: "string" } }, }, additionalProperties: false, }, @@ -58,6 +60,7 @@ export const onlyExportComponents: TSESLint.RuleModule< allowConstantExport = false, checkJS = false, allowExportNames, + customHOCs = [], } = context.options[0] ?? {}; const filename = context.filename; // Skip tests & stories files @@ -79,6 +82,16 @@ export const onlyExportComponents: TSESLint.RuleModule< ? new Set(allowExportNames) : undefined; + const reactHOCs = new Set(["memo", "forwardRef", ...customHOCs]); + const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => { + if (!init) return false; + if (init.type === "ArrowFunctionExpression") return true; + if (init.type === "CallExpression" && init.callee.type === "Identifier") { + return reactHOCs.has(init.callee.name); + } + return false; + }; + return { Program(program) { let hasExports = false; @@ -298,16 +311,6 @@ export const onlyExportComponents: TSESLint.RuleModule< }, }; -const reactHOCs = new Set(["memo", "forwardRef"]); -const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => { - if (!init) return false; - if (init.type === "ArrowFunctionExpression") return true; - if (init.type === "CallExpression" && init.callee.type === "Identifier") { - return reactHOCs.has(init.callee.name); - } - return false; -}; - type ToString = T extends `${infer V}` ? V : never; const notReactComponentExpression = new Set< ToString