Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
# Changelog

## 0.5.0

### Breaking changes

- Packages now ships as ESM and requires ESLint 9 + node 20
- Validation of HOCs calls is now more strict, you may need to add some HOCs to the `customHOCs` option
- Configs are now functions that return the config object with passed options merged with the base options of that config

Example:

```js
import { defineConfig } from "eslint/config";
import reactRefresh from "eslint-plugin-react-refresh";

export default defineConfig(
/* Main config */
reactRefresh.configs.vite({ customHOCs: ["connect"] }),
);
```

### Why

This version follows a revamp of the internal logic to better make the difference between random call expressions like `export const Enum = Object.keys(Record)` and actual React HOC calls like `export const MemoComponent = memo(Component)`. (fixes [#93](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/issues/93))

The rule now handles ternaries and patterns like `export default customHOC(props)(Component)` which makes it able to correctly support files like [this one](https://github.com/eclipse-apoapsis/ort-server/blob/ddfc624ce71b9f2ca6bad9b8c82d4c3249dd9c8b/ui/src/routes/__root.tsx) given this config:

```json
{
"react-refresh/only-export-components": [
"warn",
{ "customHOCs": ["createRootRouteWithContext"] }
]
}
```

> [!NOTE]
> Actually createRoute functions from TanStack Router are not React HOCs, they return route objects that [fake to be a memoized component](https://github.com/TanStack/router/blob/8628d0189412ccb8d3a01840aa18bac8295e18c8/packages/react-router/src/route.tsx#L263) but are not. When only doing `createRootRoute({ component: Foo })`, HMR will work fine, but as soon as you add a prop to the options that is not a React component, HMR will not work. I would recommend to avoid adding any TanStack function to `customHOCs` it you want to preserve good HMR in the long term. [Bluesky thread](https://bsky.app/profile/arnaud-barre.bsky.social/post/3ma5h5tf2sk2e).

Because I'm not 100% sure this new logic doesn't introduce any false positive, this is done in a major-like version. This also give me the occasion to remove the hardcoded `connect` from the rule. If you are using `connect` from `react-redux`, you should now add it to `customHOCs` like this:

```json
{
"react-refresh/only-export-components": [
"warn",
{ "customHOCs": ["connect"] }
]
}
```

## 0.4.26

- Revert changes to fix [#93](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/issues/93) (fixes [#95](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/issues/95))
Expand Down
49 changes: 19 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import reactRefresh from "eslint-plugin-react-refresh";

export default defineConfig(
/* Main config */
reactRefresh.configs.recommended,
reactRefresh.configs.recommended(), // Or reactRefresh.configs.vite for Vite users
);
```

Expand All @@ -52,11 +52,11 @@ import reactRefresh from "eslint-plugin-react-refresh";

export default defineConfig(
/* Main config */
reactRefresh.configs.vite,
reactRefresh.configs.vite(),
);
```

### Next config <small>(v0.4.21)</small>
### Next config

This allows exports like `fetchCache` and `revalidate` which are used in Page or Layout components and don't trigger a full page reload.

Expand All @@ -66,7 +66,7 @@ import reactRefresh from "eslint-plugin-react-refresh";

export default defineConfig(
/* Main config */
reactRefresh.configs.next,
reactRefresh.configs.next(),
);
```

Expand All @@ -87,17 +87,6 @@ export default defineConfig({
});
```

### Legacy config

```jsonc
{
"plugins": ["react-refresh"],
"rules": {
"react-refresh/only-export-components": "error",
},
}
```

## Examples

These examples are from enabling `react-refresh/only-exports-components`.
Expand Down Expand Up @@ -152,20 +141,33 @@ These options are all present on `react-refresh/only-exports-components`.

```ts
interface Options {
customHOCs?: string[];
allowExportNames?: string[];
allowConstantExport?: boolean;
customHOCs?: string[];
checkJS?: boolean;
}

const defaultOptions: Options = {
customHOCs: [],
allowExportNames: [],
allowConstantExport: false,
customHOCs: [],
checkJS: false,
};
```

### customHOCs <small>(v0.4.15)</small>

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"] }
]
}
```

### allowExportNames <small>(v0.4.4)</small>

> Default: `[]`
Expand Down Expand Up @@ -218,16 +220,3 @@ If you're using JSX inside `.js` files (which I don't recommend because it force
"react-refresh/only-export-components": ["error", { "checkJS": true }]
}
```

### customHOCs <small>(v0.4.15)</small>

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"] }
]
}
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-react-refresh",
"version": "0.4.26",
"version": "0.5.0",
"type": "module",
"license": "MIT",
"scripts": {
Expand All @@ -15,7 +15,7 @@
"experimentalOperatorPosition": "start"
},
"peerDependencies": {
"eslint": ">=8.40"
"eslint": ">=9"
},
"devDependencies": {
"@arnaud-barre/eslint-config": "^6.1.2",
Expand Down
13 changes: 9 additions & 4 deletions scripts/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ await build({
entryPoints: ["src/index.ts"],
outdir: "dist",
platform: "node",
target: "node14",
format: "esm",
target: "node20",
external: Object.keys(packageJSON.peerDependencies),
});

Expand All @@ -27,12 +28,16 @@ writeFileSync(
description:
"Validate that your components can safely be updated with Fast Refresh",
version: packageJSON.version,
type: "commonjs",
type: "module",
author: "Arnaud Barré (https://github.com/ArnaudBarre)",
license: packageJSON.license,
repository: "github:ArnaudBarre/eslint-plugin-react-refresh",
main: "index.js",
types: "index.d.ts",
exports: {
".": {
types: "./index.d.ts",
default: "./index.js",
},
},
keywords: [
"eslint",
"eslint-plugin",
Expand Down
88 changes: 46 additions & 42 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,65 @@
import { onlyExportComponents } from "./only-export-components.ts";
import type { OnlyExportComponentsOptions } from "./types.d.ts";

export const rules = {
"only-export-components": onlyExportComponents,
};

const plugin = { rules };

export const configs = {
recommended: {
name: "react-refresh/recommended",
plugins: { "react-refresh": plugin },
rules: { "react-refresh/only-export-components": "error" },
},
vite: {
name: "react-refresh/vite",
const buildConfig =
({
name,
baseOptions,
}: {
name: string;
baseOptions: OnlyExportComponentsOptions;
}) =>
(options?: OnlyExportComponentsOptions) => ({
name: `react-refresh/${name}`,
plugins: { "react-refresh": plugin },
rules: {
"react-refresh/only-export-components": [
"error",
{ allowConstantExport: true },
{ ...baseOptions, ...options },
],
},
},
next: {
name: "react-refresh/next",
plugins: { "react-refresh": plugin },
rules: {
"react-refresh/only-export-components": [
"error",
{
allowExportNames: [
// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
"experimental_ppr",
"dynamic",
"dynamicParams",
"revalidate",
"fetchCache",
"runtime",
"preferredRegion",
"maxDuration",
// https://nextjs.org/docs/app/api-reference/functions/generate-metadata
"metadata",
"generateMetadata",
// https://nextjs.org/docs/app/api-reference/functions/generate-viewport
"viewport",
"generateViewport",
// https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata
"generateImageMetadata",
// https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps
"generateSitemaps",
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params
"generateStaticParams",
],
},
});

export const configs = {
recommended: buildConfig({ name: "recommended", baseOptions: {} }),
vite: buildConfig({
name: "vite",
baseOptions: { allowConstantExport: true },
}),
next: buildConfig({
name: "next",
baseOptions: {
allowExportNames: [
// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config
"experimental_ppr",
"dynamic",
"dynamicParams",
"revalidate",
"fetchCache",
"runtime",
"preferredRegion",
"maxDuration",
// https://nextjs.org/docs/app/api-reference/functions/generate-metadata
"metadata",
"generateMetadata",
// https://nextjs.org/docs/app/api-reference/functions/generate-viewport
"viewport",
"generateViewport",
// https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata
"generateImageMetadata",
// https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps
"generateSitemaps",
// https://nextjs.org/docs/app/api-reference/functions/generate-static-params
"generateStaticParams",
],
},
},
}),
};

// Probably not needed, but keep for backwards compatibility
Expand Down
Loading