expo-modules-core 56.0.14 → 56.0.15

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,19 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.15 — 2026-06-05
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Propagate async-function promise construction failures instead of trapping the app. ([#46106](https://github.com/expo/expo/issues/46106) by [@qutrek](https://github.com/qutrek)) ([#46145](https://github.com/expo/expo/pull/46145) by [@mvincentong](https://github.com/mvincentong))
18
+ - [iOS] Read iPad-specific supported orientations from `UISupportedInterfaceOrientations~ipad`. ([#46281](https://github.com/expo/expo/issues/46281) by [@bryandent](https://github.com/bryandent)) ([#46306](https://github.com/expo/expo/pull/46306) by [@mvincentong](https://github.com/mvincentong))
19
+ - [android] Fix nested `Host` double-composing children. ([#46304](https://github.com/expo/expo/pull/46304) by [@nishan](https://github.com/intergalacticspacehighway))
20
+ - [iOS] Throw an actionable error when a worklet is used but `react-native-worklets`'s native adapter isn't linked, instead of a misleading "not an instance of Worklet" failure. ([#46571](https://github.com/expo/expo/pull/46571) by [@chrfalch](https://github.com/chrfalch))
21
+
22
+ ### 💡 Others
23
+
24
+ - Native view config attributes now carry a `process` function that unwraps shared objects to their registry id, so callers can pass shared objects directly as view props instead of unwrapping them manually. ([#46212](https://github.com/expo/expo/pull/46212) by [@tsapeta](https://github.com/tsapeta))
25
+
13
26
  ## 56.0.14 — 2026-05-29
14
27
 
15
28
  _This version does not introduce any user-facing changes._
@@ -20,10 +33,6 @@ _This version does not introduce any user-facing changes._
20
33
 
21
34
  - [Android] Create Compose props without View. ([#46256](https://github.com/expo/expo/pull/46256) by [@jakex7](https://github.com/jakex7))
22
35
 
23
- ### 💡 Others
24
-
25
- - Native view config attributes now carry a `process` function that unwraps shared objects to their registry id, so callers can pass shared objects directly as view props instead of unwrapping them manually. ([#46212](https://github.com/expo/expo/pull/46212) by [@tsapeta](https://github.com/tsapeta))
26
-
27
36
  ## 56.0.12 — 2026-05-21
28
37
 
29
38
  ### 🐛 Bug fixes
@@ -119,7 +119,7 @@ Pod::Spec.new do |s|
119
119
  s.static_framework = true
120
120
  s.header_dir = 'ExpoModulesCore'
121
121
  s.source_files = 'ios/**/*.{h,m,mm,swift,cpp}', 'common/cpp/**/*.{h,cpp}'
122
- s.exclude_files = ['ios/Tests', 'ios/Worklets', 'ios/WorkletsAdapter']
122
+ s.exclude_files = ['ios/Tests', 'ios/Worklets', 'ios/WorkletsTests', 'ios/WorkletsAdapter']
123
123
  s.compiler_flags = compiler_flags
124
124
  s.private_header_files = ['ios/**/*+Private.h', 'ios/**/Swift.h']
125
125
  end
@@ -31,4 +31,11 @@ Pod::Spec.new do |s|
31
31
 
32
32
  s.source_files = 'ios/Worklets/**/*.{h,m,mm,swift,cpp}'
33
33
  s.private_header_files = 'ios/Worklets/**/*+Private.h'
34
+
35
+ s.test_spec 'Tests' do |test_spec|
36
+ # ExpoModulesCore requires React-hermes or React-jsc in tests, add ExpoModulesTestCore for the underlying dependencies
37
+ test_spec.dependency 'ExpoModulesTestCore'
38
+
39
+ test_spec.source_files = 'ios/WorkletsTests/**/*.{m,swift}'
40
+ end
34
41
  end
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '56.0.14'
30
+ version = '56.0.15'
31
31
 
32
32
  def isExpoModulesCoreTests = {
33
33
  Gradle gradle = getGradle()
@@ -94,7 +94,7 @@ android {
94
94
  defaultConfig {
95
95
  consumerProguardFiles 'proguard-rules.pro'
96
96
  versionCode 1
97
- versionName "56.0.14"
97
+ versionName "56.0.15"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -128,6 +128,8 @@ abstract class ExpoComposeView<T : ComposeProps>(
128
128
  recomposeScope = currentRecomposeScope
129
129
  for (index in 0..<this.size) {
130
130
  val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
131
+ // Hosting children render themselves via their own ComposeView; skip to avoid double-rendering.
132
+ if (child.shouldUseAndroidLayout) continue
131
133
  key(child) {
132
134
  with(composableScope ?: ComposableScope()) {
133
135
  with(child) {
@@ -143,6 +145,7 @@ abstract class ExpoComposeView<T : ComposeProps>(
143
145
  recomposeScope = currentRecomposeScope
144
146
  for (index in 0..<this.size) {
145
147
  val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
148
+ if (child.shouldUseAndroidLayout) continue
146
149
  if (!filter(child)) {
147
150
  continue
148
151
  }
@@ -160,6 +163,7 @@ abstract class ExpoComposeView<T : ComposeProps>(
160
163
  fun Child(composableScope: ComposableScope, index: Int) {
161
164
  recomposeScope = currentRecomposeScope
162
165
  val child = getChildAt(index) as? ExpoComposeView<*> ?: return
166
+ if (child.shouldUseAndroidLayout) return
163
167
  key(child) {
164
168
  with(composableScope) {
165
169
  with(child) {
@@ -462,9 +462,7 @@ public class ExpoAppDelegateSubscriberManager: NSObject {
462
462
  */
463
463
  @objc
464
464
  public static func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
465
- let deviceOrientationMask = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
466
- let universalOrientationMask = allowedOrientations(for: .unspecified)
467
- let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
465
+ let infoPlistOrientations = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
468
466
 
469
467
  let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
470
468
  $0.responds(to: #selector(UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:)))
@@ -483,14 +481,28 @@ public class ExpoAppDelegateSubscriberManager: NSObject {
483
481
  }
484
482
 
485
483
  #if os(iOS)
486
- private func allowedOrientations(for userInterfaceIdiom: UIUserInterfaceIdiom) -> UIInterfaceOrientationMask {
487
- // For now only iPad-specific orientations are supported
488
- let deviceString = userInterfaceIdiom == .pad ? "~pad" : ""
489
- var mask: UIInterfaceOrientationMask = []
490
- guard let orientations = Bundle.main.infoDictionary?["UISupportedInterfaceOrientations\(deviceString)"] as? [String] else {
484
+ func allowedOrientations(
485
+ for userInterfaceIdiom: UIUserInterfaceIdiom,
486
+ infoDictionary: [String: Any]? = Bundle.main.infoDictionary
487
+ ) -> UIInterfaceOrientationMask {
488
+ // For now only iPad-specific orientations are supported. When the `~ipad` key is absent,
489
+ // fall back to the universal `UISupportedInterfaceOrientations` key, matching how UIKit
490
+ // resolves device-specific keys. We additionally fall back when `~ipad` is present but
491
+ // resolves to no orientations, treating a malformed entry as if it were absent.
492
+ if userInterfaceIdiom == .pad,
493
+ let mask = orientationMask(forKey: "UISupportedInterfaceOrientations~ipad", in: infoDictionary),
494
+ !mask.isEmpty {
491
495
  return mask
492
496
  }
497
+ return orientationMask(forKey: "UISupportedInterfaceOrientations", in: infoDictionary) ?? []
498
+ }
493
499
 
500
+ private func orientationMask(forKey key: String, in infoDictionary: [String: Any]?) -> UIInterfaceOrientationMask? {
501
+ guard let orientations = infoDictionary?[key] as? [String] else {
502
+ return nil
503
+ }
504
+
505
+ var mask: UIInterfaceOrientationMask = []
494
506
  for orientation in orientations {
495
507
  switch orientation {
496
508
  case "UIInterfaceOrientationPortrait":
@@ -529,8 +529,8 @@ public final class AppContext: NSObject, EXAppContextProtocol, @unchecked Sendab
529
529
  */
530
530
  @JavaScriptActor
531
531
  private func installModuleClasses(in runtime: JavaScriptRuntime) throws {
532
- let coreObject = runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
533
- let sharedObjectClass = coreObject.getPropertyAsObject("SharedObject")
532
+ let coreObject = try runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
533
+ let sharedObjectClass = try coreObject.getPropertyAsObject("SharedObject")
534
534
  let sharedObjectBaseProto = sharedObjectClass.getProperty("prototype")
535
535
 
536
536
  // Stored as JavaScriptValue (a class) because JavaScriptObject is ~Copyable
@@ -218,7 +218,7 @@ public struct Conversions {
218
218
  memcpy(arrayBuffer.data(), baseAddress, data.count)
219
219
  }
220
220
  }
221
- let uint8ArrayCtor = runtime.global().getPropertyAsFunction("Uint8Array")
221
+ let uint8ArrayCtor = try runtime.global().getPropertyAsFunction("Uint8Array")
222
222
  return try uint8ArrayCtor.callAsConstructor(arrayBuffer.asValue())
223
223
  }
224
224
 
@@ -95,7 +95,7 @@ private func getBaseSharedType(_ appContext: AppContext, nativeType: AnySharedOb
95
95
 
96
96
  private func newBaseSharedObject(_ appContext: AppContext, nativeType: AnySharedObject.Type) throws -> JavaScriptObject? {
97
97
  let jsClass = try getBaseSharedType(appContext, nativeType: nativeType)
98
- let prototype = jsClass.asObject().getPropertyAsObject("prototype")
98
+ let prototype = try jsClass.asObject().getPropertyAsObject("prototype")
99
99
  return try appContext.runtime.createObject(prototype: prototype)
100
100
  }
101
101
 
@@ -186,7 +186,7 @@ public class AsyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnyAsyncFu
186
186
  guard let appContext else {
187
187
  throw Exceptions.AppContextLost()
188
188
  }
189
- let promise = JavaScriptPromise(try appContext.runtime)
189
+ let promise = try JavaScriptPromise(appContext.runtime)
190
190
  let promiseValue = promise.asValue()
191
191
 
192
192
  self.call(appContext, this: this, arguments: arguments) { [promise] result in
@@ -209,5 +209,3 @@ public class AsyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnyAsyncFu
209
209
  return self
210
210
  }
211
211
  }
212
-
213
-
@@ -30,7 +30,7 @@ internal class ExpoRuntimeInstaller: EXJavaScriptRuntimeManager {
30
30
  */
31
31
  @JavaScriptActor
32
32
  internal func installExpoModulesHostObject() throws {
33
- let coreObject = runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
33
+ let coreObject = try runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
34
34
 
35
35
  if coreObject.hasProperty("modules") {
36
36
  // Host object already installed
@@ -16,6 +16,7 @@
16
16
  @compatibility_alias UIWindow NSWindow;
17
17
  @compatibility_alias UIHostingController NSHostingController;
18
18
  @compatibility_alias UIImage NSImage;
19
+ @compatibility_alias UIImageView NSImageView;
19
20
 
20
21
  #ifndef UIApplication
21
22
  @compatibility_alias UIApplication NSApplication;
@@ -14,6 +14,7 @@ public typealias UIHostingController = NSHostingController
14
14
  public typealias UIViewRepresentable = NSViewRepresentable
15
15
  public typealias UILabel = NSLabel
16
16
  public typealias UIImage = NSImage
17
+ public typealias UIImageView = NSImageView
17
18
  public typealias UIPasteboard = NSPasteboard
18
19
 
19
20
  extension UIApplication {
@@ -28,6 +28,10 @@ internal struct DynamicSerializableType: AnyDynamicType {
28
28
  }
29
29
  }
30
30
  guard let jsSerializable else {
31
+ // Adapter not linked → the extractor returns nil for every value; give an actionable error.
32
+ if WorkletsProviderRegistry.shared == nil {
33
+ throw WorkletsNotInstalledException()
34
+ }
31
35
  throw NotSerializableException(innerType)
32
36
  }
33
37
  return Serializable(jsSerializable)
@@ -46,3 +46,13 @@ internal final class NotWorkletException: GenericException<SerializableValueType
46
46
  "Expected Serializable of type Worklet but got \(param)"
47
47
  }
48
48
  }
49
+
50
+ /// Thrown when a worklet operation is attempted but the `react-native-worklets` native
51
+ /// adapter isn't linked (its provider is `nil`). Replaces misleading lower-level errors.
52
+ internal final class WorkletsNotInstalledException: Exception, @unchecked Sendable {
53
+ override var reason: String {
54
+ "This feature requires `react-native-worklets`, but its native adapter isn't linked — usually " +
55
+ "because the package isn't installed (it's an optional peer dependency of libraries like Expo UI " +
56
+ "and Reanimated). Install it with `npx expo install react-native-worklets`, then rebuild the app."
57
+ }
58
+ }
@@ -0,0 +1,27 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import Testing
4
+
5
+ import ExpoModulesCore
6
+ import ExpoModulesWorklets
7
+
8
+ /**
9
+ Runtime-level checks for the worklets integration wiring.
10
+
11
+ Note: these run under `use_expo_modules_tests!`, which does not link companion pods, so the
12
+ worklets adapter — and therefore `WorkletsProviderRegistry.shared` — is intentionally absent
13
+ here (the same state as an app without `react-native-worklets`). Whether the adapter is
14
+ correctly autolinked is guarded by the JS test `WorkletsAdapterAutolinking-test.ts`; the
15
+ consumer-side behaviour when it's missing is covered by `WorkletsNotInstalledTests`.
16
+ */
17
+ @Suite("WorkletRuntimeIntegration", .serialized)
18
+ struct WorkletRuntimeIntegrationTests {
19
+ @Test
20
+ func `UI worklet runtime factory registers on the app context`() {
21
+ // `WorkletIntegration.register()` installs the core-side hook (normally invoked via +load)
22
+ // that turns a worklet runtime pointer into a usable runtime. Guards the factory wiring onto
23
+ // AppContext; it does not require the adapter to be linked.
24
+ WorkletIntegration.register()
25
+ #expect(AppContext.uiRuntimeFactory != nil)
26
+ }
27
+ }
@@ -0,0 +1,33 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ import Testing
4
+
5
+ @testable import ExpoModulesCore
6
+ @testable import ExpoModulesWorklets
7
+
8
+ /**
9
+ The `use_expo_modules_tests!` harness does not link companion pods, so the worklets adapter
10
+ never registers `WorkletsProviderRegistry.shared`. That nil-provider state is exactly an app
11
+ where `react-native-worklets` isn't installed — so this is the right place to assert that a
12
+ worklet operation fails with the actionable `WorkletsNotInstalledException` rather than a
13
+ misleading lower-level error (e.g. "not an instance of Worklet").
14
+ */
15
+ @Suite("WorkletsNotInstalled", .serialized)
16
+ @JavaScriptActor
17
+ struct WorkletsNotInstalledTests {
18
+ @Test
19
+ func `converting a worklet argument reports that worklets are not installed`() throws {
20
+ // Only meaningful while the adapter is unlinked (the default in this test harness).
21
+ try #require(WorkletsProviderRegistry.shared == nil)
22
+
23
+ let appContext = AppContext.create()
24
+ let runtime = try appContext.runtime
25
+ // Any JS value works — with the adapter unlinked, extraction returns nil for everything.
26
+ let jsValue = try runtime.eval("42")
27
+ let dynamicType = DynamicSerializableType(innerType: Worklet.self)
28
+
29
+ #expect(throws: WorkletsNotInstalledException.self) {
30
+ _ = try dynamicType.cast(jsValue: jsValue, appContext: appContext)
31
+ }
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "56.0.14",
3
+ "version": "56.0.15",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@expo/expo-modules-macros-plugin": "~0.0.9",
50
- "expo-modules-jsi": "~56.0.7",
50
+ "expo-modules-jsi": "~56.0.8",
51
51
  "invariant": "^2.2.4"
52
52
  },
53
53
  "peerDependencies": {
@@ -64,9 +64,9 @@
64
64
  "@testing-library/react-native": "^13.3.0",
65
65
  "@types/react": "~19.2.0",
66
66
  "@types/invariant": "^2.2.33",
67
- "expo-module-scripts": "56.0.2"
67
+ "expo-module-scripts": "56.0.3"
68
68
  },
69
- "gitHead": "a7adc95c1747db1e92655feba56d0e62660098db",
69
+ "gitHead": "175f1e78e3444ca99ddea473faea6777a0656668",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",
package/spm.config.json CHANGED
@@ -46,6 +46,7 @@
46
46
  "headerPattern": "**/*.h",
47
47
  "exclude": [
48
48
  "Tests/**",
49
+ "WorkletsTests/**",
49
50
  "Worklets/**",
50
51
  "WorkletsAdapter/**"
51
52
  ],
@@ -87,6 +88,7 @@
87
88
  "exclude": [
88
89
  "JSI/**",
89
90
  "Tests/**",
91
+ "WorkletsTests/**",
90
92
  "BridgeModule/**",
91
93
  "Views/**",
92
94
  "Worklets/**",