Skip to content

Commit 478e778

Browse files
authored
Add basic support for custom HOCs (#60)
1 parent 94c9d7d commit 478e778

File tree

3 files changed

+39
-11
lines changed

3 files changed

+39
-11
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,16 @@ If your using JSX inside `.js` files (which I don't recommend because it forces
175175
"react-refresh/only-export-components": ["error", { "checkJS": true }]
176176
}
177177
```
178+
179+
### customHOCs <small>(v0.4.15)</small>
180+
181+
If you're exporting a component wrapped in a custom HOC, you can use this option to avoid false positives.
182+
183+
```json
184+
{
185+
"react-refresh/only-export-components": [
186+
"error",
187+
{ "customHOCs": ["observer", "withAuth"] }
188+
]
189+
}
190+
```

src/only-export-components.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ const valid = [
189189
name: "Only React context",
190190
code: "export const MyContext = createContext('test');",
191191
},
192+
{
193+
name: "Custom HOCs like mobx's observer",
194+
code: "const MyComponent = () => {}; export default observer(MyComponent);",
195+
options: [{ customHOCs: ["observer"] }],
196+
},
192197
];
193198

194199
const invalid = [
@@ -295,6 +300,11 @@ const invalid = [
295300
code: "export const MyComponent = () => {}; export const MyContext = React.createContext('test');",
296301
errorId: "reactContext",
297302
},
303+
{
304+
name: "should be invalid when custom HOC is used without adding it to the rule configuration",
305+
code: "const MyComponent = () => {}; export default observer(MyComponent);",
306+
errorId: ["localComponents", "anonymousExport"],
307+
},
298308
];
299309

300310
const it = (name: string, cases: Parameters<typeof ruleTester.run>[2]) => {
@@ -322,7 +332,9 @@ for (const { name, code, errorId, filename, options = [] } of invalid) {
322332
{
323333
filename: filename ?? "Test.jsx",
324334
code,
325-
errors: [{ messageId: errorId }],
335+
errors: Array.isArray(errorId)
336+
? errorId.map((messageId) => ({ messageId }))
337+
: [{ messageId: errorId }],
326338
options,
327339
},
328340
],

src/only-export-components.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
2121
allowConstantExport?: boolean;
2222
checkJS?: boolean;
2323
allowExportNames?: string[];
24+
customHOCs?: string[];
2425
},
2526
]
2627
> = {
@@ -47,6 +48,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
4748
allowConstantExport: { type: "boolean" },
4849
checkJS: { type: "boolean" },
4950
allowExportNames: { type: "array", items: { type: "string" } },
51+
customHOCs: { type: "array", items: { type: "string" } },
5052
},
5153
additionalProperties: false,
5254
},
@@ -58,6 +60,7 @@ export const onlyExportComponents: TSESLint.RuleModule<
5860
allowConstantExport = false,
5961
checkJS = false,
6062
allowExportNames,
63+
customHOCs = [],
6164
} = context.options[0] ?? {};
6265
const filename = context.filename;
6366
// Skip tests & stories files
@@ -79,6 +82,16 @@ export const onlyExportComponents: TSESLint.RuleModule<
7982
? new Set(allowExportNames)
8083
: undefined;
8184

85+
const reactHOCs = new Set(["memo", "forwardRef", ...customHOCs]);
86+
const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => {
87+
if (!init) return false;
88+
if (init.type === "ArrowFunctionExpression") return true;
89+
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
90+
return reactHOCs.has(init.callee.name);
91+
}
92+
return false;
93+
};
94+
8295
return {
8396
Program(program) {
8497
let hasExports = false;
@@ -298,16 +311,6 @@ export const onlyExportComponents: TSESLint.RuleModule<
298311
},
299312
};
300313

301-
const reactHOCs = new Set(["memo", "forwardRef"]);
302-
const canBeReactFunctionComponent = (init: TSESTree.Expression | null) => {
303-
if (!init) return false;
304-
if (init.type === "ArrowFunctionExpression") return true;
305-
if (init.type === "CallExpression" && init.callee.type === "Identifier") {
306-
return reactHOCs.has(init.callee.name);
307-
}
308-
return false;
309-
};
310-
311314
type ToString<T> = T extends `${infer V}` ? V : never;
312315
const notReactComponentExpression = new Set<
313316
ToString<TSESTree.Expression["type"]>

0 commit comments

Comments
 (0)