Migrate to ESLint 9 + eslint-config-expo, fix lint issues

This commit is contained in:
ponas committed 2026-07-29 00:42:13 +02:00
1 parent 6e2a6f8b4d
commit 89d8c27bc0
16 files changed
+521 -1339

No files matched your search

-85
View File
@@ -1,85 +0,0 @@
module.exports = {
root: true,
ignorePatterns: ['coverage/'],
extends: [
'@react-native',
'airbnb-base',
'airbnb-typescript/base',
'plugin:prettier/recommended',
'plugin:react/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
],
rules: {
'class-methods-use-this': 'error', // FIXME: refactor
'max-len': 'off',
'no-plusplus': 'off',
'no-console': 'off',
'no-underscore-dangle': 'off',
'no-param-reassign': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-use-before-define': ['error', { functions: true, classes: true, variables: false }], // disable the rule for variables, but enable it for functions and classes
'react/prefer-stateless-function': 1,
'react/prop-types': 'off',
'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
'react/sort-comp': 'off',
'react-native/no-inline-styles': 'off', // FIXME: refactor
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/__tests__/**',
'src/testUtils.tsx',
'jest.config.js',
'jest.setup.js',
'__mocks__/**',
],
},
],
'import/prefer-default-export': 'off',
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
},
],
},
plugins: ['prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json'],
},
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: {
project: ['./tsconfig.json'],
},
},
},
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['./**/*.js'],
},
{
// Jest infrastructure is CommonJS by nature
files: ['jest.config.js', 'jest.setup.js', '__mocks__/**'],
rules: {
'global-require': 'off',
'@typescript-eslint/no-var-requires': 'off',
'import/extensions': 'off',
'class-methods-use-this': 'off',
},
},
],
};
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = {
printWidth: 120,
singleQuote: true,
};
};
+1 -6
View File
@@ -86,8 +86,6 @@ history, code and binary blobs related to React Native functionality filtered ou
- `react-native-simple-radio-button` (last published in 2018, and untyped — it accounts for
5 of the errors `npm run check-types` reports); `@expo/ui` ships a native `Picker` in
segmented/radio style, see the SDK 56 follow-ups below
- migrate ESLint 8 + airbnb config (both EOL) to ESLint 9 flat config or
[eslint-config-expo](https://docs.expo.dev/guides/using-eslint/)
- follow-ups from the [Expo SDK 54 upgrade](https://expo.dev/changelog/sdk-54):
- decide on Sentry: `@sentry/react-native` is installed but never initialized (see the
commented-out init in `index.ts`), and its config plugin (now applied via `app.json`)
@@ -129,10 +127,7 @@ history, code and binary blobs related to React Native functionality filtered ou
`expo-build-properties` if it causes trouble
- now on TypeScript 6: `baseUrl` is deprecated (gone in TS 7) and was removed from
`tsconfig.json`, and TS 6 stopped auto-including `@types/*`, hence the explicit `types` field
- make `npm run lint` usable again: ESLint only ignores `coverage/`, so it also walks the
gitignored `android/` and `ios/` directories and reports ~126 errors from stale build output.
Add them to `ignorePatterns`, alongside the ESLint 9 migration above
- fix the 20 pre-existing `npm run check-types` errors (unchanged by the SDK upgrade)
- fix the 19 pre-existing `npm run check-types` errors (unchanged by the SDK upgrade)
- add [Apple Pay](https://docs.stripe.com/apple-pay?platform=react-native) for membership
payments: either natively in the payment sheet (register the merchant ID
`merchant.no.neuf.chateau` + Apple Pay certificate, set `merchantIdentifier` in the
+22
View File
@@ -0,0 +1,22 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig, globalIgnores } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');
const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended');
module.exports = defineConfig([
globalIgnores(['android/*', 'ios/*', 'coverage/*', '.expo/*', 'dist/*', 'web-build/*']),
expoConfig,
eslintPluginPrettierRecommended,
{
rules: {
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
},
],
},
},
]);
-1
View File
@@ -1,4 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getDefaultConfig } = require('expo/metro-config');
module.exports = (() => {
+457 -1203
View File
File diff suppressed because it is too large. Load diff
+2 -9
View File
@@ -55,21 +55,14 @@
"devDependencies": {
"@babel/core": "^7.24.9",
"@expo/config-plugins": "~56.0.14",
"@react-native/eslint-config": "^0.74.85",
"@testing-library/react-native": "^13.3.3",
"@types/jest": "^29.5.14",
"@types/react": "~19.2.10",
"@types/react-native-simple-radio-button": "^2.7.7",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint": "^9.39.5",
"eslint-config-expo": "~56.0.4",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"jest": "^29.7.0",
"jest-expo": "~56.0.5",
"prettier": "^3.3.3",
-1
View File
@@ -4,7 +4,6 @@ import { Text, StyleSheet, View } from 'react-native';
import theme from '@/theme';
import IconExclamationTriangle from '@assets/icons/exclamation-triangle.svg';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const FormErrors = ({ errors }: { errors: Record<string, any> }) => {
if (!errors) {
return [];
+1 -2
View File
@@ -6,9 +6,8 @@ import TextField, { TextFieldProps } from '@/components/TextField';
import theme from '@/theme';
type FormInputProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errors: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: any;
name: string;
label: string;
+13 -6
View File
@@ -13,15 +13,23 @@ export type ToastProps = {
const Toast = ({ message, visible, position = 'bottom', autoDismiss, onDismiss }: ToastProps) => {
const [mounted, setMounted] = useState(!!visible);
const opacity = useRef(new Animated.Value(0)).current;
const [opacity] = useState(() => new Animated.Value(0));
if (visible && !mounted) {
setMounted(true);
}
const onDismissRef = useRef(onDismiss);
useEffect(() => {
onDismissRef.current = onDismiss;
}, [onDismiss]);
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | undefined;
if (visible) {
setMounted(true);
Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }).start();
if (autoDismiss && onDismiss) {
timer = setTimeout(onDismiss, autoDismiss);
if (autoDismiss) {
timer = setTimeout(() => onDismissRef.current?.(), autoDismiss);
}
} else {
Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true }).start(({ finished }) => {
@@ -35,8 +43,7 @@ const Toast = ({ message, visible, position = 'bottom', autoDismiss, onDismiss }
clearTimeout(timer);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, autoDismiss]);
}, [visible, autoDismiss, opacity]);
if (!mounted) {
return null;
+10 -8
View File
@@ -1,5 +1,5 @@
import { useStripe } from '@stripe/stripe-react-native';
import { useCallback, useRef, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Linking } from 'react-native';
import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector, useStore } from 'react-redux';
@@ -8,15 +8,17 @@ import { logout } from './actions';
import type { AppDispatch, AppStore, RootState } from './reduxStore';
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
const [previous, setPrevious] = useState<T | undefined>(undefined);
const [current, setCurrent] = useState(value);
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // Only re-run if value changes
// Adjust state during render instead of tracking the previous render's value in a ref,
// see https://react.dev/reference/react/useState#storing-information-from-previous-renders
if (value !== current) {
setPrevious(current);
setCurrent(value);
}
// Return previous value (happens before update in useEffect above)
return ref.current;
return previous;
}
/** Handles deep links. Currently only used by stripe */
+3 -2
View File
@@ -99,13 +99,14 @@ const PurchaseButton = ({
];
const defaultValue = options[1];
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => onSelect(defaultValue.value), []);
const onSelect = (value: string) => {
setMembershipTypeSlug(value);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => onSelect(defaultValue.value), []);
if (user.is_member) {
return null;
}
+6 -10
View File
@@ -29,9 +29,14 @@ const useProof = ({ fetchUser }: UseProofProps) => {
const { scheduleMembershipExpiryNotification, hasPermission } = useNotifications();
const [paymentLoading, setPaymentLoading] = useState(false);
const [membershipTypeSlug, setMembershipTypeSlug] = useState('standard');
const [membershipPrice, setMembershipPrice] = useState(25000); // WARNING: Hardcoded due to state not being loaded initially. Should probably be fixed.
const [toast, setToast] = useState<ToastProps | null>(null);
// WARNING: Hardcoded fallback price for when the query hasn't loaded yet. Should probably be fixed.
const membershipPrice =
data?.membershipTypes?.find(
(membershipTypeOption: { slug: string; price: number }) => membershipTypeOption.slug === membershipTypeSlug,
)?.price ?? 25000;
const openPaymentSheet = async () => {
try {
await initializePaymentSheet();
@@ -85,15 +90,6 @@ const useProof = ({ fetchUser }: UseProofProps) => {
}
}, [membershipType, membershipEndDate, fetchUser, hasPermission, scheduleMembershipExpiryNotification]);
useEffect(() => {
// eslint-disable-next-line no-restricted-syntax
for (const membershipTypeOption of data?.membershipTypes ?? []) {
if (membershipTypeOption.slug === membershipTypeSlug) {
setMembershipPrice(membershipTypeOption.price);
}
}
}, [membershipTypeSlug, data?.membershipTypes]);
return {
isLoadingMembershipType: loading,
membershipPrice: membershipPrice ? membershipPrice / 100 : 0,
+2 -3
View File
@@ -44,7 +44,7 @@ const initialState: RootState = {
};
// FIXME: refactor using redux slices
// eslint-disable-next-line @typescript-eslint/default-param-last, @typescript-eslint/no-explicit-any
export default function appReducers(state = initialState, action: any) {
switch (action.type) {
/* Login */
@@ -78,9 +78,8 @@ export default function appReducers(state = initialState, action: any) {
return { ...state, isRegisteringUser: false, registerError: action.registerError };
case REGISTER_USER_SUCCESS:
// eslint-disable-next-line no-case-declarations
const userData = action.data;
// eslint-disable-next-line no-case-declarations
const token = action.data.auth_token;
delete userData.auth_token;
+1 -1
View File
@@ -32,7 +32,7 @@ export const testUser: UserType = {
interface RenderWithProvidersOptions {
preloadedState?: Partial<AppState>;
apolloMocks?: ReadonlyArray<MockedResponse>;
apolloMocks?: readonly MockedResponse[];
}
/**
+2 -1
View File
@@ -21,8 +21,9 @@ export type HomeTabParamList = {
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace ReactNavigation {
// https://reactnavigation.org/docs/typescript/#specifying-default-types-for-usenavigation-link-ref-etc
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface RootParamList extends HomeTabParamList {}
}
}