expo-modules-core 57.0.14 → 57.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,18 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.15 — 2026-09-01
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [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))
18
+ - [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))
19
+
20
+ ### 💡 Others
21
+
22
+ - 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))
23
+ - Added internal `ExpoModulesProviderModuleName` lookup key for `ExpoModulesProvider` class. ([#49539](https://github.com/expo/expo/pull/49539) by [@kudo](https://github.com/kudo))
24
+
13
25
  ## 57.0.14 — 2026-08-26
14
26
 
15
27
  ### 🎉 New features
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.14'
30
+ version = '57.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 "57.0.14"
97
+ versionName "57.0.15"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -24,10 +24,7 @@ namespace expo {
24
24
  return;
25
25
  }
26
26
 
27
- workletRuntime->executeSync([func = std::move(func)](jsi::Runtime &rt) -> jsi::Value {
28
- func(rt);
29
- return jsi::Value::undefined();
30
- });
27
+ workletRuntime->runSync(func);
31
28
  }
32
29
  } // namespace expo
33
30
 
@@ -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 separated framework/module,
728
- // we should explicitly load main bundle's `ExpoModulesProvider` class.
729
- // CFBundleExecutable is tried first: it is the product name, from which the Swift module
730
- // name is derived. CFBundleName is kept as a fallback for the uncommon case where both
731
- // values are identical valid identifiers.
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
- // Update the props just like the records
52
- try update(withDict: rawProps, appContext: appContext)
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->executeSync([worklet, arguments](jsi::Runtime &rt) -> jsi::Value {
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.14",
3
+ "version": "57.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.6.1",
50
- "expo-modules-jsi": "~57.0.6",
50
+ "expo-modules-jsi": "~57.0.7",
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": "c300d2cc60c9e684e64f48d9bc90ea18a571d01d",
69
+ "gitHead": "b76ecf192c5325793bcea31dd9bb8efd91f9ad7f",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",