expo-modules-jsi 56.0.2 → 56.0.3

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,12 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.3 — 2026-05-11
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fixed launch-time crash in apps with source-built React Native. ([#45636](https://github.com/expo/expo/pull/45636) by [@tsapeta](https://github.com/tsapeta))
18
+
13
19
  ## 56.0.2 — 2026-05-08
14
20
 
15
21
  ### 🐛 Bug fixes
@@ -51,7 +51,6 @@ Pod::Spec.new do |s|
51
51
 
52
52
  s.dependency 'React-Core'
53
53
  s.dependency 'ReactCommon'
54
- s.dependency 'React-runtimescheduler'
55
54
 
56
55
  # Create a stub xcframework if needed, so CocoaPods generates the
57
56
  # "[CP] Copy XCFrameworks" and "[CP] Embed Pods Frameworks" build phases.
@@ -14,8 +14,11 @@ let podsRoot = resolvePodsRoot()
14
14
  // framework and its headers don't get mirrored to Pods/Headers/Public. Clang
15
15
  // ignores missing `-I` paths, so they're no-ops elsewhere. `RN_ROOT` is
16
16
  // forwarded from build-xcframework.sh (Node-resolved for hoisted monorepos).
17
+ // `REACT_NATIVE_PATH` is exported by Xcode for hosts that build RN from a
18
+ // non-npm location, e.g. Expo Go.
17
19
  let publicHeaders = "\(podsRoot)/Headers/Public"
18
20
  let reactNative = ProcessInfo.processInfo.environment["RN_ROOT"]
21
+ ?? ProcessInfo.processInfo.environment["REACT_NATIVE_PATH"]
19
22
  ?? "\(podsRoot)/../../node_modules/react-native"
20
23
  let headerSearchPaths = [
21
24
  publicHeaders,
@@ -38,28 +38,55 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
38
38
  lazy var runtimeActor: JavaScriptRuntimeActor = JavaScriptRuntimeActor(runtime: self)
39
39
 
40
40
  /**
41
- Creates a runtime from the JSI runtime.
41
+ Creates a runtime from the JSI runtime. The scheduler runs tasks synchronously
42
+ on the caller's thread — for the React-backed runtime, use
43
+ `init(unsafePointer:nativeScheduler:dispatch:)` instead.
42
44
  */
43
45
  internal init(_ runtime: facebook.jsi.Runtime) {
44
46
  self.pointee = runtime
45
- self.scheduler = expo.RuntimeScheduler(runtime)
47
+ self.scheduler = expo.RuntimeScheduler()
46
48
  }
47
49
 
48
50
  /**
49
- Creates Hermes runtime.
51
+ Creates a standalone Hermes runtime. Scheduled tasks run synchronously —
52
+ no React scheduler is wired up.
50
53
  */
51
54
  public init() {
52
55
  self.pointee = expo.createHermesRuntime()
53
- self.scheduler = expo.RuntimeScheduler(pointee)
56
+ self.scheduler = expo.RuntimeScheduler()
54
57
  }
55
58
 
56
59
  /**
57
60
  Creates a runtime from a raw pointer to the underlying `facebook.jsi.Runtime`.
61
+ Scheduled tasks run synchronously — for the React-backed runtime, use
62
+ `init(unsafePointer:nativeScheduler:dispatch:)` instead.
58
63
  */
59
64
  public init(unsafePointer: UnsafeMutableRawPointer) {
60
65
  let runtime = unsafeBitCast(unsafePointer, to: facebook.jsi.Runtime.self)
61
66
  self.pointee = runtime
62
- self.scheduler = expo.RuntimeScheduler(runtime)
67
+ self.scheduler = expo.RuntimeScheduler()
68
+ }
69
+
70
+ /**
71
+ Creates a runtime bound to a host-provided React `RuntimeScheduler`. Calls to
72
+ `schedule(...)` / `.execute(...)` dispatch through `dispatch`, which the host
73
+ implements against the real `react::RuntimeScheduler`. This is the path the
74
+ React Native factory uses.
75
+
76
+ - `unsafePointer`: raw pointer to the underlying `facebook::jsi::Runtime`.
77
+ - `scheduler`: raw pointer to the `react::RuntimeScheduler` instance.
78
+ - `dispatch`: raw pointer to a C function with signature
79
+ `void (*)(void *scheduler, int priority, void (^callback)())`.
80
+ */
81
+ public init(
82
+ unsafePointer: UnsafeMutableRawPointer,
83
+ scheduler: UnsafeMutableRawPointer,
84
+ dispatch: UnsafeRawPointer
85
+ ) {
86
+ let runtime = unsafeBitCast(unsafePointer, to: facebook.jsi.Runtime.self)
87
+ let fn = unsafeBitCast(dispatch, to: expo.RuntimeScheduler.ScheduleFn.self)
88
+ self.pointee = runtime
89
+ self.scheduler = expo.RuntimeScheduler(scheduler, fn)
63
90
  }
64
91
 
65
92
  /**
@@ -348,8 +375,8 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable {
348
375
  Schedules a closure to be executed with granted synchronized access to the runtime.
349
376
  */
350
377
  public func schedule(priority: SchedulerPriority = .normal, @_implicitSelfCapture _ closure: @escaping @JavaScriptActor () -> sending Void) -> Void {
351
- let reactPriority = facebook.react.SchedulerPriority(rawValue: priority.rawValue) ?? .NormalPriority
352
- scheduler.scheduleTask(reactPriority) {
378
+ let cxxPriority = expo.RuntimeScheduler.Priority(rawValue: priority.rawValue) ?? .NormalPriority
379
+ scheduler.scheduleTask(cxxPriority) {
353
380
  JavaScriptActor.assumeIsolated(closure)
354
381
  }
355
382
  }
@@ -2,65 +2,76 @@
2
2
 
3
3
  #ifdef __cplusplus
4
4
 
5
- #include <memory>
5
+ #include <atomic>
6
6
  #include <swift/bridging>
7
- #include <jsi/jsi.h>
8
- #include <react/renderer/runtimescheduler/RuntimeScheduler.h>
9
- #include <react/renderer/runtimescheduler/RuntimeSchedulerBinding.h>
10
-
11
- namespace jsi = facebook::jsi;
12
- namespace react = facebook::react;
13
7
 
14
8
  namespace expo {
15
9
 
16
10
  /**
17
- Wrapper for RuntimeScheduler from React which for some reason cannot be constructed from Swift.
18
- Imported as a shared reference type so Swift manages its lifetime via retain/release.
11
+ Wrapper around React Native's RuntimeScheduler. The native scheduler reference
12
+ and dispatch trampoline are supplied by the host (e.g. ExpoReactNativeFactory)
13
+ at construction time. The xcframework intentionally avoids linking against
14
+ React-runtimescheduler so the prebuilt binary works with hosts that build RN
15
+ either as a dynamic framework or as a static archive — without needing
16
+ -undefined dynamic_lookup to resolve React internals.
17
+
18
+ Priority values mirror facebook::react::SchedulerPriority — kept here as a
19
+ plain enum so we don't include the React header.
19
20
  */
20
21
  class RuntimeScheduler {
21
- private:
22
- std::shared_ptr<react::RuntimeScheduler> reactRuntimeScheduler;
22
+ public:
23
+ enum class Priority : int {
24
+ ImmediatePriority = 1,
25
+ UserBlockingPriority = 2,
26
+ NormalPriority = 3,
27
+ LowPriority = 4,
28
+ IdlePriority = 5,
29
+ };
30
+
31
+ using ScheduleTaskCallback = void(^)();
23
32
 
24
33
  /**
25
- Reference count managed by Swift's ARC via SWIFT_SHARED_REFERENCE.
26
- Prevents premature deallocation when C++ shared_ptr and Swift references coexist.
34
+ Trampoline implemented by the host casts `nativeScheduler` back to
35
+ react::RuntimeScheduler* and calls scheduleTask on it. Keeping it as a
36
+ function pointer keeps React types out of this header.
27
37
  */
38
+ using ScheduleFn = void (*)(void *nativeScheduler, int priority, ScheduleTaskCallback callback);
39
+
40
+ private:
41
+ void *const nativeScheduler{nullptr};
42
+ const ScheduleFn scheduleFn{nullptr};
43
+
28
44
  std::atomic<int> refCount{1};
29
45
 
30
46
  public:
31
47
  /**
32
- Constructs the scheduler from a React Native runtime binding.
33
- Falls back to a no-op scheduler when no binding is installed.
48
+ Constructs a scheduler bound to a host-provided native RuntimeScheduler.
49
+ `scheduleTask` dispatches through `fn`, which the host implements against
50
+ the real react::RuntimeScheduler.
34
51
  */
35
- RuntimeScheduler(jsi::Runtime &runtime) {
36
- if (auto binding = react::RuntimeSchedulerBinding::getBinding(runtime)) {
37
- reactRuntimeScheduler = binding->getRuntimeScheduler();
38
- }
39
- }
52
+ RuntimeScheduler(void *scheduler, ScheduleFn fn) noexcept
53
+ : nativeScheduler(scheduler), scheduleFn(fn) {}
40
54
 
41
55
  /**
42
- Constructs a no-op scheduler for standalone runtimes (e.g. tests).
43
- Scheduled tasks are executed synchronously by the caller.
56
+ Constructs a no-op scheduler. Scheduled tasks run synchronously on the
57
+ caller's thread intended for standalone runtimes (e.g. tests) that have
58
+ no React scheduler.
44
59
  */
45
60
  RuntimeScheduler() {}
46
61
 
47
- RuntimeScheduler(const RuntimeScheduler &) = delete; // non-copyable
62
+ RuntimeScheduler(const RuntimeScheduler &) = delete;
48
63
 
49
64
  /**
50
65
  Whether the scheduler can dispatch work asynchronously to the JS thread.
51
66
  When false, tasks run synchronously and callers should avoid dispatching to background queues.
52
67
  */
53
68
  bool supportsAsyncScheduling() const noexcept {
54
- return reactRuntimeScheduler != nullptr;
69
+ return scheduleFn != nullptr;
55
70
  }
56
71
 
57
- using ScheduleTaskCallback = void(^)();
58
-
59
- void scheduleTask(react::SchedulerPriority priority, ScheduleTaskCallback callback) noexcept {
60
- if (reactRuntimeScheduler) {
61
- reactRuntimeScheduler->scheduleTask(priority, [callback = std::move(callback)](jsi::Runtime &runtime) {
62
- callback();
63
- });
72
+ void scheduleTask(Priority priority, ScheduleTaskCallback callback) noexcept {
73
+ if (scheduleFn != nullptr) {
74
+ scheduleFn(nativeScheduler, static_cast<int>(priority), callback);
64
75
  } else {
65
76
  callback();
66
77
  }
@@ -89,8 +89,10 @@ compute_hash() {
89
89
  # Force C locale so sort order is consistent regardless of the environment.
90
90
  # Xcode build phases run without locale variables, which changes sort ordering.
91
91
  (
92
- # Include PODS_ROOT so switching between worktrees invalidates the cache.
92
+ # Include PODS_ROOT and RN_ROOT so switching between worktrees or RN
93
+ # sources invalidates the cache.
93
94
  echo "PODS_ROOT=${PODS_ROOT:-}"
95
+ echo "RN_ROOT=${RN_ROOT:-}"
94
96
  echo "$all_files" | LC_ALL=C sort | while IFS= read -r file; do
95
97
  echo "$file"
96
98
  cat "$file"
@@ -243,12 +245,20 @@ fi
243
245
  # whether PODS_ROOT was passed as relative or absolute.
244
246
  PODS_ROOT="$(cd "$PODS_ROOT" && pwd)"
245
247
 
246
- # Resolve react-native via Node so the build works in any node_modules layout
247
- # (hoisted monorepos, yarn/pnpm workspaces). Falls back to the relative path
248
- # when `node` is unavailable. Forwarded to Package.swift and the modulemap
249
- # generator below.
250
- RN_ROOT="$(node -p 'require("path").dirname(require.resolve("react-native/package.json"))' 2>/dev/null \
251
- || echo "${PODS_ROOT}/../../node_modules/react-native")"
248
+ # Resolve react-native. Order:
249
+ # 1. REACT_NATIVE_PATH env var (set by Xcode from the Podfile's build setting)
250
+ # for hosts that build RN from a non-npm location, e.g. Expo Go which
251
+ # uses the `react-native-lab/react-native` submodule, not node_modules.
252
+ # 2. `node -p require.resolve(...)` so the script works in any node_modules
253
+ # layout (hoisted monorepos, pnpm/yarn workspaces).
254
+ # 3. Relative fallback from PODS_ROOT for when `node` isn't on PATH.
255
+ # Forwarded to Package.swift and the modulemap generator below.
256
+ if [[ -n "${REACT_NATIVE_PATH:-}" && -d "${REACT_NATIVE_PATH}" ]]; then
257
+ RN_ROOT="$(cd "$REACT_NATIVE_PATH" && pwd)"
258
+ else
259
+ RN_ROOT="$(node -p 'require("path").dirname(require.resolve("react-native/package.json"))' 2>/dev/null \
260
+ || echo "${PODS_ROOT}/../../node_modules/react-native")"
261
+ fi
252
262
 
253
263
  mode="$( [[ -d "${PODS_ROOT}/React-Core-prebuilt/React.xcframework" ]] && echo "prebuilt RN" || echo "source-built RN")"
254
264
  [[ -f "${PODS_ROOT}/Target Support Files/React-jsi/React-jsi-umbrella.h" ]] && mode="${mode}, static frameworks"
@@ -38,7 +38,10 @@ mkdir -p "$GENERATED_DIR"
38
38
 
39
39
  JSI_UMBRELLA="${PODS_ROOT}/Headers/Public/React-jsi/jsi/jsi.h"
40
40
  if [[ ! -f "$JSI_UMBRELLA" ]]; then
41
- RN="${RN_ROOT:-$(node -p 'require("path").dirname(require.resolve("react-native/package.json"))' 2>/dev/null || echo "${PODS_ROOT}/../../node_modules/react-native")}"
41
+ # Resolution order matches build-xcframework.sh: RN_ROOT (forwarded by the
42
+ # build script), REACT_NATIVE_PATH (exported by Xcode for hosts like Expo Go
43
+ # that build RN from a submodule), node resolve, then a relative fallback.
44
+ RN="${RN_ROOT:-${REACT_NATIVE_PATH:-$(node -p 'require("path").dirname(require.resolve("react-native/package.json"))' 2>/dev/null || echo "${PODS_ROOT}/../../node_modules/react-native")}}"
42
45
  JSI_UMBRELLA="${RN}/ReactCommon/jsi/jsi/jsi.h"
43
46
  fi
44
47
  [[ -f "$JSI_UMBRELLA" ]] || { echo "error: cannot locate jsi.h" >&2; exit 1; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-jsi",
3
- "version": "56.0.2",
3
+ "version": "56.0.3",
4
4
  "description": "The JavaScript Interface for Expo Modules",
5
5
  "main": "index.js",
6
6
  "sideEffects": [],
@@ -41,7 +41,7 @@
41
41
  "./apple/scripts/test.sh"
42
42
  ]
43
43
  },
44
- "gitHead": "a30353e69ca0d72b9fac5830abc631feda1ba3ae",
44
+ "gitHead": "42013232893cb2aa71ab218e9b422d4a8476b3f0",
45
45
  "scripts": {
46
46
  "build": "apple/scripts/build-xcframework.sh",
47
47
  "test": "apple/scripts/test.sh"