expo-modules-core 56.0.14 → 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 +29 -4
- package/ExpoModulesCore.podspec +1 -1
- package/ExpoModulesWorklets.podspec +7 -0
- package/android/build.gradle +2 -2
- package/android/src/compose/expo/modules/kotlin/views/ExpoComposeView.kt +6 -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/AppDelegates/ExpoAppDelegateSubscriberManager.swift +20 -8
- package/ios/Core/AppContext.swift +2 -2
- package/ios/Core/Arguments/Convertibles.swift +8 -1
- package/ios/Core/Conversions.swift +1 -1
- package/ios/Core/DynamicTypes/DynamicConvertibleType.swift +21 -6
- package/ios/Core/DynamicTypes/DynamicSharedObjectType.swift +1 -1
- package/ios/Core/ExpoModulesMacros.swift +44 -6
- package/ios/Core/Functions/AsyncFunctionDefinition.swift +1 -3
- 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/ios/JS/ExpoRuntimeInstaller.swift +1 -1
- package/ios/Platform/Platform.h +1 -0
- package/ios/Platform/Platform.swift +1 -0
- package/ios/Worklets/Core/DynamicSerializableType.swift +4 -0
- package/ios/Worklets/Core/Worklet.swift +10 -0
- package/ios/WorkletsTests/WorkletRuntimeIntegrationTests.swift +27 -0
- package/ios/WorkletsTests/WorkletsNotInstalledTests.swift +33 -0
- package/package.json +5 -5
- 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/spm.config.json +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@
|
|
|
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
|
+
|
|
29
|
+
## 56.0.15 — 2026-06-05
|
|
30
|
+
|
|
31
|
+
### 🐛 Bug fixes
|
|
32
|
+
|
|
33
|
+
- [iOS] Propagate async-function promise construction failures instead of trapping the app. ([#46106](https://github.com/expo/expo/issues/46106) by [@qutrek](https://github.com/qutrek)) ([#46145](https://github.com/expo/expo/pull/46145) by [@mvincentong](https://github.com/mvincentong))
|
|
34
|
+
- [iOS] Read iPad-specific supported orientations from `UISupportedInterfaceOrientations~ipad`. ([#46281](https://github.com/expo/expo/issues/46281) by [@bryandent](https://github.com/bryandent)) ([#46306](https://github.com/expo/expo/pull/46306) by [@mvincentong](https://github.com/mvincentong))
|
|
35
|
+
- [android] Fix nested `Host` double-composing children. ([#46304](https://github.com/expo/expo/pull/46304) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
36
|
+
- [iOS] Throw an actionable error when a worklet is used but `react-native-worklets`'s native adapter isn't linked, instead of a misleading "not an instance of Worklet" failure. ([#46571](https://github.com/expo/expo/pull/46571) by [@chrfalch](https://github.com/chrfalch))
|
|
37
|
+
|
|
38
|
+
### 💡 Others
|
|
39
|
+
|
|
40
|
+
- Native view config attributes now carry a `process` function that unwraps shared objects to their registry id, so callers can pass shared objects directly as view props instead of unwrapping them manually. ([#46212](https://github.com/expo/expo/pull/46212) by [@tsapeta](https://github.com/tsapeta))
|
|
41
|
+
|
|
13
42
|
## 56.0.14 — 2026-05-29
|
|
14
43
|
|
|
15
44
|
_This version does not introduce any user-facing changes._
|
|
@@ -20,10 +49,6 @@ _This version does not introduce any user-facing changes._
|
|
|
20
49
|
|
|
21
50
|
- [Android] Create Compose props without View. ([#46256](https://github.com/expo/expo/pull/46256) by [@jakex7](https://github.com/jakex7))
|
|
22
51
|
|
|
23
|
-
### 💡 Others
|
|
24
|
-
|
|
25
|
-
- Native view config attributes now carry a `process` function that unwraps shared objects to their registry id, so callers can pass shared objects directly as view props instead of unwrapping them manually. ([#46212](https://github.com/expo/expo/pull/46212) by [@tsapeta](https://github.com/tsapeta))
|
|
26
|
-
|
|
27
52
|
## 56.0.12 — 2026-05-21
|
|
28
53
|
|
|
29
54
|
### 🐛 Bug fixes
|
package/ExpoModulesCore.podspec
CHANGED
|
@@ -119,7 +119,7 @@ Pod::Spec.new do |s|
|
|
|
119
119
|
s.static_framework = true
|
|
120
120
|
s.header_dir = 'ExpoModulesCore'
|
|
121
121
|
s.source_files = 'ios/**/*.{h,m,mm,swift,cpp}', 'common/cpp/**/*.{h,cpp}'
|
|
122
|
-
s.exclude_files = ['ios/Tests', 'ios/Worklets', 'ios/WorkletsAdapter']
|
|
122
|
+
s.exclude_files = ['ios/Tests', 'ios/Worklets', 'ios/WorkletsTests', 'ios/WorkletsAdapter']
|
|
123
123
|
s.compiler_flags = compiler_flags
|
|
124
124
|
s.private_header_files = ['ios/**/*+Private.h', 'ios/**/Swift.h']
|
|
125
125
|
end
|
|
@@ -31,4 +31,11 @@ Pod::Spec.new do |s|
|
|
|
31
31
|
|
|
32
32
|
s.source_files = 'ios/Worklets/**/*.{h,m,mm,swift,cpp}'
|
|
33
33
|
s.private_header_files = 'ios/Worklets/**/*+Private.h'
|
|
34
|
+
|
|
35
|
+
s.test_spec 'Tests' do |test_spec|
|
|
36
|
+
# ExpoModulesCore requires React-hermes or React-jsc in tests, add ExpoModulesTestCore for the underlying dependencies
|
|
37
|
+
test_spec.dependency 'ExpoModulesTestCore'
|
|
38
|
+
|
|
39
|
+
test_spec.source_files = 'ios/WorkletsTests/**/*.{m,swift}'
|
|
40
|
+
end
|
|
34
41
|
end
|
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
|
|
|
@@ -128,6 +128,8 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
128
128
|
recomposeScope = currentRecomposeScope
|
|
129
129
|
for (index in 0..<this.size) {
|
|
130
130
|
val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
|
|
131
|
+
// Hosting children render themselves via their own ComposeView; skip to avoid double-rendering.
|
|
132
|
+
if (child.shouldUseAndroidLayout) continue
|
|
131
133
|
key(child) {
|
|
132
134
|
with(composableScope ?: ComposableScope()) {
|
|
133
135
|
with(child) {
|
|
@@ -143,6 +145,7 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
143
145
|
recomposeScope = currentRecomposeScope
|
|
144
146
|
for (index in 0..<this.size) {
|
|
145
147
|
val child = getChildAt(index) as? ExpoComposeView<*> ?: continue
|
|
148
|
+
if (child.shouldUseAndroidLayout) continue
|
|
146
149
|
if (!filter(child)) {
|
|
147
150
|
continue
|
|
148
151
|
}
|
|
@@ -160,6 +163,7 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
160
163
|
fun Child(composableScope: ComposableScope, index: Int) {
|
|
161
164
|
recomposeScope = currentRecomposeScope
|
|
162
165
|
val child = getChildAt(index) as? ExpoComposeView<*> ?: return
|
|
166
|
+
if (child.shouldUseAndroidLayout) return
|
|
163
167
|
key(child) {
|
|
164
168
|
with(composableScope) {
|
|
165
169
|
with(child) {
|
|
@@ -507,7 +511,8 @@ class ComposeFunctionHolder<Props : ComposeProps>(
|
|
|
507
511
|
appContext: AppContext,
|
|
508
512
|
override val name: String,
|
|
509
513
|
private val composableContent: @Composable FunctionalComposableScope.(props: Props) -> Unit,
|
|
510
|
-
override val props: Props
|
|
514
|
+
override val props: Props,
|
|
515
|
+
override val callbacksDefinition: CallbacksDefinition?
|
|
511
516
|
) : ExpoComposeView<Props>(context, appContext), ViewFunctionHolder {
|
|
512
517
|
val propsMutableState = mutableStateOf(props)
|
|
513
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
|
}
|
|
@@ -462,9 +462,7 @@ public class ExpoAppDelegateSubscriberManager: NSObject {
|
|
|
462
462
|
*/
|
|
463
463
|
@objc
|
|
464
464
|
public static func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
|
465
|
-
let
|
|
466
|
-
let universalOrientationMask = allowedOrientations(for: .unspecified)
|
|
467
|
-
let infoPlistOrientations = deviceOrientationMask.isEmpty ? universalOrientationMask : deviceOrientationMask
|
|
465
|
+
let infoPlistOrientations = allowedOrientations(for: UIDevice.current.userInterfaceIdiom)
|
|
468
466
|
|
|
469
467
|
let parsedSubscribers = ExpoAppDelegateSubscriberRepository.subscribers.filter {
|
|
470
468
|
$0.responds(to: #selector(UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:)))
|
|
@@ -483,14 +481,28 @@ public class ExpoAppDelegateSubscriberManager: NSObject {
|
|
|
483
481
|
}
|
|
484
482
|
|
|
485
483
|
#if os(iOS)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
484
|
+
func allowedOrientations(
|
|
485
|
+
for userInterfaceIdiom: UIUserInterfaceIdiom,
|
|
486
|
+
infoDictionary: [String: Any]? = Bundle.main.infoDictionary
|
|
487
|
+
) -> UIInterfaceOrientationMask {
|
|
488
|
+
// For now only iPad-specific orientations are supported. When the `~ipad` key is absent,
|
|
489
|
+
// fall back to the universal `UISupportedInterfaceOrientations` key, matching how UIKit
|
|
490
|
+
// resolves device-specific keys. We additionally fall back when `~ipad` is present but
|
|
491
|
+
// resolves to no orientations, treating a malformed entry as if it were absent.
|
|
492
|
+
if userInterfaceIdiom == .pad,
|
|
493
|
+
let mask = orientationMask(forKey: "UISupportedInterfaceOrientations~ipad", in: infoDictionary),
|
|
494
|
+
!mask.isEmpty {
|
|
491
495
|
return mask
|
|
492
496
|
}
|
|
497
|
+
return orientationMask(forKey: "UISupportedInterfaceOrientations", in: infoDictionary) ?? []
|
|
498
|
+
}
|
|
493
499
|
|
|
500
|
+
private func orientationMask(forKey key: String, in infoDictionary: [String: Any]?) -> UIInterfaceOrientationMask? {
|
|
501
|
+
guard let orientations = infoDictionary?[key] as? [String] else {
|
|
502
|
+
return nil
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
var mask: UIInterfaceOrientationMask = []
|
|
494
506
|
for orientation in orientations {
|
|
495
507
|
switch orientation {
|
|
496
508
|
case "UIInterfaceOrientationPortrait":
|
|
@@ -529,8 +529,8 @@ public final class AppContext: NSObject, EXAppContextProtocol, @unchecked Sendab
|
|
|
529
529
|
*/
|
|
530
530
|
@JavaScriptActor
|
|
531
531
|
private func installModuleClasses(in runtime: JavaScriptRuntime) throws {
|
|
532
|
-
let coreObject = runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
|
|
533
|
-
let sharedObjectClass = coreObject.getPropertyAsObject("SharedObject")
|
|
532
|
+
let coreObject = try runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
|
|
533
|
+
let sharedObjectClass = try coreObject.getPropertyAsObject("SharedObject")
|
|
534
534
|
let sharedObjectBaseProto = sharedObjectClass.getProperty("prototype")
|
|
535
535
|
|
|
536
536
|
// Stored as JavaScriptValue (a class) because JavaScriptObject is ~Copyable
|
|
@@ -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
|
}
|
|
@@ -218,7 +218,7 @@ public struct Conversions {
|
|
|
218
218
|
memcpy(arrayBuffer.data(), baseAddress, data.count)
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
let uint8ArrayCtor = runtime.global().getPropertyAsFunction("Uint8Array")
|
|
221
|
+
let uint8ArrayCtor = try runtime.global().getPropertyAsFunction("Uint8Array")
|
|
222
222
|
return try uint8ArrayCtor.callAsConstructor(arrayBuffer.asValue())
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -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
|
|
@@ -95,7 +95,7 @@ private func getBaseSharedType(_ appContext: AppContext, nativeType: AnySharedOb
|
|
|
95
95
|
|
|
96
96
|
private func newBaseSharedObject(_ appContext: AppContext, nativeType: AnySharedObject.Type) throws -> JavaScriptObject? {
|
|
97
97
|
let jsClass = try getBaseSharedType(appContext, nativeType: nativeType)
|
|
98
|
-
let prototype = jsClass.asObject().getPropertyAsObject("prototype")
|
|
98
|
+
let prototype = try jsClass.asObject().getPropertyAsObject("prototype")
|
|
99
99
|
return try appContext.runtime.createObject(prototype: prototype)
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -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")
|
|
@@ -186,7 +186,7 @@ public class AsyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnyAsyncFu
|
|
|
186
186
|
guard let appContext else {
|
|
187
187
|
throw Exceptions.AppContextLost()
|
|
188
188
|
}
|
|
189
|
-
let promise = JavaScriptPromise(
|
|
189
|
+
let promise = try JavaScriptPromise(appContext.runtime)
|
|
190
190
|
let promiseValue = promise.asValue()
|
|
191
191
|
|
|
192
192
|
self.call(appContext, this: this, arguments: arguments) { [promise] result in
|
|
@@ -209,5 +209,3 @@ public class AsyncFunctionDefinition<Args, FirstArgType, ReturnType>: AnyAsyncFu
|
|
|
209
209
|
return self
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
|
|
@@ -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
|
|
|
@@ -30,7 +30,7 @@ internal class ExpoRuntimeInstaller: EXJavaScriptRuntimeManager {
|
|
|
30
30
|
*/
|
|
31
31
|
@JavaScriptActor
|
|
32
32
|
internal func installExpoModulesHostObject() throws {
|
|
33
|
-
let coreObject = runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
|
|
33
|
+
let coreObject = try runtime.global().getPropertyAsObject(EXGlobalCoreObjectPropertyName)
|
|
34
34
|
|
|
35
35
|
if coreObject.hasProperty("modules") {
|
|
36
36
|
// Host object already installed
|
package/ios/Platform/Platform.h
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
@compatibility_alias UIWindow NSWindow;
|
|
17
17
|
@compatibility_alias UIHostingController NSHostingController;
|
|
18
18
|
@compatibility_alias UIImage NSImage;
|
|
19
|
+
@compatibility_alias UIImageView NSImageView;
|
|
19
20
|
|
|
20
21
|
#ifndef UIApplication
|
|
21
22
|
@compatibility_alias UIApplication NSApplication;
|
|
@@ -14,6 +14,7 @@ public typealias UIHostingController = NSHostingController
|
|
|
14
14
|
public typealias UIViewRepresentable = NSViewRepresentable
|
|
15
15
|
public typealias UILabel = NSLabel
|
|
16
16
|
public typealias UIImage = NSImage
|
|
17
|
+
public typealias UIImageView = NSImageView
|
|
17
18
|
public typealias UIPasteboard = NSPasteboard
|
|
18
19
|
|
|
19
20
|
extension UIApplication {
|
|
@@ -28,6 +28,10 @@ internal struct DynamicSerializableType: AnyDynamicType {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
guard let jsSerializable else {
|
|
31
|
+
// Adapter not linked → the extractor returns nil for every value; give an actionable error.
|
|
32
|
+
if WorkletsProviderRegistry.shared == nil {
|
|
33
|
+
throw WorkletsNotInstalledException()
|
|
34
|
+
}
|
|
31
35
|
throw NotSerializableException(innerType)
|
|
32
36
|
}
|
|
33
37
|
return Serializable(jsSerializable)
|
|
@@ -46,3 +46,13 @@ internal final class NotWorkletException: GenericException<SerializableValueType
|
|
|
46
46
|
"Expected Serializable of type Worklet but got \(param)"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/// Thrown when a worklet operation is attempted but the `react-native-worklets` native
|
|
51
|
+
/// adapter isn't linked (its provider is `nil`). Replaces misleading lower-level errors.
|
|
52
|
+
internal final class WorkletsNotInstalledException: Exception, @unchecked Sendable {
|
|
53
|
+
override var reason: String {
|
|
54
|
+
"This feature requires `react-native-worklets`, but its native adapter isn't linked — usually " +
|
|
55
|
+
"because the package isn't installed (it's an optional peer dependency of libraries like Expo UI " +
|
|
56
|
+
"and Reanimated). Install it with `npx expo install react-native-worklets`, then rebuild the app."
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Testing
|
|
4
|
+
|
|
5
|
+
import ExpoModulesCore
|
|
6
|
+
import ExpoModulesWorklets
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
Runtime-level checks for the worklets integration wiring.
|
|
10
|
+
|
|
11
|
+
Note: these run under `use_expo_modules_tests!`, which does not link companion pods, so the
|
|
12
|
+
worklets adapter — and therefore `WorkletsProviderRegistry.shared` — is intentionally absent
|
|
13
|
+
here (the same state as an app without `react-native-worklets`). Whether the adapter is
|
|
14
|
+
correctly autolinked is guarded by the JS test `WorkletsAdapterAutolinking-test.ts`; the
|
|
15
|
+
consumer-side behaviour when it's missing is covered by `WorkletsNotInstalledTests`.
|
|
16
|
+
*/
|
|
17
|
+
@Suite("WorkletRuntimeIntegration", .serialized)
|
|
18
|
+
struct WorkletRuntimeIntegrationTests {
|
|
19
|
+
@Test
|
|
20
|
+
func `UI worklet runtime factory registers on the app context`() {
|
|
21
|
+
// `WorkletIntegration.register()` installs the core-side hook (normally invoked via +load)
|
|
22
|
+
// that turns a worklet runtime pointer into a usable runtime. Guards the factory wiring onto
|
|
23
|
+
// AppContext; it does not require the adapter to be linked.
|
|
24
|
+
WorkletIntegration.register()
|
|
25
|
+
#expect(AppContext.uiRuntimeFactory != nil)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Copyright 2025-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import Testing
|
|
4
|
+
|
|
5
|
+
@testable import ExpoModulesCore
|
|
6
|
+
@testable import ExpoModulesWorklets
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
The `use_expo_modules_tests!` harness does not link companion pods, so the worklets adapter
|
|
10
|
+
never registers `WorkletsProviderRegistry.shared`. That nil-provider state is exactly an app
|
|
11
|
+
where `react-native-worklets` isn't installed — so this is the right place to assert that a
|
|
12
|
+
worklet operation fails with the actionable `WorkletsNotInstalledException` rather than a
|
|
13
|
+
misleading lower-level error (e.g. "not an instance of Worklet").
|
|
14
|
+
*/
|
|
15
|
+
@Suite("WorkletsNotInstalled", .serialized)
|
|
16
|
+
@JavaScriptActor
|
|
17
|
+
struct WorkletsNotInstalledTests {
|
|
18
|
+
@Test
|
|
19
|
+
func `converting a worklet argument reports that worklets are not installed`() throws {
|
|
20
|
+
// Only meaningful while the adapter is unlinked (the default in this test harness).
|
|
21
|
+
try #require(WorkletsProviderRegistry.shared == nil)
|
|
22
|
+
|
|
23
|
+
let appContext = AppContext.create()
|
|
24
|
+
let runtime = try appContext.runtime
|
|
25
|
+
// Any JS value works — with the adapter unlinked, extraction returns nil for everything.
|
|
26
|
+
let jsValue = try runtime.eval("42")
|
|
27
|
+
let dynamicType = DynamicSerializableType(innerType: Worklet.self)
|
|
28
|
+
|
|
29
|
+
#expect(throws: WorkletsNotInstalledException.self) {
|
|
30
|
+
_ = try dynamicType.cast(jsValue: jsValue, appContext: appContext)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
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": {
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"@testing-library/react-native": "^13.3.0",
|
|
65
65
|
"@types/react": "~19.2.0",
|
|
66
66
|
"@types/invariant": "^2.2.33",
|
|
67
|
-
"expo-module-scripts": "56.0.
|
|
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
|
package/spm.config.json
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"headerPattern": "**/*.h",
|
|
47
47
|
"exclude": [
|
|
48
48
|
"Tests/**",
|
|
49
|
+
"WorkletsTests/**",
|
|
49
50
|
"Worklets/**",
|
|
50
51
|
"WorkletsAdapter/**"
|
|
51
52
|
],
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
"exclude": [
|
|
88
89
|
"JSI/**",
|
|
89
90
|
"Tests/**",
|
|
91
|
+
"WorkletsTests/**",
|
|
90
92
|
"BridgeModule/**",
|
|
91
93
|
"Views/**",
|
|
92
94
|
"Worklets/**",
|