expo-modules-core 57.0.13 → 57.0.14

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,17 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 57.0.14 — 2026-08-26
14
+
15
+ ### 🎉 New features
16
+
17
+ - Added `getNativeRef()` to views created with `requireNativeViewManager`, which returns the underlying host component. ([#48788](https://github.com/expo/expo/pull/48788) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
18
+
19
+ ### 🐛 Bug fixes
20
+
21
+ - [iOS] Measure hosted React Native views where SwiftUI placed them, instead of at their Yoga box. ([#48969](https://github.com/expo/expo/pull/48969) by [@nishan](https://github.com/intergalacticspacehighway))
22
+ - [Android] Measure hosted React Native views where Jetpack Compose placed them, instead of at their Yoga box. ([#48970](https://github.com/expo/expo/pull/48970) by [@nishan](https://github.com/intergalacticspacehighway))
23
+
13
24
  ## 57.0.13 — 2026-08-24
14
25
 
15
26
  ### 🐛 Bug fixes
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.13'
30
+ version = '57.0.14'
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.13"
97
+ versionName "57.0.14"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -1,5 +1,6 @@
1
1
  #include "NativeStatePropsGetter.h"
2
2
  #include "AndroidExpoViewState.h"
3
+ #include "ContentOriginRegistry.h"
3
4
  #include <react/fabric/StateWrapperImpl.h>
4
5
 
5
6
  #include <react/renderer/core/ConcreteState.h>
@@ -11,6 +12,9 @@ void NativeStatePropsGetter::registerNatives() {
11
12
  makeNativeMethod("getStateProps", NativeStatePropsGetter::getStateProps),
12
13
  makeNativeMethod("updateStyleSizeImmediateImpl", NativeStatePropsGetter::updateStyleSizeImmediate),
13
14
  makeNativeMethod("updateViewSizeImmediateImpl", NativeStatePropsGetter::updateViewSizeImmediate),
15
+ makeNativeMethod("setContentOriginImpl", NativeStatePropsGetter::setContentOrigin),
16
+ makeNativeMethod("clearContentOriginImpl", NativeStatePropsGetter::clearContentOrigin),
17
+ makeNativeMethod("clearAllContentOriginsImpl", NativeStatePropsGetter::clearAllContentOrigins),
14
18
  });
15
19
  }
16
20
 
@@ -57,6 +61,31 @@ void NativeStatePropsGetter::updateViewSizeImmediate(
57
61
  state->updateState(std::move(newState), react::EventQueue::UpdateMode::unstable_Immediate);
58
62
  }
59
63
 
64
+ void NativeStatePropsGetter::setContentOrigin(
65
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
66
+ jint tag,
67
+ jdouble x,
68
+ jdouble y
69
+ ) {
70
+ ContentOriginRegistry::set(
71
+ static_cast<react::Tag>(tag),
72
+ react::Point{.x = static_cast<react::Float>(x), .y = static_cast<react::Float>(y)}
73
+ );
74
+ }
75
+
76
+ void NativeStatePropsGetter::clearContentOrigin(
77
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
78
+ jint tag
79
+ ) {
80
+ ContentOriginRegistry::clear(static_cast<react::Tag>(tag));
81
+ }
82
+
83
+ void NativeStatePropsGetter::clearAllContentOrigins(
84
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self
85
+ ) {
86
+ ContentOriginRegistry::clearAll();
87
+ }
88
+
60
89
  jni::local_ref<jni::JMap<jstring, jobject>> NativeStatePropsGetter::getStateProps(
61
90
  jni::alias_ref<NativeStatePropsGetter::javaobject> self,
62
91
  jni::alias_ref<jobject> stateWrapper
@@ -35,6 +35,23 @@ public:
35
35
  jdouble width,
36
36
  jdouble height
37
37
  );
38
+
39
+ // Publishes where Compose actually placed a hosted view inside its `Host`, which Yoga cannot see.
40
+ static void setContentOrigin(
41
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
42
+ jint tag,
43
+ jdouble x,
44
+ jdouble y
45
+ );
46
+
47
+ static void clearContentOrigin(
48
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self,
49
+ jint tag
50
+ );
51
+
52
+ static void clearAllContentOrigins(
53
+ jni::alias_ref<NativeStatePropsGetter::javaobject> self
54
+ );
38
55
  };
39
56
 
40
57
  } // namespace expo
@@ -24,6 +24,29 @@ class NativeStatePropsGetter {
24
24
  updateViewSizeImmediateImpl(stateWrapper, width, height)
25
25
  }
26
26
 
27
+ /**
28
+ * Publishes where the native layout system placed a hosted view inside its `Host`, keyed by the
29
+ * view's tag. Read while measuring, so `measure()` reports the position the content occupies
30
+ * rather than its Yoga box.
31
+ */
32
+ fun setContentOrigin(tag: Int, x: Double, y: Double) {
33
+ setContentOriginImpl(tag, x, y)
34
+ }
35
+
36
+ fun clearContentOrigin(tag: Int) {
37
+ clearContentOriginImpl(tag)
38
+ }
39
+
40
+ fun clearAllContentOrigins() {
41
+ clearAllContentOriginsImpl()
42
+ }
43
+
44
+ private external fun setContentOriginImpl(tag: Int, x: Double, y: Double)
45
+
46
+ private external fun clearContentOriginImpl(tag: Int)
47
+
48
+ private external fun clearAllContentOriginsImpl()
49
+
27
50
  private external fun updateStyleSizeImmediateImpl(
28
51
  stateWrapper: Any,
29
52
  styleWidth: Double,
@@ -28,6 +28,20 @@ class ShadowNodeProxy(expoView: ExpoView) {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * Reports where the native layout system drew this view inside its `Host`, in dp relative to the
33
+ * `Host`'s origin.
34
+ */
35
+ fun setContentOrigin(x: Double, y: Double) {
36
+ val view = weakExpoView.get() ?: return
37
+ stateUpdater.setContentOrigin(view.id, x, y)
38
+ }
39
+
40
+ fun clearContentOrigin() {
41
+ val view = weakExpoView.get() ?: return
42
+ stateUpdater.clearContentOrigin(view.id)
43
+ }
44
+
31
45
  private fun scheduleFlush(flush: (stateWrapper: Any) -> Unit) {
32
46
  pendingFlush = flush
33
47
  val view = weakExpoView.get() ?: return
@@ -1 +1 @@
1
- {"version":3,"file":"NativeViewManagerAdapter.native.d.ts","sourceRoot":"","sources":["../src/NativeViewManagerAdapter.native.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAA4B,MAAM,OAAO,CAAC;AA4HrF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAAC,CAAC,CAAC,CAsClB"}
1
+ {"version":3,"file":"NativeViewManagerAdapter.native.d.ts","sourceRoot":"","sources":["../src/NativeViewManagerAdapter.native.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAA4B,MAAM,OAAO,CAAC;AA4HrF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAAC,CAAC,CAAC,CA6ClB"}
@@ -0,0 +1,63 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ #pragma once
4
+
5
+ #ifdef __cplusplus
6
+
7
+ #include <mutex>
8
+ #include <optional>
9
+ #include <unordered_map>
10
+
11
+ #include <react/renderer/core/ReactPrimitives.h>
12
+ #include <react/renderer/graphics/Point.h>
13
+
14
+ namespace expo {
15
+
16
+ /**
17
+ Where the native layout system drew a view's contents, keyed by React tag.
18
+
19
+ This sits outside the shadow tree on purpose. `ExpoViewState` would be the idiomatic home, but a
20
+ state update enqueues a commit, and the origin changes on every native layout pass — a commit per
21
+ pass can re-run layout, move the origin, and commit again. Writing here schedules no work.
22
+ */
23
+ class ContentOriginRegistry {
24
+ public:
25
+ static void set(facebook::react::Tag tag, facebook::react::Point origin) {
26
+ std::lock_guard<std::mutex> lock(mutex());
27
+ storage()[tag] = origin;
28
+ }
29
+
30
+ static void clear(facebook::react::Tag tag) {
31
+ std::lock_guard<std::mutex> lock(mutex());
32
+ storage().erase(tag);
33
+ }
34
+
35
+ static void clearAll() {
36
+ std::lock_guard<std::mutex> lock(mutex());
37
+ storage().clear();
38
+ }
39
+
40
+ static std::optional<facebook::react::Point> find(facebook::react::Tag tag) {
41
+ std::lock_guard<std::mutex> lock(mutex());
42
+ auto it = storage().find(tag);
43
+ if (it == storage().end()) {
44
+ return std::nullopt;
45
+ }
46
+ return it->second;
47
+ }
48
+
49
+ private:
50
+ static std::mutex &mutex() {
51
+ static std::mutex instance;
52
+ return instance;
53
+ }
54
+
55
+ static std::unordered_map<facebook::react::Tag, facebook::react::Point> &storage() {
56
+ static std::unordered_map<facebook::react::Tag, facebook::react::Point> instance;
57
+ return instance;
58
+ }
59
+ };
60
+
61
+ } // namespace expo
62
+
63
+ #endif // __cplusplus
@@ -6,6 +6,7 @@
6
6
 
7
7
  #include <react/renderer/components/view/ConcreteViewShadowNode.h>
8
8
 
9
+ #include "ContentOriginRegistry.h"
9
10
  #include "ExpoViewEventEmitter.h"
10
11
  #include "ExpoViewProps.h"
11
12
  #include "ExpoViewState.h"
@@ -51,6 +52,31 @@ public:
51
52
  return traits;
52
53
  }
53
54
 
55
+ /**
56
+ Used by `RNHostView`. Reports where this view's contents were actually drawn, for views laid
57
+ out by SwiftUI or Compose instead of Yoga. Without it `measure()` returns this node's Yoga box,
58
+ which is not where the hosted content ended up.
59
+ */
60
+ facebook::react::Point getContentOriginOffset(bool includeTransform) const override {
61
+ // A `false` caller is asking for a scroll offset, which this view never has. Only the layout
62
+ // path passes `true`, and that is the one this override is for.
63
+ if (!includeTransform) {
64
+ return ConcreteViewShadowNode::getContentOriginOffset(includeTransform);
65
+ }
66
+
67
+ auto contentOrigin = ContentOriginRegistry::find(this->getTag());
68
+ if (!contentOrigin) {
69
+ return ConcreteViewShadowNode::getContentOriginOffset(includeTransform);
70
+ }
71
+
72
+ // `computeRelativeLayoutMetrics` adds this node's own Yoga origin before consulting us,
73
+ // so subtract them as we only want content origin to be considered.
74
+ // Publish only the part Yoga cannot see: where
75
+ // the native layout system placed the content inside its host.
76
+ auto ownOrigin = this->getLayoutMetrics().frame.origin;
77
+ return {.x = contentOrigin->x - ownOrigin.x, .y = contentOrigin->y - ownOrigin.y};
78
+ }
79
+
54
80
  private:
55
81
  void initialize() noexcept {
56
82
  auto &viewProps = static_cast<const ExpoViewProps &>(*this->props_);
@@ -12,5 +12,8 @@ extension ExpoSwiftUI {
12
12
 
13
13
  public var setViewSize: ((CGSize) -> Void)?
14
14
  public var setStyleSize: ((NSNumber?, NSNumber?) -> Void)?
15
+ // Where the native layout system drew this view's contents, relative to its host.
16
+ public var setContentOrigin: ((CGPoint) -> Void)?
17
+ public var clearContentOrigin: (() -> Void)?
15
18
  }
16
19
  }
@@ -51,6 +51,12 @@ extension ExpoSwiftUI {
51
51
  props.shadowNodeProxy.setStyleSize = { [weak self] width, height in
52
52
  self?.setStyleSize(width, height: height)
53
53
  }
54
+ props.shadowNodeProxy.setContentOrigin = { [weak self] origin in
55
+ self?.setContentOrigin(origin)
56
+ }
57
+ props.shadowNodeProxy.clearContentOrigin = { [weak self] in
58
+ self?.clearContentOrigin()
59
+ }
54
60
 
55
61
  installEventDispatchers()
56
62
  }
@@ -213,6 +219,12 @@ extension ExpoSwiftUI {
213
219
  props.shadowNodeProxy.setStyleSize = { [weak self] width, height in
214
220
  self?.setStyleSize(width, height: height)
215
221
  }
222
+ props.shadowNodeProxy.setContentOrigin = { [weak self] origin in
223
+ self?.setContentOrigin(origin)
224
+ }
225
+ props.shadowNodeProxy.clearContentOrigin = { [weak self] in
226
+ self?.clearContentOrigin()
227
+ }
216
228
 
217
229
  installEventDispatchers()
218
230
  }
@@ -14,6 +14,15 @@ NS_ASSUME_NONNULL_BEGIN
14
14
  @property (nonatomic) NSInteger tag;
15
15
  @property (nonatomic, copy, nullable) NSString *componentName;
16
16
 
17
+ /**
18
+ The tag this view last published a content origin under.
19
+
20
+ React Native zeroes `tag` when it recycles a component view, and it does so *before* calling
21
+ `prepareForRecycle` or `invalidate`, so a teardown hook cannot use `tag` to find its own registry
22
+ entry. Remembering it here is what lets the entry be removed rather than leaked.
23
+ */
24
+ @property (nonatomic) NSInteger publishedContentOriginTag;
25
+
17
26
  - (void)dispatchEvent:(nonnull NSString *)eventName payload:(nullable id)payload;
18
27
 
19
28
  - (void)updateProps:(nonnull NSDictionary<NSString *, id> *)props NS_SWIFT_UI_ACTOR;
@@ -24,6 +33,14 @@ NS_ASSUME_NONNULL_BEGIN
24
33
 
25
34
  - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height;
26
35
 
36
+ /**
37
+ Publishes where SwiftUI drew this view's contents, relative to its host, so `measure()` matches
38
+ what is on screen. Pass `CGPointZero`-equivalent by calling `clearContentOrigin`.
39
+ */
40
+ - (void)setContentOrigin:(CGPoint)contentOrigin;
41
+
42
+ - (void)clearContentOrigin;
43
+
27
44
  - (BOOL)supportsPropWithName:(nonnull NSString *)name;
28
45
 
29
46
  /*
@@ -17,6 +17,15 @@ NS_ASSUME_NONNULL_BEGIN
17
17
  // `tag` is inherited from UIView
18
18
  @property (nonatomic, copy, nullable) NSString *componentName;
19
19
 
20
+ /**
21
+ The tag this view last published a content origin under.
22
+
23
+ React Native zeroes `tag` when it recycles a component view, and it does so *before* calling
24
+ `prepareForRecycle` or `invalidate`, so a teardown hook cannot use `tag` to find its own registry
25
+ entry. Remembering it here is what lets the entry be removed rather than leaked.
26
+ */
27
+ @property (nonatomic) NSInteger publishedContentOriginTag;
28
+
20
29
  - (void)dispatchEvent:(nonnull NSString *)eventName payload:(nullable id)payload;
21
30
 
22
31
  - (void)updateProps:(nonnull NSDictionary<NSString *, id> *)props NS_SWIFT_UI_ACTOR;
@@ -27,6 +36,10 @@ NS_ASSUME_NONNULL_BEGIN
27
36
 
28
37
  - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height;
29
38
 
39
+ - (void)setContentOrigin:(CGPoint)contentOrigin;
40
+
41
+ - (void)clearContentOrigin;
42
+
30
43
  - (BOOL)supportsPropWithName:(nonnull NSString *)name;
31
44
 
32
45
  /*
@@ -98,6 +98,7 @@ namespace react = facebook::react;
98
98
  - (void)prepareForRecycle
99
99
  {
100
100
  // Default implementation does nothing.
101
+ [self clearContentOrigin];
101
102
  _eventEmitter.reset();
102
103
  }
103
104
 
@@ -186,6 +187,23 @@ namespace react = facebook::react;
186
187
  }
187
188
  }
188
189
 
190
+ - (void)setContentOrigin:(CGPoint)contentOrigin
191
+ {
192
+ self.publishedContentOriginTag = self.tag;
193
+ expo::ContentOriginRegistry::set(
194
+ (react::Tag)self.tag,
195
+ react::Point{.x = (react::Float)contentOrigin.x, .y = (react::Float)contentOrigin.y});
196
+ }
197
+
198
+ - (void)clearContentOrigin
199
+ {
200
+ // Not `self.tag`: by the time a teardown hook runs, React Native has already zeroed it.
201
+ if (self.publishedContentOriginTag != 0) {
202
+ expo::ContentOriginRegistry::clear((react::Tag)self.publishedContentOriginTag);
203
+ self.publishedContentOriginTag = 0;
204
+ }
205
+ }
206
+
189
207
  - (BOOL)supportsPropWithName:(nonnull NSString *)name
190
208
  {
191
209
  // Implemented in `SwiftUIVirtualView.swift`
@@ -0,0 +1,17 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ NS_SWIFT_NAME(ContentOriginRegistry)
8
+ @interface EXContentOriginRegistry : NSObject
9
+
10
+ /**
11
+ Drops every published content origin
12
+ */
13
+ + (void)clearAll;
14
+
15
+ @end
16
+
17
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,13 @@
1
+ // Copyright 2025-present 650 Industries. All rights reserved.
2
+
3
+ #import <ExpoModulesCore/EXContentOriginRegistry.h>
4
+ #import <ExpoModulesCore/ContentOriginRegistry.h>
5
+
6
+ @implementation EXContentOriginRegistry
7
+
8
+ + (void)clearAll
9
+ {
10
+ expo::ContentOriginRegistry::clearAll();
11
+ }
12
+
13
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.13",
3
+ "version": "57.0.14",
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.5",
50
+ "expo-modules-jsi": "~57.0.6",
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": "52fc013ec8c75e89ba8f83c167e4b6b0416e6bf7",
69
+ "gitHead": "c300d2cc60c9e684e64f48d9bc90ea18a571d01d",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",
@@ -150,6 +150,13 @@ export function requireNativeViewManager<P>(
150
150
  this.nativeTag = findNodeHandle(this.nativeRef.current);
151
151
  }
152
152
 
153
+ /**
154
+ * The underlying host component
155
+ */
156
+ getNativeRef(): (Component & ReactNativeElement) | null {
157
+ return this.nativeRef.current;
158
+ }
159
+
153
160
  render() {
154
161
  return <ReactNativeComponent {...this.props} ref={this.nativeRef} />;
155
162
  }