Upgrade ESLint to v9 with flat config (#9680)

* Upgrade ESLint to v9 with flat config

- Upgrade eslint from v8 to v9.18.0
- Migrate from .eslintrc.js to eslint.config.mjs (flat config)
- Upgrade typescript-eslint to v8.20.0 (unified package)
- Replace eslint-plugin-import with eslint-plugin-import-x for flat config support
- Add globals package for environment globals
- Update eslint-plugin-bsky-internal with proper meta objects for ESLint v9
- Fix deprecated context.getScope() API usage
- Update bskyembed to use flat config
- Remove deprecated --ext flag from lint scripts
- Configure rules to maintain previous behavior while using new ESLint version

* Fix varsIgnorePattern to require character after underscore

Restore the original pattern `^_.+` instead of `^_` so that lingui's
`const { _ } = useLingui()` will still be flagged when unused.

* Update ESLint rule tests for flat config format

- Update RuleTester to use flat config languageOptions instead of
  eslintrc parser format
- Remove duplicate test case that ESLint v9 now detects
- Add Jest globals for test files

* update eslint package versions

* lint android a11y

* enable typechecked rules, switch them to warn

* fix yarn lock ci

* Fix CI failure

* Remove unused globals?

* Organize a bit, add quiet to main lint command

* Allow ternary

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Samuel Newman
2026-01-17 00:24:28 +02:00
committed by GitHub
parent acc26c965d
commit bd510d8468
38 changed files with 1914 additions and 2139 deletions
@@ -215,18 +215,18 @@ export function Controls({
const seekLeft = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime - 5, 0, duration))
}, [onSeek, videoRef])
const seekRight = useCallback(() => {
if (!videoRef.current) return
// eslint-disable-next-line @typescript-eslint/no-shadow
const currentTime = videoRef.current.currentTime
// eslint-disable-next-line @typescript-eslint/no-shadow
const duration = videoRef.current.duration || 0
onSeek(clamp(currentTime + 5, 0, duration))
}, [onSeek, videoRef])
+1
View File
@@ -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 <Text emoji />'`,
)
}
+1 -1
View File
@@ -98,7 +98,7 @@ function DialogInner({
} = useRemoveLiveStatusMutation()
const {minutesUntilExpiry, expiryDateTime} = useMemo(() => {
tick!
void tick
const expiry = new Date(status.expiresAt ?? new Date())
return {
+6 -3
View File
@@ -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)
@@ -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},
@@ -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.Outer>
<toast.Icon />
<toast.Text>
Toast.show(
<Toast.Outer>
<Toast.Icon />
<Toast.Text>
<Trans>Your live event preferences have been updated.</Trans>
</toast.Text>
</Toast.Text>
{undoAction && (
<toast.Action
<Toast.Action
label={_(msg`Undo`)}
onPress={() => {
if (undoAction) {
@@ -76,12 +76,10 @@ function Inner({
}
}}>
<Trans>Undo</Trans>
</toast.Action>
</Toast.Action>
)}
</toast.Outer>,
{
type: 'success',
},
</Toast.Outer>,
{type: 'success'},
)
/*
+1 -1
View File
@@ -128,7 +128,7 @@ export function useGeolocationServiceResponse() {
useEffect(() => {
return onGeolocationServiceResponseUpdate(config => {
setConfig(config!)
setConfig(config)
})
}, [])
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -3,13 +3,13 @@ import {
getInfoAsync,
readDirectoryAsync,
} from 'expo-file-system/legacy'
import {type ImagePickerResult} from 'expo-image-picker'
import ExpoImageCropTool, {
type OpenCropperOptions,
} from '@bsky.app/expo-image-crop-tool'
import {compressIfNeeded} from './manip'
import {type PickerImage} from './picker.shared'
import {ImagePickerResult} from 'expo-image-picker'
async function getFile() {
const imagesDir = documentDirectory!
@@ -138,11 +138,10 @@ function ChatListItemReady({
const {lastMessage, lastMessageSentAt, latestReportableMessage} =
useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
let lastMessage = _(msg`No messages yet`)
// eslint-disable-next-line @typescript-eslint/no-shadow
let lastMessageSentAt: string | null = null
// eslint-disable-next-line @typescript-eslint/no-shadow
let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
-1
View File
@@ -64,7 +64,6 @@ export function PostThread({uri}: {uri: string}) {
*/
const thread = usePostThread({anchor: uri})
const {anchor, hasParents} = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
let hasParents = false
for (const item of thread.data.items) {
if (item.type === 'threadPost' && item.depth === 0) {
+1 -1
View File
@@ -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)
+5
View File
@@ -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<K extends keyof Schema>(key: K): Schema[K] {
}
get satisfies PersistedApi['get']
// eslint-disable-next-line @typescript-eslint/require-await
export async function write<K extends keyof Schema>(
key: K,
value: Schema[K],
@@ -82,6 +85,7 @@ export function onUpdate<K extends keyof Schema>(
}
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' &&
+1 -1
View File
@@ -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'
+7 -1
View File
@@ -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
}
@@ -1,4 +1,3 @@
/* eslint-disable no-labels */
import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api'
import {
+1 -1
View File
@@ -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
@@ -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'
-1
View File
@@ -437,7 +437,6 @@ let PostFeed = ({
for (const page of data.pages) {
for (const slice of page.slices) {
const item = slice.items.find(
// eslint-disable-next-line @typescript-eslint/no-shadow
item => item.uri === slice.feedPostUri,
)
if (
+4 -2
View File
@@ -1,9 +1,9 @@
import {useState} from 'react'
import {LogBox, Pressable, View, TextInput} from 'react-native'
import {LogBox, Pressable, TextInput, View} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {BLUESKY_PROXY_HEADER} from '#/lib/constants'
import {useSessionApi, useAgent} from '#/state/session'
import {useAgent, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {navigate} from '../../../Navigation'
@@ -50,6 +50,8 @@ export function TestCtrls() {
return (
<View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
<TextInput
accessibilityLabel="Text input field"
accessibilityHint="Enter proxy header"
testID="e2eProxyHeaderInput"
onChangeText={val => setProxyHeader(val as any)}
onSubmitEditing={() => {
-2
View File
@@ -3,12 +3,10 @@ import {type AlertButton, type AlertStatic} from 'react-native'
class WebAlert implements Pick<AlertStatic, 'alert'> {
public alert(title: string, message?: string, buttons?: AlertButton[]): void {
if (buttons === undefined || buttons.length === 0) {
// eslint-disable-next-line no-alert
window.alert([title, message].filter(Boolean).join('\n'))
return
}
// eslint-disable-next-line no-alert
const result = window.confirm([title, message].filter(Boolean).join('\n'))
if (result === true) {
+2 -2
View File
@@ -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],
)
+1
View File
@@ -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 <Text emoji />'`,
)
}