expo-modules-core 57.0.14 → 57.0.16
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 +16 -0
- package/android/build.gradle +2 -2
- package/android/src/main/cpp/worklets/WorkletJSCallInvoker.cpp +1 -4
- package/common/cpp/fabric/ExpoViewComponentDescriptor.h +41 -0
- package/common/cpp/fabric/ExpoViewShadowNode.h +110 -0
- package/ios/Core/AppContext.swift +7 -5
- package/ios/Core/Conversions.swift +17 -0
- package/ios/Core/Views/SwiftUI/SwiftUIViewProps.swift +24 -2
- package/ios/Fabric/ExpoFabricView.swift +1 -16
- package/ios/WorkletsAdapter/ExpoWorkletsBridgeProvider.mm +1 -1
- package/package.json +3 -3
- package/prebuilds/output/debug/xcframeworks/ExpoModulesCore.tar.gz +0 -0
- package/prebuilds/output/debug/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
- package/prebuilds/output/release/xcframeworks/ExpoModulesCore.tar.gz +0 -0
- package/prebuilds/output/release/xcframeworks/ExpoModulesWorklets.tar.gz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,22 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.0.16 — 2026-09-04
|
|
14
|
+
|
|
15
|
+
_This version does not introduce any user-facing changes._
|
|
16
|
+
|
|
17
|
+
## 57.0.15 — 2026-09-01
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- [iOS][Android] Fixed a `matchContents` `RNHostView` and the `matchContents` host around it feeding each other's size back and forth, which grew the layout on every pass. ([#49483](https://github.com/expo/expo/pull/49483) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
22
|
+
- [iOS] Fixed SwiftUI view props re-decoding every field on every props update. Fabric sends the whole props map rather than a delta, so an unrelated prop change replaced each decoded value with an equal but distinct one, which cost a decode per field and stopped SwiftUI from pruning the view tree that reads it. A field whose raw value is unchanged now keeps the value decoded before, the same way `ExpoFabricView.updateProps` already worked for UIKit views. ([#48426](https://github.com/expo/expo/pull/48426) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
23
|
+
|
|
24
|
+
### 💡 Others
|
|
25
|
+
|
|
26
|
+
- Migrated from deprecated react-native-worklets WorkletRuntime API `executeSync` to up-to-date `runSync`. `runSync` is available since 0.7.0. ([#48691](https://github.com/expo/expo/pull/48691) by [@tjzel](https://github.com/tjzel))
|
|
27
|
+
- Added internal `ExpoModulesProviderModuleName` lookup key for `ExpoModulesProvider` class. ([#49539](https://github.com/expo/expo/pull/49539) by [@kudo](https://github.com/kudo))
|
|
28
|
+
|
|
13
29
|
## 57.0.14 — 2026-08-26
|
|
14
30
|
|
|
15
31
|
### 🎉 New features
|
package/android/build.gradle
CHANGED
|
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
group = 'host.exp.exponent'
|
|
30
|
-
version = '57.0.
|
|
30
|
+
version = '57.0.16'
|
|
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 "57.0.
|
|
97
|
+
versionName "57.0.16"
|
|
98
98
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
99
99
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
100
100
|
|
|
@@ -30,6 +30,32 @@ public:
|
|
|
30
30
|
return std::static_pointer_cast<std::string const>(this->flavor_)->c_str();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
static bool isRNHostView(const react::Props::Shared &props) {
|
|
34
|
+
// Currently, only RNHostView can have `sizesToContent` set to true.
|
|
35
|
+
return ShadowNodeType::sizesToContent(props);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
std::shared_ptr<facebook::react::ShadowNode> createShadowNode(
|
|
39
|
+
const facebook::react::ShadowNodeFragment &fragment,
|
|
40
|
+
const facebook::react::ShadowNodeFamily::Shared &family
|
|
41
|
+
) const override {
|
|
42
|
+
if (!isRNHostView(fragment.props)) {
|
|
43
|
+
return facebook::react::ConcreteComponentDescriptor<ShadowNodeType>::createShadowNode(
|
|
44
|
+
fragment, family);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Treat RNHostView as a leaf node and measurable node in Yoga, so that it can be measured by its children.
|
|
48
|
+
auto traits = this->getTraits();
|
|
49
|
+
traits.set(facebook::react::ShadowNodeTraits::Trait::LeafYogaNode);
|
|
50
|
+
traits.set(facebook::react::ShadowNodeTraits::Trait::MeasurableYogaNode);
|
|
51
|
+
|
|
52
|
+
auto shadowNode = std::make_shared<ShadowNodeType>(fragment, family, traits);
|
|
53
|
+
|
|
54
|
+
this->adopt(*shadowNode);
|
|
55
|
+
|
|
56
|
+
return shadowNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
void adopt(facebook::react::ShadowNode &shadowNode) const override {
|
|
34
60
|
react_native_assert(dynamic_cast<ShadowNodeType *>(&shadowNode));
|
|
35
61
|
|
|
@@ -81,6 +107,21 @@ public:
|
|
|
81
107
|
// Updates yoga style from props and sets the node dirty
|
|
82
108
|
snode->updateYogaProps();
|
|
83
109
|
}
|
|
110
|
+
|
|
111
|
+
if (isRNHostView(snode->getProps())) {
|
|
112
|
+
auto const &props = *std::static_pointer_cast<const facebook::react::ViewProps>(
|
|
113
|
+
snode->getProps());
|
|
114
|
+
auto &style = const_cast<facebook::yoga::Style &>(props.yogaStyle);
|
|
115
|
+
|
|
116
|
+
// If RNHostView has align self set to auto or stretch, we should override it to flex-start so that the node can size itself to its content
|
|
117
|
+
auto const alignSelf = style.alignSelf();
|
|
118
|
+
|
|
119
|
+
if (alignSelf == facebook::yoga::Align::Auto || alignSelf == facebook::yoga::Align::Stretch) {
|
|
120
|
+
style.setAlignSelf(facebook::yoga::Align::FlexStart);
|
|
121
|
+
snode->updateYogaProps();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
84
125
|
facebook::react::ConcreteComponentDescriptor<ShadowNodeType>::adopt(shadowNode);
|
|
85
126
|
}
|
|
86
127
|
};
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
#ifdef __cplusplus
|
|
6
6
|
|
|
7
7
|
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
|
|
8
|
+
#include <react/renderer/core/LayoutConstraints.h>
|
|
9
|
+
#include <react/renderer/core/LayoutContext.h>
|
|
10
|
+
#include <react/renderer/core/LayoutableShadowNode.h>
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
8
13
|
|
|
9
14
|
#include "ContentOriginRegistry.h"
|
|
10
15
|
#include "ExpoViewEventEmitter.h"
|
|
@@ -77,7 +82,112 @@ public:
|
|
|
77
82
|
return {.x = contentOrigin->x - ownOrigin.x, .y = contentOrigin->y - ownOrigin.y};
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
// Currently, only `RNHostView` declares `expoInternalSizeFromChildren`
|
|
86
|
+
static bool sizesToContent(const react::Props::Shared &props) {
|
|
87
|
+
auto const *viewProps = dynamic_cast<const ExpoViewProps *>(props.get());
|
|
88
|
+
|
|
89
|
+
if (viewProps == nullptr) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
auto const it = viewProps->propsMap.find("expoInternalSizeFromChildren");
|
|
94
|
+
return it != viewProps->propsMap.end() && it->second.isBool() && it->second.getBool();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Yoga calls this method for RNHostView when it has matchContents set
|
|
98
|
+
react::Size measureContent(
|
|
99
|
+
const react::LayoutContext &layoutContext,
|
|
100
|
+
const react::LayoutConstraints &layoutConstraints
|
|
101
|
+
) const override {
|
|
102
|
+
// Return default behavior when RNHostView does not have `sizesToContent` set to true
|
|
103
|
+
if (!sizesToContent(this->getProps())) {
|
|
104
|
+
return ConcreteViewShadowNode::measureContent(layoutContext, layoutConstraints);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
auto const *content = hostedContent();
|
|
108
|
+
|
|
109
|
+
if (content == nullptr) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return content->measure(layoutContext, hostedContentConstraints(*content));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// We override this so RNHostView can lay out it's children
|
|
117
|
+
// We marked it as a Leaf node so we need to manually lay out the hosted content
|
|
118
|
+
void layout(react::LayoutContext layoutContext) override {
|
|
119
|
+
ConcreteViewShadowNode::layout(layoutContext);
|
|
120
|
+
|
|
121
|
+
if (!sizesToContent(this->getProps())) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
auto const *content = hostedContent();
|
|
126
|
+
|
|
127
|
+
if (content == nullptr) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Use the same constraint that was used to measure the content, so that the layout is consistent with the measurement
|
|
132
|
+
auto const clonedContent = content->clone({});
|
|
133
|
+
static_cast<react::LayoutableShadowNode &>(*clonedContent).layoutTree(
|
|
134
|
+
layoutContext,
|
|
135
|
+
hostedContentConstraints(*content)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
this->replaceChild(*content, clonedContent, 0);
|
|
139
|
+
|
|
140
|
+
if (layoutContext.affectedNodes != nullptr) {
|
|
141
|
+
layoutContext.affectedNodes->push_back(
|
|
142
|
+
static_cast<const react::LayoutableShadowNode *>(clonedContent.get()));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
80
146
|
private:
|
|
147
|
+
const react::LayoutableShadowNode *hostedContent() const {
|
|
148
|
+
auto const &children = this->getChildren();
|
|
149
|
+
|
|
150
|
+
return children.empty()
|
|
151
|
+
? nullptr
|
|
152
|
+
: dynamic_cast<const react::LayoutableShadowNode *>(children.front().get());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
react::LayoutDirection resolvedLayoutDirection() const {
|
|
156
|
+
return YGNodeLayoutGetDirection(&this->yogaNode_) == YGDirectionRTL
|
|
157
|
+
? react::LayoutDirection::RightToLeft
|
|
158
|
+
: react::LayoutDirection::LeftToRight;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
react::LayoutConstraints hostedContentConstraints(const react::ShadowNode &content) const {
|
|
162
|
+
react::LayoutConstraints constraints{};
|
|
163
|
+
constraints.layoutDirection = resolvedLayoutDirection();
|
|
164
|
+
|
|
165
|
+
auto const *contentProps = dynamic_cast<const react::ViewProps *>(content.getProps().get());
|
|
166
|
+
|
|
167
|
+
if (contentProps == nullptr) {
|
|
168
|
+
return constraints;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
auto const &style = contentProps->yogaStyle;
|
|
172
|
+
|
|
173
|
+
constrainToPoints(style.minDimension(facebook::yoga::Dimension::Width),
|
|
174
|
+
constraints.minimumSize.width);
|
|
175
|
+
constrainToPoints(style.minDimension(facebook::yoga::Dimension::Height),
|
|
176
|
+
constraints.minimumSize.height);
|
|
177
|
+
constrainToPoints(style.maxDimension(facebook::yoga::Dimension::Width),
|
|
178
|
+
constraints.maximumSize.width);
|
|
179
|
+
constrainToPoints(style.maxDimension(facebook::yoga::Dimension::Height),
|
|
180
|
+
constraints.maximumSize.height);
|
|
181
|
+
|
|
182
|
+
return constraints;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static void constrainToPoints(facebook::yoga::StyleSizeLength length, react::Float &constraint) {
|
|
186
|
+
if (length.isPoints() && length.value().isDefined()) {
|
|
187
|
+
constraint = std::max<react::Float>(0, length.value().unwrap());
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
81
191
|
void initialize() noexcept {
|
|
82
192
|
auto &viewProps = static_cast<const ExpoViewProps &>(*this->props_);
|
|
83
193
|
|
|
@@ -724,12 +724,14 @@ public final class AppContext: NSObject, EXAppContextProtocol, @unchecked Sendab
|
|
|
724
724
|
*/
|
|
725
725
|
@objc
|
|
726
726
|
public static func modulesProvider(withName providerName: String = "ExpoModulesProvider") -> ModulesProvider {
|
|
727
|
-
// [0] When ExpoModulesCore is built as
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
727
|
+
// [0] When ExpoModulesCore is built as a separate framework/module,
|
|
728
|
+
// explicitly load the main bundle's `ExpoModulesProvider` class.
|
|
729
|
+
// `ExpoModulesProviderModuleName` is an internal key that allows repack-app to
|
|
730
|
+
// preserve the original Swift module name. Try `CFBundleExecutable` next because
|
|
731
|
+
// it usually matches the Swift module name. Keep `CFBundleName` as a final fallback
|
|
732
|
+
// for cases where it is also a valid module identifier.
|
|
732
733
|
let mainBundleNames = [
|
|
734
|
+
Bundle.main.infoDictionary?["ExpoModulesProviderModuleName"],
|
|
733
735
|
Bundle.main.infoDictionary?["CFBundleExecutable"],
|
|
734
736
|
Bundle.main.infoDictionary?["CFBundleName"]
|
|
735
737
|
].compactMap { $0 as? String }
|
|
@@ -34,6 +34,23 @@ public struct Conversions {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
Compares two prop values for equality, to tell an actual prop change from a re-delivery of the
|
|
39
|
+
same value.
|
|
40
|
+
*/
|
|
41
|
+
static func areValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
|
42
|
+
switch (lhs, rhs) {
|
|
43
|
+
case (nil, nil):
|
|
44
|
+
return true
|
|
45
|
+
case let (lhsValue as AnyHashable, rhsValue as AnyHashable):
|
|
46
|
+
return lhsValue == rhsValue
|
|
47
|
+
case let (lhsValue as NSObjectProtocol, rhsValue as NSObjectProtocol):
|
|
48
|
+
return lhsValue.isEqual(rhsValue)
|
|
49
|
+
default:
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
static func fromNSObject(_ object: Any) -> Any {
|
|
38
55
|
switch object {
|
|
39
56
|
case let object as NSArray:
|
|
@@ -47,9 +47,31 @@ extension ExpoSwiftUI {
|
|
|
47
47
|
*/
|
|
48
48
|
public let globalEventDispatcher = EventDispatcher(GLOBAL_EVENT_NAME)
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
A dictionary to store previous raw prop values for change detection.
|
|
52
|
+
*/
|
|
53
|
+
private var previousRawProps: [String: Any] = [:]
|
|
54
|
+
|
|
50
55
|
internal func updateRawProps(_ rawProps: [String: Any], appContext: AppContext) throws {
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
try fieldsOf(self).forEach { field in
|
|
57
|
+
guard let key = field.key else {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
guard rawProps.keys.contains(key) else {
|
|
61
|
+
if field.isRequired {
|
|
62
|
+
try field.set(nil, appContext: appContext)
|
|
63
|
+
}
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
let newValue = rawProps[key]
|
|
67
|
+
let previousValue = previousRawProps[key]
|
|
68
|
+
|
|
69
|
+
if !Conversions.areValuesEqual(previousValue, newValue) {
|
|
70
|
+
try field.set(newValue, appContext: appContext)
|
|
71
|
+
|
|
72
|
+
previousRawProps[key] = newValue
|
|
73
|
+
}
|
|
74
|
+
}
|
|
53
75
|
|
|
54
76
|
// Notify subscribed views about the change to re-render them.
|
|
55
77
|
objectWillChange.send()
|
|
@@ -91,7 +91,7 @@ open class ExpoFabricView: ExpoFabricViewObjC, AnyExpoView {
|
|
|
91
91
|
let previousValue = previousProps[key]
|
|
92
92
|
|
|
93
93
|
// only set the prop if the value has changed
|
|
94
|
-
if !areValuesEqual(previousValue, convertedNewValue) {
|
|
94
|
+
if !Conversions.areValuesEqual(previousValue, convertedNewValue) {
|
|
95
95
|
// TODO: @tsapeta: Figure out better way to rethrow errors from here.
|
|
96
96
|
// Adding `throws` keyword to the function results in different
|
|
97
97
|
// method signature in Objective-C. Maybe just call `RCTLogError`?
|
|
@@ -102,21 +102,6 @@ open class ExpoFabricView: ExpoFabricViewObjC, AnyExpoView {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
Helper function to compare two values for equality using string representation.
|
|
107
|
-
*/
|
|
108
|
-
private func areValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
|
109
|
-
switch (lhs, rhs) {
|
|
110
|
-
case (nil, nil):
|
|
111
|
-
return true
|
|
112
|
-
case let (lhsValue as AnyHashable, rhsValue as AnyHashable):
|
|
113
|
-
return lhsValue == rhsValue
|
|
114
|
-
case let (lhsValue as NSObjectProtocol, rhsValue as NSObjectProtocol):
|
|
115
|
-
return lhsValue.isEqual(rhsValue)
|
|
116
|
-
default:
|
|
117
|
-
return false
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
105
|
/**
|
|
121
106
|
Calls lifecycle methods registered by `OnViewDidUpdateProps` definition component.
|
|
122
107
|
*/
|
|
@@ -233,7 +233,7 @@ static jsi::Value callWorklet(jsi::Runtime &rt, std::shared_ptr<worklets::Serial
|
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
workletRuntime->
|
|
236
|
+
workletRuntime->runSync([worklet, arguments](jsi::Runtime &rt) -> jsi::Value {
|
|
237
237
|
return callWorklet(rt, worklet, arguments);
|
|
238
238
|
});
|
|
239
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "57.0.
|
|
3
|
+
"version": "57.0.16",
|
|
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.6.1",
|
|
50
|
-
"expo-modules-jsi": "~57.0.
|
|
50
|
+
"expo-modules-jsi": "~57.0.8",
|
|
51
51
|
"invariant": "^2.2.4"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@types/invariant": "^2.2.33",
|
|
67
67
|
"expo-module-scripts": "56.0.3"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "d36f125067e9c6e67689f2b75cf4fb19254c3c59",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|