From b29f3d2d2e9b50d944dc4160a114edd219d9fdee Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Tue, 13 Aug 2024 15:51:54 +0200 Subject: [PATCH 01/12] refactor(a11y.ts): fix camel case typo in config name --- packages/eslint-plugin/lib/configs/a11y.ts | 2 +- packages/eslint-plugin/lib/configs/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/lib/configs/a11y.ts b/packages/eslint-plugin/lib/configs/a11y.ts index 85fdc20..edae110 100644 --- a/packages/eslint-plugin/lib/configs/a11y.ts +++ b/packages/eslint-plugin/lib/configs/a11y.ts @@ -1,6 +1,6 @@ import { defineConfig } from "eslint-define-config"; -export const a11yconfig = defineConfig({ +export const a11yConfig = defineConfig({ extends: ["plugin:react-native-a11y/all"], rules: { "react-native-a11y/has-accessibility-hint": "off", diff --git a/packages/eslint-plugin/lib/configs/index.ts b/packages/eslint-plugin/lib/configs/index.ts index b38a5ac..1196aab 100644 --- a/packages/eslint-plugin/lib/configs/index.ts +++ b/packages/eslint-plugin/lib/configs/index.ts @@ -1,4 +1,4 @@ -import { a11yconfig } from "./a11y"; +import { a11yConfig } from "./a11y"; import { importConfig } from "./import"; import { recommendedConfig } from "./recommended"; import { testsConfig } from "./tests"; @@ -6,6 +6,6 @@ import { testsConfig } from "./tests"; export default { recommended: recommendedConfig, tests: testsConfig, - a11y: a11yconfig, + a11y: a11yConfig, import: importConfig, }; From acd4e1f0f465293af29d2f000fb4c9be49711250 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 14 Aug 2024 10:09:37 +0200 Subject: [PATCH 02/12] BREAKING CHANGE: add new file to improve performance with eslint rules --- example-app/.eslintrc | 6 ++++- .../break-flatlist-import-rule.tsx | 10 ++++++++ packages/eslint-plugin/lib/configs/index.ts | 2 ++ .../eslint-plugin/lib/configs/performance.ts | 24 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx create mode 100644 packages/eslint-plugin/lib/configs/performance.ts diff --git a/example-app/.eslintrc b/example-app/.eslintrc index 0eb90e7..6f0f40a 100644 --- a/example-app/.eslintrc +++ b/example-app/.eslintrc @@ -1,6 +1,10 @@ { "root": true, - "extends": ["plugin:@bam.tech/recommended", "plugin:@bam.tech/a11y"], + "extends": [ + "plugin:@bam.tech/recommended", + "plugin:@bam.tech/a11y", + "plugin:@bam.tech/performance" + ], "overrides": [ { "files": ["*.test.tsx"], diff --git a/example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx b/example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx new file mode 100644 index 0000000..77914be --- /dev/null +++ b/example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx @@ -0,0 +1,10 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger one error breaking eslint-plugin-react-native: +// no-restricted-imports + +import { FlatList } from "react-native"; + +export const MyCustomButton = () => { + return ; +}; diff --git a/packages/eslint-plugin/lib/configs/index.ts b/packages/eslint-plugin/lib/configs/index.ts index 1196aab..e304f4d 100644 --- a/packages/eslint-plugin/lib/configs/index.ts +++ b/packages/eslint-plugin/lib/configs/index.ts @@ -2,10 +2,12 @@ import { a11yConfig } from "./a11y"; import { importConfig } from "./import"; import { recommendedConfig } from "./recommended"; import { testsConfig } from "./tests"; +import { performanceConfig } from "./performance"; export default { recommended: recommendedConfig, tests: testsConfig, a11y: a11yConfig, import: importConfig, + performance: performanceConfig, }; diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts new file mode 100644 index 0000000..d24ced2 --- /dev/null +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "eslint-define-config"; + +export const performanceConfig = defineConfig({ + rules: { + "no-restricted-imports": [ + "warn", + { + paths: [ + { + name: "react-native", + importNames: ["FlatList"], + message: + "Please use FlashList from @shopify/flash-list instead of FlatList from react-native.", + }, + ], + }, + ], + }, + overrides: [ + { + files: ["*.js"], + }, + ], +}); From 6cab249f3530c02d28272cd474af898ff5e514c4 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 14 Aug 2024 10:36:12 +0200 Subject: [PATCH 03/12] refactor(README.md): fix typos in readme file --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 18193b5..f20df36 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Monorepo with packages for setting up ESLint and Typescript for any new React Na ## Presentation -The goal of the project is too have a set of configuration files that can be easily imported into a new project, which would reduce the burden of starting new projects. +The goal of the project is to have a set of configuration files that can be easily imported into a new project, which would reduce the burden of starting new projects. This repo uses [lerna](https://lerna.js.org/) to maintain, version and publish various packages for configuring ESLint and Typescript. @@ -39,7 +39,7 @@ Here are some useful commands: We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to automate the release process. -> If you add a new rule to a config, this is a breaking change, because it could make the CI fail on projects that use the plugin. The commit name where you add the new rule needs to follow this patern `BREAKING CHANGE : the description of your commit` +> If you add a new rule to a config, this is a breaking change, because it could make the CI fail on projects that use the plugin. The commit name where you add the new rule needs to follow this pattern `BREAKING CHANGE : the description of your commit` ## Publishing a new version of a package @@ -68,7 +68,7 @@ It will then push a tagged commit `chore(release): Publish` which will then trig ## Unpublish a package version -If you want to unpublish a package, you have to be contributor of @bam.tech/eslint-plugin (in this case for the eslint plugin). Use the following commad : +If you want to unpublish a package, you have to be contributor of @bam.tech/eslint-plugin (in this case for the eslint plugin). Use the following command : `npm unpublish @bam.tech/eslint-plugin@X.Y.Z` ## Running commands From f0292aac7a891e5d3aeab346bd6747a3cebacbd1 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 14 Aug 2024 11:57:56 +0200 Subject: [PATCH 04/12] BREAKING CHANGE: add performance rule to prevent using imports from @react-navigation/stack in favor of @react-navigation/native-stack --- .../break-react-navigation-stack-import-rule.tsx | 12 ++++++++++++ example-app/package.json | 1 + packages/eslint-plugin/lib/configs/performance.ts | 5 +++++ 3 files changed, 18 insertions(+) create mode 100644 example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx diff --git a/example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx b/example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx new file mode 100644 index 0000000..2d10c23 --- /dev/null +++ b/example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx @@ -0,0 +1,12 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger one error breaking eslint-plugin-react-native: +// no-restricted-imports + +import { createStackNavigator } from "@react-navigation/stack"; + +const Stack = createStackNavigator(); + +export const MyStack = () => { + return ; +}; diff --git a/example-app/package.json b/example-app/package.json index 47f73a5..9666e0d 100644 --- a/example-app/package.json +++ b/example-app/package.json @@ -11,6 +11,7 @@ "devDependencies": { "@bam.tech/eslint-plugin": "*", "@bam.tech/typescript-config": "*", + "@react-navigation/stack": "^6.4.1", "@testing-library/react-native": "^12.3.1", "@types/jest": "^29.5.2", "@types/react": "^18.2.14", diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index d24ced2..80d0b1c 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -12,6 +12,11 @@ export const performanceConfig = defineConfig({ message: "Please use FlashList from @shopify/flash-list instead of FlatList from react-native.", }, + { + name: "@react-navigation/stack", + message: + 'Please use "@react-navigation/native-stack" instead of "@react-navigation/stack".', + }, ], }, ], From 2306c74a733bfd260ed7f44e70ea269717d094c5 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 14 Aug 2024 12:24:36 +0200 Subject: [PATCH 05/12] BREAKING CHANGE: add performance rule to prevent using useIsFocused --- .../break-use-is-focused-import-rule.tsx | 13 +++++++++++++ example-app/package.json | 1 + packages/eslint-plugin/lib/configs/performance.ts | 6 ++++++ 3 files changed, 20 insertions(+) create mode 100644 example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx diff --git a/example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx b/example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx new file mode 100644 index 0000000..d31d103 --- /dev/null +++ b/example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx @@ -0,0 +1,13 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger one error breaking eslint-plugin-react-native: +// no-restricted-imports + +import { useIsFocused } from "@react-navigation/native"; +import { Text } from "react-native"; + +export const MyComponent = () => { + const isFocused = useIsFocused(); + + return {isFocused ? "focused" : "unfocused"}; +}; diff --git a/example-app/package.json b/example-app/package.json index 9666e0d..3a377f9 100644 --- a/example-app/package.json +++ b/example-app/package.json @@ -11,6 +11,7 @@ "devDependencies": { "@bam.tech/eslint-plugin": "*", "@bam.tech/typescript-config": "*", + "@react-navigation/native": "^6.1.18", "@react-navigation/stack": "^6.4.1", "@testing-library/react-native": "^12.3.1", "@types/jest": "^29.5.2", diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index 80d0b1c..b05e6c1 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -17,6 +17,12 @@ export const performanceConfig = defineConfig({ message: 'Please use "@react-navigation/native-stack" instead of "@react-navigation/stack".', }, + { + name: "@react-navigation/native", + importNames: ["useIsFocused"], + message: + "Please use useFocusEffect instead of useIsFocused to avoid excessive rerenders.", + }, ], }, ], From 72ae38df7371f70339803f5ba89a78a81ac5df29 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 14 Aug 2024 15:48:13 +0200 Subject: [PATCH 06/12] BREAKING CHANGE: add performance rule to prevent usage of react native svg --- .../break-react-native-svg-import-rule.tsx | 30 +++++++++++++++++++ example-app/package.json | 3 +- .../eslint-plugin/lib/configs/performance.ts | 5 ++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 example-app/eslint-breaking-examples/break-react-native-svg-import-rule.tsx diff --git a/example-app/eslint-breaking-examples/break-react-native-svg-import-rule.tsx b/example-app/eslint-breaking-examples/break-react-native-svg-import-rule.tsx new file mode 100644 index 0000000..15bda5b --- /dev/null +++ b/example-app/eslint-breaking-examples/break-react-native-svg-import-rule.tsx @@ -0,0 +1,30 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger one error breaking eslint-plugin-react-native: +// no-restricted-imports + +import Svg, { Circle, Rect } from "react-native-svg"; + +export const SvgComponent = () => { + return ( + + + + + ); +}; diff --git a/example-app/package.json b/example-app/package.json index 3a377f9..ba11e3f 100644 --- a/example-app/package.json +++ b/example-app/package.json @@ -39,6 +39,7 @@ "dependencies": { "expo": "^49.0.0", "react": "^18.2.0", - "react-native": "^0.73.0" + "react-native": "^0.73.0", + "react-native-svg": "^15.5.0" } } diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index b05e6c1..81b5eca 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -23,6 +23,11 @@ export const performanceConfig = defineConfig({ message: "Please use useFocusEffect instead of useIsFocused to avoid excessive rerenders.", }, + { + name: "react-native-svg", + message: + "Usage of react-native-svg is discouraged. Consider alternatives if applicable.", + }, ], }, ], From b99a1139ff729dfe9d24dd0b9e99e013535bdee1 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 28 Aug 2024 12:06:24 +0200 Subject: [PATCH 07/12] BREAKING CHANGE: add custom rule for native driver use with Animated to performance rules --- .../break-native-driver-rule.tsx | 21 +++++ packages/eslint-plugin/README.md | 14 ++-- .../no-animated-without-native-driver.md | 29 +++++++ .../eslint-plugin/lib/configs/performance.ts | 1 + packages/eslint-plugin/lib/rules/index.ts | 2 + .../no-animated-without-native-driver.ts | 81 +++++++++++++++++++ .../no-animated-without-native-driver.test.ts | 41 ++++++++++ 7 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 example-app/eslint-breaking-examples/break-native-driver-rule.tsx create mode 100644 packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md create mode 100644 packages/eslint-plugin/lib/rules/no-animated-without-native-driver.ts create mode 100644 packages/eslint-plugin/tests/lib/rules/no-animated-without-native-driver.test.ts diff --git a/example-app/eslint-breaking-examples/break-native-driver-rule.tsx b/example-app/eslint-breaking-examples/break-native-driver-rule.tsx new file mode 100644 index 0000000..0d45ebb --- /dev/null +++ b/example-app/eslint-breaking-examples/break-native-driver-rule.tsx @@ -0,0 +1,21 @@ +import { Animated, ScrollView, Text } from "react-native"; + +const fadeAnim = new Animated.Value(0); + +Animated.timing(fadeAnim, { + toValue: 1, + duration: 500, + useNativeDriver: false, // This line breaks the custom rule +}).start(); + +export const CustomScrollView = () => { + return ( + + {"Something to scroll"} + + ); +}; diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 783392a..bc0dd7d 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -107,12 +107,14 @@ This plugin exports some custom rules that you can optionally use in your projec 🧪 Set in the `tests` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Name | Description | 💼 | 🔧 | -| :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------- | :-- | :-- | -| [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | 🧪 | 🔧 | -| [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | 🔧 | -| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | 🔧 | -| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | +| Name | Description | 💼 | 🔧 | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | :--------------------- | :-- | +| [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | 🧪 | 🔧 | +| [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | 🔧 | +| [no-animated-without-native-driver](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md) | Disallow the use of `Animated` with `useNativeDriver: false` | ![badge-performance][] | | +| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | 🔧 | +| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | +| Name                              | diff --git a/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md b/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md new file mode 100644 index 0000000..0f62f47 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md @@ -0,0 +1,29 @@ +# Disallow the use of `Animated` with `useNativeDriver: false` (`@bam.tech/no-animated-without-native-driver`) + +💼 This rule is enabled in the `performance` config. + + + +Enforces the usage of native driver when using `Animated` from `react-native` to improve performance. + +## Rule details + +Example of **incorrect** code for this rule: + +```jsx +Animated.timing(fadeAnim, { + toValue: 1, + duration: 500, + useNativeDriver: false, +}).start(); +``` + +Example of **correct** code for this rule: + +```jsx +Animated.timing(fadeAnim, { + toValue: 1, + duration: 500, + useNativeDriver: true, +}).start(); +``` diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index 81b5eca..5123675 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -31,6 +31,7 @@ export const performanceConfig = defineConfig({ ], }, ], + "@bam.tech/no-animated-without-native-driver": "error", }, overrides: [ { diff --git a/packages/eslint-plugin/lib/rules/index.ts b/packages/eslint-plugin/lib/rules/index.ts index a9e6841..f51521a 100644 --- a/packages/eslint-plugin/lib/rules/index.ts +++ b/packages/eslint-plugin/lib/rules/index.ts @@ -1,5 +1,6 @@ import { awaitUserEventRule } from "./await-user-event"; import { noDifferentDisplaynameRule } from "./no-different-displayname"; +import { noAnimatedWithoutNativeDriverRule } from "./no-animated-without-native-driver"; import { preferUserEventRule } from "./prefer-user-event"; import { requireNamedEffectRule } from "./require-named-effect"; @@ -8,4 +9,5 @@ export default { "prefer-user-event": preferUserEventRule, "require-named-effect": requireNamedEffectRule, "no-different-displayname": noDifferentDisplaynameRule, + "no-animated-without-native-driver": noAnimatedWithoutNativeDriverRule, }; diff --git a/packages/eslint-plugin/lib/rules/no-animated-without-native-driver.ts b/packages/eslint-plugin/lib/rules/no-animated-without-native-driver.ts new file mode 100644 index 0000000..96dad51 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/no-animated-without-native-driver.ts @@ -0,0 +1,81 @@ +import type { Rule } from "eslint"; + +// Custom Rule: No Animated with useNativeDriver: false +export const noAnimatedWithoutNativeDriverRule: Rule.RuleModule = { + meta: { + type: "problem", + docs: { + description: + "Disallow the use of `Animated` with `useNativeDriver: false`", + category: "Possible Errors", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md", + }, + messages: { + noNativeDriverFalse: + "Do not use Animated with useNativeDriver: false. Always set useNativeDriver: true for better performance.", + }, + schema: [], + }, + + create(context) { + return { + CallExpression(node) { + // Check if the node is a call to `Animated` object + if ( + node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && + node.callee.object.name === "Animated" + ) { + // Handle the case: Animated.someMethod(..., { useNativeDriver: false }) + if ( + node.arguments.length > 0 && + node.arguments[1].type === "ObjectExpression" + ) { + const useNativeDriverPropertyIsFalse = + node.arguments[1].properties.find( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "useNativeDriver" && + prop.value.type === "Literal" && + prop.value.value === false, + ); + + if (useNativeDriverPropertyIsFalse) { + context.report({ + node: useNativeDriverPropertyIsFalse, + messageId: "noNativeDriverFalse", + }); + } + } + + // Handle the case: Animated.event([...], { useNativeDriver: false }) + if ( + node.callee.property.type === "Identifier" && + node.callee.property.name === "event" && + node.arguments.length > 1 && + node.arguments[1].type === "ObjectExpression" + ) { + const useNativeDriverPropertyIsFalse = + node.arguments[1].properties.find( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "useNativeDriver" && + prop.value.type === "Literal" && + prop.value.value === false, + ); + + if (useNativeDriverPropertyIsFalse) { + context.report({ + node: useNativeDriverPropertyIsFalse, + messageId: "noNativeDriverFalse", + }); + } + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/tests/lib/rules/no-animated-without-native-driver.test.ts b/packages/eslint-plugin/tests/lib/rules/no-animated-without-native-driver.test.ts new file mode 100644 index 0000000..6c8fd57 --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/no-animated-without-native-driver.test.ts @@ -0,0 +1,41 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger an error breaking eslint-plugin-bam-custom-rules: +// bam-custom-rules/no-animated-without-native-driver + +import { noAnimatedWithoutNativeDriverRule } from "../../../lib/rules/no-animated-without-native-driver"; +import { RuleTester } from "eslint"; + +const ruleTester = new RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), +}); + +const valid = [ + `Animated.timing(fadeAnim, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }).start();`, +]; + +const invalid = [ + `Animated.timing(fadeAnim, { + toValue: 1, + duration: 500, + useNativeDriver: false, + }).start();`, +]; + +ruleTester.run( + "no-animated-without-native-driver", + noAnimatedWithoutNativeDriverRule, + { + valid, + invalid: invalid.map((code) => ({ + code, + errors: [ + "Do not use Animated with useNativeDriver: false. Always set useNativeDriver: true for better performance.", + ], + })), + }, +); From bba0bdec6f1c705285dcdf0c40b9dc066cf86619 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 28 Aug 2024 16:15:43 +0200 Subject: [PATCH 08/12] BREAKING CHANGE: prevents the usage of Intl.NumberFormat for performance purposes --- .../break-intl-number-format-rule.ts | 14 ++++++ packages/eslint-plugin/README.md | 15 ++++--- .../docs/rules/avoid-intl-number-format.md | 7 +++ .../eslint-plugin/lib/configs/performance.ts | 1 + .../lib/rules/avoid-intl-number-format.ts | 39 +++++++++++++++++ packages/eslint-plugin/lib/rules/index.ts | 2 + .../rules/avoid-intl-number-format.test.ts | 43 +++++++++++++++++++ 7 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 example-app/eslint-breaking-examples/break-intl-number-format-rule.ts create mode 100644 packages/eslint-plugin/docs/rules/avoid-intl-number-format.md create mode 100644 packages/eslint-plugin/lib/rules/avoid-intl-number-format.ts create mode 100644 packages/eslint-plugin/tests/lib/rules/avoid-intl-number-format.test.ts diff --git a/example-app/eslint-breaking-examples/break-intl-number-format-rule.ts b/example-app/eslint-breaking-examples/break-intl-number-format-rule.ts new file mode 100644 index 0000000..2b99350 --- /dev/null +++ b/example-app/eslint-breaking-examples/break-intl-number-format-rule.ts @@ -0,0 +1,14 @@ +// This code should trigger the ESLint rule `avoidIntlNumberFormatRule` + +const number = 1234567.89; + +// Incorrect usage: This will be flagged by the ESLint rule +const formatter = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", +}); + +const formattedNumber = formatter.format(number); + +// eslint-disable-next-line no-console +console.log(formattedNumber); // Outputs: $1,234,567.89 diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index bc0dd7d..82e8cbc 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -107,13 +107,14 @@ This plugin exports some custom rules that you can optionally use in your projec 🧪 Set in the `tests` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Name | Description | 💼 | 🔧 | -| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | :--------------------- | :-- | -| [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | 🧪 | 🔧 | -| [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | 🔧 | -| [no-animated-without-native-driver](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md) | Disallow the use of `Animated` with `useNativeDriver: false` | ![badge-performance][] | | -| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | 🔧 | -| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | +| Name | Description | 💼 | 🔧 | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :--------------------- | :-- | +| [avoid-intl-number-format](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/avoid-intl-number-format.md) | Disallow the use of `Intl.NumberFormat` due to potential performance issues. | ![badge-performance][] | | +| [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | 🧪 | 🔧 | +| [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | 🔧 | +| [no-animated-without-native-driver](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md) | Disallow the use of `Animated` with `useNativeDriver: false` | ![badge-performance][] | | +| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | 🔧 | +| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | | Name                              | diff --git a/packages/eslint-plugin/docs/rules/avoid-intl-number-format.md b/packages/eslint-plugin/docs/rules/avoid-intl-number-format.md new file mode 100644 index 0000000..a1b9913 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/avoid-intl-number-format.md @@ -0,0 +1,7 @@ +# Disallow the use of `Intl.NumberFormat` due to potential performance issues (`@bam.tech/avoid-intl-number-format`) + +💼 This rule is enabled in the `performance` config. + + + +Prevents from the using `Intl.NumberFormat` to improve performance. diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index 5123675..a7ed082 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -32,6 +32,7 @@ export const performanceConfig = defineConfig({ }, ], "@bam.tech/no-animated-without-native-driver": "error", + "@bam.tech/avoid-intl-number-format": "error", }, overrides: [ { diff --git a/packages/eslint-plugin/lib/rules/avoid-intl-number-format.ts b/packages/eslint-plugin/lib/rules/avoid-intl-number-format.ts new file mode 100644 index 0000000..5830e27 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/avoid-intl-number-format.ts @@ -0,0 +1,39 @@ +import type { Rule } from "eslint"; + +// Custom Rule: No Intl.NumberFormat Usage +export const avoidIntlNumberFormatRule: Rule.RuleModule = { + meta: { + type: "problem", + docs: { + description: + "Disallow the use of `Intl.NumberFormat` due to potential performance issues.", + category: "Possible Errors", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/no-intl-numberformat.md", + }, + messages: { + noIntlNumberFormat: + "Avoid using `Intl.NumberFormat` as it can lead to performance issues. Consider using a lightweight formatting alternative or memoizing the formatter instance.", + }, + schema: [], + }, + + create(context) { + return { + NewExpression(node) { + if ( + node.callee.type === "MemberExpression" && + node.callee.object.type === "Identifier" && + node.callee.object.name === "Intl" && + node.callee.property.type === "Identifier" && + node.callee.property.name === "NumberFormat" + ) { + context.report({ + node, + messageId: "noIntlNumberFormat", + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/lib/rules/index.ts b/packages/eslint-plugin/lib/rules/index.ts index f51521a..06d45bd 100644 --- a/packages/eslint-plugin/lib/rules/index.ts +++ b/packages/eslint-plugin/lib/rules/index.ts @@ -1,3 +1,4 @@ +import { avoidIntlNumberFormatRule } from "./avoid-intl-number-format"; import { awaitUserEventRule } from "./await-user-event"; import { noDifferentDisplaynameRule } from "./no-different-displayname"; import { noAnimatedWithoutNativeDriverRule } from "./no-animated-without-native-driver"; @@ -10,4 +11,5 @@ export default { "require-named-effect": requireNamedEffectRule, "no-different-displayname": noDifferentDisplaynameRule, "no-animated-without-native-driver": noAnimatedWithoutNativeDriverRule, + "avoid-intl-number-format": avoidIntlNumberFormatRule, }; diff --git a/packages/eslint-plugin/tests/lib/rules/avoid-intl-number-format.test.ts b/packages/eslint-plugin/tests/lib/rules/avoid-intl-number-format.test.ts new file mode 100644 index 0000000..e52e0f5 --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/avoid-intl-number-format.test.ts @@ -0,0 +1,43 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger an error breaking eslint-plugin-bam-custom-rules: +// bam-custom-rules/avoid-intl-number-format + +import { avoidIntlNumberFormatRule } from "../../../lib/rules/avoid-intl-number-format"; +import { RuleTester } from "eslint"; + +const ruleTester = new RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), +}); + +const valid = [ + ` const formatCurrency = (number: number) => { + return numeral(number).format('$0,0.00'); + }; + + const number = 1234567.89; + console.log(formatCurrency(number));`, +]; + +const invalid = [ + ` const number = 1234567.89; + + const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }); + + const formattedNumber = formatter.format(number); + + console.log(formattedNumber);`, +]; + +ruleTester.run("no-animated-without-native-driver", avoidIntlNumberFormatRule, { + valid, + invalid: invalid.map((code) => ({ + code, + errors: [ + "Avoid using `Intl.NumberFormat` as it can lead to performance issues. Consider using a lightweight formatting alternative or memoizing the formatter instance.", + ], + })), +}); From a22a30175f8f16e1e49f5511e5ce0f659146bb2f Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Wed, 28 Aug 2024 16:43:08 +0200 Subject: [PATCH 09/12] BREAKING CHANGE: refactor -> import rules show an error and made a custom rule for rn svg to show a warning --- packages/eslint-plugin/README.md | 3 +- .../docs/rules/avoid-react-native-svg.md | 19 ++++++++ .../eslint-plugin/lib/configs/performance.ts | 8 +--- .../lib/rules/avoid-react-native-svg.ts | 46 +++++++++++++++++++ packages/eslint-plugin/lib/rules/index.ts | 2 + .../lib/rules/avoid-react-native-svg.test.ts | 28 +++++++++++ 6 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/avoid-react-native-svg.md create mode 100644 packages/eslint-plugin/lib/rules/avoid-react-native-svg.ts create mode 100644 packages/eslint-plugin/tests/lib/rules/avoid-react-native-svg.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 82e8cbc..a8fcd8e 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -103,13 +103,14 @@ This plugin exports some custom rules that you can optionally use in your projec 💼 Configurations enabled in.\ -✅ Set in the `recommended` configuration.\ +⚠️ Configurations set to warn in.\ 🧪 Set in the `tests` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). | Name | Description | 💼 | 🔧 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :--------------------- | :-- | | [avoid-intl-number-format](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/avoid-intl-number-format.md) | Disallow the use of `Intl.NumberFormat` due to potential performance issues. | ![badge-performance][] | | +| [avoid-react-native-svg](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/avoid-react-native-svg.md) | Disallow importing the `react-native-svg` package. | | ![badge-performance][] | | | [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | 🧪 | 🔧 | | [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | 🔧 | | [no-animated-without-native-driver](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md) | Disallow the use of `Animated` with `useNativeDriver: false` | ![badge-performance][] | | diff --git a/packages/eslint-plugin/docs/rules/avoid-react-native-svg.md b/packages/eslint-plugin/docs/rules/avoid-react-native-svg.md new file mode 100644 index 0000000..d9f355b --- /dev/null +++ b/packages/eslint-plugin/docs/rules/avoid-react-native-svg.md @@ -0,0 +1,19 @@ +# Disallow importing the `react-native-svg` package (`@bam.tech/avoid-react-native-svg`) + +⚠️ This rule _warns_ in the `performance` config. + + + +Prevents from using "react-native-svg" import to avoid performance issues. + +## Rule details + +Examples of **incorrect** code for this rule: + +```jsx +import Svg from "react-native-svg"; +``` + +```jsx +const Svg = require("react-native-svg"); +``` diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index a7ed082..6c8fe6d 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -3,7 +3,7 @@ import { defineConfig } from "eslint-define-config"; export const performanceConfig = defineConfig({ rules: { "no-restricted-imports": [ - "warn", + "error", { paths: [ { @@ -23,16 +23,12 @@ export const performanceConfig = defineConfig({ message: "Please use useFocusEffect instead of useIsFocused to avoid excessive rerenders.", }, - { - name: "react-native-svg", - message: - "Usage of react-native-svg is discouraged. Consider alternatives if applicable.", - }, ], }, ], "@bam.tech/no-animated-without-native-driver": "error", "@bam.tech/avoid-intl-number-format": "error", + "@bam.tech/avoid-react-native-svg": "warn", }, overrides: [ { diff --git a/packages/eslint-plugin/lib/rules/avoid-react-native-svg.ts b/packages/eslint-plugin/lib/rules/avoid-react-native-svg.ts new file mode 100644 index 0000000..b82489a --- /dev/null +++ b/packages/eslint-plugin/lib/rules/avoid-react-native-svg.ts @@ -0,0 +1,46 @@ +import type { Rule } from "eslint"; + +// Custom Rule: No react-native-svg Import +export const avoidReactNativeSvgImportRule: Rule.RuleModule = { + meta: { + type: "problem", + docs: { + description: "Disallow importing the `react-native-svg` package.", + category: "Possible Errors", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/no-react-native-svg-import.md", + }, + messages: { + noReactNativeSvgImport: + "Do not import `react-native-svg`. Consider using an alternative method for SVG handling or ensure it's necessary for your use case.", + }, + schema: [], + }, + + create(context) { + return { + ImportDeclaration(node) { + if (node.source.value === "react-native-svg") { + context.report({ + node, + messageId: "noReactNativeSvgImport", + }); + } + }, + CallExpression(node) { + if ( + node.callee.type === "Identifier" && + node.callee.name === "require" && + node.arguments.length > 0 && + node.arguments[0].type === "Literal" && + node.arguments[0].value === "react-native-svg" + ) { + context.report({ + node, + messageId: "noReactNativeSvgImport", + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/lib/rules/index.ts b/packages/eslint-plugin/lib/rules/index.ts index 06d45bd..baa9df4 100644 --- a/packages/eslint-plugin/lib/rules/index.ts +++ b/packages/eslint-plugin/lib/rules/index.ts @@ -1,4 +1,5 @@ import { avoidIntlNumberFormatRule } from "./avoid-intl-number-format"; +import { avoidReactNativeSvgImportRule } from "./avoid-react-native-svg"; import { awaitUserEventRule } from "./await-user-event"; import { noDifferentDisplaynameRule } from "./no-different-displayname"; import { noAnimatedWithoutNativeDriverRule } from "./no-animated-without-native-driver"; @@ -12,4 +13,5 @@ export default { "no-different-displayname": noDifferentDisplaynameRule, "no-animated-without-native-driver": noAnimatedWithoutNativeDriverRule, "avoid-intl-number-format": avoidIntlNumberFormatRule, + "avoid-react-native-svg": avoidReactNativeSvgImportRule, }; diff --git a/packages/eslint-plugin/tests/lib/rules/avoid-react-native-svg.test.ts b/packages/eslint-plugin/tests/lib/rules/avoid-react-native-svg.test.ts new file mode 100644 index 0000000..b60af82 --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/avoid-react-native-svg.test.ts @@ -0,0 +1,28 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger an error breaking eslint-plugin-bam-custom-rules: +// bam-custom-rules/avoid-react-native-svg + +import { avoidReactNativeSvgImportRule } from "../../../lib/rules/avoid-react-native-svg"; +import { RuleTester } from "eslint"; + +const ruleTester = new RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), +}); + +const valid = [``]; + +const invalid = [ + `import Svg from 'react-native-svg';`, + `const Svg = require('react-native-svg');`, +]; + +ruleTester.run("avoid-react-native-svg", avoidReactNativeSvgImportRule, { + valid, + invalid: invalid.map((code) => ({ + code, + errors: [ + "Do not import `react-native-svg`. Consider using an alternative method for SVG handling or ensure it's necessary for your use case.", + ], + })), +}); From f074874657f49e7e3356510105eb3383aeb4317e Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Tue, 3 Sep 2024 10:53:38 +0200 Subject: [PATCH 10/12] BREAKING CHANGE: refactor -> create custom rule for FlatList import to prevent from overriding with the existing import rule --- .../break-flatlist-import-rule.tsx | 4 +- packages/eslint-plugin/README.md | 21 +++--- .../eslint-plugin/docs/rules/no-flatlist.md | 27 ++++++++ .../eslint-plugin/lib/configs/performance.ts | 7 +- packages/eslint-plugin/lib/rules/index.ts | 2 + .../eslint-plugin/lib/rules/no-flatlist.ts | 68 +++++++++++++++++++ .../tests/lib/rules/no-flatlist.test.ts | 28 ++++++++ 7 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-flatlist.md create mode 100644 packages/eslint-plugin/lib/rules/no-flatlist.ts create mode 100644 packages/eslint-plugin/tests/lib/rules/no-flatlist.test.ts diff --git a/example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx b/example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx index 77914be..68199cc 100644 --- a/example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx +++ b/example-app/eslint-breaking-examples/break-flatlist-import-rule.tsx @@ -1,7 +1,7 @@ // Save without formatting: [⌘ + K] > [S] -// This should trigger one error breaking eslint-plugin-react-native: -// no-restricted-imports +// This should trigger one error breaking custom FlatList import rule: +// @bam.tech/no-flatlist import { FlatList } from "react-native"; diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index a8fcd8e..5d9c57f 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -104,19 +104,20 @@ This plugin exports some custom rules that you can optionally use in your projec 💼 Configurations enabled in.\ ⚠️ Configurations set to warn in.\ +✅ Set in the `recommended` configuration.\ 🧪 Set in the `tests` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Name | Description | 💼 | 🔧 | -| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | :--------------------- | :-- | -| [avoid-intl-number-format](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/avoid-intl-number-format.md) | Disallow the use of `Intl.NumberFormat` due to potential performance issues. | ![badge-performance][] | | -| [avoid-react-native-svg](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/avoid-react-native-svg.md) | Disallow importing the `react-native-svg` package. | | ![badge-performance][] | | -| [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | 🧪 | 🔧 | -| [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | 🔧 | -| [no-animated-without-native-driver](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md) | Disallow the use of `Animated` with `useNativeDriver: false` | ![badge-performance][] | | -| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | 🔧 | -| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | -| Name                              | +| Name                              | Description | 💼 | ⚠️ | 🔧 | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ | :--------------------- | :--------------------- | :-- | +| [avoid-intl-number-format](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/avoid-intl-number-format.md) | Disallow the use of `Intl.NumberFormat` due to potential performance issues. | ![badge-performance][] | | | +| [avoid-react-native-svg](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/avoid-react-native-svg.md) | Disallow importing the `react-native-svg` package. | | ![badge-performance][] | | +| [await-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/await-user-event.md) | Enforces awaiting userEvent calls | 🧪 | | 🔧 | +| [no-animated-without-native-driver](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md) | Disallow the use of `Animated` with `useNativeDriver: false` | ![badge-performance][] | | | +| [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | | 🔧 | +| [no-flatlist](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-flatlist.md) | Disallow importing `FlatList` from `react-native` due to potential performance concerns or the preference for alternative components. | ![badge-performance][] | | 🔧 | +| [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | | 🔧 | +| [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | | diff --git a/packages/eslint-plugin/docs/rules/no-flatlist.md b/packages/eslint-plugin/docs/rules/no-flatlist.md new file mode 100644 index 0000000..70d1f24 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-flatlist.md @@ -0,0 +1,27 @@ +# Disallow importing `FlatList` from `react-native` due to potential performance concerns or the preference for alternative components (`@bam.tech/no-flatlist`) + +💼 This rule is enabled in the `performance` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Prevents from using "FlatList" import to avoid performance issues. FlashList should be used instead. + +## Rule details + +Examples of **incorrect** code for this rule: + +```jsx +import { FlatList } from "react-native"; +``` + +```jsx +import { FlatList, SectionList } from "react-native"; +``` + +Examples of **correct** alternative for this rule: + +```jsx +import { FlashList } from "@shopify/flash-list"; +``` diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index 6c8fe6d..99d2527 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -6,12 +6,6 @@ export const performanceConfig = defineConfig({ "error", { paths: [ - { - name: "react-native", - importNames: ["FlatList"], - message: - "Please use FlashList from @shopify/flash-list instead of FlatList from react-native.", - }, { name: "@react-navigation/stack", message: @@ -29,6 +23,7 @@ export const performanceConfig = defineConfig({ "@bam.tech/no-animated-without-native-driver": "error", "@bam.tech/avoid-intl-number-format": "error", "@bam.tech/avoid-react-native-svg": "warn", + "@bam.tech/no-flatlist": "error", }, overrides: [ { diff --git a/packages/eslint-plugin/lib/rules/index.ts b/packages/eslint-plugin/lib/rules/index.ts index baa9df4..145ae18 100644 --- a/packages/eslint-plugin/lib/rules/index.ts +++ b/packages/eslint-plugin/lib/rules/index.ts @@ -5,6 +5,7 @@ import { noDifferentDisplaynameRule } from "./no-different-displayname"; import { noAnimatedWithoutNativeDriverRule } from "./no-animated-without-native-driver"; import { preferUserEventRule } from "./prefer-user-event"; import { requireNamedEffectRule } from "./require-named-effect"; +import { noFlatListImportRule } from "./no-flatlist"; export default { "await-user-event": awaitUserEventRule, @@ -14,4 +15,5 @@ export default { "no-animated-without-native-driver": noAnimatedWithoutNativeDriverRule, "avoid-intl-number-format": avoidIntlNumberFormatRule, "avoid-react-native-svg": avoidReactNativeSvgImportRule, + "no-flatlist": noFlatListImportRule, }; diff --git a/packages/eslint-plugin/lib/rules/no-flatlist.ts b/packages/eslint-plugin/lib/rules/no-flatlist.ts new file mode 100644 index 0000000..84f1069 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/no-flatlist.ts @@ -0,0 +1,68 @@ +import type { Rule } from "eslint"; + +// Custom Rule: No FlatList Import +export const noFlatListImportRule: Rule.RuleModule = { + meta: { + type: "problem", + docs: { + description: + "Disallow importing `FlatList` from `react-native` due to potential performance concerns or the preference for alternative components.", + category: "Possible Errors", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/no-flatlist.md", + }, + messages: { + noFlatListImport: + "FlatList is poorly optimized for performance, use FlashList from @shopify/flash-list for adequate list performance.", + }, + schema: [], + fixable: "code", + }, + + create(context) { + return { + ImportDeclaration(node) { + if ( + node.source.value === "react-native" && + node.specifiers.some( + (specifier) => + specifier.type === "ImportSpecifier" && + specifier.imported.name === "FlatList", + ) + ) { + context.report({ + node, + messageId: "noFlatListImport", + }); + } + }, + VariableDeclarator(node) { + if ( + node.init && + node.init.type === "CallExpression" && + node.init.callee.type === "Identifier" && + node.init.callee.name === "require" && + node.init.arguments.length > 0 && + node.init.arguments[0].type === "Literal" && + node.init.arguments[0].value === "react-native" + ) { + const flatListBinding = + node.id.type === "ObjectPattern" && + node.id.properties.some( + (property) => + property.type === "Property" && + property.key.type === "Identifier" && + property.key.name === "FlatList", + ); + + if (flatListBinding) { + context.report({ + node, + messageId: "noFlatListImport", + }); + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/tests/lib/rules/no-flatlist.test.ts b/packages/eslint-plugin/tests/lib/rules/no-flatlist.test.ts new file mode 100644 index 0000000..4d2487a --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/no-flatlist.test.ts @@ -0,0 +1,28 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger an error breaking eslint-plugin-bam-custom-rules: +// bam-custom-rules/no-flatlist + +import { noFlatListImportRule } from "../../../lib/rules/no-flatlist"; +import { RuleTester } from "eslint"; + +const ruleTester = new RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), +}); + +const valid = [`import { FlashList } from "@shopify/flash-list";`]; + +const invalid = [ + `import { FlatList } from "react-native";`, + `import { FlatList, SectionList} from 'react-native';`, +]; + +ruleTester.run("no-flatlist", noFlatListImportRule, { + valid, + invalid: invalid.map((code) => ({ + code, + errors: [ + "FlatList is poorly optimized for performance, use FlashList from @shopify/flash-list for adequate list performance.", + ], + })), +}); From 3d5458e7794e82f269770d645ce8c4b5ce037d78 Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Tue, 3 Sep 2024 16:02:38 +0200 Subject: [PATCH 11/12] BREAKING CHANGE: refactor -> create custom rule for import of rn stack to prevent override with eslint import rule --- ...eak-react-navigation-stack-import-rule.tsx | 4 +- packages/eslint-plugin/README.md | 2 + .../docs/rules/no-react-navigation-stack.md | 21 ++++++++ .../eslint-plugin/lib/configs/performance.ts | 6 +-- packages/eslint-plugin/lib/rules/index.ts | 2 + .../lib/rules/no-react-navigation-stack.ts | 49 +++++++++++++++++++ .../rules/no-react-navigation-stack.test.ts | 30 ++++++++++++ 7 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-react-navigation-stack.md create mode 100644 packages/eslint-plugin/lib/rules/no-react-navigation-stack.ts create mode 100644 packages/eslint-plugin/tests/lib/rules/no-react-navigation-stack.test.ts diff --git a/example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx b/example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx index 2d10c23..0186d3d 100644 --- a/example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx +++ b/example-app/eslint-breaking-examples/break-react-navigation-stack-import-rule.tsx @@ -1,7 +1,7 @@ // Save without formatting: [⌘ + K] > [S] -// This should trigger one error breaking eslint-plugin-react-native: -// no-restricted-imports +// This should trigger one error breaking custom react-navigation/stack rule: +// @bam.tech/no-react-navigation-stack import { createStackNavigator } from "@react-navigation/stack"; diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 5d9c57f..f0ca4ad 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -116,6 +116,8 @@ This plugin exports some custom rules that you can optionally use in your projec | [no-animated-without-native-driver](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-animated-without-native-driver.md) | Disallow the use of `Animated` with `useNativeDriver: false` | ![badge-performance][] | | | | [no-different-displayname](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-different-displayname.md) | Enforce component displayName to match with component name | ✅ | | 🔧 | | [no-flatlist](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-flatlist.md) | Disallow importing `FlatList` from `react-native` due to potential performance concerns or the preference for alternative components. | ![badge-performance][] | | 🔧 | +| [no-react-navigation-stack](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-react-navigation-stack.md) | Disallow importing from `@react-navigation/stack` and suggest using `@react-navigation/native-stack` instead. | ![badge-performance][] | | | +| [no-use-is-focused](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/no-use-is-focused.md) | Disallow importing `useIsFocused` from `@react-navigation/native` to encourage using `useFocusEffect` instead. | ![badge-performance][] | | 🔧 | | [prefer-user-event](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/prefer-user-event.md) | Enforces usage of userEvent over fireEvent in tests. | | | 🔧 | | [require-named-effect](https://github.com/bamlab/react-native-project-config/blob/main/packages/eslint-plugin/docs/rules/require-named-effect.md) | Enforces the use of named functions inside a useEffect | | | | diff --git a/packages/eslint-plugin/docs/rules/no-react-navigation-stack.md b/packages/eslint-plugin/docs/rules/no-react-navigation-stack.md new file mode 100644 index 0000000..89ded8a --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-react-navigation-stack.md @@ -0,0 +1,21 @@ +# Disallow importing from `@react-navigation/stack` and suggest using `@react-navigation/native-stack` instead (`@bam.tech/no-react-navigation-stack`) + +💼 This rule is enabled in the `performance` config. + + + +Prevents from using "react-navigation/stack" import to avoid performance issues. "react-navigation/native-stack" should be used instead. + +## Rule details + +Examples of **incorrect** code for this rule: + +```jsx +import { createStackNavigator } from "@react-navigation/stack"; +``` + +Examples of **correct** alternative for this rule: + +```jsx +import { createStackNavigator } from "@react-navigation/native-stack"; +``` diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index 99d2527..0ee9848 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -6,11 +6,6 @@ export const performanceConfig = defineConfig({ "error", { paths: [ - { - name: "@react-navigation/stack", - message: - 'Please use "@react-navigation/native-stack" instead of "@react-navigation/stack".', - }, { name: "@react-navigation/native", importNames: ["useIsFocused"], @@ -24,6 +19,7 @@ export const performanceConfig = defineConfig({ "@bam.tech/avoid-intl-number-format": "error", "@bam.tech/avoid-react-native-svg": "warn", "@bam.tech/no-flatlist": "error", + "@bam.tech/no-react-navigation-stack": "error", }, overrides: [ { diff --git a/packages/eslint-plugin/lib/rules/index.ts b/packages/eslint-plugin/lib/rules/index.ts index 145ae18..c3565b1 100644 --- a/packages/eslint-plugin/lib/rules/index.ts +++ b/packages/eslint-plugin/lib/rules/index.ts @@ -6,6 +6,7 @@ import { noAnimatedWithoutNativeDriverRule } from "./no-animated-without-native- import { preferUserEventRule } from "./prefer-user-event"; import { requireNamedEffectRule } from "./require-named-effect"; import { noFlatListImportRule } from "./no-flatlist"; +import { noReactNavigationStackImportRule } from "./no-react-navigation-stack"; export default { "await-user-event": awaitUserEventRule, @@ -16,4 +17,5 @@ export default { "avoid-intl-number-format": avoidIntlNumberFormatRule, "avoid-react-native-svg": avoidReactNativeSvgImportRule, "no-flatlist": noFlatListImportRule, + "no-react-navigation-stack": noReactNavigationStackImportRule, }; diff --git a/packages/eslint-plugin/lib/rules/no-react-navigation-stack.ts b/packages/eslint-plugin/lib/rules/no-react-navigation-stack.ts new file mode 100644 index 0000000..b3c312d --- /dev/null +++ b/packages/eslint-plugin/lib/rules/no-react-navigation-stack.ts @@ -0,0 +1,49 @@ +import type { Rule } from "eslint"; + +// Custom Rule: No Import from @react-navigation/stack +export const noReactNavigationStackImportRule: Rule.RuleModule = { + meta: { + type: "problem", + docs: { + description: + "Disallow importing from `@react-navigation/stack` and suggest using `@react-navigation/native-stack` instead.", + category: "Best Practices", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/no-react-navigation-stack.md", + }, + messages: { + noReactNavigationStackImport: + '"@react-navigation/native-stack" provides out of the box native screens and native transitions for better performance and user experience.', + }, + schema: [], + }, + + create(context) { + return { + ImportDeclaration(node) { + // Check if the import is from "@react-navigation/stack" + if (node.source.value === "@react-navigation/stack") { + context.report({ + node, + messageId: "noReactNavigationStackImport", + }); + } + }, + CallExpression(node) { + // Check if require() is used to import "@react-navigation/stack" + if ( + node.callee.type === "Identifier" && + node.callee.name === "require" && + node.arguments.length > 0 && + node.arguments[0].type === "Literal" && + node.arguments[0].value === "@react-navigation/stack" + ) { + context.report({ + node, + messageId: "noReactNavigationStackImport", + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/tests/lib/rules/no-react-navigation-stack.test.ts b/packages/eslint-plugin/tests/lib/rules/no-react-navigation-stack.test.ts new file mode 100644 index 0000000..641b740 --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/no-react-navigation-stack.test.ts @@ -0,0 +1,30 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger an error breaking eslint-plugin-bam-custom-rules: +// bam-custom-rules/no-react-navigation-stack + +import { noReactNavigationStackImportRule } from "../../../lib/rules/no-react-navigation-stack"; +import { RuleTester } from "eslint"; + +const ruleTester = new RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), +}); + +const valid = [ + `import { createStackNavigator } from "@react-navigation/native-stack";`, +]; + +const invalid = [ + `import { createStackNavigator } from "@react-navigation/stack";`, + `import {createStackNavigator} from '@react-navigation/stack';`, +]; + +ruleTester.run("no-react-navigation-stack", noReactNavigationStackImportRule, { + valid, + invalid: invalid.map((code) => ({ + code, + errors: [ + `"@react-navigation/native-stack" provides out of the box native screens and native transitions for better performance and user experience.`, + ], + })), +}); From 8368a20fcfd53557519adbb339040df9fed9eabd Mon Sep 17 00:00:00 2001 From: Fanny Tavart Date: Tue, 3 Sep 2024 16:43:00 +0200 Subject: [PATCH 12/12] BREAKING CHANGE: refactor -> create custom rule for import of useIsFocused to prevent from override by import eslint rule --- .../break-use-is-focused-import-rule.tsx | 4 +- .../docs/rules/no-use-is-focused.md | 17 +++++ .../eslint-plugin/lib/configs/performance.ts | 14 +--- packages/eslint-plugin/lib/rules/index.ts | 2 + .../lib/rules/no-use-is-focused.ts | 74 +++++++++++++++++++ .../tests/lib/rules/no-use-is-focused.test.ts | 25 +++++++ 6 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-use-is-focused.md create mode 100644 packages/eslint-plugin/lib/rules/no-use-is-focused.ts create mode 100644 packages/eslint-plugin/tests/lib/rules/no-use-is-focused.test.ts diff --git a/example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx b/example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx index d31d103..c5167e5 100644 --- a/example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx +++ b/example-app/eslint-breaking-examples/break-use-is-focused-import-rule.tsx @@ -1,7 +1,7 @@ // Save without formatting: [⌘ + K] > [S] -// This should trigger one error breaking eslint-plugin-react-native: -// no-restricted-imports +// This should trigger one error breaking custom performance rule: +// @bam.tech/no-use-is-focused import { useIsFocused } from "@react-navigation/native"; import { Text } from "react-native"; diff --git a/packages/eslint-plugin/docs/rules/no-use-is-focused.md b/packages/eslint-plugin/docs/rules/no-use-is-focused.md new file mode 100644 index 0000000..6d91a4d --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-use-is-focused.md @@ -0,0 +1,17 @@ +# Disallow importing `useIsFocused` from `@react-navigation/native` to encourage using `useFocusEffect` instead (`@bam.tech/no-use-is-focused`) + +💼 This rule is enabled in the `performance` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Prevents from using "useIsFocused" to avoid performance issues. "useFocusEffect" should be used instead. + +## Rule details + +Examples of **incorrect** code for this rule: + +```jsx +import { useIsFocused } from "@react-navigation/native"; +``` diff --git a/packages/eslint-plugin/lib/configs/performance.ts b/packages/eslint-plugin/lib/configs/performance.ts index 0ee9848..2a59ff8 100644 --- a/packages/eslint-plugin/lib/configs/performance.ts +++ b/packages/eslint-plugin/lib/configs/performance.ts @@ -2,24 +2,12 @@ import { defineConfig } from "eslint-define-config"; export const performanceConfig = defineConfig({ rules: { - "no-restricted-imports": [ - "error", - { - paths: [ - { - name: "@react-navigation/native", - importNames: ["useIsFocused"], - message: - "Please use useFocusEffect instead of useIsFocused to avoid excessive rerenders.", - }, - ], - }, - ], "@bam.tech/no-animated-without-native-driver": "error", "@bam.tech/avoid-intl-number-format": "error", "@bam.tech/avoid-react-native-svg": "warn", "@bam.tech/no-flatlist": "error", "@bam.tech/no-react-navigation-stack": "error", + "@bam.tech/no-use-is-focused": "error", }, overrides: [ { diff --git a/packages/eslint-plugin/lib/rules/index.ts b/packages/eslint-plugin/lib/rules/index.ts index c3565b1..26fc809 100644 --- a/packages/eslint-plugin/lib/rules/index.ts +++ b/packages/eslint-plugin/lib/rules/index.ts @@ -7,6 +7,7 @@ import { preferUserEventRule } from "./prefer-user-event"; import { requireNamedEffectRule } from "./require-named-effect"; import { noFlatListImportRule } from "./no-flatlist"; import { noReactNavigationStackImportRule } from "./no-react-navigation-stack"; +import { noUseIsFocusedImportRule } from "./no-use-is-focused"; export default { "await-user-event": awaitUserEventRule, @@ -18,4 +19,5 @@ export default { "avoid-react-native-svg": avoidReactNativeSvgImportRule, "no-flatlist": noFlatListImportRule, "no-react-navigation-stack": noReactNavigationStackImportRule, + "no-use-is-focused": noUseIsFocusedImportRule, }; diff --git a/packages/eslint-plugin/lib/rules/no-use-is-focused.ts b/packages/eslint-plugin/lib/rules/no-use-is-focused.ts new file mode 100644 index 0000000..6d42404 --- /dev/null +++ b/packages/eslint-plugin/lib/rules/no-use-is-focused.ts @@ -0,0 +1,74 @@ +import type { Rule } from "eslint"; +import type { ImportDeclaration, CallExpression, Property } from "estree"; + +// Custom Rule: No Import of useIsFocused from @react-navigation/native +export const noUseIsFocusedImportRule: Rule.RuleModule = { + meta: { + type: "problem", + docs: { + description: + "Disallow importing `useIsFocused` from `@react-navigation/native` to encourage using `useFocusEffect` instead.", + category: "Best Practices", + recommended: true, + url: "https://github.com/bamlab/react-native-project-config/tree/main/packages/eslint-plugin/docs/rules/no-use-is-focused.md", + }, + messages: { + noUseIsFocusedImport: + "Please use 'useFocusEffect' instead of 'useIsFocused' to avoid excessive rerenders: 'useIsFocused' will trigger rerender both when the page goes in and out of focus.", + }, + schema: [], + fixable: "code", + }, + + create(context) { + return { + ImportDeclaration(node: ImportDeclaration) { + if (node.source.value === "@react-navigation/native") { + node.specifiers.forEach((specifier) => { + if ( + specifier.type === "ImportSpecifier" && + specifier.imported.name === "useIsFocused" + ) { + context.report({ + node: specifier, + messageId: "noUseIsFocusedImport", + }); + } + }); + } + }, + CallExpression(node: CallExpression) { + if ( + node.callee.type === "Identifier" && + node.callee.name === "require" && + node.arguments.length > 0 && + node.arguments[0].type === "Literal" && + node.arguments[0].value === "@react-navigation/native" + ) { + const ancestors = context.getAncestors(); + const parent = ancestors[ancestors.length - 1]; // Get the direct parent of the node + + if ( + parent.type === "VariableDeclarator" && + parent.id.type === "ObjectPattern" + ) { + const properties = parent.id.properties as Property[]; + const useIsFocusedProperty = properties.find( + (prop) => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "useIsFocused", + ); + + if (useIsFocusedProperty) { + context.report({ + node: useIsFocusedProperty, + messageId: "noUseIsFocusedImport", + }); + } + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin/tests/lib/rules/no-use-is-focused.test.ts b/packages/eslint-plugin/tests/lib/rules/no-use-is-focused.test.ts new file mode 100644 index 0000000..9c865a9 --- /dev/null +++ b/packages/eslint-plugin/tests/lib/rules/no-use-is-focused.test.ts @@ -0,0 +1,25 @@ +// Save without formatting: [⌘ + K] > [S] + +// This should trigger an error breaking eslint-plugin-bam-custom-rules: +// bam-custom-rules/no-use-is-focused + +import { noUseIsFocusedImportRule } from "../../../lib/rules/no-use-is-focused"; +import { RuleTester } from "eslint"; + +const ruleTester = new RuleTester({ + parser: require.resolve("@typescript-eslint/parser"), +}); + +const valid = [`import { useFocusEffect } from "@react-navigation/native";`]; + +const invalid = [`import { useIsFocused } from "@react-navigation/native";`]; + +ruleTester.run("no-use-is-focused", noUseIsFocusedImportRule, { + valid, + invalid: invalid.map((code) => ({ + code, + errors: [ + `Please use 'useFocusEffect' instead of 'useIsFocused' to avoid excessive rerenders: 'useIsFocused' will trigger rerender both when the page goes in and out of focus.`, + ], + })), +});