expo-modules-core 3.0.18 → 3.0.20

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,19 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 3.0.20 — 2025-10-01
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fix NSURL to JSIString conversion returning nil. ([#39567](https://github.com/expo/expo/pull/39567) by [@behenate](https://github.com/behenate))
18
+ - Fix SharedObject created with `useReleasingSharedObject` getting destroyed after a fast refresh caused by a change in its dependencies. ([#39753](https://github.com/expo/expo/pull/39753) by [@behenate](https://github.com/behenate))
19
+
20
+ ## 3.0.19 — 2025-10-01
21
+
22
+ ### 💡 Others
23
+
24
+ - [ios] - Set host dimension synchronously on native ([#40017](https://github.com/expo/expo/pull/40017) by [@nishan](https://github.com/intergalacticspacehighway))
25
+
13
26
  ## 3.0.18 — 2025-09-22
14
27
 
15
28
  ### 🐛 Bug fixes
@@ -25,6 +38,7 @@ _This version does not introduce any user-facing changes._
25
38
  ### 🎉 New features
26
39
 
27
40
  - [Android] Starts using precompiled headers to improve build times. ([#39641](https://github.com/expo/expo/pull/39641) by [@lukmccall](https://github.com/lukmccall))
41
+ - Remove `ExpoAppDelegate` inheritance requirement in ExpoReactNativeFactory ([#39844](https://github.com/expo/expo/pull/39844) by [@gabrieldonadel](https://github.com/gabrieldonadel))
28
42
 
29
43
  ### 🐛 Bug fixes
30
44
 
@@ -29,7 +29,7 @@ if (shouldIncludeCompose) {
29
29
  }
30
30
 
31
31
  group = 'host.exp.exponent'
32
- version = '3.0.18'
32
+ version = '3.0.20'
33
33
 
34
34
  def isExpoModulesCoreTests = {
35
35
  Gradle gradle = getGradle()
@@ -79,7 +79,7 @@ android {
79
79
  defaultConfig {
80
80
  consumerProguardFiles 'proguard-rules.pro'
81
81
  versionCode 1
82
- versionName "3.0.18"
82
+ versionName "3.0.20"
83
83
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
84
84
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString()
85
85
 
@@ -1 +1 @@
1
- {"version":3,"file":"useReleasingSharedObject.d.ts","sourceRoot":"","sources":["../../src/hooks/useReleasingSharedObject.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAA8B,MAAM,OAAO,CAAC;AAEnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,SAAS,YAAY,EACzE,OAAO,EAAE,MAAM,aAAa,EAC5B,YAAY,EAAE,cAAc,GAC3B,aAAa,CAwCf"}
1
+ {"version":3,"file":"useReleasingSharedObject.d.ts","sourceRoot":"","sources":["../../src/hooks/useReleasingSharedObject.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAA8B,MAAM,OAAO,CAAC;AAEnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,SAAS,YAAY,EACzE,OAAO,EAAE,MAAM,aAAa,EAC5B,YAAY,EAAE,cAAc,GAC3B,aAAa,CA0Cf"}
@@ -44,6 +44,34 @@ void ExpoViewComponentDescriptor::adopt(facebook::react::ShadowNode &shadowNode)
44
44
 
45
45
  snode->setSize({width, height});
46
46
  }
47
+
48
+ // handle layout style prop update
49
+ auto styleWidth = state._styleWidth;
50
+ auto styleHeight = state._styleHeight;
51
+
52
+ if (!isnan(styleWidth) || !isnan(styleHeight)) {
53
+ auto const &props = *std::static_pointer_cast<const facebook::react::ViewProps>(snode->getProps());
54
+
55
+ auto& style = const_cast<facebook::yoga::Style&>(props.yogaStyle);
56
+ bool changedStyle = false;
57
+
58
+ if (!isnan(styleWidth)) {
59
+ style.setDimension(facebook::yoga::Dimension::Width, facebook::yoga::StyleSizeLength::points(styleWidth));
60
+ changedStyle = true;
61
+ }
62
+
63
+ if (!isnan(styleHeight)) {
64
+ style.setDimension(facebook::yoga::Dimension::Height, facebook::yoga::StyleSizeLength::points(styleHeight));
65
+ changedStyle = true;
66
+ }
67
+
68
+ // Update yoga props and dirty layout if we changed the style
69
+ if (changedStyle) {
70
+ auto* expoNode = const_cast<ExpoViewShadowNode*>(snode);
71
+ expoNode->updateYogaProps();
72
+ expoNode->dirtyLayout();
73
+ }
74
+ }
47
75
  ConcreteComponentDescriptor::adopt(shadowNode);
48
76
  }
49
77
 
@@ -28,11 +28,28 @@ public:
28
28
  _height = std::numeric_limits<float>::quiet_NaN();
29
29
  }
30
30
  };
31
+
32
+ static ExpoViewState withStyleDimensions(float styleWidth, float styleHeight) {
33
+ ExpoViewState state;
34
+ if (styleWidth >= 0) {
35
+ state._styleWidth = styleWidth;
36
+ } else {
37
+ state._styleWidth = std::numeric_limits<float>::quiet_NaN();
38
+ }
39
+ if (styleHeight >= 0) {
40
+ state._styleHeight = styleHeight;
41
+ } else {
42
+ state._styleHeight = std::numeric_limits<float>::quiet_NaN();
43
+ }
44
+ return state;
45
+ }
31
46
 
32
47
  #ifdef ANDROID
33
48
  ExpoViewState(ExpoViewState const &previousState, folly::dynamic data)
34
49
  : _width((float)data["width"].getDouble()),
35
- _height((float)data["height"].getDouble()){};
50
+ _height((float)data["height"].getDouble()),
51
+ _styleWidth(data.count("styleWidth") ? (float)data["styleWidth"].getDouble() : std::numeric_limits<float>::quiet_NaN()),
52
+ _styleHeight(data.count("styleHeight") ? (float)data["styleHeight"].getDouble() : std::numeric_limits<float>::quiet_NaN()){};
36
53
  folly::dynamic getDynamic() const {
37
54
  return {};
38
55
  };
@@ -44,6 +61,8 @@ public:
44
61
 
45
62
  float _width = std::numeric_limits<float>::quiet_NaN();
46
63
  float _height = std::numeric_limits<float>::quiet_NaN();
64
+ float _styleWidth = std::numeric_limits<float>::quiet_NaN();
65
+ float _styleHeight = std::numeric_limits<float>::quiet_NaN();
47
66
 
48
67
  };
49
68
 
@@ -0,0 +1,12 @@
1
+ public protocol ExpoReactNativeFactoryProtocol: AnyObject {
2
+ /**
3
+ To decouple RCTAppDelegate dependency from expo-modules-core,
4
+ expo-modules-core doesn't include the concrete `RCTReactNativeFactory` type and let the callsite to include the type
5
+ */
6
+ func recreateRootView(
7
+ withBundleURL: URL?,
8
+ moduleName: String?,
9
+ initialProps: [AnyHashable: Any]?,
10
+ launchOptions: [AnyHashable: Any]?
11
+ ) -> UIView
12
+ }
@@ -81,6 +81,13 @@ extension ExpoSwiftUI {
81
81
  self.setViewSize(size)
82
82
  #endif
83
83
  }
84
+
85
+ shadowNodeProxy.setStyleSize = { width, height in
86
+ #if RCT_NEW_ARCH_ENABLED
87
+ self.setStyleSize(width, height: height)
88
+ #endif
89
+ }
90
+
84
91
  shadowNodeProxy.objectWillChange.send()
85
92
 
86
93
  #if os(iOS) || os(tvOS)
@@ -136,6 +143,12 @@ extension ExpoSwiftUI {
136
143
  return true
137
144
  }
138
145
 
146
+ public override func layoutSubviews() {
147
+ super.layoutSubviews()
148
+ // TODO: Use updateLayoutMetrics from RN. Add support in ExpoFabricView.
149
+ setupHostingViewConstraints()
150
+ }
151
+
139
152
  #if RCT_NEW_ARCH_ENABLED
140
153
  /**
141
154
  Fabric calls this function when mounting (attaching) a child component view.
@@ -187,14 +200,13 @@ extension ExpoSwiftUI {
187
200
  guard let view = hostingController.view as UIView? else {
188
201
  return
189
202
  }
190
- view.translatesAutoresizingMaskIntoConstraints = false
191
-
192
- NSLayoutConstraint.activate([
193
- view.topAnchor.constraint(equalTo: topAnchor),
194
- view.bottomAnchor.constraint(equalTo: bottomAnchor),
195
- view.leftAnchor.constraint(equalTo: leftAnchor),
196
- view.rightAnchor.constraint(equalTo: rightAnchor)
197
- ])
203
+ let frame = self.bounds;
204
+ view.frame = frame;
205
+ #if os(iOS) || os(tvOS)
206
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
207
+ #elseif os(macOS)
208
+ view.autoresizingMask = [.width, .height]
209
+ #endif
198
210
  }
199
211
 
200
212
  // MARK: - UIView lifecycle
@@ -10,5 +10,6 @@ extension ExpoSwiftUI {
10
10
  static let SHADOW_NODE_MOCK_PROXY = ShadowNodeProxy()
11
11
 
12
12
  public var setViewSize: ((CGSize) -> Void)?
13
+ public var setStyleSize: ((NSNumber?, NSNumber?) -> Void)?
13
14
  }
14
15
  }
@@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
21
21
 
22
22
  - (void)setShadowNodeSize:(float) width height:(float) height;
23
23
 
24
+ - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height;
25
+
24
26
  - (BOOL)supportsPropWithName:(nonnull NSString *)name;
25
27
 
26
28
  /*
@@ -357,6 +357,15 @@ static std::unordered_map<std::string, expo::ExpoViewComponentDescriptor::Flavor
357
357
  }
358
358
  }
359
359
 
360
+ - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height
361
+ {
362
+ if (_state) {
363
+ float widthValue = width ? [width floatValue] : std::numeric_limits<float>::quiet_NaN();
364
+ float heightValue = height ? [height floatValue] : std::numeric_limits<float>::quiet_NaN();
365
+ _state->updateState(expo::ExpoViewState::withStyleDimensions(widthValue, heightValue));
366
+ }
367
+ }
368
+
360
369
  - (BOOL)supportsPropWithName:(nonnull NSString *)name
361
370
  {
362
371
  // Implemented in `SwiftUIVirtualView.swift`
@@ -40,6 +40,8 @@
40
40
 
41
41
  - (void)setShadowNodeSize:(float)width height:(float)height;
42
42
 
43
+ - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height;
44
+
43
45
  - (BOOL)supportsPropWithName:(nonnull NSString *)name;
44
46
 
45
47
  // MARK: - Derived from RCTComponentViewProtocol
@@ -179,6 +179,20 @@ static std::unordered_map<std::string, ExpoViewComponentDescriptor::Flavor> _com
179
179
  return NO;
180
180
  }
181
181
 
182
+ - (void)setStyleSize:(nullable NSNumber *)width height:(nullable NSNumber *)height
183
+ {
184
+ if (_state) {
185
+ float widthValue = width ? [width floatValue] : std::numeric_limits<float>::quiet_NaN();
186
+ float heightValue = height ? [height floatValue] : std::numeric_limits<float>::quiet_NaN();
187
+ #if REACT_NATIVE_TARGET_VERSION >= 82
188
+ // synchronous update is only available in React Native 0.82 and above
189
+ _state->updateState(expo::ExpoViewState::withStyleDimensions(widthValue, heightValue), EventQueue::UpdateMode::unstable_Immediate);
190
+ #else
191
+ _state->updateState(expo::ExpoViewState::withStyleDimensions(widthValue, heightValue));
192
+ #endif
193
+ }
194
+ }
195
+
182
196
  @end
183
197
 
184
198
  #endif // RCT_NEW_ARCH_ENABLED
@@ -24,6 +24,8 @@ namespace expo
24
24
 
25
25
  jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value);
26
26
 
27
+ jsi::String convertNSURLToJSIString(jsi::Runtime &runtime, NSURL *value);
28
+
27
29
  jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value);
28
30
 
29
31
  jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value);
@@ -9,6 +9,7 @@
9
9
  #import <ExpoModulesCore/EXJavaScriptRuntime.h>
10
10
  #import <ExpoModulesCore/EXJavaScriptSharedObjectBinding.h>
11
11
  #import <ExpoModulesCore/EXStringUtils.h>
12
+ #import <Foundation/NSURL.h>
12
13
 
13
14
  namespace expo {
14
15
 
@@ -41,6 +42,12 @@ jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value)
41
42
  #endif
42
43
  }
43
44
 
45
+ jsi::String convertNSURLToJSIString(jsi::Runtime &runtime, NSURL *value)
46
+ {
47
+ NSString *stringValue = [value absoluteString];
48
+ return convertNSStringToJSIString(runtime, stringValue);
49
+ }
50
+
44
51
  jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value)
45
52
  {
46
53
  jsi::Object result = jsi::Object(runtime);
@@ -107,6 +114,8 @@ jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value)
107
114
  return convertNSArrayToJSIArray(runtime, (NSArray *)value);
108
115
  } else if ([value isKindOfClass:[NSData class]]) {
109
116
  return createUint8Array(runtime, (NSData *)value);
117
+ } else if ([value isKindOfClass:[NSURL class]]) {
118
+ return convertNSURLToJSIString(runtime, (NSURL *)value);
110
119
  } else if (value == (id)kCFNull) {
111
120
  return jsi::Value::null();
112
121
  }
@@ -6,9 +6,11 @@
6
6
  @objc(EXReactDelegate)
7
7
  public class ExpoReactDelegate: NSObject {
8
8
  private let handlers: [ExpoReactDelegateHandler]
9
+ public let reactNativeFactory: ExpoReactNativeFactoryProtocol
9
10
 
10
- public init(handlers: [ExpoReactDelegateHandler]) {
11
+ public init(handlers: [ExpoReactDelegateHandler], reactNativeFactory: ExpoReactNativeFactoryProtocol) {
11
12
  self.handlers = handlers
13
+ self.reactNativeFactory = reactNativeFactory
12
14
  }
13
15
 
14
16
  @objc
@@ -23,7 +25,7 @@ public class ExpoReactDelegate: NSObject {
23
25
  ?? {
24
26
  guard let appDelegate = (UIApplication.shared.delegate as? (any ReactNativeFactoryProvider)) ??
25
27
  ((UIApplication.shared.delegate as? NSObject)?.value(forKey: "_expoAppDelegate") as? (any ReactNativeFactoryProvider)) else {
26
- fatalError("`UIApplication.shared.delegate` must be an `ExpoAppDelegate` or `EXAppDelegateWrapper`")
28
+ return reactNativeFactory.recreateRootView(withBundleURL: nil, moduleName: moduleName, initialProps: initialProperties, launchOptions: launchOptions)
27
29
  }
28
30
 
29
31
  return appDelegate.recreateRootView(
@@ -245,6 +245,17 @@ class FunctionSpec: ExpoSpec {
245
245
  @Field var property: String = "expo"
246
246
  }
247
247
 
248
+ struct TestURLRecord: Record {
249
+ static let defaultURLString = "https://expo.dev"
250
+ static let defaultURL = URL(string: defaultURLString)!
251
+
252
+ @Field var url: URL = defaultURL
253
+ }
254
+
255
+ afterEach {
256
+ try runtime.eval("globalThis.result = undefined")
257
+ }
258
+
248
259
  beforeSuite {
249
260
  appContext.moduleRegistry.register(holder: mockModuleHolder(appContext) {
250
261
  Name("TestModule")
@@ -286,10 +297,18 @@ class FunctionSpec: ExpoSpec {
286
297
  return "\(f.property)"
287
298
  }
288
299
 
300
+ Function("withURL") {
301
+ return TestURLRecord.defaultURL
302
+ }
303
+
304
+ Function("withNestedURL") {
305
+ return TestURLRecord()
306
+ }
307
+
289
308
  Function("withOptionalRecord") { (f: TestRecord?) in
290
309
  return "\(f?.property ?? "no value")"
291
310
  }
292
-
311
+
293
312
  Function("withSharedObject") {
294
313
  return SharedString("Test")
295
314
  }
@@ -305,7 +324,15 @@ class FunctionSpec: ExpoSpec {
305
324
  AsyncFunction("withSharedObjectPromise") { (p: Promise) in
306
325
  p.resolve(SharedString("Test with Promise"))
307
326
  }
308
-
327
+
328
+ AsyncFunction("withURLAsync") {
329
+ return TestURLRecord.defaultURL
330
+ }
331
+
332
+ AsyncFunction("withNestedURLAsync") {
333
+ return TestURLRecord()
334
+ }
335
+
309
336
  Class("Shared", SharedString.self) {
310
337
  Property("value") { shared in
311
338
  return shared.ref
@@ -360,7 +387,43 @@ class FunctionSpec: ExpoSpec {
360
387
  it("accepts optional record") {
361
388
  expect(try runtime.eval("expo.modules.TestModule.withOptionalRecord({property: \"123\"})").asString()) == "123"
362
389
  }
363
-
390
+
391
+ it("returns URL (sync)") {
392
+ let result = try runtime.eval("globalThis.result = expo.modules.TestModule.withURL()")
393
+ expect(result.kind) == .string
394
+ expect(result.getString()) == TestURLRecord.defaultURLString
395
+ }
396
+
397
+ it("returns URL (async)") {
398
+ try runtime.eval("expo.modules.TestModule.withURLAsync().then((result) => { globalThis.result = result; })")
399
+ expect(safeBoolEval("!!globalThis.result")).toEventually(beTrue(), timeout: .milliseconds(2000))
400
+
401
+ let urlValue = try runtime.eval("url = globalThis.result")
402
+ expect(urlValue.kind) == .string
403
+ expect(urlValue.getString()) == TestURLRecord.defaultURLString
404
+ }
405
+
406
+ it("returns a record wit url (sync)") {
407
+ let object = try runtime.eval("globalThis.result = expo.modules.TestModule.withNestedURL()")
408
+ expect(object.kind) == .object
409
+ expect(object.getObject().hasProperty("url")) == true
410
+ expect(object.getObject().getProperty("url").getString()) == TestURLRecord.defaultURLString
411
+ }
412
+
413
+ it("returns a record with url (async)") {
414
+ try runtime.eval("expo.modules.TestModule.withNestedURLAsync().then((result) => { globalThis.result = result; })")
415
+
416
+ expect(safeBoolEval("!!globalThis.result.url")).toEventually(beTrue(), timeout: .milliseconds(2000))
417
+
418
+ let object = try runtime.eval("object = globalThis.result")
419
+ expect(object.kind) == .object
420
+ expect(object.getObject().hasProperty("url")) == true
421
+
422
+ let urlValue = try runtime.eval("object.url")
423
+ expect(urlValue.kind) == .string
424
+ expect(urlValue.getString()) == TestURLRecord.defaultURLString
425
+ }
426
+
364
427
  it("returns a SharedObject (sync)") {
365
428
  let object = try runtime.eval("expo.modules.TestModule.withSharedObject()")
366
429
 
@@ -385,7 +448,7 @@ class FunctionSpec: ExpoSpec {
385
448
  expect(result.kind) == .string
386
449
  expect(result.getString()) == "Test"
387
450
  }
388
-
451
+
389
452
  it("returns an Array of SharedObjects (async)") {
390
453
  try runtime
391
454
  .eval(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "3.0.18",
3
+ "version": "3.0.20",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -65,5 +65,5 @@
65
65
  "@testing-library/react-native": "^13.2.0",
66
66
  "expo-module-scripts": "^5.0.7"
67
67
  },
68
- "gitHead": "6523053d0d997d2a21f580d2752b2f873c122038"
68
+ "gitHead": "73d9bc0ee4f1e28cfda349dc36ee53d16fad0c5d"
69
69
  }
@@ -26,18 +26,20 @@ export function useReleasingSharedObject<TSharedObject extends SharedObject>(
26
26
  dependencies.every((value, index) => value === previousDependencies.current[index]);
27
27
 
28
28
  // If the dependencies have changed, release the previous object and create a new one, otherwise this has been called
29
- // because of a fast refresh, and we don't want to release the object.
29
+ // because of an unrelated fast refresh, and we don't want to release the object.
30
30
  if (!newObject || !dependenciesAreEqual) {
31
31
  objectRef.current?.release();
32
32
  newObject = factory();
33
33
  objectRef.current = newObject;
34
34
  previousDependencies.current = dependencies;
35
- } else {
36
- isFastRefresh.current = true;
37
35
  }
38
36
  return newObject;
39
37
  }, dependencies);
40
38
 
39
+ useMemo(() => {
40
+ isFastRefresh.current = true;
41
+ }, []);
42
+
41
43
  useEffect(() => {
42
44
  isFastRefresh.current = false;
43
45