expo-modules-core 56.0.15 → 56.0.16
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 +16 -0
- package/android/build.gradle +2 -2
- package/android/src/compose/expo/modules/kotlin/views/ExpoComposeView.kt +2 -1
- 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 +6 -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/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,22 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 56.0.16 — 2026-06-10
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [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))
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- [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))
|
|
22
|
+
- [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))
|
|
23
|
+
- [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))
|
|
24
|
+
|
|
25
|
+
### 💡 Others
|
|
26
|
+
|
|
27
|
+
- [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))
|
|
28
|
+
|
|
13
29
|
## 56.0.15 — 2026-06-05
|
|
14
30
|
|
|
15
31
|
### 🐛 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.16'
|
|
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.16"
|
|
98
98
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
99
99
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
100
100
|
|
|
@@ -511,7 +511,8 @@ class ComposeFunctionHolder<Props : ComposeProps>(
|
|
|
511
511
|
appContext: AppContext,
|
|
512
512
|
override val name: String,
|
|
513
513
|
private val composableContent: @Composable FunctionalComposableScope.(props: Props) -> Unit,
|
|
514
|
-
override val props: Props
|
|
514
|
+
override val props: Props,
|
|
515
|
+
override val callbacksDefinition: CallbacksDefinition?
|
|
515
516
|
) : ExpoComposeView<Props>(context, appContext), ViewFunctionHolder {
|
|
516
517
|
val propsMutableState = mutableStateOf(props)
|
|
517
518
|
|
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("updateStyleSizeImmediate", NativeStatePropsGetter::updateStyleSizeImmediate),
|
|
13
|
+
makeNativeMethod("updateViewSizeImmediate", 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
|
|
@@ -6,4 +6,10 @@ import com.facebook.yoga.annotations.DoNotStrip
|
|
|
6
6
|
class NativeStatePropsGetter {
|
|
7
7
|
// We can't use StateWrapper directly, as this class is not exposed
|
|
8
8
|
external fun getStateProps(stateWrapper: Any): Map<String, Any?>?
|
|
9
|
+
|
|
10
|
+
// Synchronously flush a style property size update in the current frame (pass NaN for "unset").
|
|
11
|
+
external fun updateStyleSizeImmediate(stateWrapper: Any, styleWidth: Double, styleHeight: Double)
|
|
12
|
+
|
|
13
|
+
// Synchronously flush a size update in the current frame.
|
|
14
|
+
external fun updateViewSizeImmediate(stateWrapper: Any, width: Double, height: Double)
|
|
9
15
|
}
|
|
@@ -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
|
}
|
|
@@ -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.16",
|
|
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.9",
|
|
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": "b1e94a5c1c5b19472a42ca25752a3533699bc46a",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|