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
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..68199cc
--- /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 custom FlatList import rule:
+// @bam.tech/no-flatlist
+
+import { FlatList } from "react-native";
+
+export const MyCustomButton = () => {
+ return ;
+};
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/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/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/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..0186d3d
--- /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 custom react-navigation/stack rule:
+// @bam.tech/no-react-navigation-stack
+
+import { createStackNavigator } from "@react-navigation/stack";
+
+const Stack = createStackNavigator();
+
+export const MyStack = () => {
+ return ;
+};
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..c5167e5
--- /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 custom performance rule:
+// @bam.tech/no-use-is-focused
+
+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 47f73a5..ba11e3f 100644
--- a/example-app/package.json
+++ b/example-app/package.json
@@ -11,6 +11,8 @@
"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",
"@types/react": "^18.2.14",
@@ -37,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/README.md b/packages/eslint-plugin/README.md
index 783392a..f0ca4ad 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -103,16 +103,23 @@ 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 | 💼 | 🔧 |
-| :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------- | :-- | :-- |
-| [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 | 💼 | ⚠️ | 🔧 |
+| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ | :--------------------- | :--------------------- | :-- |
+| [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][] | | 🔧 |
+| [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/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/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/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/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/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/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/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..e304f4d 100644
--- a/packages/eslint-plugin/lib/configs/index.ts
+++ b/packages/eslint-plugin/lib/configs/index.ts
@@ -1,11 +1,13 @@
-import { a11yconfig } from "./a11y";
+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,
+ 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..2a59ff8
--- /dev/null
+++ b/packages/eslint-plugin/lib/configs/performance.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from "eslint-define-config";
+
+export const performanceConfig = defineConfig({
+ rules: {
+ "@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: [
+ {
+ files: ["*.js"],
+ },
+ ],
+});
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/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 a9e6841..26fc809 100644
--- a/packages/eslint-plugin/lib/rules/index.ts
+++ b/packages/eslint-plugin/lib/rules/index.ts
@@ -1,11 +1,23 @@
+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";
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,
"prefer-user-event": preferUserEventRule,
"require-named-effect": requireNamedEffectRule,
"no-different-displayname": noDifferentDisplaynameRule,
+ "no-animated-without-native-driver": noAnimatedWithoutNativeDriverRule,
+ "avoid-intl-number-format": avoidIntlNumberFormatRule,
+ "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-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/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/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/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/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.",
+ ],
+ })),
+});
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.",
+ ],
+ })),
+});
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.",
+ ],
+ })),
+ },
+);
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.",
+ ],
+ })),
+});
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.`,
+ ],
+ })),
+});
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.`,
+ ],
+ })),
+});