expo-modules-core 56.0.15 → 56.0.17
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 +23 -0
- package/android/build.gradle +2 -2
- package/android/src/compose/expo/modules/kotlin/views/ExpoComposeView.kt +51 -8
- package/android/src/compose/expo/modules/kotlin/views/ModuleDefinitionBuilderComposeExtension.kt +1 -1
- package/android/src/main/cpp/fabric/NativeStatePropsGetter.cpp +45 -0
- package/android/src/main/cpp/fabric/NativeStatePropsGetter.h +17 -0
- package/android/src/main/java/expo/modules/kotlin/jni/fabric/NativeStatePropsGetter.kt +30 -0
- package/android/src/main/java/expo/modules/kotlin/viewevent/ViewEvent.kt +19 -14
- package/android/src/main/java/expo/modules/kotlin/views/ShadowNodeProxy.kt +40 -10
- package/android/src/main/java/expo/modules/kotlin/views/ViewFunctionHolder.kt +5 -2
- package/android/src/main/java/expo/modules/kotlin/views/ViewManagerWrapperDelegate.kt +9 -0
- package/ios/Core/Arguments/Convertibles.swift +8 -1
- package/ios/Core/DynamicTypes/DynamicConvertibleType.swift +21 -6
- package/ios/Core/ExpoModulesMacros.swift +44 -6
- package/ios/Core/ModuleHolder.swift +12 -6
- package/ios/Core/Protocols/AnyModule.swift +16 -2
- package/ios/Core/Records/FormattedRecord.swift +3 -3
- package/ios/Core/Records/Record.swift +61 -5
- package/package.json +4 -4
- 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,29 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 56.0.17 — 2026-06-15
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [Android] Fixed Expo UI re-compose when switching screens in react-native-screens. ([#46650](https://github.com/expo/expo/pull/46650) by [@kudo](https://github.com/kudo))
|
|
18
|
+
|
|
19
|
+
## 56.0.16 — 2026-06-10
|
|
20
|
+
|
|
21
|
+
### 🎉 New features
|
|
22
|
+
|
|
23
|
+
- [iOS] Added the `@Record` macro that synthesizes a record from a type's stored properties, with no `@Field` wrappers needed. ([#46547](https://github.com/expo/expo/pull/46547) by [@tsapeta](https://github.com/tsapeta))
|
|
24
|
+
|
|
25
|
+
### 🐛 Bug fixes
|
|
26
|
+
|
|
27
|
+
- [android] Add a synchronous shadow node size update path, fixing a layout shift for `Host` `matchContents` views. ([#46604](https://github.com/expo/expo/pull/46604) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
28
|
+
- [Android] Fix `NullPointerException` crash when a `matchContents` view is unmounted while a shadow node size update is pending (e.g. closing a bottom sheet mid-resize). ([#46785](https://github.com/expo/expo/pull/46785) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
29
|
+
- [iOS] Accept JS `Double` timestamps in `Date` Convertible. JS numbers arrive across the JSI bridge as Swift `Double`; the prior `as? Int` branch never matched, throwing `ConvertingException<Date>` whenever a JS caller passed `someDate.getTime()` to a `Date` / `Date?` argument. ([#46340](https://github.com/expo/expo/pull/46340) by [@kyleasaff](https://github.com/kyleasaff))
|
|
30
|
+
- [Android] Fix events being silently dropped for Compose views in custom modules. ([#46623](https://github.com/expo/expo/issues/46623) by [@benjaminkomen](https://github.com/benjaminkomen)) ([#46624](https://github.com/expo/expo/pull/46624) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
31
|
+
|
|
32
|
+
### 💡 Others
|
|
33
|
+
|
|
34
|
+
- [iOS] `@ExpoModule` now synthesizes a `_decorateModule` that binds the module's `@JS` functions directly onto the JS object, letting the module holder skip the dynamic definition path for synthesized modules. ([#46612](https://github.com/expo/expo/pull/46612) by [@tsapeta](https://github.com/tsapeta))
|
|
35
|
+
|
|
13
36
|
## 56.0.15 — 2026-06-05
|
|
14
37
|
|
|
15
38
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
group = 'host.exp.exponent'
|
|
30
|
-
version = '56.0.
|
|
30
|
+
version = '56.0.17'
|
|
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 "56.0.
|
|
97
|
+
versionName "56.0.17"
|
|
98
98
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
99
99
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
100
100
|
|
|
@@ -17,6 +17,10 @@ import androidx.annotation.UiThread
|
|
|
17
17
|
import androidx.compose.ui.platform.ComposeView
|
|
18
18
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
19
19
|
import androidx.core.view.size
|
|
20
|
+
import androidx.lifecycle.LifecycleOwner
|
|
21
|
+
import androidx.lifecycle.setViewTreeLifecycleOwner
|
|
22
|
+
import androidx.savedstate.SavedStateRegistryOwner
|
|
23
|
+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
20
24
|
import expo.modules.kotlin.AppContext
|
|
21
25
|
import expo.modules.kotlin.exception.CodedException
|
|
22
26
|
import expo.modules.kotlin.types.enforceType
|
|
@@ -52,10 +56,14 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
52
56
|
context: Context,
|
|
53
57
|
appContext: AppContext,
|
|
54
58
|
private val withHostingView: Boolean = false
|
|
55
|
-
) : ExpoView(context, appContext) {
|
|
59
|
+
) : ExpoView(context, appContext), ComposeHostingView {
|
|
56
60
|
open val props: T? = null
|
|
57
61
|
protected var recomposeScope: RecomposeScope? = null
|
|
58
62
|
|
|
63
|
+
// Retained so the composition can be disposed on unmount: its strategy is
|
|
64
|
+
// pinned to the Activity lifecycle, so nothing disposes it on window detach.
|
|
65
|
+
private var hostingComposeView: ComposeView? = null
|
|
66
|
+
|
|
59
67
|
private val globalEvent = ViewEvent<Pair<String, Map<String, Any?>>>(GLOBAL_EVENT_NAME, this, null)
|
|
60
68
|
|
|
61
69
|
/**
|
|
@@ -191,22 +199,56 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
191
199
|
|
|
192
200
|
private fun addComposeView() {
|
|
193
201
|
val composeView = ComposeView(context).also {
|
|
202
|
+
// Give each Host a unique id so its rememberSaveable state gets its own key.
|
|
203
|
+
// All Hosts share the Activity's SavedStateRegistry (set below), so without an id
|
|
204
|
+
// they'd collide on one key and only the first could save/restore state.
|
|
205
|
+
it.id = generateViewId()
|
|
194
206
|
it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
195
|
-
it
|
|
207
|
+
// Pin the composition to the Activity lifecycle so it survives
|
|
208
|
+
// react-native-screens detaching inactive screens on every switch.
|
|
209
|
+
// The strategy alone isn't enough: Compose's WrappedComposition also
|
|
210
|
+
// observes the view-tree lifecycle owner found at first attach — the
|
|
211
|
+
// screen fragment's, which RN-screens destroys per switch — and
|
|
212
|
+
// self-disposes on its ON_DESTROY, leaving a dead composition that
|
|
213
|
+
// never recreates. Overriding the owners on the ComposeView (nearest
|
|
214
|
+
// tag wins) points both at the Activity. Unmount disposes explicitly
|
|
215
|
+
// via disposeHostedComposition().
|
|
216
|
+
val activity = appContext.currentActivity
|
|
217
|
+
if (activity is LifecycleOwner && activity is SavedStateRegistryOwner) {
|
|
218
|
+
it.setViewTreeLifecycleOwner(activity)
|
|
219
|
+
it.setViewTreeSavedStateRegistryOwner(activity)
|
|
220
|
+
it.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(activity.lifecycle))
|
|
221
|
+
} else {
|
|
222
|
+
// No Activity to pin to: keep the prior behavior, including the
|
|
223
|
+
// dispose-on-reattach workaround for blank compositions after
|
|
224
|
+
// navigation (https://github.com/expo/expo/pull/34689).
|
|
225
|
+
it.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
|
226
|
+
it.addOnAttachStateChangeListener(
|
|
227
|
+
OnAttachAfterDetachmentListener(onAttachAfterDetachment = {
|
|
228
|
+
it.disposeComposition()
|
|
229
|
+
})
|
|
230
|
+
)
|
|
231
|
+
}
|
|
196
232
|
it.setContent {
|
|
197
233
|
with(ComposableScope()) {
|
|
198
234
|
Content()
|
|
199
235
|
}
|
|
200
236
|
}
|
|
201
|
-
it.addOnAttachStateChangeListener(
|
|
202
|
-
OnAttachAfterDetachmentListener(onAttachAfterDetachment = {
|
|
203
|
-
it.disposeComposition()
|
|
204
|
-
})
|
|
205
|
-
)
|
|
206
237
|
}
|
|
238
|
+
hostingComposeView = composeView
|
|
207
239
|
addView(composeView)
|
|
208
240
|
}
|
|
209
241
|
|
|
242
|
+
override fun disposeHostedComposition() {
|
|
243
|
+
hostingComposeView?.let {
|
|
244
|
+
// disposeComposition() alone leaves the composition strategy's lifecycle observer
|
|
245
|
+
// registered on the Activity, which leaks this view.
|
|
246
|
+
// Swapping the strategy first detaches that observer, then we dispose.
|
|
247
|
+
it.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
|
|
248
|
+
it.disposeComposition()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
210
252
|
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) {
|
|
211
253
|
val view = if (child !is ExpoComposeView<*> && child !is ComposeView && this !is RNHostViewInterface) {
|
|
212
254
|
ExpoComposeAndroidView(child, appContext)
|
|
@@ -511,7 +553,8 @@ class ComposeFunctionHolder<Props : ComposeProps>(
|
|
|
511
553
|
appContext: AppContext,
|
|
512
554
|
override val name: String,
|
|
513
555
|
private val composableContent: @Composable FunctionalComposableScope.(props: Props) -> Unit,
|
|
514
|
-
override val props: Props
|
|
556
|
+
override val props: Props,
|
|
557
|
+
override val callbacksDefinition: CallbacksDefinition?
|
|
515
558
|
) : ExpoComposeView<Props>(context, appContext), ViewFunctionHolder {
|
|
516
559
|
val propsMutableState = mutableStateOf(props)
|
|
517
560
|
|
package/android/src/compose/expo/modules/kotlin/views/ModuleDefinitionBuilderComposeExtension.kt
CHANGED
|
@@ -143,7 +143,7 @@ class ComposeViewFunctionDefinitionBuilder<Props : ComposeProps> @PublishedApi i
|
|
|
143
143
|
} catch (e: Exception) {
|
|
144
144
|
throw IllegalStateException("Could not instantiate props instance of $name compose component.", e)
|
|
145
145
|
}
|
|
146
|
-
ComposeFunctionHolder(context, appContext, name, viewFunction, instance)
|
|
146
|
+
ComposeFunctionHolder(context, appContext, name, viewFunction, instance, eventBuilder.callbacksDefinition)
|
|
147
147
|
},
|
|
148
148
|
callbacksDefinition = eventBuilder.callbacksDefinition,
|
|
149
149
|
viewType = ComposeFunctionHolder::class.java,
|
|
@@ -9,9 +9,54 @@ namespace expo {
|
|
|
9
9
|
void NativeStatePropsGetter::registerNatives() {
|
|
10
10
|
javaClassLocal()->registerNatives({
|
|
11
11
|
makeNativeMethod("getStateProps", NativeStatePropsGetter::getStateProps),
|
|
12
|
+
makeNativeMethod("updateStyleSizeImmediateImpl", NativeStatePropsGetter::updateStyleSizeImmediate),
|
|
13
|
+
makeNativeMethod("updateViewSizeImmediateImpl", NativeStatePropsGetter::updateViewSizeImmediate),
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
static std::shared_ptr<const react::ConcreteState<AndroidExpoViewState>> concreteStateFrom(
|
|
18
|
+
jni::alias_ref<jobject> stateWrapper
|
|
19
|
+
) {
|
|
20
|
+
auto stateWrapperImpl = jni::alias_ref<react::StateWrapperImpl::javaobject>{
|
|
21
|
+
static_cast<react::StateWrapperImpl::javaobject>(stateWrapper.get())
|
|
22
|
+
};
|
|
23
|
+
return std::dynamic_pointer_cast<const react::ConcreteState<AndroidExpoViewState>>(
|
|
24
|
+
stateWrapperImpl->cthis()->getState()
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void NativeStatePropsGetter::updateStyleSizeImmediate(
|
|
29
|
+
jni::alias_ref<NativeStatePropsGetter::javaobject> self,
|
|
30
|
+
jni::alias_ref<jobject> stateWrapper,
|
|
31
|
+
jdouble styleWidth,
|
|
32
|
+
jdouble styleHeight
|
|
33
|
+
) {
|
|
34
|
+
const auto state = concreteStateFrom(stateWrapper);
|
|
35
|
+
if (state == nullptr) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
AndroidExpoViewState newState;
|
|
39
|
+
newState._styleWidth = static_cast<float>(styleWidth);
|
|
40
|
+
newState._styleHeight = static_cast<float>(styleHeight);
|
|
41
|
+
state->updateState(std::move(newState), react::EventQueue::UpdateMode::unstable_Immediate);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void NativeStatePropsGetter::updateViewSizeImmediate(
|
|
45
|
+
jni::alias_ref<NativeStatePropsGetter::javaobject> self,
|
|
46
|
+
jni::alias_ref<jobject> stateWrapper,
|
|
47
|
+
jdouble width,
|
|
48
|
+
jdouble height
|
|
49
|
+
) {
|
|
50
|
+
const auto state = concreteStateFrom(stateWrapper);
|
|
51
|
+
if (state == nullptr) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
AndroidExpoViewState newState;
|
|
55
|
+
newState._width = static_cast<float>(width);
|
|
56
|
+
newState._height = static_cast<float>(height);
|
|
57
|
+
state->updateState(std::move(newState), react::EventQueue::UpdateMode::unstable_Immediate);
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
jni::local_ref<jni::JMap<jstring, jobject>> NativeStatePropsGetter::getStateProps(
|
|
16
61
|
jni::alias_ref<NativeStatePropsGetter::javaobject> self,
|
|
17
62
|
jni::alias_ref<jobject> stateWrapper
|
|
@@ -18,6 +18,23 @@ public:
|
|
|
18
18
|
jni::alias_ref<NativeStatePropsGetter::javaobject> self,
|
|
19
19
|
jni::alias_ref<jobject> stateWrapper
|
|
20
20
|
);
|
|
21
|
+
|
|
22
|
+
// Synchronously flushes a shadow-node size state update in the current frame
|
|
23
|
+
// (`UpdateMode::unstable_Immediate`) — the same path iOS uses. This avoids the layout shift an
|
|
24
|
+
// asynchronous `StateWrapper.updateState` causes for matchContents Hosts in Expo UI Compose views
|
|
25
|
+
static void updateStyleSizeImmediate(
|
|
26
|
+
jni::alias_ref<NativeStatePropsGetter::javaobject> self,
|
|
27
|
+
jni::alias_ref<jobject> stateWrapper,
|
|
28
|
+
jdouble styleWidth,
|
|
29
|
+
jdouble styleHeight
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
static void updateViewSizeImmediate(
|
|
33
|
+
jni::alias_ref<NativeStatePropsGetter::javaobject> self,
|
|
34
|
+
jni::alias_ref<jobject> stateWrapper,
|
|
35
|
+
jdouble width,
|
|
36
|
+
jdouble height
|
|
37
|
+
);
|
|
21
38
|
};
|
|
22
39
|
|
|
23
40
|
} // namespace expo
|
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
package expo.modules.kotlin.jni.fabric
|
|
2
2
|
|
|
3
|
+
import com.facebook.jni.HybridData
|
|
3
4
|
import com.facebook.yoga.annotations.DoNotStrip
|
|
4
5
|
|
|
5
6
|
@DoNotStrip
|
|
6
7
|
class NativeStatePropsGetter {
|
|
7
8
|
// We can't use StateWrapper directly, as this class is not exposed
|
|
8
9
|
external fun getStateProps(stateWrapper: Any): Map<String, Any?>?
|
|
10
|
+
|
|
11
|
+
// Synchronously flush a style property size update in the current frame (pass NaN for "unset").
|
|
12
|
+
fun updateStyleSizeImmediate(stateWrapper: Any, styleWidth: Double, styleHeight: Double) {
|
|
13
|
+
if (!isStateValid(stateWrapper)) {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
updateStyleSizeImmediateImpl(stateWrapper, styleWidth, styleHeight)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Synchronously flush a size update in the current frame.
|
|
20
|
+
fun updateViewSizeImmediate(stateWrapper: Any, width: Double, height: Double) {
|
|
21
|
+
if (!isStateValid(stateWrapper)) {
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
updateViewSizeImmediateImpl(stateWrapper, width, height)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private external fun updateStyleSizeImmediateImpl(
|
|
28
|
+
stateWrapper: Any,
|
|
29
|
+
styleWidth: Double,
|
|
30
|
+
styleHeight: Double
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
private external fun updateViewSizeImmediateImpl(stateWrapper: Any, width: Double, height: Double)
|
|
34
|
+
|
|
35
|
+
// The only `StateWrapper` is RN's `StateWrapperImpl`, a fbjni `HybridData`. When the shadow node is
|
|
36
|
+
// destroyed its native pointer is reset and calling into it throws. Skip the update like RN does
|
|
37
|
+
// before its own native state accesses.
|
|
38
|
+
private fun isStateValid(stateWrapper: Any): Boolean = (stateWrapper as? HybridData)?.isValid == true
|
|
9
39
|
}
|
|
@@ -4,10 +4,12 @@ import android.view.View
|
|
|
4
4
|
import com.facebook.react.bridge.ReactContext
|
|
5
5
|
import com.facebook.react.bridge.WritableMap
|
|
6
6
|
import expo.modules.core.utilities.ifNull
|
|
7
|
+
import expo.modules.kotlin.ModuleRegistry
|
|
7
8
|
import expo.modules.kotlin.getUnimoduleProxy
|
|
8
9
|
import expo.modules.kotlin.logger
|
|
9
10
|
import expo.modules.kotlin.types.JSTypeConverterProvider
|
|
10
11
|
import expo.modules.kotlin.types.putGeneric
|
|
12
|
+
import expo.modules.kotlin.views.CallbacksDefinition
|
|
11
13
|
import expo.modules.kotlin.views.ViewFunctionHolder
|
|
12
14
|
|
|
13
15
|
fun interface ViewEventCallback<T> {
|
|
@@ -27,24 +29,13 @@ open class ViewEvent<T>(
|
|
|
27
29
|
val appContext = nativeModulesProxy.kotlinInteropModuleRegistry.appContext
|
|
28
30
|
|
|
29
31
|
if (!isValidated) {
|
|
30
|
-
val
|
|
31
|
-
logger.warn("⚠️ Cannot get
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
val callbacksDefinition = if (view is ViewFunctionHolder) {
|
|
36
|
-
appContext.registry.getViewDefinition(holder, view.name)?.callbacksDefinition
|
|
37
|
-
} else {
|
|
38
|
-
appContext.registry.getViewDefinition(holder, view::class.java)?.callbacksDefinition
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
val callbacks = callbacksDefinition.ifNull {
|
|
42
|
-
logger.warn("⚠️ Cannot get callbacks for ${holder.module::class.java}")
|
|
32
|
+
val callbacks = resolveCallbacksDefinition(view, appContext.registry).ifNull {
|
|
33
|
+
logger.warn("⚠️ Cannot get callbacks for ${view::class.java}")
|
|
43
34
|
return
|
|
44
35
|
}
|
|
45
36
|
|
|
46
37
|
if (!callbacks.names.any { it == name }) {
|
|
47
|
-
logger.warn("⚠️ Event $name wasn't exported from ${
|
|
38
|
+
logger.warn("⚠️ Event $name wasn't exported from ${view::class.java}")
|
|
48
39
|
return
|
|
49
40
|
}
|
|
50
41
|
|
|
@@ -71,3 +62,17 @@ open class ViewEvent<T>(
|
|
|
71
62
|
}
|
|
72
63
|
}
|
|
73
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the callbacks declared for [view]. Views that reuse a single class
|
|
68
|
+
* (e.g. ComposeFunctionHolder) carry their own [CallbacksDefinition] because they
|
|
69
|
+
* can't be matched by class; everything else is looked up by class in the registry.
|
|
70
|
+
* See https://github.com/expo/expo/issues/46623.
|
|
71
|
+
*/
|
|
72
|
+
internal fun resolveCallbacksDefinition(view: View, registry: ModuleRegistry): CallbacksDefinition? {
|
|
73
|
+
if (view is ViewFunctionHolder) {
|
|
74
|
+
return view.callbacksDefinition
|
|
75
|
+
}
|
|
76
|
+
val holder = registry.getModuleHolder(view::class.java) ?: return null
|
|
77
|
+
return registry.getViewDefinition(holder, view::class.java)?.callbacksDefinition
|
|
78
|
+
}
|
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
package expo.modules.kotlin.views
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import android.view.ViewTreeObserver
|
|
4
|
+
import expo.modules.kotlin.jni.fabric.NativeStatePropsGetter
|
|
4
5
|
import java.lang.ref.WeakReference
|
|
5
6
|
|
|
6
7
|
class ShadowNodeProxy(expoView: ExpoView) {
|
|
7
8
|
val weakExpoView = WeakReference(expoView)
|
|
9
|
+
private val stateUpdater = NativeStatePropsGetter()
|
|
8
10
|
|
|
11
|
+
private var pendingFlush: ((stateWrapper: Any) -> Unit)? = null
|
|
12
|
+
private var preDrawListener: ViewTreeObserver.OnPreDrawListener? = null
|
|
13
|
+
|
|
14
|
+
// Schedule in predraw listener to avoid early return in re-entrancy
|
|
15
|
+
// We have a proper fix [here](https://github.com/facebook/react-native/pull/56311)
|
|
16
|
+
// but it needs to be merged in RN
|
|
17
|
+
// TODO: Remove the workaround when RN PR gets merged.
|
|
9
18
|
fun setViewSize(width: Double, height: Double) {
|
|
10
|
-
|
|
19
|
+
scheduleFlush { stateWrapper ->
|
|
20
|
+
stateUpdater.updateViewSizeImmediate(stateWrapper, width, height)
|
|
21
|
+
}
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
fun setStyleSize(width: Double?, height: Double?) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
25
|
+
scheduleFlush { stateWrapper ->
|
|
26
|
+
stateUpdater.updateStyleSizeImmediate(stateWrapper, width ?: Double.NaN, height ?: Double.NaN)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private fun scheduleFlush(flush: (stateWrapper: Any) -> Unit) {
|
|
31
|
+
pendingFlush = flush
|
|
32
|
+
val observer = weakExpoView.get()?.viewTreeObserver?.takeIf { it.isAlive } ?: return
|
|
33
|
+
|
|
34
|
+
// Remove the previous attached listener
|
|
35
|
+
preDrawListener?.let(observer::removeOnPreDrawListener)
|
|
36
|
+
|
|
37
|
+
val listener = object : ViewTreeObserver.OnPreDrawListener {
|
|
38
|
+
override fun onPreDraw(): Boolean {
|
|
39
|
+
preDrawListener = null
|
|
40
|
+
// The view is attached while drawing, so this re-fetch returns the same
|
|
41
|
+
// observer that is dispatching us. removeOnPreDrawListener throws on a dead
|
|
42
|
+
// observer, hence the isAlive guard.
|
|
43
|
+
weakExpoView.get()?.viewTreeObserver?.takeIf { it.isAlive }?.removeOnPreDrawListener(this)
|
|
44
|
+
val flushNow = pendingFlush
|
|
45
|
+
pendingFlush = null
|
|
46
|
+
weakExpoView.get()?.stateWrapper?.let { flushNow?.invoke(it) }
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
preDrawListener = listener
|
|
51
|
+
observer.addOnPreDrawListener(listener)
|
|
22
52
|
}
|
|
23
53
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
package expo.modules.kotlin.views
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* A marker interface
|
|
5
|
-
*
|
|
4
|
+
* A marker interface for views that reuse a single class (like ComposeFunctionHolder) and so
|
|
5
|
+
* can't be resolved by class. They're identified by [name] and carry their
|
|
6
|
+
* own [callbacksDefinition], which event invocation reads directly instead of looking it up in the
|
|
7
|
+
* registry. See https://github.com/expo/expo/issues/46623.
|
|
6
8
|
*/
|
|
7
9
|
interface ViewFunctionHolder {
|
|
8
10
|
val name: String
|
|
11
|
+
val callbacksDefinition: CallbacksDefinition?
|
|
9
12
|
}
|
|
@@ -148,6 +148,7 @@ class ViewManagerWrapperDelegate(
|
|
|
148
148
|
|
|
149
149
|
fun onDestroy(view: View) {
|
|
150
150
|
try {
|
|
151
|
+
(view as? ComposeHostingView)?.disposeHostedComposition()
|
|
151
152
|
definition.onViewDestroys?.invoke(view)
|
|
152
153
|
} catch (exception: Throwable) {
|
|
153
154
|
// The view wasn't constructed correctly, so errors are expected.
|
|
@@ -177,3 +178,11 @@ class ViewManagerWrapperDelegate(
|
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Implemented by Compose-based views to dispose their composition when the
|
|
184
|
+
* view is destroyed.
|
|
185
|
+
*/
|
|
186
|
+
interface ComposeHostingView {
|
|
187
|
+
fun disposeHostedComposition()
|
|
188
|
+
}
|
|
@@ -153,7 +153,14 @@ extension Date: Convertible {
|
|
|
153
153
|
}
|
|
154
154
|
return date
|
|
155
155
|
}
|
|
156
|
-
//
|
|
156
|
+
// JS numbers arrive across the JSI bridge as Swift Double (all JS numbers
|
|
157
|
+
// are doubles). `as? Int` does NOT downcast a Double, so without this
|
|
158
|
+
// branch any JS caller passing `someDate.getTime()` to a `Date` / `Date?`
|
|
159
|
+
// argument throws ConvertingException<Date>.
|
|
160
|
+
if let value = value as? Double {
|
|
161
|
+
return Date(timeIntervalSince1970: value / 1000.0)
|
|
162
|
+
}
|
|
163
|
+
// Kept for parity with explicit Int values (rare but possible from native callers).
|
|
157
164
|
if let value = value as? Int {
|
|
158
165
|
return Date(timeIntervalSince1970: Double(value) / 1000.0)
|
|
159
166
|
}
|
|
@@ -21,10 +21,10 @@ internal struct DynamicConvertibleType: AnyDynamicType {
|
|
|
21
21
|
|
|
22
22
|
@JavaScriptActor
|
|
23
23
|
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
|
|
24
|
+
// `from(object:)` dispatches dynamically: reflection-based hydration for `@Field` records,
|
|
25
|
+
// a direct statically-typed factory for `@Record`-synthesized ones.
|
|
24
26
|
if let recordType = innerType as? any Record.Type {
|
|
25
|
-
|
|
26
|
-
try record.update(withObject: try jsValue.asObject(), appContext: appContext)
|
|
27
|
-
return record
|
|
27
|
+
return try recordType.from(object: jsValue.asObject(), appContext: appContext)
|
|
28
28
|
}
|
|
29
29
|
return try innerType.convert(from: jsValue.getAny(), appContext: appContext)
|
|
30
30
|
}
|
|
@@ -53,6 +53,19 @@ internal struct DynamicConvertibleType: AnyDynamicType {
|
|
|
53
53
|
return try convertOriginalValueToJS(value, appContext: appContext)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// The protocol default for the `in: runtime` variant pre-converts the value via
|
|
57
|
+
// `convertFunctionResult` (which calls `convertResult` → `Record.toDictionary` for records) before
|
|
58
|
+
// reaching `castToJS`, defeating the direct object path. Override it to try the direct path first —
|
|
59
|
+
// the same short-circuit as the no-runtime `convertToJS` above — so records (including
|
|
60
|
+
// `@Record`-synthesized ones) convert straight through `toObject` on the return side too.
|
|
61
|
+
func convertToJS<ValueType>(_ value: ValueType, appContext: AppContext, in runtime: JavaScriptRuntime) throws -> JavaScriptValue {
|
|
62
|
+
if let directJSValue = try directJSValueIfPossible(value, appContext: appContext) {
|
|
63
|
+
return directJSValue
|
|
64
|
+
}
|
|
65
|
+
let result = Conversions.convertFunctionResult(value, appContext: appContext, dynamicType: self)
|
|
66
|
+
return try castToJS(result, appContext: appContext, in: runtime)
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
func convertResult<ResultType>(_ result: ResultType, appContext: AppContext) throws -> Any {
|
|
57
70
|
return try innerType.convertResult(result, appContext: appContext)
|
|
58
71
|
}
|
|
@@ -62,15 +75,17 @@ internal struct DynamicConvertibleType: AnyDynamicType {
|
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
private func directJSValueIfPossible<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue? {
|
|
78
|
+
// `toObject` is a `Record` requirement, so the synthesized override dispatches dynamically;
|
|
79
|
+
// `@Field` records fall through to the reflection-based default.
|
|
65
80
|
if let value = value as? any Record {
|
|
66
81
|
return try JavaScriptActor.assumeIsolated {
|
|
67
|
-
try value.
|
|
82
|
+
try value.toObject(appContext: appContext).asValue()
|
|
68
83
|
}
|
|
69
84
|
}
|
|
70
85
|
// `FormattedRecord` isn't a `Record`, so it needs this separate branch to preserve the direct path.
|
|
71
|
-
if let value = value as? any
|
|
86
|
+
if let value = value as? any RecordObjectConvertible {
|
|
72
87
|
return try JavaScriptActor.assumeIsolated {
|
|
73
|
-
try value.
|
|
88
|
+
try value.toObject(appContext: appContext).asValue()
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
return nil
|
|
@@ -38,14 +38,21 @@ public macro OptimizedFunction() =
|
|
|
38
38
|
///
|
|
39
39
|
/// @JS
|
|
40
40
|
/// var status: String { "ok" }
|
|
41
|
-
|
|
41
|
+
///
|
|
42
|
+
/// Also emits a never-called `_assertTypesConformance_<member>` peer that statically asserts every
|
|
43
|
+
/// type crossing the JS boundary conforms to the JS-convertible protocol, so a non-conforming type
|
|
44
|
+
/// fails to compile on the user's declaration. The peer name embeds the member name, hence
|
|
45
|
+
/// `names: arbitrary`.
|
|
46
|
+
@attached(peer, names: arbitrary)
|
|
42
47
|
public macro JS(_ jsName: String? = nil) =
|
|
43
48
|
#externalMacro(module: "ExpoModulesMacros", type: "JSMacro")
|
|
44
49
|
|
|
45
50
|
/// Member macro applied to a `Module` subclass. Scans the class body for declarations
|
|
46
|
-
/// marked with `@JS` and synthesizes a framework-internal `
|
|
51
|
+
/// marked with `@JS` and synthesizes a framework-internal `_synthesizedDefinition()` method.
|
|
47
52
|
/// `expo-modules-core` calls it automatically and merges the result into the module's
|
|
48
|
-
/// definition, so the user doesn't have to reference it from `definition()`.
|
|
53
|
+
/// definition, so the user doesn't have to reference it from `definition()`. `@JS` functions are
|
|
54
|
+
/// additionally bound directly into the module's JS object by a synthesized
|
|
55
|
+
/// `_decorateModule(object:in:appContext:)`.
|
|
49
56
|
///
|
|
50
57
|
/// Usage:
|
|
51
58
|
///
|
|
@@ -58,13 +65,15 @@ public macro JS(_ jsName: String? = nil) =
|
|
|
58
65
|
/// @JS
|
|
59
66
|
/// func greet(name: String) -> String { "Hi, \(name)" }
|
|
60
67
|
/// }
|
|
61
|
-
@attached(
|
|
68
|
+
@attached(
|
|
69
|
+
member,
|
|
70
|
+
names: named(_synthesizedDefinition), named(appContext), named(init), named(_decorateModule))
|
|
62
71
|
public macro ExpoModule(_ name: String? = nil, classes: [Any.Type] = []) =
|
|
63
72
|
#externalMacro(module: "ExpoModulesMacros", type: "ExpoModuleMacro")
|
|
64
73
|
|
|
65
74
|
/// Member macro applied to a `SharedObject` subclass. Scans the class body for declarations
|
|
66
75
|
/// marked with `@JS` (including a single `@JS init(...)` for the JS constructor) and
|
|
67
|
-
/// synthesizes a `
|
|
76
|
+
/// synthesizes a `_synthesizedClassDefinition()` static method returning a `ClassDefinition`.
|
|
68
77
|
/// The companion `@ExpoModule(classes: [Foo.self])` wires the class into the module's
|
|
69
78
|
/// exposed surface.
|
|
70
79
|
///
|
|
@@ -81,6 +90,35 @@ public macro ExpoModule(_ name: String? = nil, classes: [Any.Type] = []) =
|
|
|
81
90
|
/// @JS
|
|
82
91
|
/// var size: Int { 42 }
|
|
83
92
|
/// }
|
|
84
|
-
@attached(member, names: named(
|
|
93
|
+
@attached(member, names: named(_synthesizedClassDefinition))
|
|
85
94
|
public macro SharedObject(_ name: String? = nil) =
|
|
86
95
|
#externalMacro(module: "ExpoModulesMacros", type: "SharedObjectMacro")
|
|
96
|
+
|
|
97
|
+
/// Member + extension macro applied to a record `struct` or `class`. Every non-`static`,
|
|
98
|
+
/// non-`private`/`fileprivate`, non-`lazy`, non-computed stored property is part of the record — no
|
|
99
|
+
/// `@Field` wrapper needed — and the macro synthesizes the whole conversion surface from each
|
|
100
|
+
/// property's static type: a memberwise `init`, the `from(object:appContext:)` /
|
|
101
|
+
/// `from(dictionary:appContext:)` factories, and the `toDictionary(appContext:)` /
|
|
102
|
+
/// `toObject(appContext:)` write side. The type is auto-conformed to `Record`; the synthesized
|
|
103
|
+
/// methods override `Record`'s reflection-based defaults, so it stays usable anywhere a `Record`
|
|
104
|
+
/// argument is expected.
|
|
105
|
+
///
|
|
106
|
+
/// Requiredness is inferred from each property: a default value makes it optional, an optional type
|
|
107
|
+
/// makes it nullable and optional, and a non-optional property without a default is required (the
|
|
108
|
+
/// factories throw `RecordPropertyRequiredException` when the source omits it).
|
|
109
|
+
///
|
|
110
|
+
/// Usage:
|
|
111
|
+
///
|
|
112
|
+
/// @Record
|
|
113
|
+
/// struct Options {
|
|
114
|
+
/// var name: String // required
|
|
115
|
+
/// var count: Int = 0 // optional (has default)
|
|
116
|
+
/// var note: String? // nullable + optional
|
|
117
|
+
/// }
|
|
118
|
+
@attached(
|
|
119
|
+
member,
|
|
120
|
+
names: named(init), named(from), named(toDictionary), named(toObject),
|
|
121
|
+
named(_assertTypesConformance))
|
|
122
|
+
@attached(extension, conformances: Record)
|
|
123
|
+
public macro Record() =
|
|
124
|
+
#externalMacro(module: "ExpoModulesMacros", type: "RecordMacro")
|
|
@@ -52,20 +52,20 @@ public final class ModuleHolder {
|
|
|
52
52
|
|
|
53
53
|
/// Combines the user-authored definition with the entries synthesized by the
|
|
54
54
|
/// `@ExpoModule` macro on this module's class (if any). The macro emits a
|
|
55
|
-
/// `
|
|
55
|
+
/// `_synthesizedDefinition()` method returning an `[AnyDefinition]` array of the
|
|
56
56
|
/// `Function` / `Property` / `Constructor` entries it generated from `@JS`
|
|
57
57
|
/// members. Those entries are prepended to the user's definitions and the
|
|
58
58
|
/// whole list is fed back through `ModuleDefinition.init` so the merged
|
|
59
59
|
/// result is rebucketed (into `functions`, `properties`, etc.) just like a
|
|
60
60
|
/// hand-written definition. Modules that don't use the macro fall through
|
|
61
|
-
/// the empty-
|
|
61
|
+
/// the empty-synthesized fast path and return the user's definition unchanged.
|
|
62
62
|
private static func buildDefinition(for module: AnyModule) -> ModuleDefinition {
|
|
63
63
|
let userDefinition = module.definition()
|
|
64
|
-
let
|
|
65
|
-
if
|
|
64
|
+
let synthesized = module._synthesizedDefinition()
|
|
65
|
+
if synthesized.isEmpty {
|
|
66
66
|
return userDefinition
|
|
67
67
|
}
|
|
68
|
-
return ModuleDefinition(definitions:
|
|
68
|
+
return ModuleDefinition(definitions: synthesized + userDefinition.rawDefinitions)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// MARK: Constants
|
|
@@ -106,7 +106,13 @@ public final class ModuleHolder {
|
|
|
106
106
|
}
|
|
107
107
|
do {
|
|
108
108
|
log.info("Creating JS object for module '\(name)'")
|
|
109
|
-
|
|
109
|
+
let object = try definition.build(appContext: appContext)
|
|
110
|
+
|
|
111
|
+
// Install the `@JS` members the `@ExpoModule` macro binds directly into the JS object
|
|
112
|
+
// (the direct-JSI path). A no-op for modules that don't use the macro.
|
|
113
|
+
try module._decorateModule(object: object, in: appContext.runtime, appContext: appContext)
|
|
114
|
+
|
|
115
|
+
return object
|
|
110
116
|
} catch {
|
|
111
117
|
log.error("Building the module object failed: \(error)")
|
|
112
118
|
return nil
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import ExpoModulesJSI
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
A protocol for any type-erased module that provides functions used by the core.
|
|
3
5
|
*/
|
|
@@ -19,11 +21,23 @@ public protocol AnyModule: AnyObject, AnyArgument {
|
|
|
19
21
|
/// Framework-internal: the leading underscore signals this is not part of the public API and
|
|
20
22
|
/// should only be called by `expo-modules-core` itself. Modules that don't use the macro fall
|
|
21
23
|
/// back to the default empty implementation.
|
|
22
|
-
func
|
|
24
|
+
func _synthesizedDefinition() -> [AnyDefinition]
|
|
25
|
+
|
|
26
|
+
/// Binds the module's `@JS` members directly into its JavaScript object, generated by the
|
|
27
|
+
/// `@ExpoModule` macro. Core calls this after building the module object so the synthesized
|
|
28
|
+
/// host functions are installed alongside the DSL-described surface. Framework-internal
|
|
29
|
+
/// (leading underscore). Modules that don't use the macro fall back to the default no-op.
|
|
30
|
+
@JavaScriptActor
|
|
31
|
+
func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
public extension AnyModule {
|
|
26
|
-
func
|
|
35
|
+
func _synthesizedDefinition() -> [AnyDefinition] {
|
|
27
36
|
return []
|
|
28
37
|
}
|
|
38
|
+
|
|
39
|
+
@JavaScriptActor
|
|
40
|
+
func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
|
|
41
|
+
// No-op by default — only `@ExpoModule`-macro modules synthesize a real implementation.
|
|
42
|
+
}
|
|
29
43
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Class that binds a formatter with a record.
|
|
3
3
|
It can be converted to JS, but it can't be converted from a JS value.
|
|
4
4
|
*/
|
|
5
|
-
public struct FormattedRecord<RecordType: Record>: Convertible,
|
|
5
|
+
public struct FormattedRecord<RecordType: Record>: Convertible, RecordObjectConvertible {
|
|
6
6
|
internal final class FormattedRecordCannotBeUsedAsParameterException: Exception, @unchecked Sendable {
|
|
7
7
|
override var reason: String {
|
|
8
8
|
"FormattedRecord cannot be used as a parameter"
|
|
@@ -39,7 +39,7 @@ public struct FormattedRecord<RecordType: Record>: Convertible, RecordJavaScript
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
@JavaScriptActor
|
|
42
|
-
func
|
|
42
|
+
func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
43
43
|
let object = try appContext.runtime.createObject()
|
|
44
44
|
|
|
45
45
|
for field in fieldsOf(record) {
|
|
@@ -58,6 +58,6 @@ public struct FormattedRecord<RecordType: Record>: Convertible, RecordJavaScript
|
|
|
58
58
|
let jsValue = try recordFieldValueToJSValue(value, appContext: appContext)
|
|
59
59
|
object.setProperty(key, value: jsValue)
|
|
60
60
|
}
|
|
61
|
-
return object
|
|
61
|
+
return object
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -20,15 +20,53 @@ public protocol Record: Convertible {
|
|
|
20
20
|
*/
|
|
21
21
|
init(from: Dict, appContext: AppContext) throws
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
Reads the record's members off a `JavaScriptObject`. The `@Field`-based default goes through
|
|
25
|
+
reflection (`init()` + `update(withObject:)`); the `@Record` macro overrides it with a direct,
|
|
26
|
+
factory that reads each stored property by its declared type.
|
|
27
|
+
*/
|
|
28
|
+
@JavaScriptActor
|
|
29
|
+
static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
Reads the record's members from a `[String: Any]` dictionary. The `@Field`-based default goes
|
|
33
|
+
through reflection (`init()` + `update(withDict:)`); the `@Record` macro overrides it with a
|
|
34
|
+
direct, statically-typed factory.
|
|
35
|
+
*/
|
|
36
|
+
static func from(dictionary: Dict, appContext: AppContext) throws -> Self
|
|
37
|
+
|
|
23
38
|
/**
|
|
24
39
|
Converts the record back to the dictionary. Only members wrapped by `@Field` will be set in the dictionary.
|
|
25
40
|
*/
|
|
26
41
|
func toDictionary(appContext: AppContext?) -> Dict
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
Converts the record to a `JavaScriptObject`. The `@Field`-based default builds the object via
|
|
45
|
+
reflection; the `@Record` macro overrides it with a direct, statically-typed conversion.
|
|
46
|
+
*/
|
|
47
|
+
@JavaScriptActor
|
|
48
|
+
func toObject(appContext: AppContext) throws -> JavaScriptObject
|
|
27
49
|
}
|
|
28
50
|
|
|
29
|
-
|
|
51
|
+
/**
|
|
52
|
+
Adopted by record-like types that produce a `JavaScriptObject` directly but aren't themselves a
|
|
53
|
+
`Record` — currently just `FormattedRecord`. Lets `DynamicConvertibleType` take the direct
|
|
54
|
+
object-building path for them, same as for records.
|
|
55
|
+
*/
|
|
56
|
+
internal protocol RecordObjectConvertible {
|
|
30
57
|
@JavaScriptActor
|
|
31
|
-
func
|
|
58
|
+
func toObject(appContext: AppContext) throws -> JavaScriptObject
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
Thrown by a synthesized record's factories when a required property is missing from the source.
|
|
63
|
+
Public because the `@Record`-generated code lives in user modules and references it directly.
|
|
64
|
+
The `@Field`-based path has its own internal `FieldRequiredException`.
|
|
65
|
+
*/
|
|
66
|
+
public final class RecordPropertyRequiredException: GenericException<String>, @unchecked Sendable {
|
|
67
|
+
override public var reason: String {
|
|
68
|
+
return "Value for property '\(param)' is required, got nil"
|
|
69
|
+
}
|
|
32
70
|
}
|
|
33
71
|
|
|
34
72
|
/**
|
|
@@ -37,7 +75,9 @@ internal protocol RecordJavaScriptValueConvertible {
|
|
|
37
75
|
public extension Record {
|
|
38
76
|
static func convert(from value: Any?, appContext: AppContext) throws -> Self {
|
|
39
77
|
if let value = value as? Dict {
|
|
40
|
-
|
|
78
|
+
// `from(dictionary:)` dispatches dynamically — reflection for `@Field` records, the
|
|
79
|
+
// synthesized factory for `@Record` ones — so both read correctly off a dictionary.
|
|
80
|
+
return try Self.from(dictionary: value, appContext: appContext)
|
|
41
81
|
}
|
|
42
82
|
// It's possible that the current implementation tries to convert a value that is already of the desired type.
|
|
43
83
|
// Handle that gracefully instead of throwing an exception.
|
|
@@ -52,6 +92,22 @@ public extension Record {
|
|
|
52
92
|
try update(withDict: dict, appContext: appContext)
|
|
53
93
|
}
|
|
54
94
|
|
|
95
|
+
@JavaScriptActor
|
|
96
|
+
static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
97
|
+
// Reflection-based default for `@Field` records. The `@Record` macro synthesizes a direct,
|
|
98
|
+
// statically-typed override that takes precedence over this.
|
|
99
|
+
let record = Self()
|
|
100
|
+
try record.update(withObject: object, appContext: appContext)
|
|
101
|
+
return record
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static func from(dictionary: Dict, appContext: AppContext) throws -> Self {
|
|
105
|
+
// Reflection-based default for `@Field` records. The `@Record` macro synthesizes a direct,
|
|
106
|
+
// statically-typed override that takes precedence over this. Delegates to `init(from:appContext:)`
|
|
107
|
+
// so any custom dictionary initializer a record provides still runs.
|
|
108
|
+
return try Self(from: dictionary, appContext: appContext)
|
|
109
|
+
}
|
|
110
|
+
|
|
55
111
|
func update(withDict dict: Dict, appContext: AppContext) throws {
|
|
56
112
|
let dictKeys = dict.keys
|
|
57
113
|
|
|
@@ -107,7 +163,7 @@ public extension Record {
|
|
|
107
163
|
}
|
|
108
164
|
|
|
109
165
|
@JavaScriptActor
|
|
110
|
-
func
|
|
166
|
+
func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
111
167
|
let object = try appContext.runtime.createObject()
|
|
112
168
|
|
|
113
169
|
for field in fieldsOf(self) {
|
|
@@ -117,7 +173,7 @@ public extension Record {
|
|
|
117
173
|
let value = try recordFieldValueToJSValue(field.get(), dynamicType: field.fieldType, appContext: appContext)
|
|
118
174
|
object.setProperty(key, value: value)
|
|
119
175
|
}
|
|
120
|
-
return object
|
|
176
|
+
return object
|
|
121
177
|
}
|
|
122
178
|
}
|
|
123
179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.17",
|
|
4
4
|
"description": "The core of Expo Modules architecture",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"preset": "expo-module-scripts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@expo/expo-modules-macros-plugin": "
|
|
50
|
-
"expo-modules-jsi": "~56.0.
|
|
49
|
+
"@expo/expo-modules-macros-plugin": "0.2.2",
|
|
50
|
+
"expo-modules-jsi": "~56.0.10",
|
|
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": "812dc007aefed0c432c0439fdfe05ee2f4f21da2",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|