From 73f6bd5df165af278dc724fa6897d5ca9460985c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nishan=20=28o=5E=E2=96=BD=5Eo=29?= Date: Sat, 2 May 2026 05:55:35 +0530 Subject: [PATCH 1/2] [core][ios] Fix Record.toDictionary race crash under concurrent access (#45072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Why Concurrent `Record.toDictionary` calls on Records sharing `@Field` class instances (e.g. expo-video's VideoTrack payloads emitted from multiple background Tasks) causes crash https://github.com/expo/expo/issues/40688#issuecomment-3647715535. Also @AdityaPahilwani reached out to me with below stack trace that happens because of the same issue (concurrent video events dispatched from different queues that uses same field/record) ``` Thread 54 Crashed: 0 Quince 0x00000001044a0c30 Field.key.getter + 56 1 Quince 0x00000001044a0c30 Field.key.getter + 56 2 Quince 0x00000001044a0c30 Field.key.getter + 56 (Field.swift:19) 3 Quince 0x00000001044a1508 protocol witness for AnyFieldInternal.key.getter in conformance Field + 20 4 Quince 0x00000001044bae98 closure #1 (String?, Any) in fieldsOf(Record) + 0 (Record.swift:77) 5 Quince 0x00000001044bae98 specialized fieldsOf(Record) + 456 6 Quince 0x00000001044bae98 specialized fieldsOf(Record) + 456 7 Quince 0x00000001044bae98 specialized fieldsOf(Record) + 456 (Record.swift:76) 8 Quince 0x00000001044ba578 Record.toDictionary(appContext: AppContext?) + 32 9 Quince 0x00000001044ba578 Record.toDictionary(appContext: AppContext?) + 32 (Record.swift:63) 10 Quince 0x000000010448d088 DynamicConvertibleType.convertResult(_: A, appContext: AppContext) + 204 (DynamicConvertibleType.swift:26) 11 Quince 0x000000010448d19c protocol witness for AnyDynamicType.convertResult(_: A1, appContext: AppContext) in conformance DynamicConvertibleType + 16 12 Quince 0x000000010449053c DynamicOptionalType.convertResult(_: A, appContext: AppContext) + 100 (DynamicOptionalType.swift:40) 13 Quince 0x000000010449064c protocol witness for AnyDynamicType.convertResult(_: A1, appContext: AppContext) in conformance DynamicOptionalType + 12 14 Quince 0x00000001044c3660 $s15ExpoModulesCore11ConversionsV21convertFunctionResult_10appContext11dynamicTypeypxSg_AA03AppI0CSgAA010AnyDynamicK0_pSgtlFZyp_Tt3g5 + 628 (Conversions.swift:193) 15 Quince 0x00000001044bac30 closure #1 (inout [String : Any], AnyFieldInternal) in Record.toDictionary(appContext: AppContext?) + 164 16 Quince 0x00000001044bb43c partial apply for closure #1 (inout [String : Any], AnyFieldInternal) in Record.toDictionary(appContext: AppContext?) + 16 17 libswiftCore.dylib 0x00000001920dd148 Sequence.reduce(into: __owned A1, _: (inout A1, A.Element)) + 764 18 Quince 0x00000001044ba5e0 Record.toDictionary(appContext: AppContext?) + 136 19 Quince 0x00000001044f6820 VideoPlayer.safeEmit(event: String, payload: Record?) + 160 (VideoPlayer.swift:422) 20 Quince 0x00000001044f6fec VideoPlayer.currentVideoTrack.setter + 356 21 Quince 0x00000001044f87f8 protocol witness for VideoPlayerObserverDelegate.onVideoTrackChanged(player: AVPlayer, oldVideoTrack: VideoTrack?, newVideoTrack: VideoTrack?) in conformance VideoPlayer + 48 22 Quince 0x00000001044fe648 closure #1 (WeakPlayerObserverDelegate) in VideoPlayerObserver.currentVideoTrack.didset + 0 (VideoPlayerObserver.swift:87) 23 Quince 0x00000001044fe648 VideoPlayerObserver.currentVideoTrack.didset + 896 24 Quince 0x00000001044fe648 VideoPlayerObserver.currentVideoTrack.didset + 896 (VideoPlayerObserver.swift:86) 25 Quince 0x00000001045005a4 closure #1 () in closure #4 @Sendable (AVPlayerItem, NSKeyValueObservedChange<[AVPlayerItemTrack]>) in VideoPlayerObserver.initializeCurrentPlayerItemObservers(player: AVPlayer, playerItem: AVPlayerItem) + 160 26 libswift_Concurrency.dylib 0x000000019336a8b4 swift::runJobInEstablishedExecutorContext(swift::Job*) + 288 27 libswift_Concurrency.dylib 0x000000019336bd28 swift_job_runImpl(swift::Job*, swift::SerialExecutorRef) + 156 28 libdispatch.dylib 0x00000001cdbc2f48 _dispatch_root_queue_drain + 364 29 libdispatch.dylib 0x00000001cdbc36fc _dispatch_worker_thread2 + 180 30 libsystem_pthread.dylib 0x00000001f19fd37c _pthread_wqthread + 232 31 libsystem_pthread.dylib 0x00000001f19fc8c0 start_wqthread + 8 ``` I managed to repro it (not the exact expo video crash but conditions under which it can happen) with an added testcase. The crash points to the `field.options` set call. Screenshot 2026-04-24 at 8 59 31 PM # How Made `options` property private. Read and write needs to happen via `withOptions` function, which provides locked access to the `options` property # Test Plan Added a testcase for the crash. Run the tests with and without changes in Record.swift. When trying to repro the crash on test run it 2-3 times if not reproducible. # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/expo-modules-core/CHANGELOG.md | 1 + .../ios/Core/Records/AnyField.swift | 4 ++- .../ios/Core/Records/Field.swift | 20 +++++++---- .../ios/Core/Records/Record.swift | 9 +++-- .../ios/Tests/RecordSpec.swift | 33 +++++++++++++++++++ 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/packages/expo-modules-core/CHANGELOG.md b/packages/expo-modules-core/CHANGELOG.md index 128168bc73e554..615ab0bf90d8b2 100644 --- a/packages/expo-modules-core/CHANGELOG.md +++ b/packages/expo-modules-core/CHANGELOG.md @@ -22,6 +22,7 @@ ### 🐛 Bug fixes +- [iOS] Fix random crash when concurrent access of the same `Record` on `Field.options`. The set is now guarded by a per-field lock and accessed through a single `withOptions` API. ([#45072](https://github.com/expo/expo/pull/45072) by [@nishan](https://github.com/intergalacticspacehighway)) - [iOS] Fixed converting `Enumerable` function results to JS values. ([#45168](https://github.com/expo/expo/pull/45168) by [@tsapeta](https://github.com/tsapeta)) - [iOS] Fixed prebuild dependencies for `ExpoModulesJSI` ([#45124](https://github.com/expo/expo/pull/45124) by [@chrfalch](https://github.com/chrfalch)) - [iOS] Add async/await overload for StaticAsyncFunction ([#44471](https://github.com/expo/expo/pull/44471) by [@Wenszel](https://github.com/Wenszel)) diff --git a/packages/expo-modules-core/ios/Core/Records/AnyField.swift b/packages/expo-modules-core/ios/Core/Records/AnyField.swift index 57e3707d54d1cd..ad40bc77cf3b2d 100644 --- a/packages/expo-modules-core/ios/Core/Records/AnyField.swift +++ b/packages/expo-modules-core/ios/Core/Records/AnyField.swift @@ -12,7 +12,6 @@ public protocol AnyField { */ internal protocol AnyFieldInternal: AnyField { var key: String? { get } - var options: Set { get set } var fieldType: AnyDynamicType { get } /** @@ -22,6 +21,9 @@ internal protocol AnyFieldInternal: AnyField { */ var isRequired: Bool { get } + // Read‑modify‑write inside `body` is atomic. + func withOptions(_ body: (inout Set) -> T) -> T + func set(_ newValue: Any?, appContext: AppContext) throws @JavaScriptActor diff --git a/packages/expo-modules-core/ios/Core/Records/Field.swift b/packages/expo-modules-core/ios/Core/Records/Field.swift index 911a91731d2270..79c1c66f3a61af 100644 --- a/packages/expo-modules-core/ios/Core/Records/Field.swift +++ b/packages/expo-modules-core/ios/Core/Records/Field.swift @@ -16,16 +16,23 @@ public final class Field: AnyFieldInternal, @unchecked Sendab Sadly, property wrappers don't receive properties' label, so we must wait until it's assigned by `Record`. */ internal var key: String? { - return options.first { $0.rawValue == FieldOption.keyed("").rawValue }?.key + return withOptions { options in + options.first { $0.rawValue == FieldOption.keyed("").rawValue }?.key + } } /** Additional options of the field, such is if providing the value is required (`FieldOption.required`). + Locked because `fieldsOf` lazily writes the keyed option while other threads may read. Access only via `withOptions`. */ - internal var options: Set = Set() + private let _options: Mutex> + + internal func withOptions(_ body: (inout Set) -> T) -> T { + return _options.withLock { body(&$0) } + } internal var isRequired: Bool { - options.contains(.required) + withOptions { $0.contains(.required) } } /** @@ -33,7 +40,7 @@ public final class Field: AnyFieldInternal, @unchecked Sendab */ public init(wrappedValue: Type, _ options: FieldOption...) { self.wrappedValue = wrappedValue - self.options = Set(options) + self._options = Mutex(Set(options)) } /** @@ -42,7 +49,7 @@ public final class Field: AnyFieldInternal, @unchecked Sendab */ public init(wrappedValue: Type, _ options: [FieldOption]) { self.wrappedValue = wrappedValue - self.options = Set(options) + self._options = Mutex(Set(options)) } /** @@ -51,11 +58,12 @@ public final class Field: AnyFieldInternal, @unchecked Sendab */ public init(wrappedValue: Type = nil) where Type: ExpressibleByNilLiteral { self.wrappedValue = wrappedValue + self._options = Mutex(Set()) } public init(wrappedValue: Type = nil, _ options: FieldOption...) where Type: ExpressibleByNilLiteral { self.wrappedValue = wrappedValue - self.options = Set(options) + self._options = Mutex(Set(options)) } /** diff --git a/packages/expo-modules-core/ios/Core/Records/Record.swift b/packages/expo-modules-core/ios/Core/Records/Record.swift index 8aa18ce439e6ea..d459c81bc46a42 100644 --- a/packages/expo-modules-core/ios/Core/Records/Record.swift +++ b/packages/expo-modules-core/ios/Core/Records/Record.swift @@ -140,10 +140,15 @@ internal func allMirrorChildren(_ mirror: Mirror) -> [Mirror.Child] { internal func fieldsOf(_ record: Record) -> [AnyFieldInternal] { let mirror = Mirror(reflecting: record) return allMirrorChildren(mirror).compactMap { (label: String?, value: Any) in - guard var field = value as? AnyFieldInternal, let key = field.key ?? convertLabelToKey(label) else { + guard let field = value as? AnyFieldInternal, let key = field.key ?? convertLabelToKey(label) else { return nil } - field.options = field.options.union([.keyed(key)]) + field.withOptions { options in + let alreadyKeyed = options.contains { $0.rawValue == FieldOption.keyed("").rawValue } + if !alreadyKeyed { + options.insert(.keyed(key)) + } + } return field } } diff --git a/packages/expo-modules-core/ios/Tests/RecordSpec.swift b/packages/expo-modules-core/ios/Tests/RecordSpec.swift index e0f2884f2c46f8..9a8b48b260382c 100644 --- a/packages/expo-modules-core/ios/Tests/RecordSpec.swift +++ b/packages/expo-modules-core/ios/Tests/RecordSpec.swift @@ -116,5 +116,38 @@ class RecordSpec: ExpoSpec { expect(error).to(beAKindOf(FieldInvalidTypeException.self)) }) } + + it("serializes concurrently on a shared record without crashing") { + struct StressRecord: Record { + @Field var a: String? = nil + @Field var b: String? = nil + @Field var c: String? = nil + } + + let record = StressRecord(a: "a", b: "b", c: "c") + let workers = 16 + let iterations = 100 + let group = DispatchGroup() + let startGate = DispatchSemaphore(value: 0) + + for _ in 0.. Date: Sat, 2 May 2026 01:33:52 +0100 Subject: [PATCH 2/2] chore(tools): upgrade to ESLint 9 (#45088) # Why We should be using a consistent version of ESLint across the monorepo to make it easier for upgrades and eventually deprecating the old (non-flat) ESLint configurations in `eslint-config-universe`. # How Bumped the version of ESLint in `tools/` and migrated the ESLint configuration file to the new flat config style. After doing the above, fixed a handful of minor warnings that started showing up. Additionally, disabled the `import/no-named-as-default` in both the core and TypeScript presets as it doesn't seem to be a pattern we use in `expo/expo`. # Test Plan - CI # Checklist - [x] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [x] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- packages/eslint-config-universe/CHANGELOG.md | 2 + .../flat/shared/core.js | 1 + .../flat/shared/typescript.js | 3 + .../eslint-config-universe/shared/core.js | 1 + .../shared/typescript.js | 3 + pnpm-lock.yaml | 255 +----------------- tools/.eslintrc.js | 12 - tools/bin/expotools.js | 1 - tools/eslint.config.mjs | 20 ++ tools/package.json | 4 +- tools/src/NpmOtp.ts | 1 - .../updateGradleFiles.ts | 2 +- .../check-packages/checkDependenciesAsync.ts | 2 +- tools/src/commands/BumpReactNativeVersion.ts | 1 - tools/src/commands/CheckSdkPrsCommand.ts | 2 +- 15 files changed, 42 insertions(+), 268 deletions(-) delete mode 100644 tools/.eslintrc.js create mode 100644 tools/eslint.config.mjs diff --git a/packages/eslint-config-universe/CHANGELOG.md b/packages/eslint-config-universe/CHANGELOG.md index d72bb34a6c1d20..5a85b97f8820d6 100644 --- a/packages/eslint-config-universe/CHANGELOG.md +++ b/packages/eslint-config-universe/CHANGELOG.md @@ -6,6 +6,8 @@ ### 🎉 New features +- Disabled `import/no-named-as-default` in both core and TypeScript presets ([#45088](https://github.com/expo/expo/pull/45088) by [@hassankhan](https://github.com/hassankhan)) + ### 🐛 Bug fixes ### 💡 Others diff --git a/packages/eslint-config-universe/flat/shared/core.js b/packages/eslint-config-universe/flat/shared/core.js index 0a622f80aa91ba..ed5c53a3bddd7a 100644 --- a/packages/eslint-config-universe/flat/shared/core.js +++ b/packages/eslint-config-universe/flat/shared/core.js @@ -260,6 +260,7 @@ module.exports = defineConfig([ ], 'import/default': 'off', + 'import/no-named-as-default': 'off', 'import/export': 'error', 'import/first': 'warn', diff --git a/packages/eslint-config-universe/flat/shared/typescript.js b/packages/eslint-config-universe/flat/shared/typescript.js index e9431e9bd47897..ddde0f0522e0b6 100644 --- a/packages/eslint-config-universe/flat/shared/typescript.js +++ b/packages/eslint-config-universe/flat/shared/typescript.js @@ -101,6 +101,9 @@ module.exports = defineConfig([ // TODO (Kadi): Enable this. Disabling for now because import/recommended adds it, but we didn't use to have it enabled 'import/no-unresolved': 'off', + + // NOTE(@hassankhan): This is disabled in `core`, but `eslint-plugin-import`'s recommended rules re-enable it + 'import/no-named-as-default': 'off', }, }, ]); diff --git a/packages/eslint-config-universe/shared/core.js b/packages/eslint-config-universe/shared/core.js index f3e09f926b287f..4521b91308208d 100644 --- a/packages/eslint-config-universe/shared/core.js +++ b/packages/eslint-config-universe/shared/core.js @@ -141,6 +141,7 @@ module.exports = { yoda: ['warn', 'never', { exceptRange: true }], 'import/default': 'off', + 'import/no-named-as-default': 'off', 'import/export': 'error', 'import/first': 'warn', 'import/namespace': ['error', { allowComputed: true }], diff --git a/packages/eslint-config-universe/shared/typescript.js b/packages/eslint-config-universe/shared/typescript.js index d09b1b102d8b62..6a9d3a6773677a 100644 --- a/packages/eslint-config-universe/shared/typescript.js +++ b/packages/eslint-config-universe/shared/typescript.js @@ -55,6 +55,9 @@ module.exports = { // The typescript-eslint FAQ recommends turning off "no-undef" in favor of letting tsc check for // undefined variables, including types 'no-undef': 'off', + + // NOTE(@hassankhan): This is disabled in `core`, but `eslint-plugin-import`'s recommended rules re-enable it + 'import/no-named-as-default': 'off', }, settings: { 'import/extensions': allExtensions, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb25da9aae8e1b..5afd278d6c7d2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6164,14 +6164,14 @@ importers: specifier: ^9.0.2 version: 9.0.8 eslint: - specifier: ^8.57.1 - version: 8.57.1 + specifier: ^9.39.4 + version: 9.39.4(jiti@1.21.7) eslint-config-universe: - specifier: ^14.0.0 - version: 14.3.0(@types/eslint@9.6.1)(eslint@8.57.1)(prettier@3.5.3)(typescript@5.9.3) + specifier: workspace:^15.0.3 + version: link:../packages/eslint-config-universe eslint-plugin-lodash: specifier: ^7.4.0 - version: 7.4.0(eslint@8.57.1) + version: 7.4.0(eslint@9.39.4(jiti@1.21.7)) prettier: specifier: ^3.3.3 version: 3.5.3 @@ -10965,15 +10965,6 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-config-universe@14.3.0: - resolution: {integrity: sha512-AQjW1QxwrJPq8CZjTAK7F5q5FeITXcCMwzzoB6pMiDBCEyuUjPE1RbpQNaF/7kpVnxgzgks6rxNwnNIpd50ALg==} - peerDependencies: - eslint: '>=9' - prettier: '>=3' - peerDependenciesMeta: - prettier: - optional: true - eslint-formatter-pretty@4.1.0: resolution: {integrity: sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==} engines: {node: '>=10'} @@ -19756,22 +19747,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - eslint: 8.57.1 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -19788,18 +19763,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.1(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - eslint: 8.57.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.1 @@ -19812,15 +19775,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.57.1(typescript@6.0.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@6.0.3) @@ -19849,26 +19803,10 @@ snapshots: '@typescript-eslint/types': 8.57.1 '@typescript-eslint/visitor-keys': 8.57.1 - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.57.1(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@8.57.1)(typescript@5.9.3) - debug: 4.4.3 - eslint: 8.57.1 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.57.1 @@ -19883,21 +19821,6 @@ snapshots: '@typescript-eslint/types@8.57.1': {} - '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - minimatch: 10.2.4 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.57.1(typescript@6.0.3)': dependencies: '@typescript-eslint/project-service': 8.57.1(typescript@6.0.3) @@ -19913,17 +19836,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.1(eslint@8.57.1)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - eslint: 8.57.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) @@ -21616,46 +21528,15 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-compat-utils@0.5.1(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - semver: 7.7.4 - eslint-compat-utils@0.5.1(eslint@9.39.4(jiti@1.21.7)): dependencies: eslint: 9.39.4(jiti@1.21.7) semver: 7.7.4 - eslint-config-prettier@9.1.2(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@1.21.7)): dependencies: eslint: 9.39.4(jiti@1.21.7) - eslint-config-universe@14.3.0(@types/eslint@9.6.1)(eslint@8.57.1)(prettier@3.5.3)(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': 8.57.1(eslint@8.57.1)(typescript@5.9.3) - eslint: 8.57.1 - eslint-config-prettier: 9.1.2(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) - eslint-plugin-n: 17.24.0(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-node: 11.1.0(eslint@8.57.1) - eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3) - eslint-plugin-react: 7.37.5(eslint@8.57.1) - eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) - globals: 16.5.0 - optionalDependencies: - prettier: 3.5.3 - transitivePeerDependencies: - - '@types/eslint' - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - typescript - eslint-formatter-pretty@4.1.0: dependencies: '@types/eslint': 7.29.0 @@ -21690,16 +21571,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@8.57.1)(typescript@5.9.3) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)): dependencies: debug: 3.2.7 @@ -21711,13 +21582,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@8.57.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.2 - eslint: 8.57.1 - eslint-compat-utils: 0.5.1(eslint@8.57.1) - eslint-plugin-es-x@7.8.0(eslint@9.39.4(jiti@1.21.7)): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) @@ -21725,12 +21589,6 @@ snapshots: eslint: 9.39.4(jiti@1.21.7) eslint-compat-utils: 0.5.1(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-es@3.0.1(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.4(jiti@1.21.7)): dependencies: eslint: 9.39.4(jiti@1.21.7) @@ -21743,35 +21601,6 @@ snapshots: eslint: 9.39.4(jiti@1.21.7) estraverse: 5.3.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.57.1(eslint@8.57.1)(typescript@5.9.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@rtsao/scc': 1.1.0 @@ -21801,26 +21630,11 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-lodash@7.4.0(eslint@8.57.1): + eslint-plugin-lodash@7.4.0(eslint@9.39.4(jiti@1.21.7)): dependencies: - eslint: 8.57.1 + eslint: 9.39.4(jiti@1.21.7) lodash: 4.17.23 - eslint-plugin-n@17.24.0(eslint@8.57.1)(typescript@5.9.3): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - enhanced-resolve: 5.20.1 - eslint: 8.57.1 - eslint-plugin-es-x: 7.8.0(eslint@8.57.1) - get-tsconfig: 4.13.6 - globals: 15.15.0 - globrex: 0.1.2 - ignore: 5.3.2 - semver: 7.7.4 - ts-declaration-location: 1.0.7(typescript@5.9.3) - transitivePeerDependencies: - - typescript - eslint-plugin-n@17.24.0(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) @@ -21836,16 +21650,6 @@ snapshots: transitivePeerDependencies: - typescript - eslint-plugin-node@11.1.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-plugin-es: 3.0.1(eslint@8.57.1) - eslint-utils: 2.1.0 - ignore: 5.3.2 - minimatch: 3.1.5 - resolve: 1.22.11 - semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.4(jiti@1.21.7)): dependencies: eslint: 9.39.4(jiti@1.21.7) @@ -21856,16 +21660,6 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3): - dependencies: - eslint: 8.57.1 - prettier: 3.5.3 - prettier-linter-helpers: 1.0.1 - synckit: 0.11.12 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 9.1.2(eslint@8.57.1) - eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@9.39.4(jiti@1.21.7)))(eslint@9.39.4(jiti@1.21.7))(prettier@3.5.3): dependencies: eslint: 9.39.4(jiti@1.21.7) @@ -21876,36 +21670,10 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 9.1.2(eslint@9.39.4(jiti@1.21.7)) - eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@1.21.7)): dependencies: eslint: 9.39.4(jiti@1.21.7) - eslint-plugin-react@7.37.5(eslint@8.57.1): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.3.1 - eslint: 8.57.1 - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.5 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.6 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@1.21.7)): dependencies: array-includes: 3.1.9 @@ -27080,19 +26848,10 @@ snapshots: trim-newlines@3.0.1: {} - ts-api-utils@2.4.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - ts-api-utils@2.4.0(typescript@6.0.3): dependencies: typescript: 6.0.3 - ts-declaration-location@1.0.7(typescript@5.9.3): - dependencies: - picomatch: 4.0.3 - typescript: 5.9.3 - ts-declaration-location@1.0.7(typescript@6.0.3): dependencies: picomatch: 4.0.3 diff --git a/tools/.eslintrc.js b/tools/.eslintrc.js deleted file mode 100644 index 4435e42674e0f1..00000000000000 --- a/tools/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - root: true, - extends: ['universe/node'], - plugins: ['lodash'], - ignorePatterns: ['**/build', '**/cache', '**/node_modules'], - rules: { - 'lodash/import-scope': [2, 'method'], - // note(simek): I'm not brave enough to touch the RegExps I did not write, - // if you are, feel free to remove line below and fix the reported issues - 'no-useless-escape': 0, - }, -}; diff --git a/tools/bin/expotools.js b/tools/bin/expotools.js index 47108fa641dac2..8c35ba04650fff 100755 --- a/tools/bin/expotools.js +++ b/tools/bin/expotools.js @@ -1,5 +1,4 @@ 'use strict'; -/* eslint-env node */ // This script is just a wrapper around expotools that ensures node modules are installed // and TypeScript files are compiled. To make it work even when node_modules are empty, diff --git a/tools/eslint.config.mjs b/tools/eslint.config.mjs new file mode 100644 index 00000000000000..0bf5780b21517a --- /dev/null +++ b/tools/eslint.config.mjs @@ -0,0 +1,20 @@ +// https://docs.expo.dev/guides/using-eslint/ +import { defineConfig, globalIgnores } from 'eslint/config'; +import universeNodeConfig from 'eslint-config-universe/flat/node.js'; +import lodash from 'eslint-plugin-lodash'; + +export default defineConfig([ + globalIgnores(['**/build', '**/cache', '**/node_modules']), + universeNodeConfig, + { + plugins: { + lodash, + }, + rules: { + 'lodash/import-scope': [2, 'method'], + // note(simek): I'm not brave enough to touch the RegExps I did not write, + // if you are, feel free to remove line below and fix the reported issues + 'no-useless-escape': 0, + }, + }, +]); diff --git a/tools/package.json b/tools/package.json index 7f119180bb4408..c2e57c19bf650a 100644 --- a/tools/package.json +++ b/tools/package.json @@ -86,8 +86,8 @@ "@types/npm-registry-fetch": "^8.0.7", "@types/semver": "^7.5.8", "@types/uuid": "^9.0.2", - "eslint": "^8.57.1", - "eslint-config-universe": "^14.0.0", + "eslint": "^9.39.4", + "eslint-config-universe": "workspace:^15.0.3", "eslint-plugin-lodash": "^7.4.0", "prettier": "^3.3.3", "taskr": "1.1.0", diff --git a/tools/src/NpmOtp.ts b/tools/src/NpmOtp.ts index a1e678867534d1..6f67df28de17be 100644 --- a/tools/src/NpmOtp.ts +++ b/tools/src/NpmOtp.ts @@ -30,7 +30,6 @@ export async function promptOtp(): Promise { * succeeds or a non-OTP error is thrown. */ export async function withOtpRetry(fn: () => Promise): Promise { - // eslint-disable-next-line no-constant-condition while (true) { try { await fn(); diff --git a/tools/src/android-update-native-dependencies/updateGradleFiles.ts b/tools/src/android-update-native-dependencies/updateGradleFiles.ts index 826dc6b67b6db1..63e727016d9426 100644 --- a/tools/src/android-update-native-dependencies/updateGradleFiles.ts +++ b/tools/src/android-update-native-dependencies/updateGradleFiles.ts @@ -47,7 +47,7 @@ function replaceVersionInGradleFile( }); // val PUBG_API_WRAPPER by extra("0.8.1") - // eslint-disable-next-line no-useless-escape + const regexKotlinValExtra = new RegExp(`${variableName}.+\(("|')${oldVersion}("|')\)`); regexKotlinValExtra .exec(modifiedBody) diff --git a/tools/src/check-packages/checkDependenciesAsync.ts b/tools/src/check-packages/checkDependenciesAsync.ts index ec641f67fac5c9..5e635d7de027b2 100644 --- a/tools/src/check-packages/checkDependenciesAsync.ts +++ b/tools/src/check-packages/checkDependenciesAsync.ts @@ -13,7 +13,7 @@ import { DependencyKind, type PackageDependency, type Package } from '../Package type PackageCheckType = ActionOptions['checkPackageType']; -/** The three levels of of which dangerous dependencies are allowed. +/** The three levels of which dangerous dependencies are allowed. * @remarks * We can configure selectively invalid dependencies to be allowed in `SPECIAL_DEPENDENCIES` below. * - `types-only` means we allow any type-only import diff --git a/tools/src/commands/BumpReactNativeVersion.ts b/tools/src/commands/BumpReactNativeVersion.ts index f96ad1b8a1f0b8..b00411e2e2e3c7 100644 --- a/tools/src/commands/BumpReactNativeVersion.ts +++ b/tools/src/commands/BumpReactNativeVersion.ts @@ -158,7 +158,6 @@ async function updateBundledNativeModules(newVersion: string): Promise { async function podInstallAsync(cwd: string): Promise { const depsToUpdate = new Set(); - // eslint-disable-next-line no-constant-condition while (true) { try { if (depsToUpdate.size > 0) { diff --git a/tools/src/commands/CheckSdkPrsCommand.ts b/tools/src/commands/CheckSdkPrsCommand.ts index 5a7ae75c1a23e5..f282e32b5b0a30 100644 --- a/tools/src/commands/CheckSdkPrsCommand.ts +++ b/tools/src/commands/CheckSdkPrsCommand.ts @@ -147,7 +147,7 @@ function formatPublishInfo(info: PublishInfo): string { } // Matches SGR codes (\x1b[...m) and OSC 8 hyperlinks (\x1b]8;;...\x07) -// eslint-disable-next-line no-control-regex + const ANSI_RE = /\x1b\[[0-9;]*m|\x1b\]8;;[^\x07]*\x07/g; function pad(str: string, width: number): string {