diff --git a/eslint.config.mjs b/eslint.config.mjs index e4b7ccb8f1..a3398d3371 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,9 +1,12 @@ // @ts-check import js from '@eslint/js' import tseslint from 'typescript-eslint' +import { defineConfig } from 'eslint/config'; import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' +// @ts-expect-error no types import reactNative from 'eslint-plugin-react-native' +// @ts-expect-error no types import reactNativeA11y from 'eslint-plugin-react-native-a11y' import simpleImportSort from 'eslint-plugin-simple-import-sort' import importX from 'eslint-plugin-import-x' @@ -11,8 +14,9 @@ import lingui from 'eslint-plugin-lingui' import reactCompiler from 'eslint-plugin-react-compiler' import bskyInternal from 'eslint-plugin-bsky-internal' import globals from 'globals' +import tsParser from '@typescript-eslint/parser' -export default tseslint.config( +export default defineConfig( // Global ignores { ignores: [ @@ -40,18 +44,25 @@ export default tseslint.config( js.configs.recommended, // TypeScript rules - ...tseslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, + + // React Hooks rules + reactHooks.configs.flat.recommended, + + // Import X + // @ts-expect-error https://github.com/un-ts/eslint-plugin-import-x/issues/439 + importX.flatConfigs.recommended, + importX.flatConfigs.typescript, + importX.flatConfigs['react-native'], // Main configuration for all JS/TS/JSX/TSX files { files: ['**/*.{js,jsx,ts,tsx}'], plugins: { react, - 'react-hooks': reactHooks, 'react-native': reactNative, 'react-native-a11y': reactNativeA11y, 'simple-import-sort': simpleImportSort, - 'import-x': importX, lingui, 'react-compiler': reactCompiler, 'bsky-internal': bskyInternal, @@ -65,6 +76,8 @@ export default tseslint.config( ...globals.es2021, }, parserOptions: { + parser: tsParser, + projectService: true, ecmaFeatures: { jsx: true, }, @@ -83,8 +96,6 @@ export default tseslint.config( 'react/no-unescaped-entities': 'off', 'react/prop-types': 'off', - // React Hooks rules - ...reactHooks.configs.recommended.rules, // React Native rules 'react-native/no-inline-styles': 'off', @@ -188,13 +199,27 @@ export default tseslint.config( ], '@typescript-eslint/no-require-imports': 'off', // Maintain previous behavior - these are stricter in typescript-eslint v8 + // `warn` ones are probably worth fixing. `off` ones are a bit too nit-picky '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-empty-object-type': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', - '@typescript-eslint/no-wrapper-object-types': 'off', '@typescript-eslint/no-unsafe-function-type': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-member-access': 'warn', + '@typescript-eslint/no-unsafe-call': 'warn', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-misused-promises': 'warn', + '@typescript-eslint/require-await': 'warn', + '@typescript-eslint/no-unsafe-enum-comparison': 'warn', + '@typescript-eslint/no-unnecessary-type-assertion': 'warn', + '@typescript-eslint/no-redundant-type-constituents': 'warn', + '@typescript-eslint/no-duplicate-type-constituents': 'warn', + '@typescript-eslint/no-base-to-string': 'warn', + '@typescript-eslint/prefer-promise-reject-errors': 'warn', + '@typescript-eslint/await-thenable': 'warn', // Import rules 'import-x/consistent-type-specifier-style': ['warn', 'prefer-inline'], diff --git a/package.json b/package.json index 3b8d0eddd6..7daebb86eb 100644 --- a/package.json +++ b/package.json @@ -255,6 +255,7 @@ "babel-plugin-react-compiler": "^19.1.0-rc.3", "babel-preset-expo": "~54.0.0", "eslint": "^9.39.2", + "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-bsky-internal": "link:./eslint", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-lingui": "^0.11.0", diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index cf250faf79..085d936c44 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -80,9 +80,8 @@ export function Outer({ ) const handleBackgroundPress = React.useCallback( - async (e: GestureResponderEvent) => { - webOptions?.onBackgroundPress ? webOptions.onBackgroundPress(e) : close() - }, + async (e: GestureResponderEvent) => + webOptions?.onBackgroundPress ? webOptions.onBackgroundPress(e) : close(), [webOptions, close], ) diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx index 10dabf2f95..17ab93272c 100644 --- a/src/components/Typography.tsx +++ b/src/components/Typography.tsx @@ -35,6 +35,7 @@ export function Text({ if (__DEV__) { if (!emoji && childHasEmoji(children)) { logger.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`, ) } diff --git a/src/components/live/EditLiveDialog.tsx b/src/components/live/EditLiveDialog.tsx index ba5e2b5b7a..585e95bba1 100644 --- a/src/components/live/EditLiveDialog.tsx +++ b/src/components/live/EditLiveDialog.tsx @@ -98,7 +98,7 @@ function DialogInner({ } = useRemoveLiveStatusMutation() const {minutesUntilExpiry, expiryDateTime} = useMemo(() => { - tick! + void tick const expiry = new Date(status.expiresAt ?? new Date()) return { diff --git a/src/components/live/GoLiveDialog.tsx b/src/components/live/GoLiveDialog.tsx index 9d866fd6a5..72627e0248 100644 --- a/src/components/live/GoLiveDialog.tsx +++ b/src/components/live/GoLiveDialog.tsx @@ -13,7 +13,11 @@ import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' -import {getLiveServiceNames} from '#/components/live/utils' +import { + displayDuration, + getLiveServiceNames, + useDebouncedValue, +} from '#/components/live/utils' import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import * as Select from '#/components/Select' @@ -21,7 +25,6 @@ import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' import {LinkPreview} from './LinkPreview' import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries' -import {displayDuration, useDebouncedValue} from './utils' export function GoLiveDialog({ control, @@ -57,7 +60,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) { const time = useCallback( (offset: number) => { - tick! + void tick const date = new Date() date.setMinutes(date.getMinutes() + offset) diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index 14354332cb..487cf6b299 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -211,8 +211,8 @@ function Inner(props: ReportDialogProps) { logger.metric( 'reportDialog:success', { - reason: state.selectedOption?.reason!, - labeler: state.selectedLabeler?.creator.handle!, + reason: state.selectedOption?.reason ?? '', + labeler: state.selectedLabeler?.creator.handle ?? '', details: !!state.details, }, {statsig: false}, diff --git a/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx index b2f50840a9..52d3a7ca24 100644 --- a/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx +++ b/src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx @@ -9,7 +9,7 @@ import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Loader} from '#/components/Loader' -import * as toast from '#/components/Toast' +import * as Toast from '#/components/Toast' import {Span, Text} from '#/components/Typography' import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences' import { @@ -61,14 +61,14 @@ function Inner({ feed, metricContext, onUpdateSuccess({undoAction}) { - toast.show( - - - + Toast.show( + + + Your live event preferences have been updated. - + {undoAction && ( - { if (undoAction) { @@ -76,12 +76,10 @@ function Inner({ } }}> Undo - + )} - , - { - type: 'success', - }, + , + {type: 'success'}, ) /* diff --git a/src/geolocation/service.ts b/src/geolocation/service.ts index d04e0a5afb..2d9285b676 100644 --- a/src/geolocation/service.ts +++ b/src/geolocation/service.ts @@ -128,7 +128,7 @@ export function useGeolocationServiceResponse() { useEffect(() => { return onGeolocationServiceResponseUpdate(config => { - setConfig(config!) + setConfig(config) }) }, []) diff --git a/src/lib/actor-status.ts b/src/lib/actor-status.ts index 7c18b5dc66..495d6f4e66 100644 --- a/src/lib/actor-status.ts +++ b/src/lib/actor-status.ts @@ -17,7 +17,7 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { const config = useLiveNowConfig() return useMemo(() => { - tick! // revalidate every minute + void tick // revalidate every minute if (shadowed && 'status' in shadowed && shadowed.status) { const isValid = validateStatus(shadowed.status, config) diff --git a/src/lib/functions.ts b/src/lib/functions.ts index e0d44ce2d7..c22a25bcd7 100644 --- a/src/lib/functions.ts +++ b/src/lib/functions.ts @@ -67,7 +67,7 @@ export function isPlainArray(value: unknown) { } // Copied from: https://github.com/jonschlinkert/is-plain-object -export function isPlainObject(o: any): o is Object { +export function isPlainObject(o: any): o is object { if (!hasObjectPrototype(o)) { return false } diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index c5fd8f017c..4e301b8420 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -95,7 +95,7 @@ export class Convo { this.convoId = params.convoId this.agent = params.agent this.events = params.events - this.senderUserDid = params.agent.session?.did! + this.senderUserDid = params.agent.assertDid if (params.placeholderData) { this.setupPlaceholderData(params.placeholderData) diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts index 49fa224c05..ff278b74be 100644 --- a/src/state/persisted/index.web.ts +++ b/src/state/persisted/index.web.ts @@ -22,6 +22,8 @@ const UPDATE_EVENT = 'BSKY_UPDATE' let _state: Schema = defaults const _emitter = new EventEmitter() +// async, to match native implementation +// eslint-disable-next-line @typescript-eslint/require-await export async function init() { broadcast.onmessage = onBroadcastMessage window.onstorage = onStorage @@ -37,6 +39,7 @@ export function get(key: K): Schema[K] { } get satisfies PersistedApi['get'] +// eslint-disable-next-line @typescript-eslint/require-await export async function write( key: K, value: Schema[K], @@ -82,6 +85,7 @@ export function onUpdate( } onUpdate satisfies PersistedApi['onUpdate'] +// eslint-disable-next-line @typescript-eslint/require-await export async function clearStorage() { try { localStorage.removeItem(BSKY_STORAGE) @@ -102,6 +106,7 @@ function onStorage() { } } +// eslint-disable-next-line @typescript-eslint/require-await async function onBroadcastMessage({data}: MessageEvent) { if ( typeof data === 'object' && diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index 740ea4bf35..c3cd0925ee 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -23,7 +23,7 @@ export { useExternalEmbedsPrefs, useSetExternalEmbedPref, } from './external-embeds-prefs' -export * from './hidden-posts' +export {useHiddenPosts, useHiddenPostsApi} from './hidden-posts' export {useLabelDefinitions} from './label-defs' export {useLanguagePrefs, useLanguagePrefsApi} from './languages' export {useSetSubtitlesEnabled, useSubtitlesEnabled} from './subtitles' diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index d46cef89fa..ab0a957522 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -15,6 +15,12 @@ export type UsePreferencesQueryResponse = Omit< } export type ThreadViewPreferences = { - sort: 'hotness' | 'oldest' | 'newest' | 'most-likes' | 'random' | string + sort: + | 'hotness' + | 'oldest' + | 'newest' + | 'most-likes' + | 'random' + | (string & {}) lab_treeViewEnabled?: boolean } diff --git a/src/state/shell/logged-out.tsx b/src/state/shell/logged-out.tsx index 3617a1eca9..bda5251ffd 100644 --- a/src/state/shell/logged-out.tsx +++ b/src/state/shell/logged-out.tsx @@ -26,7 +26,7 @@ type Controls = { /** * The did of the account to populate the login form with. */ - requestedAccount?: string | 'none' | 'new' | 'starterpack' + requestedAccount?: (string & {}) | 'none' | 'new' | 'starterpack' }) => void /** * Clears the requested account so that next time the logged out view is diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 8b3e61b0e9..f2245bab71 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -14,7 +14,9 @@ import { import {AppBskyRichtextFacet, RichText} from '@atproto/api' import PasteInput, { type PastedFile, - type PasteInputRef, // @ts-expect-error no types when installing from github + type PasteInputRef, + // @ts-expect-error no types when installing from github + // eslint-disable-next-line import-x/no-unresolved } from '@mattermost/react-native-paste-input' import {POST_IMG_MAX} from '#/lib/constants' diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx index 24d478fe54..9424bd504e 100644 --- a/src/view/com/util/forms/Button.tsx +++ b/src/view/com/util/forms/Button.tsx @@ -153,9 +153,9 @@ export function Button({ async (event: GestureResponderEvent) => { event.stopPropagation() event.preventDefault() - withLoading && setIsLoading(true) + if (withLoading) setIsLoading(true) await onPress?.(event) - withLoading && setIsLoading(false) + if (withLoading) setIsLoading(false) }, [onPress, withLoading], ) diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx index 1e49605f49..dbab95561d 100644 --- a/src/view/com/util/text/Text.tsx +++ b/src/view/com/util/text/Text.tsx @@ -51,6 +51,7 @@ function Text_DEPRECATED({ if (__DEV__) { if (!emoji && childHasEmoji(children)) { logger.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`, ) } diff --git a/yarn.lock b/yarn.lock index 40879996fc..ad13610450 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11176,7 +11176,7 @@ eslint-config-prettier@^8.5.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== -eslint-import-context@^0.1.9: +eslint-import-context@^0.1.8, eslint-import-context@^0.1.9: version "0.1.9" resolved "https://registry.yarnpkg.com/eslint-import-context/-/eslint-import-context-0.1.9.tgz#967b0b2f0a90ef4b689125e088f790f0b7756dbe" integrity sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg== @@ -11184,6 +11184,19 @@ eslint-import-context@^0.1.9: get-tsconfig "^4.10.1" stable-hash-x "^0.2.0" +eslint-import-resolver-typescript@^4.4.4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz#3e83a9c25f4a053fe20e1b07b47e04e8519a8720" + integrity sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw== + dependencies: + debug "^4.4.1" + eslint-import-context "^0.1.8" + get-tsconfig "^4.10.1" + is-bun-module "^2.0.0" + stable-hash-x "^0.2.0" + tinyglobby "^0.2.14" + unrs-resolver "^1.7.11" + "eslint-plugin-bsky-internal@link:./eslint": version "0.0.0" uid "" @@ -13321,6 +13334,13 @@ is-boolean-object@^1.2.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" +is-bun-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" + integrity sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ== + dependencies: + semver "^7.7.1" + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -18141,7 +18161,7 @@ semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -semver@^7.7.2, semver@^7.7.3: +semver@^7.7.1, semver@^7.7.2, semver@^7.7.3: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== @@ -19291,7 +19311,7 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -tinyglobby@^0.2.11, tinyglobby@^0.2.15: +tinyglobby@^0.2.11, tinyglobby@^0.2.14, tinyglobby@^0.2.15: version "0.2.15" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== @@ -19777,7 +19797,7 @@ unraw@^3.0.0: resolved "https://registry.yarnpkg.com/unraw/-/unraw-3.0.0.tgz#73443ed70d2ab09ccbac2b00525602d5991fbbe3" integrity sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg== -unrs-resolver@^1.9.2: +unrs-resolver@^1.7.11, unrs-resolver@^1.9.2: version "1.11.1" resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==