Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/expo-audio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 💡 Others

- Migrated to the single-payload `SharedObject.emit` API. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))

## 56.0.6 — 2026-05-13

_This version does not introduce any user-facing changes._
Expand Down
4 changes: 2 additions & 2 deletions packages/expo-audio/ios/AudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public class AudioPlayer: SharedRef<AVPlayer>, Playable {
arguments.merge(dict) { _, new in
new
}
self.emit(event: AudioConstants.playbackStatus, arguments: arguments)
self.emit(event: AudioConstants.playbackStatus, payload: arguments)

if isActiveForLockScreen {
MediaController.shared.updateNowPlayingInfo(for: self)
Expand Down Expand Up @@ -390,7 +390,7 @@ public class AudioPlayer: SharedRef<AVPlayer>, Playable {
return ["frames": channelData]
}

self.emit(event: AudioConstants.audioSample, arguments: [
self.emit(event: AudioConstants.audioSample, payload: [
"channels": channels,
"timestamp": timestamp
])
Expand Down
4 changes: 2 additions & 2 deletions packages/expo-audio/ios/AudioPlaylist.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,15 @@ public class AudioPlaylist: SharedRef<AVQueuePlayer>, Playable {
func updateStatus(with dict: [String: Any]) {
var arguments = currentStatus()
arguments.merge(dict) { _, new in new }
self.emit(event: PlaylistConstants.playlistStatusUpdate, arguments: arguments)
self.emit(event: PlaylistConstants.playlistStatusUpdate, payload: arguments)
}

private func validIndex(_ index: Int) -> Bool {
return index >= 0 && index < sources.count
}

private func emitTrackChanged(previousIndex: Int, currentIndex: Int) {
self.emit(event: PlaylistConstants.trackChanged, arguments: [
self.emit(event: PlaylistConstants.trackChanged, payload: [
"previousIndex": previousIndex,
"currentIndex": currentIndex
])
Expand Down
8 changes: 4 additions & 4 deletions packages/expo-audio/ios/AudioRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class AudioRecorder: SharedRef<AVAudioRecorder>, RecordingResultHandler {
if let options = currentOptions {
do {
try prepare(options: options, sessionOptions: currentSessionOptions)
emit(event: recordingStatus, arguments: [
emit(event: recordingStatus, payload: [
"id": id,
"isFinished": true,
"hasError": false,
Expand All @@ -196,7 +196,7 @@ class AudioRecorder: SharedRef<AVAudioRecorder>, RecordingResultHandler {
}

currentState = .error
emit(event: recordingStatus, arguments: [
emit(event: recordingStatus, payload: [
"id": id,
"isFinished": true,
"hasError": true,
Expand All @@ -211,7 +211,7 @@ class AudioRecorder: SharedRef<AVAudioRecorder>, RecordingResultHandler {
currentState = .stopped
resetDurationTracking()

emit(event: recordingStatus, arguments: [
emit(event: recordingStatus, payload: [
"id": id,
"isFinished": true,
"hasError": false,
Expand All @@ -225,7 +225,7 @@ class AudioRecorder: SharedRef<AVAudioRecorder>, RecordingResultHandler {
currentState = .error
resetDurationTracking()

emit(event: recordingStatus, arguments: [
emit(event: recordingStatus, payload: [
"id": id,
"isFinished": true,
"hasError": true,
Expand Down
4 changes: 2 additions & 2 deletions packages/expo-audio/ios/AudioStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class AudioStream: SharedObject {
}

private func emitStatus() {
emit(event: AUDIO_STREAM_STATUS, arguments: [
emit(event: AUDIO_STREAM_STATUS, payload: [
"isStreaming": isStreaming
])
}
Expand All @@ -180,7 +180,7 @@ class AudioStream: SharedObject {
return
}

emit(event: AUDIO_STREAM_BUFFER, arguments: [
emit(event: AUDIO_STREAM_BUFFER, payload: [
"data": data,
"sampleRate": sampleRate,
"channels": channels,
Expand Down
2 changes: 2 additions & 0 deletions packages/expo-brownfield/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

### 💡 Others

- Migrated to the single-payload `SharedObject.emit` API. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))

## 56.0.9 — 2026-05-13

_This version does not introduce any user-facing changes._
Expand Down
2 changes: 1 addition & 1 deletion packages/expo-brownfield/ios/SharedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public final class SharedState: SharedObject {
value = newValue
lock.unlock()

emit(event: "change", arguments: ["value": newValue])
emit(event: "change", payload: ["value": newValue])
BrownfieldStateInternal.shared.notifySubscribers(key, newValue)
BrownfieldStateInternal.shared.maybeNotifyKeyRecreated(key)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/expo-codemod/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 💡 Others

- Fixed unit test failure on Windows. ([#45777](https://github.com/expo/expo/pull/45777) by [@kudo](https://github.com/kudo))

## 56.0.3 — 2026-05-11

### 🎉 New features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ describe('type imports', () => {
`import { createStackNavigator, type StackScreenProps } from '@react-navigation/stack';`,
].join('\n');
const output = runTS(input);
expect(output).toBe(
expect(output.replace(/\r\n/g, '\n')).toBe(
[
`import { useNavigation, type NavigationProp } from "expo-router/react-navigation";`,
`import { createStackNavigator, type StackScreenProps } from "expo-router/js-stack";`,
Expand Down
2 changes: 2 additions & 0 deletions packages/expo-file-system/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 💡 Others

- Migrated to the single-payload `SharedObject.emit` API. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))

## 56.0.4 — 2026-05-08

### 💡 Others
Expand Down
2 changes: 1 addition & 1 deletion packages/expo-file-system/ios/FileSystemDownloadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ private final class DownloadTaskDelegate: NSObject, NetworkTaskDelegate {

if shouldEmit {
lastProgressTime = currentTime
sharedObject?.emit(event: "progress", arguments: [
sharedObject?.emit(event: "progress", payload: [
"bytesWritten": totalBytesWritten,
"totalBytes": totalBytesExpectedToWrite,
])
Expand Down
2 changes: 1 addition & 1 deletion packages/expo-file-system/ios/FileSystemUploadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class FileSystemUploadTask: SharedObject {

if shouldEmit {
lastProgressTime = currentTime
emit(event: "progress", arguments: [
emit(event: "progress", payload: [
"bytesSent": bytesSent,
"totalBytes": totalBytes
])
Expand Down
2 changes: 2 additions & 0 deletions packages/expo-modules-autolinking/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
### 🐛 Bug fixes

- [iOS] Fixed `pod install` failing with `bad component (expected absolute path component)` for precompiled Expo modules when the project lives under a path containing non-ASCII characters (e.g. emoji). ([#45779](https://github.com/expo/expo/pull/45779) by [@tsapeta](https://github.com/tsapeta))
- [iOS] Cache prebuilt module status lookups to reduce repeated `File.exist?` calls during `pod install`. ([#45742](https://github.com/expo/expo/pull/45742) by [@chrfalch](https://github.com/chrfalch))
- [iOS] Wire the macro plugin flag into `ExpoModulesCore`'s own xcconfig so SourceKit can resolve `#externalMacro` references in core source files. ([#45778](https://github.com/expo/expo/pull/45778) by [@tsapeta](https://github.com/tsapeta))

### 💡 Others

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ module PrecompiledModules
@warned_no_prebuilt_react = false
@target_platform = nil
@xcframework_slice_cache = nil
@status_cache = {} # Hash: pod_name -> resolve_prebuilt_status result

class << self
# Returns the build flavor (debug/release) for precompiled modules.
Expand Down Expand Up @@ -124,6 +125,7 @@ def configure(target_platform: nil, build_from_source: nil)

def build_from_source=(patterns)
@build_from_source_patterns = (patterns || []).map { |p| Regexp.new("^#{p}$") }
@status_cache = {}
end

def target_platform=(platform)
Expand All @@ -135,6 +137,7 @@ def target_platform=(platform)
@claimed_vendored_frameworks = nil
@framework_owner_map = nil
@xcframework_slice_cache = nil
@status_cache = {}
end

# Checks if a pod is configured to be built from source via buildFromSource.
Expand Down Expand Up @@ -1818,6 +1821,11 @@ def resolve_own_prebuilt_info(pod_name)
# A pod may use a prebuilt xcframework only when its own prebuilt artifact
# exists and every local Expo dependency also uses prebuilt.
def resolve_prebuilt_status(pod_name, visiting = Set.new)
return _resolve_prebuilt_status_uncached(pod_name, visiting) unless visiting.empty?
@status_cache[pod_name] ||= _resolve_prebuilt_status_uncached(pod_name, visiting)
end

def _resolve_prebuilt_status_uncached(pod_name, visiting)
return { available: false, reason: :build_from_source } if build_from_source?(pod_name)
return { available: true } if visiting.include?(pod_name)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ def self.integrate_core_macro_plugins(targets)
end

target.pod_targets.each do |pod_target|
is_core = pod_target.name == 'ExpoModulesCore'
has_core_dependency = pod_target.dependencies.find { |dependency| dependency == 'ExpoModulesCore' }
next unless has_core_dependency
next unless is_core || has_core_dependency
pod_target.build_settings.each do |build_configuration_name, build_settings|
xcconfig = build_settings.xcconfig
swift_flags = xcconfig.attributes[SWIFT_FLAGS] || '$(inherited)'
Expand Down
3 changes: 3 additions & 0 deletions packages/expo-modules-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Added single-payload overloads for `SharedObject.emit` on iOS and Android. The iOS API also accepts an already-converted `JavaScriptValue` payload to skip the native-to-JS conversion step. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))

### 🐛 Bug fixes

### 💡 Others
Expand All @@ -28,6 +30,7 @@
### 💡 Others

- [iOS] `AppContext.setRuntime` now takes the native React `RuntimeScheduler` pointer and a dispatch trampoline alongside the runtime pointer. ([#45636](https://github.com/expo/expo/pull/45636) by [@tsapeta](https://github.com/tsapeta))
- Deprecated `SharedObject.emit(event:arguments:)` (iOS) and the `vararg` `emit` (Android) in favor of the new single-payload overloads. Existing single-argument call sites keep working unchanged. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))

## 56.0.5 — 2026-05-08

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,29 +87,62 @@ class SharedObjectTest {
"sharedObject = new $moduleRef.SharedObjectExampleClass()"
).getObject()

// Add a listener that adds three arguments
evaluateScript(
"total = 0",
"sharedObject.addListener('test event', (a, b, c) => { total = a + b + c })"
"sharedObject.addListener('test event', (payload) => { total = payload.a + payload.b + payload.c })"
)

// Get the native instance
val nativeObject = jsiInterop
.runtimeHolder
.get()
?.sharedObjectRegistry
?.toNativeObjectOrNull(jsObject)

// Send an event from the native object to JS
nativeObject?.emit("test event", 1, 2, 3)
nativeObject?.emit("test event", mapOf("a" to 1, "b" to 2, "c" to 3))

// Check the value that is set by the listener
val total = evaluateScript("total")

Truth.assertThat(total.isNumber()).isTrue()
Truth.assertThat(total.getInt()).isEqualTo(6)
}

@Test
fun sends_events_with_primitive_payloads() = withExampleSharedClass {
val jsObject = evaluateScript(
"sharedObject = new $moduleRef.SharedObjectExampleClass()"
).getObject()

evaluateScript(
"results = []",
"sharedObject.addListener('primitive', (payload) => { results.push(payload) })"
)

val nativeObject = jsiInterop
.runtimeHolder
.get()
?.sharedObjectRegistry
?.toNativeObjectOrNull(jsObject)

nativeObject?.emit("primitive", 42)
nativeObject?.emit("primitive", "hello")
nativeObject?.emit("primitive", true)

Truth.assertThat(evaluateScript("results.length").getInt()).isEqualTo(3)
Truth.assertThat(evaluateScript("results[0]").getInt()).isEqualTo(42)
Truth.assertThat(evaluateScript("results[1]").getString()).isEqualTo("hello")
Truth.assertThat(evaluateScript("results[2]").getBool()).isTrue()
}

@Test
fun does_not_crash_when_emitting_from_a_shared_object_not_associated_with_a_js_object() {
// No runtime/JS object backing this instance, so the defensive branches in `emit` should
// log and return cleanly without touching JSI.
val detached = SharedObjectExampleClass()
detached.emit("ignored")
detached.emit("ignored", mapOf("key" to "value"))
detached.emit("ignored", 42)
}

@Test
fun should_be_able_to_throw_from_constructor() = withSingleModule({
Class("ThrowingSharedObject") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2018-present 650 Industries. All rights reserved.

#include "ExpoComponentDescriptorFactory.h"
#include "AndroidExpoViewComponentDescriptor.h"

namespace react = facebook::react;

namespace expo {

StatePropMapType statePropMap = {};

react::ComponentDescriptor::Unique concreteExpoComponentDescriptorConstructor(
const react::ComponentDescriptorParameters &parameters
) {
auto descriptor = std::make_unique<AndroidExpoViewComponentDescriptor>(
parameters,
react::RawPropsParser(/*useRawPropsJsiValue=*/true)
);

if (statePropMap.contains(std::static_pointer_cast<std::string const>(parameters.flavor))) {
descriptor->setStateProps(
statePropMap.at(
std::static_pointer_cast<std::string const>(parameters.flavor)
)
);
}

return descriptor;
}

} // namespace expo
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2018-present 650 Industries. All rights reserved.

#pragma once

#include <react/renderer/core/ComponentDescriptor.h>
#include "../types/FrontendConverter.h"

namespace react = facebook::react;

namespace expo {

using StatePropMapType = std::unordered_map<
react::ComponentDescriptor::Flavor,
std::unordered_map<std::string, std::shared_ptr<FrontendConverter>>
>;

extern StatePropMapType statePropMap;

react::ComponentDescriptor::Unique concreteExpoComponentDescriptorConstructor(
const react::ComponentDescriptorParameters &parameters
);

} // namespace expo
Loading
Loading