expo-modules-core 57.0.7 β†’ 57.0.9

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,25 @@
10
10
 
11
11
  ### πŸ’‘ Others
12
12
 
13
+ ## 57.0.9 β€” 2026-08-04
14
+
15
+ ### πŸŽ‰ New features
16
+
17
+ - [iOS] Added module lifecycle hooks that replace the `OnCreate`, `OnDestroy`, `OnStartObserving` and `OnStopObserving` DSL components: `didCreate`, `willDestroy`, `didStartListening` and `didStopListening`. ([#47542](https://github.com/expo/expo/pull/47542) by [@tsapeta](https://github.com/tsapeta))
18
+ - [iOS] Turn `getModule(implementing:)` into a public function. ([#47337](https://github.com/expo/expo/pull/47337) by [@Ubax](https://github.com/Ubax))
19
+
20
+ ### πŸ› Bug fixes
21
+
22
+ - [Android] Fixed hosted Compose views missing layout after reattachment or in-place configuration changes. ([#48370](https://github.com/expo/expo/issues/48370) by [@lujjjh](https://github.com/lujjjh))
23
+ - [iOS] Fixed infinite recursion and `EXC_BAD_ACCESS` when encoding native `Date` values to JavaScript. ([#48239](https://github.com/expo/expo/issues/48239), [#48240](https://github.com/expo/expo/pull/48240) by [@samuelcorsan](https://github.com/samuelcorsan))
24
+ - [iOS] Fixed a stack overflow crash when converting to JavaScript a `Convertible` value that keeps the default `convertResult` implementation, such as `CMTime`. The value now converts to `undefined` and logs a warning. ([#48266](https://github.com/expo/expo/pull/48266) by [@tsapeta](https://github.com/tsapeta))
25
+
26
+ ## 57.0.8 β€” 2026-07-29
27
+
28
+ ### πŸŽ‰ New features
29
+
30
+ - Added a `layoutRoot` prop to Expo views that marks the shadow node with the `RootNodeKind` trait, so `measure()` reports coordinates relative to that node β€” for views that dispatch touch events from their own root view, like `RNHostView` in `@expo/ui`. ([#48217](https://github.com/expo/expo/pull/48217) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
31
+
13
32
  ## 57.0.7 β€” 2026-07-22
14
33
 
15
34
  _This version does not introduce any user-facing changes._
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '57.0.7'
30
+ version = '57.0.9'
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.7"
97
+ versionName "57.0.9"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -2,6 +2,7 @@ package expo.modules.kotlin.views
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.content.Context
5
+ import android.content.res.Configuration
5
6
  import android.util.Log
6
7
  import android.view.View
7
8
  import android.view.ViewGroup
@@ -113,6 +114,15 @@ abstract class ExpoComposeView<T : ComposeProps>(
113
114
  }
114
115
  }
115
116
 
117
+ override fun dispatchConfigurationChanged(newConfig: Configuration) {
118
+ super.dispatchConfigurationChanged(newConfig)
119
+ // React Native owns this view's bounds and may not schedule another Android layout pass
120
+ // when a configuration change leaves its Yoga layout unchanged.
121
+ if (withHostingView && isAttachedToWindow && isLaidOut) {
122
+ requestLayout()
123
+ }
124
+ }
125
+
116
126
  /**
117
127
  * Validates that this non-hosting Compose view has a valid Compose parent.
118
128
  *
@@ -193,6 +203,15 @@ abstract class ExpoComposeView<T : ComposeProps>(
193
203
  if (withHostingView) {
194
204
  clipChildren = false
195
205
  clipToPadding = false
206
+ addOnAttachStateChangeListener(
207
+ OnAttachAfterDetachmentListener(
208
+ onAttachAfterDetachment = {
209
+ // Restore the Android layout pass after a real detach. The listener deliberately
210
+ // ignores the first attach and React Native's same-loop reparenting.
211
+ requestLayout()
212
+ }
213
+ )
214
+ )
196
215
  addComposeView()
197
216
  } else {
198
217
  this.visibility = GONE
@@ -69,6 +69,21 @@ private:
69
69
  this->traits_.unset(react::ShadowNodeTraits::Trait::ForceFlattenView);
70
70
  }
71
71
  }
72
+
73
+ {
74
+ // Views that dispatch touch events from their own root view (e.g. RNHostView) set
75
+ // `layoutRoot` so that `measure()` reports coordinates relative to this node instead of
76
+ // the surface root, matching the coordinate space of the dispatched touches. The same
77
+ // reason React Native's <Modal> shadow node sets this trait.
78
+ auto it = viewProps.propsMap.find("layoutRoot");
79
+ bool layoutRoot = (it != viewProps.propsMap.end()) && it->second.getBool();
80
+
81
+ if (layoutRoot) {
82
+ this->traits_.set(react::ShadowNodeTraits::Trait::RootNodeKind);
83
+ } else {
84
+ this->traits_.unset(react::ShadowNodeTraits::Trait::RootNodeKind);
85
+ }
86
+ }
72
87
  }
73
88
  };
74
89
 
@@ -169,4 +169,13 @@ extension Date: Convertible {
169
169
  }
170
170
  throw Conversions.ConvertingException<Date>(value)
171
171
  }
172
+
173
+ public static func convertResult(_ result: Any, appContext: AppContext) throws -> Any {
174
+ if let date = result as? Date {
175
+ let formatter = ISO8601DateFormatter()
176
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
177
+ return formatter.string(from: date)
178
+ }
179
+ return result
180
+ }
172
181
  }
@@ -101,7 +101,12 @@ internal struct DynamicConvertibleType: AnyDynamicType {
101
101
  return result
102
102
  }
103
103
  if let result = value as? AnyArgument {
104
- return try type(of: result).getDynamicType().castToJS(result, appContext: appContext)
104
+ let dynamicType = type(of: result).getDynamicType()
105
+ // A `Convertible` that keeps the default `convertResult` returns the value unchanged,
106
+ // so redispatching it into the same dynamic type would recurse until the stack overflows.
107
+ if !dynamicType.equals(self) {
108
+ return try dynamicType.castToJS(result, appContext: appContext)
109
+ }
105
110
  }
106
111
  return try Conversions.unknownToJavaScriptValue(value, appContext: appContext)
107
112
  }
@@ -1,8 +1,6 @@
1
1
  import ExpoModulesJSI
2
2
 
3
- /**
4
- Holds a reference to the module instance and caches its definition.
5
- */
3
+ /// Holds a reference to the module instance and caches its definition.
6
4
  public final class ModuleHolder {
7
5
  /**
8
6
  Name of the module.
@@ -70,7 +68,8 @@ public final class ModuleHolder {
70
68
  private static func buildDefinition(for module: AnyModule) -> ModuleDefinition {
71
69
  let userDefinition = module.definition()
72
70
  let synthesized = module._synthesizedDefinition()
73
- let definition = synthesized.isEmpty
71
+ let definition =
72
+ synthesized.isEmpty
74
73
  ? userDefinition
75
74
  : ModuleDefinition(definitions: synthesized + userDefinition.rawDefinitions)
76
75
 
@@ -141,6 +140,8 @@ public final class ModuleHolder {
141
140
  // (the direct-JSI path). A no-op for modules that don't use the macro.
142
141
  try module._decorateModule(object: object, in: appContext.runtime)
143
142
 
143
+ try installListeningHooks(on: object, in: appContext.runtime)
144
+
144
145
  return object
145
146
  } catch {
146
147
  log.error("Building the module object failed: \(error)")
@@ -171,9 +172,37 @@ public final class ModuleHolder {
171
172
  // MARK: Deallocation
172
173
 
173
174
  deinit {
175
+ module.willDestroy()
174
176
  post(event: .moduleDestroy)
175
177
  }
176
178
 
179
+ // MARK: - Privates
180
+
181
+ /// Installs host functions that the JavaScript `EventEmitter` implementation calls when
182
+ /// the number of listeners for an event switches between zero and non-zero. They notify
183
+ /// the module through the `didStartListening` and `didStopListening` lifecycle hooks.
184
+ @JavaScriptActor
185
+ private func installListeningHooks(on object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) {
186
+ let startListening = runtime.createFunction("__expo_onStartListeningToEvent") {
187
+ [weak module = self.module] _, arguments in
188
+ guard let module, arguments.count > 0 else {
189
+ return .undefined
190
+ }
191
+ module.didStartListening(event: try arguments[0].asString())
192
+ return .undefined
193
+ }
194
+ let stopListening = runtime.createFunction("__expo_onStopListeningToEvent") {
195
+ [weak module = self.module] _, arguments in
196
+ guard let module, arguments.count > 0 else {
197
+ return .undefined
198
+ }
199
+ module.didStopListening(event: try arguments[0].asString())
200
+ return .undefined
201
+ }
202
+ object.defineProperty("__expo_onStartListeningToEvent", value: startListening.asValue(), options: [])
203
+ object.defineProperty("__expo_onStopListeningToEvent", value: stopListening.asValue(), options: [])
204
+ }
205
+
177
206
  // MARK: - Exceptions
178
207
 
179
208
  internal class ModuleNotFoundException: GenericException<String> {
@@ -19,13 +19,21 @@ public final class ModuleRegistry: Sequence {
19
19
  if preventModuleOverriding {
20
20
  overrideDisallowModules.insert(holder.name)
21
21
  }
22
- registry.withLock { registry in
22
+ let didRegister = registry.withLock { registry -> Bool in
23
23
  // if overriding is disallowed for this module and the module already registered, don't re-register
24
24
  if overrideDisallowModules.contains(holder.name) && registry[holder.name] != nil {
25
- log.info("Not re-registering module '\(holder.name)' since a previous registration specified preventModuleOverriding: true")
26
- return
25
+ log.info(
26
+ "Not re-registering module '\(holder.name)' since a previous registration specified preventModuleOverriding: true"
27
+ )
28
+ return false
27
29
  }
28
30
  registry[holder.name] = holder
31
+ return true
32
+ }
33
+ if didRegister {
34
+ // Call the `didCreate` lifecycle hook only after the holder is in the registry, so the module
35
+ // can already look itself up (e.g. through `withEventTarget`). Rejected registrations don't get it.
36
+ holder.module.didCreate()
29
37
  }
30
38
  }
31
39
 
@@ -37,7 +45,9 @@ public final class ModuleRegistry: Sequence {
37
45
  log.error("Unable to register a module '\(module)', the app context is unavailable")
38
46
  return
39
47
  }
40
- register(holder: ModuleHolder(appContext: appContext, module: module, name: name), preventModuleOverriding: preventModuleOverriding)
48
+ register(
49
+ holder: ModuleHolder(appContext: appContext, module: module, name: name),
50
+ preventModuleOverriding: preventModuleOverriding)
41
51
  }
42
52
 
43
53
  /**
@@ -48,7 +58,8 @@ public final class ModuleRegistry: Sequence {
48
58
  log.error("Unable to register a module '\(moduleType)', the app context is unavailable")
49
59
  return
50
60
  }
51
- register(module: moduleType.init(appContext: appContext), name: name, preventModuleOverriding: preventModuleOverriding)
61
+ register(
62
+ module: moduleType.init(appContext: appContext), name: name, preventModuleOverriding: preventModuleOverriding)
52
63
  }
53
64
 
54
65
  /**
@@ -98,7 +109,7 @@ public final class ModuleRegistry: Sequence {
98
109
  }
99
110
  }
100
111
 
101
- internal func getModule<ModuleProtocol>(implementing protocol: ModuleProtocol.Type) -> ModuleProtocol? {
112
+ public func getModule<ModuleProtocol>(implementing protocol: ModuleProtocol.Type) -> ModuleProtocol? {
102
113
  return registry.withLock { registry in
103
114
  for holder in registry.values {
104
115
  if let module = holder.module as? ModuleProtocol {
@@ -2,15 +2,16 @@
2
2
 
3
3
  import ExpoModulesJSI
4
4
 
5
- /**
6
- `BaseModule` is just a stub class that fulfils `AnyModule` protocol requirement of public default initializer,
7
- but doesn't implement that protocol explicitly, though β€”Β it would have to provide a definition which would require
8
- other modules to use `override` keyword in the function returning the definition.
9
- */
5
+ /// `BaseModule` is just a stub class that fulfils `AnyModule` protocol requirement of public default initializer,
6
+ /// but doesn't implement that protocol explicitly, though β€”Β it would have to provide a definition which would require
7
+ /// other modules to use `override` keyword in the function returning the definition.
10
8
  open class BaseModule {
11
9
  public private(set) weak var appContext: AppContext?
12
10
 
13
- @available(*, unavailable, message: "Module's initializer cannot be overriden, use \"onCreate\" definition component instead.")
11
+ @available(
12
+ *, unavailable,
13
+ message: "Module's initializer cannot be overridden, override the \"didCreate\" lifecycle hook instead."
14
+ )
14
15
  public init() {}
15
16
 
16
17
  required public init(appContext: AppContext) {
@@ -23,6 +24,29 @@ open class BaseModule {
23
24
  public func sendEvent(_ eventName: String, _ body: [String: Any?] = [:]) {
24
25
  appContext?.eventEmitter?.sendEvent(withName: eventName, body: body)
25
26
  }
27
+
28
+ // MARK: - Lifecycle hooks
29
+
30
+ // No-op defaults for the `AnyModule` lifecycle hooks. They are declared here as `open`
31
+ // class methods rather than only as protocol-extension defaults, so that they dispatch
32
+ // dynamically: a subclass override is called even when the `Module` conformance comes
33
+ // from an ancestor that doesn't implement the hook itself.
34
+
35
+ /// A lifecycle hook that is called once the module is initialized and registered in the app context.
36
+ /// An equivalent of the `OnCreate` DSL component.
37
+ open func didCreate() {}
38
+
39
+ /// A lifecycle hook that is called when the module is about to be deallocated.
40
+ /// An equivalent of the `OnDestroy` DSL component.
41
+ open func willDestroy() {}
42
+
43
+ /// A lifecycle hook that is called when the first JavaScript listener for the given event is added.
44
+ /// An equivalent of the `OnStartObserving` DSL component.
45
+ open func didStartListening(event: String) {}
46
+
47
+ /// A lifecycle hook that is called when the last JavaScript listener for the given event is removed.
48
+ /// An equivalent of the `OnStopObserving` DSL component.
49
+ open func didStopListening(event: String) {}
26
50
  }
27
51
 
28
52
  extension BaseModule: EventEmitter {
@@ -40,7 +64,5 @@ extension BaseModule: EventEmitter {
40
64
  }
41
65
  }
42
66
 
43
- /**
44
- An alias for `AnyModule` extended by the `BaseModule` class that provides public default initializer.
45
- */
67
+ /// An alias for `AnyModule` extended by the `BaseModule` class that provides public default initializer.
46
68
  public typealias Module = AnyModule & BaseModule
@@ -1,8 +1,6 @@
1
1
  import ExpoModulesJSI
2
2
 
3
- /**
4
- A protocol for any type-erased module that provides functions used by the core.
5
- */
3
+ /// A protocol for any type-erased module that provides functions used by the core.
6
4
  public protocol AnyModule: AnyObject, AnyArgument {
7
5
  /**
8
6
  The default initializer. Must be public, but the module class does *not* need to
@@ -35,10 +33,26 @@ public protocol AnyModule: AnyObject, AnyArgument {
35
33
  /// (leading underscore). Modules that don't use the macro fall back to the default no-op.
36
34
  @JavaScriptActor
37
35
  func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws
36
+
37
+ /// A lifecycle hook that is called once the module is initialized and registered in the app context.
38
+ /// An equivalent of the `OnCreate` DSL component.
39
+ func didCreate()
40
+
41
+ /// A lifecycle hook that is called when the module is about to be deallocated.
42
+ /// An equivalent of the `OnDestroy` DSL component.
43
+ func willDestroy()
44
+
45
+ /// A lifecycle hook that is called when the first JavaScript listener for the given event is added.
46
+ /// An equivalent of the `OnStartObserving` DSL component.
47
+ func didStartListening(event: String)
48
+
49
+ /// A lifecycle hook that is called when the last JavaScript listener for the given event is removed.
50
+ /// An equivalent of the `OnStopObserving` DSL component.
51
+ func didStopListening(event: String)
38
52
  }
39
53
 
40
- public extension AnyModule {
41
- static var _jsName: String {
54
+ extension AnyModule {
55
+ public static var _jsName: String {
42
56
  return String(describing: self)
43
57
  }
44
58
 
@@ -46,14 +60,28 @@ public extension AnyModule {
46
60
  /// through `@JS` members don't have to write an empty `definition()` of their own. Modules that use
47
61
  /// the DSL provide their own.
48
62
  @ModuleDefinitionBuilder
49
- func definition() -> ModuleDefinition {}
63
+ public func definition() -> ModuleDefinition {}
50
64
 
51
- func _synthesizedDefinition() -> [AnyDefinition] {
65
+ public func _synthesizedDefinition() -> [AnyDefinition] {
52
66
  return []
53
67
  }
54
68
 
55
69
  @JavaScriptActor
56
- func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
70
+ public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime) throws {
57
71
  // No-op by default β€” only `@ExpoModule`-macro modules synthesize a real implementation.
58
72
  }
73
+
74
+ // No-op defaults of the lifecycle hooks for modules that conform without inheriting
75
+ // `BaseModule`, e.g. plain `@ExpoModule` classes whose conformance comes from the macro's
76
+ // extension. These defaults are statically bound into the conformance witness, so
77
+ // `BaseModule` descendants are backed by its dynamically-dispatched open methods instead,
78
+ // which take precedence during witness resolution.
79
+
80
+ public func didCreate() {}
81
+
82
+ public func willDestroy() {}
83
+
84
+ public func didStartListening(event: String) {}
85
+
86
+ public func didStopListening(event: String) {}
59
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "57.0.7",
3
+ "version": "57.0.9",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  "@types/invariant": "^2.2.33",
67
67
  "expo-module-scripts": "56.0.3"
68
68
  },
69
- "gitHead": "a4789f1e53353f4929b0baddcfe5a7c622b99c71",
69
+ "gitHead": "fb5049481ebd5cadd499c4d1f1d5e47f9ac678be",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",