expo-modules-core 58.0.12 → 58.0.13

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
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 58.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - [macOS] Attach SwiftUI hosting views even when no `NSViewController` is in the responder chain, such as a React root view set directly as `NSWindow.contentView`. Previously, `@expo/ui` content in such windows rendered nothing. ([#50995](https://github.com/expo/expo/pull/50995) by [@gabrieldonadel](https://github.com/gabrieldonadel))
8
+ - [iOS] Update `expo-modules-macros` to 0.15.0, which generates only the unowned decode for `@Union` and skips cases whose `decodableKinds` can't match. ([#50894](https://github.com/expo/expo/pull/50894) by [@tsapeta](https://github.com/tsapeta))
9
+ - [Internal] Make `AppContext` non-final, so that `TestAppContext` from `expo-modules-test-core` can subclass it. ([#51015](https://github.com/expo/expo/pull/51015) by [@tsapeta](https://github.com/tsapeta))
10
+ - [Android] Fixed a race where a promise resolved and rejected from different threads at the same time could settle twice and throw on the JavaScript thread. ([#51052](https://github.com/expo/expo/pull/51052) by [@alanjhughes](https://github.com/alanjhughes))
11
+ - [Android] An Expo Modules API 2.0 module that throws a `CodedException` now reports its `code` to JavaScript. ([#51036](https://github.com/expo/expo/pull/51036) by [@lukmccall](https://github.com/lukmccall))
12
+ - Updated dependencies. ([#50859](https://github.com/expo/expo/pull/50859), [#51015](https://github.com/expo/expo/pull/51015), [#51004](https://github.com/expo/expo/pull/51004), [#51040](https://github.com/expo/expo/pull/51040), [#51055](https://github.com/expo/expo/pull/51055))
13
+ - expo-modules-jsi@58.0.9
14
+
3
15
  ## 58.0.12
4
16
 
5
17
  ### Patch Changes
@@ -46,7 +46,7 @@ if (shouldIncludeCompose) {
46
46
  }
47
47
 
48
48
  group = 'host.exp.exponent'
49
- version = '58.0.12'
49
+ version = '58.0.13'
50
50
 
51
51
  def isExpoModulesCoreTests = {
52
52
  Gradle gradle = getGradle()
@@ -97,7 +97,7 @@ if (workletsProject != null) {
97
97
 
98
98
  def reactNativeWorkletsRootDir = workletsProject?.projectDir?.parentFile
99
99
 
100
- def expoModulesV2Version = "0.2.2"
100
+ def expoModulesV2Version = "0.2.4"
101
101
 
102
102
  configurations {
103
103
  // Artifact-only: the C++ sources, not something to put on any classpath.
@@ -175,7 +175,7 @@ android {
175
175
  defaultConfig {
176
176
  consumerProguardFiles 'proguard-rules.pro'
177
177
  versionCode 1
178
- versionName "58.0.12"
178
+ versionName "58.0.13"
179
179
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
180
180
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
181
181
 
@@ -27,6 +27,8 @@ target_link_libraries(
27
27
  ReactAndroid::reactnative
28
28
  )
29
29
 
30
+ include(${CMAKE_SOURCE_DIR}/cmake/pch-ccache.cmake)
31
+
30
32
  function(use_expo_common target_name)
31
33
  target_link_libraries(${target_name} PRIVATE EXPO_COMMON)
32
34
  endfunction()
@@ -47,10 +49,12 @@ target_include_directories(
47
49
  "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni"
48
50
  )
49
51
 
52
+ set(EXPO_PCH_INCLUDE_OPTION "$<$<COMPILE_LANGUAGE:CXX>:-idirafter${CMAKE_SOURCE_DIR}/src/main/cpp>")
53
+ target_compile_options(expo-modules-pch PRIVATE ${EXPO_PCH_INCLUDE_OPTION})
50
54
  target_precompile_headers(
51
55
  expo-modules-pch
52
56
  PRIVATE
53
- ${CMAKE_SOURCE_DIR}/src/main/cpp/ExpoHeader.pch
57
+ "<ExpoHeader.pch>"
54
58
  )
55
59
 
56
60
  # Drop the timestamp embedded in the .pch so it stays bit-for-bit stable across
@@ -60,3 +64,11 @@ target_compile_options(
60
64
  PRIVATE
61
65
  "$<$<COMPILE_LANGUAGE:CXX>:-Xclang;-fno-pch-timestamp>"
62
66
  )
67
+
68
+ pch_ccache_owner(expo-modules-pch)
69
+
70
+ function(use_expo_pch target_name)
71
+ target_precompile_headers(${target_name} REUSE_FROM expo-modules-pch)
72
+ target_compile_options(${target_name} PRIVATE ${EXPO_PCH_INCLUDE_OPTION})
73
+ pch_ccache_consumer(${target_name} expo-modules-pch)
74
+ endfunction()
@@ -10,7 +10,7 @@ add_library(
10
10
 
11
11
  use_expo_common(expo-modules-jsi)
12
12
 
13
- target_precompile_headers(expo-modules-jsi REUSE_FROM expo-modules-pch)
13
+ use_expo_pch(expo-modules-jsi)
14
14
 
15
15
  target_include_directories(
16
16
  expo-modules-jsi
@@ -27,7 +27,7 @@ add_library(
27
27
 
28
28
  use_expo_common(expo-modules-core)
29
29
 
30
- target_precompile_headers(expo-modules-core REUSE_FROM expo-modules-pch)
30
+ use_expo_pch(expo-modules-core)
31
31
 
32
32
  target_include_directories(
33
33
  expo-modules-core
@@ -0,0 +1,139 @@
1
+ # Run as custom command
2
+ if(CMAKE_SCRIPT_MODE_FILE)
3
+ set(target_option)
4
+ if(COMPILER_TARGET)
5
+ set(target_option "--target=${COMPILER_TARGET}")
6
+ endif()
7
+
8
+ execute_process(
9
+ COMMAND "${COMPILER}" ${target_option} -module-file-info "${PCH}"
10
+ OUTPUT_VARIABLE info
11
+ RESULT_VARIABLE result
12
+ )
13
+
14
+ if(NOT result EQUAL 0)
15
+ # Hash the .pch itself, which is what ccache does without a .sum. The build stays correct, but
16
+ # checkouts at different paths don't share the results of the users of this PCH.
17
+ message(WARNING "`${COMPILER} -module-file-info ${PCH}` failed, so ccache can't share the results "
18
+ "of the users of this PCH between checkouts at different paths.")
19
+ file(SHA256 "${PCH}" digest)
20
+ file(WRITE "${PCH}.sum" "${digest}\n")
21
+ return()
22
+ endif()
23
+
24
+ # Remove `ShowColors` and `PCH_CCACHE_BUILD_DIR`
25
+ string(REGEX REPLACE "PCH_CCACHE_BUILD_DIR=[^\n]*|ShowColors: [^\n]*" "" info "${info}")
26
+
27
+ # Get all input files
28
+ string(REGEX MATCHALL "Input file: [^\n]*" input_files "${info}")
29
+ foreach(input_file IN LISTS input_files)
30
+ string(REGEX REPLACE "^Input file: | \\[[A-Za-z, ]+\\]$" "" path "${input_file}")
31
+ if(EXISTS "${path}")
32
+ file(SHA256 "${path}" digest)
33
+ string(APPEND info "${digest}\n")
34
+ endif()
35
+ endforeach()
36
+
37
+ # Remove base dir
38
+ if(BASE_DIR)
39
+ string(REGEX REPLACE "(.)/+$" "\\1" base_dir "${BASE_DIR}")
40
+ string(REGEX REPLACE "([][+.*()^$?|\\\\])" "\\\\\\1" base_dir_regex "${base_dir}")
41
+ # The directory itself or a path in it, followed by a space, a quote or a line break. A directory whose
42
+ # name only starts with the same text doesn't match.
43
+ string(REGEX MATCHALL "${base_dir_regex}(/[^ '\n]*)?[ '\n]" paths "${info}")
44
+ list(REMOVE_DUPLICATES paths)
45
+ foreach(path IN LISTS paths)
46
+ string(REGEX REPLACE "[ '\n]$" "" path "${path}")
47
+ file(RELATIVE_PATH relative_path "${BUILD_DIR}" "${path}")
48
+ string(REGEX REPLACE "([][+.*()^$?|\\\\])" "\\\\\\1" path_regex "${path}")
49
+ string(REGEX REPLACE "${path_regex}([ '\n])" "${relative_path}\\1" info "${info}")
50
+ endforeach()
51
+ endif()
52
+
53
+ string(SHA256 digest "${info}")
54
+ file(WRITE "${PCH}.sum" "${digest}\n")
55
+ return()
56
+ endif()
57
+
58
+ set(PCH_CCACHE_FILE "${CMAKE_CURRENT_LIST_FILE}")
59
+
60
+ # Find ccache program if it's used by the target
61
+ function(_pch_ccache_program target out_var)
62
+ get_target_property(launcher ${target} CXX_COMPILER_LAUNCHER)
63
+ get_property(global_rule GLOBAL PROPERTY RULE_LAUNCH_COMPILE)
64
+ get_property(directory_rule DIRECTORY PROPERTY RULE_LAUNCH_COMPILE)
65
+ separate_arguments(rules UNIX_COMMAND "${global_rule} ${directory_rule}")
66
+ get_filename_component(compiler "${CMAKE_CXX_COMPILER}" REALPATH)
67
+ foreach(program IN LISTS launcher rules compiler)
68
+ get_filename_component(name "${program}" NAME_WE)
69
+ if(name STREQUAL "ccache")
70
+ find_program(PCH_CCACHE_PROGRAM NAMES "${program}")
71
+ set(${out_var} "${PCH_CCACHE_PROGRAM}" PARENT_SCOPE)
72
+ return()
73
+ endif()
74
+ endforeach()
75
+ set(${out_var} "" PARENT_SCOPE)
76
+ endfunction()
77
+
78
+ function(pch_ccache_owner owner)
79
+ # CMake's location of the PCH for the Ninja and Makefile generators.
80
+ set(pch_dir "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/${owner}.dir")
81
+ set(pch "${pch_dir}/cmake_pch.hxx.pch")
82
+
83
+ # Used by the custom command
84
+ set_property(SOURCE "${pch_dir}/cmake_pch.hxx.cxx" APPEND PROPERTY
85
+ COMPILE_DEFINITIONS "PCH_CCACHE_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}"
86
+ )
87
+
88
+ set(pch_external_checksum "")
89
+ set(base_dir "")
90
+ _pch_ccache_program(${owner} ccache)
91
+ if(NOT ccache AND "$ENV{EXPO_FORCE_PCH_CCACHE_SUM}")
92
+ # ccache runs through a wrapper, so take its configuration from the ccache on PATH
93
+ find_program(PCH_CCACHE_PATH_PROGRAM ccache)
94
+ set(ccache "${PCH_CCACHE_PATH_PROGRAM}")
95
+ endif()
96
+ if(ccache)
97
+ foreach(option IN ITEMS pch_external_checksum base_dir)
98
+ execute_process(
99
+ COMMAND "${ccache}" --get-config ${option}
100
+ OUTPUT_VARIABLE ${option}
101
+ OUTPUT_STRIP_TRAILING_WHITESPACE
102
+ ERROR_QUIET
103
+ )
104
+ endforeach()
105
+ endif()
106
+
107
+ if("$ENV{EXPO_FORCE_PCH_CCACHE_SUM}")
108
+ set(pch_external_checksum "true")
109
+ endif()
110
+
111
+ if(NOT pch_external_checksum STREQUAL "true")
112
+ # Remove the .sum of an earlier configuration: it can belong to an older PCH, and ccache would hash it
113
+ # if `pch_external_checksum` is enabled later.
114
+ file(REMOVE "${pch}.sum")
115
+ return()
116
+ endif()
117
+
118
+ add_custom_command(
119
+ OUTPUT "${pch}.sum"
120
+ COMMAND "${CMAKE_COMMAND}"
121
+ "-DCOMPILER=${CMAKE_CXX_COMPILER}"
122
+ "-DCOMPILER_TARGET=${CMAKE_CXX_COMPILER_TARGET}"
123
+ "-DPCH=${pch}"
124
+ "-DBASE_DIR=${base_dir}"
125
+ "-DBUILD_DIR=${CMAKE_BINARY_DIR}"
126
+ -P "${PCH_CCACHE_FILE}"
127
+ DEPENDS "${pch}" "${PCH_CCACHE_FILE}"
128
+ COMMENT "Writing the ccache checksum of the ${owner} PCH"
129
+ VERBATIM
130
+ )
131
+ add_custom_target(${owner}-pch-ccache-sum DEPENDS "${pch}.sum")
132
+ endfunction()
133
+
134
+ function(pch_ccache_consumer consumer owner)
135
+ # ccache reads the .sum when it compiles a source of <consumer>, so it must be written before.
136
+ if(TARGET ${owner}-pch-ccache-sum)
137
+ add_dependencies(${consumer} ${owner}-pch-ccache-sum)
138
+ endif()
139
+ endfunction()
@@ -14,7 +14,7 @@ add_library(
14
14
 
15
15
  use_expo_common(expo-modules-core-tests)
16
16
 
17
- target_precompile_headers(expo-modules-core-tests REUSE_FROM expo-modules-pch)
17
+ use_expo_pch(expo-modules-core-tests)
18
18
 
19
19
  target_include_directories(
20
20
  expo-modules-core-tests
@@ -14,7 +14,7 @@ add_library(
14
14
 
15
15
  use_expo_common(expo-modules-worklets)
16
16
 
17
- target_precompile_headers(expo-modules-worklets REUSE_FROM expo-modules-pch)
17
+ use_expo_pch(expo-modules-worklets)
18
18
 
19
19
  target_include_directories(
20
20
  expo-modules-worklets
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "reactNativeVersion": "0.88.0",
3
- "sourceHash": "c852e8e2eba26473f3af1a4e907188f6",
3
+ "sourceHash": "6f9421c9afcc3239b0995cea1757806a",
4
4
  "variants": [
5
5
  "debug",
6
6
  "release"
@@ -16,5 +16,5 @@
16
16
  "libexpo-modules-jsi.so"
17
17
  ],
18
18
  "stripped": true,
19
- "createdAt": "2026-09-25T14:27:57.725201Z"
19
+ "createdAt": "2026-10-05T21:13:27.466871505Z"
20
20
  }
Binary file
@@ -3,6 +3,7 @@ package expo.modules.kotlin.exception
3
3
  import com.facebook.react.bridge.ReadableType
4
4
  import expo.modules.core.interfaces.DoNotStrip
5
5
  import expo.modules.kotlin.types.descriptors.TypeDescriptor
6
+ import io.github.expo.modules.v2.JavaScriptThrowable
6
7
  import java.util.*
7
8
  import kotlin.reflect.KClass
8
9
  import kotlin.reflect.KProperty1
@@ -23,13 +24,13 @@ inline fun Throwable?.toCodedException() = when (this) {
23
24
  open class CodedException(
24
25
  message: String?,
25
26
  cause: Throwable? = null
26
- ) : Exception(message, cause) {
27
+ ) : JavaScriptThrowable(message, cause) {
27
28
  // We need that secondary property, cause we can't access
28
29
  // the javaClass property in the constructor.
29
30
  private var providedCode: String? = null
30
31
 
31
32
  @get:DoNotStrip
32
- val code
33
+ final override val code: String
33
34
  get() = providedCode ?: inferCode(javaClass)
34
35
 
35
36
  constructor(code: String?, message: String?, cause: Throwable?) : this(message = message, cause = cause) {
@@ -4,13 +4,15 @@ import expo.modules.core.interfaces.DoNotStrip
4
4
  import expo.modules.kotlin.AppContext
5
5
  import expo.modules.kotlin.Promise
6
6
  import java.lang.ref.WeakReference
7
+ import java.util.concurrent.atomic.AtomicBoolean
7
8
 
8
9
  @DoNotStrip
9
10
  class PromiseImpl @DoNotStrip internal constructor(
10
11
  @DoNotStrip internal val callback: JavaCallback
11
12
  ) : Promise {
12
- internal var wasSettled = false
13
- private set
13
+ private val settled = AtomicBoolean(false)
14
+ internal val wasSettled: Boolean
15
+ get() = settled.get()
14
16
  private var appContextHolder: WeakReference<AppContext>? = null
15
17
  private var fullFunctionName: String? = null
16
18
 
@@ -57,12 +59,19 @@ class PromiseImpl @DoNotStrip internal constructor(
57
59
  }
58
60
 
59
61
  private inline fun checkIfWasSettled(body: () -> Unit) {
60
- if (wasSettled) {
62
+ if (!settled.compareAndSet(false, true)) {
61
63
  return
62
64
  }
63
65
 
64
- body()
65
- wasSettled = true
66
+ var didSettle = false
67
+ try {
68
+ body()
69
+ didSettle = true
70
+ } finally {
71
+ if (!didSettle) {
72
+ settled.set(false)
73
+ }
74
+ }
66
75
  }
67
76
 
68
77
  fun decorateWithDebugInformation(
@@ -3,7 +3,6 @@ package expo.modules.v2
3
3
  import com.facebook.react.bridge.ReactApplicationContext
4
4
  import expo.modules.kotlin.logger
5
5
  import io.github.expo.modules.v2.Module
6
- import io.github.expo.modules.v2.modules.ModuleRegistry
7
6
  import io.github.expo.modules.v2.react.ReactRuntime
8
7
  import java.lang.reflect.Modifier
9
8
 
@@ -31,9 +30,16 @@ object ExpoModulesV2Host {
31
30
  }
32
31
 
33
32
  try {
34
- val registry = ModuleRegistry()
35
- modules.forEach(registry::register)
36
- runtime = ReactRuntime.attach(reactContext, registry)
33
+ // The runtime creates a context of its own, and serves the modules of that context.
34
+ val attached = ReactRuntime.attach(reactContext)
35
+ try {
36
+ modules.forEach(attached.moduleRegistry::register)
37
+ } catch (e: Throwable) {
38
+ // Closing the runtime closes its context, so no module stays bound to it.
39
+ attached.close()
40
+ throw e
41
+ }
42
+ runtime = attached
37
43
  logger.info("✅ Expo Modules v2 installed ${modules.size} module(s) on globalThis.expo.modules")
38
44
  } catch (e: Throwable) {
39
45
  logger.error("❌ Cannot install Expo Modules v2: $e", e)
@@ -33,7 +33,7 @@ dependencies {
33
33
  implementation("com.facebook.react:react-native-gradle-plugin")
34
34
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
35
35
  implementation("io.github.expo.pika:pika-gradle:1.1.0")
36
- implementation("io.github.expo:expo-modules-v2-gradle-plugin:0.2.2")
36
+ implementation("io.github.expo:expo-modules-v2-gradle-plugin:0.2.4")
37
37
 
38
38
  if (isExpoAutolinkingSettingsPluginAvailable) {
39
39
  implementation("expo.modules:expo-autolinking-plugin-shared")
@@ -3,9 +3,10 @@ import ExpoModulesJSI
3
3
 
4
4
  /**
5
5
  The app context is an interface to a single Expo app.
6
+ Not `final` so that `TestAppContext` from `ExpoModulesTestCore` can subclass it.
6
7
  */
7
8
  @objc(EXAppContext)
8
- public final class AppContext: NSObject, EXAppContextProtocol, @unchecked Sendable {
9
+ public class AppContext: NSObject, EXAppContextProtocol, @unchecked Sendable {
9
10
  internal static func create() -> AppContext {
10
11
  let appContext = AppContext()
11
12
 
@@ -223,26 +223,35 @@ extension ExpoSwiftUI {
223
223
  }
224
224
  #endif
225
225
 
226
- if window != nil, let parentController = reactViewController() {
227
- #if !os(macOS)
228
- if parentController as? UINavigationController == nil && parentController as? UITabBarController == nil {
229
- // Swift automatically adds the hostingController in the correct place when the parentController
230
- // is UINavigationController, since its children are supposed to be only screens.
231
- // Similarly, for UITabBarController we expect its children to be only tabs.
232
- parentController.addChild(hostingController)
233
- }
234
- #else
235
- parentController.addChild(hostingController)
236
- #endif
237
- addSubview(hostingController.view)
238
- #if os(iOS) || os(tvOS)
239
- hostingController.didMove(toParent: parentController)
240
- #endif
241
- setupHostingViewConstraints()
242
- } else {
226
+ guard window != nil else {
227
+ hostingController.view.removeFromSuperview()
228
+ hostingController.removeFromParent()
229
+ return
230
+ }
231
+
232
+ let parentController = reactViewController()
233
+ #if os(macOS)
234
+ // An `NSHostingController` view renders without a parent controller, and a React root view used
235
+ // directly as `NSWindow.contentView` has none in its responder chain, so don't require one.
236
+ parentController?.addChild(hostingController)
237
+ #else
238
+ guard let parentController else {
243
239
  hostingController.view.removeFromSuperview()
244
240
  hostingController.removeFromParent()
241
+ return
242
+ }
243
+ if parentController as? UINavigationController == nil && parentController as? UITabBarController == nil {
244
+ // Swift automatically adds the hostingController in the correct place when the parentController
245
+ // is UINavigationController, since its children are supposed to be only screens.
246
+ // Similarly, for UITabBarController we expect its children to be only tabs.
247
+ parentController.addChild(hostingController)
245
248
  }
249
+ #endif
250
+ addSubview(hostingController.view)
251
+ #if os(iOS) || os(tvOS)
252
+ hostingController.didMove(toParent: parentController)
253
+ #endif
254
+ setupHostingViewConstraints()
246
255
  }
247
256
 
248
257
  #if os(macOS)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "58.0.12",
3
+ "version": "58.0.13",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "keywords": [
6
6
  "expo",
@@ -64,8 +64,8 @@
64
64
  "./package.json": "./package.json"
65
65
  },
66
66
  "dependencies": {
67
- "expo-modules-jsi": "~58.0.8",
68
- "expo-modules-macros": "0.14.0",
67
+ "expo-modules-jsi": "~58.0.9",
68
+ "expo-modules-macros": "0.15.0",
69
69
  "invariant": "^2.2.4"
70
70
  },
71
71
  "devDependencies": {
@@ -73,7 +73,7 @@
73
73
  "@types/invariant": "^2.2.33",
74
74
  "@types/react": "~19.3.0",
75
75
  "@types/react-dom": "^19.1.7",
76
- "expo-module-scripts": "56.0.6",
76
+ "expo-module-scripts": "56.0.7",
77
77
  "react-dom": "19.3.0"
78
78
  },
79
79
  "peerDependencies": {
@@ -89,6 +89,7 @@
89
89
  "scripts": {
90
90
  "build": "expo-build src",
91
91
  "precompile-ios": "et prebuild-package-for-publish",
92
+ "precompile-android": "et prebuild-android-package-for-publish",
92
93
  "clean": "expo-module clean",
93
94
  "lint": "oxlint --config oxlint.config.mjs .",
94
95
  "format": "expo-module format",