expo-modules-core 55.0.17 → 55.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 55.0.18 — 2026-03-27
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fixed adding SwiftUI views to tabs BottomAccessory. ([#44247](https://github.com/expo/expo/pull/44247) by [@t0maboro](https://github.com/t0maboro))
18
+ - [iOS] Fix crash that can occur when reloading JavaScript bundle due to object deallocating off the JavaScript thread. ([#43937](https://github.com/expo/expo/pull/43937) by [@cltnschlosser](https://github.com/cltnschlosser))
19
+ - [iOS] Fixed runtime setup crashing in the old bridge module. ([#44121](https://github.com/expo/expo/pull/44121) by [@tsapeta](https://github.com/tsapeta))
20
+ - [iOS] Make access to module registry thread safe. ([#44042](https://github.com/expo/expo/pull/44042) by [@alanjhughes](https://github.com/alanjhughes))
21
+ - [iOS] Fixed `PersistentFileLog.readEntries` race condition by serializing reads on the dispatch queue. ([#43958](https://github.com/expo/expo/pull/43958) by [@ramonclaudio](https://github.com/ramonclaudio))
22
+
13
23
  ## 55.0.17 — 2026-03-18
14
24
 
15
25
  _This version does not introduce any user-facing changes._
@@ -24,8 +34,6 @@ _This version does not introduce any user-facing changes._
24
34
 
25
35
  - [iOS] Fixed compilation issues due to missing fallthrough case in `EXJavaScriptSerializable`. ([#43634](https://github.com/expo/expo/pull/43634) by [@tjzel](https://github.com/tjzel))
26
36
 
27
- ## 55.0.15 — 2026-03-11
28
-
29
37
  ### 💡 Others
30
38
 
31
39
  - [iOS] Add `ViewWrapper` protocol and `AnyContentViewProvider` for type-erased access to wrapped SwiftUI views. ([#43669](https://github.com/expo/expo/pull/43669) by [@nishan](https://github.com/intergalacticspacehighway))
@@ -29,7 +29,7 @@ if (shouldIncludeCompose) {
29
29
  }
30
30
 
31
31
  group = 'host.exp.exponent'
32
- version = '55.0.17'
32
+ version = '55.0.18'
33
33
 
34
34
  def isExpoModulesCoreTests = {
35
35
  Gradle gradle = getGradle()
@@ -96,7 +96,7 @@ android {
96
96
  defaultConfig {
97
97
  consumerProguardFiles 'proguard-rules.pro'
98
98
  versionCode 1
99
- versionName "55.0.17"
99
+ versionName "55.0.18"
100
100
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
101
101
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
102
102
 
@@ -36,7 +36,6 @@ RCT_EXPORT_MODULE(ExpoModulesCore);
36
36
  - (void)setBridge:(RCTBridge *)bridge
37
37
  {
38
38
  _bridge = bridge;
39
- [self maybeSetupAppContext];
40
39
  }
41
40
 
42
41
  - (void)maybeSetupAppContext
@@ -46,10 +45,15 @@ RCT_EXPORT_MODULE(ExpoModulesCore);
46
45
  }
47
46
  EXRuntime *runtime = [EXJavaScriptRuntimeManager runtimeFromBridge:_bridge];
48
47
 
48
+ if (!runtime) {
49
+ NSLog(@"Unable to get the JSI runtime from the bridge instance, make sure to use ExpoReactNativeFactory for newer bridgeless integration");
50
+ return;
51
+ }
52
+
49
53
  // If `global.expo` is defined, the app context has already been initialized from `ExpoReactNativeFactory`.
50
54
  // The factory was introduced in SDK 55 and requires migration in bare workflow projects.
51
55
  // We keep this as an alternative way during the transitional period.
52
- if (runtime && ![[runtime global] hasProperty:@"expo"]) {
56
+ if (![[runtime global] hasProperty:@"expo"]) {
53
57
  NSLog(@"Expo is being initialized from the deprecated ExpoBridgeModule, make sure to migrate to ExpoReactNativeFactory in your project");
54
58
 
55
59
  _appContext.reactBridge = _bridge;
@@ -43,10 +43,12 @@ public class PersistentFileLog {
43
43
  Read entries from log file
44
44
  */
45
45
  public func readEntries() -> [String] {
46
- if getFileSize() == 0 {
47
- return []
46
+ return PersistentFileLog.serialQueue.sync {
47
+ if getFileSize() == 0 {
48
+ return []
49
+ }
50
+ return (try? self.readFileSync()) ?? []
48
51
  }
49
- return (try? self.readFileSync()) ?? []
50
52
  }
51
53
 
52
54
  /**
@@ -5,6 +5,7 @@ public final class ModuleRegistry: Sequence {
5
5
 
6
6
  private var registry: [String: ModuleHolder] = [:]
7
7
  private var overrideDisallowModules = Set<String>()
8
+ private let lock = NSLock()
8
9
 
9
10
  init(appContext: AppContext) {
10
11
  self.appContext = appContext
@@ -16,6 +17,8 @@ public final class ModuleRegistry: Sequence {
16
17
  internal func register(holder: ModuleHolder, preventModuleOverriding: Bool = false) {
17
18
  log.info("Registering module '\(holder.name)'")
18
19
 
20
+ lock.lock()
21
+ defer { lock.unlock() }
19
22
  // if overriding is disallowed for this module and the module already registered, don't re-register
20
23
  if overrideDisallowModules.contains(holder.name) && registry[holder.name] != nil {
21
24
  log.info("Not re-registering module '\(holder.name)' since a previous registration specified preventModuleOverriding: true")
@@ -63,45 +66,61 @@ public final class ModuleRegistry: Sequence {
63
66
  Unregisters given module from the registry.
64
67
  */
65
68
  public func unregister(module: AnyModule) {
66
- if let index = registry.firstIndex(where: { $1.module === module }) {
67
- registry.remove(at: index)
69
+ withLock {
70
+ if let index = registry.firstIndex(where: { $1.module === module }) {
71
+ registry.remove(at: index)
72
+ }
68
73
  }
69
74
  }
70
75
 
71
76
  public func unregister(moduleName: String) {
72
- if registry[moduleName] != nil {
73
- log.info("Unregistering module '\(moduleName)'")
74
- registry[moduleName] = nil
77
+ withLock {
78
+ if registry[moduleName] != nil {
79
+ log.info("Unregistering module '\(moduleName)'")
80
+ registry[moduleName] = nil
81
+ }
75
82
  }
76
83
  }
77
84
 
78
85
  public func has(moduleWithName moduleName: String) -> Bool {
79
- return registry[moduleName] != nil
86
+ return withLock {
87
+ return registry[moduleName] != nil
88
+ }
80
89
  }
81
90
 
82
91
  public func get(moduleHolderForName moduleName: String) -> ModuleHolder? {
83
- return registry[moduleName]
92
+ return withLock {
93
+ return registry[moduleName]
94
+ }
84
95
  }
85
96
 
86
97
  public func get(moduleWithName moduleName: String) -> AnyModule? {
87
- return registry[moduleName]?.module
98
+ return withLock {
99
+ registry[moduleName]?.module
100
+ }
88
101
  }
89
102
 
90
103
  internal func getModule<ModuleProtocol>(implementing protocol: ModuleProtocol.Type) -> ModuleProtocol? {
91
- for holder in registry.values {
92
- if let module = holder.module as? ModuleProtocol {
93
- return module
104
+ return withLock {
105
+ for holder in registry.values {
106
+ if let module = holder.module as? ModuleProtocol {
107
+ return module
108
+ }
94
109
  }
110
+ return nil
95
111
  }
96
- return nil
97
112
  }
98
113
 
99
114
  public func getModuleNames() -> [String] {
100
- return Array(registry.keys)
115
+ return withLock {
116
+ return Array(registry.keys)
117
+ }
101
118
  }
102
119
 
103
120
  public func makeIterator() -> IndexingIterator<[ModuleHolder]> {
104
- return registry.map({ $1 }).makeIterator()
121
+ return withLock {
122
+ return registry.map({ $1 }).makeIterator()
123
+ }
105
124
  }
106
125
 
107
126
  internal func post(event: EventName) {
@@ -117,4 +136,10 @@ public final class ModuleRegistry: Sequence {
117
136
  holder.post(event: event, payload: payload)
118
137
  }
119
138
  }
139
+
140
+ private func withLock<T>(block: () -> T) -> T {
141
+ lock.lock()
142
+ defer { lock.unlock() }
143
+ return block()
144
+ }
120
145
  }
@@ -29,23 +29,23 @@ public final class SharedObjectRegistry {
29
29
  The counter of IDs to assign to the shared object pairs.
30
30
  The next pair added to the registry will be saved using this ID.
31
31
  */
32
- internal var nextId: SharedObjectId = 1
32
+ internal /* visible for testing */ var nextId: SharedObjectId = 1
33
33
 
34
34
  /**
35
35
  A dictionary of shared object pairs.
36
36
  */
37
- internal var pairs = [SharedObjectId: SharedObjectPair]()
37
+ private var pairs = [SharedObjectId: SharedObjectPair]()
38
38
 
39
39
  /**
40
- The lock queue to keep thread safety for internal data structures.
40
+ The lock to keep thread safety for internal data structures.
41
41
  */
42
- private static let lockQueue = DispatchQueue(label: "expo.modules.core.SharedObjectRegistry")
42
+ private let lock = NSLock()
43
43
 
44
44
  /**
45
45
  A number of all pairs stored in the registry.
46
46
  */
47
47
  internal var size: Int {
48
- return Self.lockQueue.sync {
48
+ return lock.withLock {
49
49
  return pairs.count
50
50
  }
51
51
  }
@@ -69,7 +69,7 @@ public final class SharedObjectRegistry {
69
69
  */
70
70
  @discardableResult
71
71
  internal func pullNextId() -> SharedObjectId {
72
- return Self.lockQueue.sync {
72
+ return lock.withLock {
73
73
  let id = nextId
74
74
  nextId += 1
75
75
  return id
@@ -80,7 +80,7 @@ public final class SharedObjectRegistry {
80
80
  Returns a pair of shared objects with given ID or `nil` when there is no such pair in the registry.
81
81
  */
82
82
  internal func get(_ id: SharedObjectId) -> SharedObjectPair? {
83
- return Self.lockQueue.sync {
83
+ return lock.withLock {
84
84
  return pairs[id]
85
85
  }
86
86
  }
@@ -117,7 +117,7 @@ public final class SharedObjectRegistry {
117
117
 
118
118
  // Save the pair in the dictionary.
119
119
  let jsWeakObject = jsObject.createWeak()
120
- Self.lockQueue.async {
120
+ lock.withLock {
121
121
  self.pairs[id] = (native: nativeObject, javaScript: jsWeakObject)
122
122
  }
123
123
 
@@ -128,7 +128,7 @@ public final class SharedObjectRegistry {
128
128
  Deletes the shared objects pair with a given ID.
129
129
  */
130
130
  internal func delete(_ id: SharedObjectId) {
131
- Self.lockQueue.async {
131
+ lock.withLock {
132
132
  if let pair = self.pairs[id] {
133
133
  pair.native.sharedObjectWillRelease()
134
134
  // Reset an ID on the objects.
@@ -146,7 +146,7 @@ public final class SharedObjectRegistry {
146
146
  */
147
147
  internal func toNativeObject(_ jsObject: JavaScriptObject) -> SharedObject? {
148
148
  if let objectId = try? jsObject.getProperty(sharedObjectIdPropertyName).asInt() {
149
- return Self.lockQueue.sync {
149
+ return lock.withLock {
150
150
  return pairs[objectId]?.native
151
151
  }
152
152
  }
@@ -158,7 +158,7 @@ public final class SharedObjectRegistry {
158
158
  */
159
159
  internal func toJavaScriptObject(_ nativeObject: SharedObject) -> JavaScriptObject? {
160
160
  let objectId = nativeObject.sharedObjectId
161
- return Self.lockQueue.sync {
161
+ return lock.withLock {
162
162
  return pairs[objectId]?.javaScript.lock()
163
163
  }
164
164
  }
@@ -184,7 +184,7 @@ public final class SharedObjectRegistry {
184
184
  }
185
185
 
186
186
  internal func clear() {
187
- Self.lockQueue.async {
187
+ lock.withLock {
188
188
  self.pairs.removeAll()
189
189
  }
190
190
  }
@@ -224,9 +224,10 @@ extension ExpoSwiftUI {
224
224
 
225
225
  if window != nil, let parentController = reactViewController() {
226
226
  #if !os(macOS)
227
- if parentController as? UINavigationController == nil {
227
+ if parentController as? UINavigationController == nil && parentController as? UITabBarController == nil {
228
228
  // Swift automatically adds the hostingController in the correct place when the parentController
229
- // is UINavigationController, since it's children are supposed to be only screens
229
+ // is UINavigationController, since it's children are supposed to be only screens.
230
+ // Similarly, for UITabBarController we expect its children to be only tabs.
230
231
  parentController.addChild(hostingController)
231
232
  }
232
233
  #else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "55.0.17",
3
+ "version": "55.0.18",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -66,5 +66,5 @@
66
66
  "@testing-library/react-native": "^13.3.0",
67
67
  "expo-module-scripts": "^55.0.2"
68
68
  },
69
- "gitHead": "31afdbe5613d666148711cb3ec58c4b617a00c14"
69
+ "gitHead": "f63217deac00dcd278f75d9846933c11e5c6f9a3"
70
70
  }