expo-modules-core 56.0.8 → 56.0.10
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 +17 -0
- package/android/build.gradle +2 -2
- package/android/src/compose/expo/modules/kotlin/views/ExpoComposeView.kt +35 -0
- package/android/src/main/cpp/decorators/JSPropertiesDecorator.h +2 -0
- package/android/src/main/cpp/fabric/AndroidExpoViewState.h +2 -0
- package/android/src/main/cpp/fabric/ExpoComponentDescriptorFactory.cpp +31 -0
- package/android/src/main/cpp/fabric/ExpoComponentDescriptorFactory.h +23 -0
- package/android/src/main/cpp/fabric/FabricComponentsRegistry.cpp +3 -25
- package/android/src/main/cpp/fabric/FabricComponentsRegistry.h +0 -17
- package/android/src/main/java/expo/modules/kotlin/sharedobjects/SharedObject.kt +26 -4
- package/expo-module-gradle-plugin/src/main/kotlin/expo/modules/plugin/android/MavenPublicationExtension.kt +1 -1
- package/ios/Core/AnyCodingKey.swift +21 -0
- package/ios/Core/DynamicTypes/{DynamicEncodableType.swift → DynamicCodableType.swift} +18 -14
- package/ios/Core/DynamicTypes/DynamicType.swift +4 -4
- package/ios/Core/ExpoModulesMacros.swift +64 -5
- package/ios/Core/JSValueDecoder.swift +452 -0
- package/ios/Core/JSValueEncoder.swift +1 -22
- package/ios/Core/ModuleHolder.swift +19 -1
- package/ios/Core/Modules/ModuleDefinition.swift +7 -0
- package/ios/Core/Protocols/AnyModule.swift +12 -0
- package/ios/Core/SharedObjects/SharedObject.swift +64 -43
- 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.10 — 2026-05-19
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] `Decodable` types can now be used as native function arguments. JS values are decoded through the dynamic-type registry, so arrays, dictionaries, optionals, `RawRepresentable` enums and `Convertible`s are coerced consistently. ([#45705](https://github.com/expo/expo/pull/45705) by [@tsapeta](https://github.com/tsapeta))
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- [Android] Keep `ExpoComposeView` content visible during parent view transitions (e.g., `react-native-screens` pop navigation). ([#45942](https://github.com/expo/expo/pull/45942) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
22
|
+
|
|
23
|
+
## 56.0.9 — 2026-05-15
|
|
24
|
+
|
|
25
|
+
### 🎉 New features
|
|
26
|
+
|
|
27
|
+
- Added single-payload overloads for `SharedObject.emit` on iOS and Android. The iOS API also accepts an already-converted `JavaScriptValue` payload to skip the native-to-JS conversion step. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))
|
|
28
|
+
|
|
13
29
|
## 56.0.8 — 2026-05-13
|
|
14
30
|
|
|
15
31
|
### 🐛 Bug fixes
|
|
@@ -28,6 +44,7 @@
|
|
|
28
44
|
### 💡 Others
|
|
29
45
|
|
|
30
46
|
- [iOS] `AppContext.setRuntime` now takes the native React `RuntimeScheduler` pointer and a dispatch trampoline alongside the runtime pointer. ([#45636](https://github.com/expo/expo/pull/45636) by [@tsapeta](https://github.com/tsapeta))
|
|
47
|
+
- Deprecated `SharedObject.emit(event:arguments:)` (iOS) and the `vararg` `emit` (Android) in favor of the new single-payload overloads. Existing single-argument call sites keep working unchanged. ([#45596](https://github.com/expo/expo/pull/45596) by [@tsapeta](https://github.com/tsapeta))
|
|
31
48
|
|
|
32
49
|
## 56.0.5 — 2026-05-08
|
|
33
50
|
|
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.10'
|
|
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.10"
|
|
98
98
|
buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
|
|
99
99
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
|
|
100
100
|
|
|
@@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue
|
|
|
13
13
|
import androidx.compose.runtime.key
|
|
14
14
|
import androidx.compose.runtime.mutableStateOf
|
|
15
15
|
import androidx.compose.runtime.rememberUpdatedState
|
|
16
|
+
import androidx.annotation.UiThread
|
|
16
17
|
import androidx.compose.ui.platform.ComposeView
|
|
17
18
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
18
19
|
import androidx.core.view.size
|
|
@@ -218,8 +219,42 @@ abstract class ExpoComposeView<T : ComposeProps>(
|
|
|
218
219
|
|
|
219
220
|
override fun onViewRemoved(child: View?) {
|
|
220
221
|
super.onViewRemoved(child)
|
|
222
|
+
// Keep compose views alive when view is transitioning
|
|
223
|
+
// e.g. pop transition from RN screens https://github.com/expo/expo/issues/45914
|
|
224
|
+
if (child != null && isViewTransitioning(child)) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
221
227
|
recomposeScope?.invalidate()
|
|
222
228
|
}
|
|
229
|
+
|
|
230
|
+
// Children currently animating out via startViewTransition. While a view is in this set,
|
|
231
|
+
// onViewRemoved skips invalidating the recompose scope so the child's compose subtree
|
|
232
|
+
// stays alive for the duration of the transition. Mirrors ViewGroup.mTransitioningViews.
|
|
233
|
+
private val transitioningChildren: MutableSet<View> = mutableSetOf()
|
|
234
|
+
|
|
235
|
+
override fun startViewTransition(view: View) {
|
|
236
|
+
super.startViewTransition(view)
|
|
237
|
+
if (view.parent == this) {
|
|
238
|
+
transitioningChildren.add(view)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
override fun endViewTransition(view: View) {
|
|
243
|
+
super.endViewTransition(view)
|
|
244
|
+
if (transitioningChildren.remove(view) && view.parent != this) {
|
|
245
|
+
recomposeScope?.invalidate()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@UiThread
|
|
250
|
+
private fun isViewTransitioning(view: View): Boolean {
|
|
251
|
+
return transitioningChildren.contains(view)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
override fun onDetachedFromWindow() {
|
|
255
|
+
super.onDetachedFromWindow()
|
|
256
|
+
transitioningChildren.clear()
|
|
257
|
+
}
|
|
223
258
|
}
|
|
224
259
|
|
|
225
260
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Copyright 2018-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
#include "ExpoComponentDescriptorFactory.h"
|
|
4
|
+
#include "AndroidExpoViewComponentDescriptor.h"
|
|
5
|
+
|
|
6
|
+
namespace react = facebook::react;
|
|
7
|
+
|
|
8
|
+
namespace expo {
|
|
9
|
+
|
|
10
|
+
StatePropMapType statePropMap = {};
|
|
11
|
+
|
|
12
|
+
react::ComponentDescriptor::Unique concreteExpoComponentDescriptorConstructor(
|
|
13
|
+
const react::ComponentDescriptorParameters ¶meters
|
|
14
|
+
) {
|
|
15
|
+
auto descriptor = std::make_unique<AndroidExpoViewComponentDescriptor>(
|
|
16
|
+
parameters,
|
|
17
|
+
react::RawPropsParser(/*useRawPropsJsiValue=*/true)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (statePropMap.contains(std::static_pointer_cast<std::string const>(parameters.flavor))) {
|
|
21
|
+
descriptor->setStateProps(
|
|
22
|
+
statePropMap.at(
|
|
23
|
+
std::static_pointer_cast<std::string const>(parameters.flavor)
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return descriptor;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
} // namespace expo
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Copyright 2018-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
#pragma once
|
|
4
|
+
|
|
5
|
+
#include <react/renderer/core/ComponentDescriptor.h>
|
|
6
|
+
#include "../types/FrontendConverter.h"
|
|
7
|
+
|
|
8
|
+
namespace react = facebook::react;
|
|
9
|
+
|
|
10
|
+
namespace expo {
|
|
11
|
+
|
|
12
|
+
using StatePropMapType = std::unordered_map<
|
|
13
|
+
react::ComponentDescriptor::Flavor,
|
|
14
|
+
std::unordered_map<std::string, std::shared_ptr<FrontendConverter>>
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
extern StatePropMapType statePropMap;
|
|
18
|
+
|
|
19
|
+
react::ComponentDescriptor::Unique concreteExpoComponentDescriptorConstructor(
|
|
20
|
+
const react::ComponentDescriptorParameters ¶meters
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
} // namespace expo
|
|
@@ -1,38 +1,16 @@
|
|
|
1
1
|
// Copyright 2018-present 650 Industries. All rights reserved.
|
|
2
2
|
|
|
3
3
|
#include "FabricComponentsRegistry.h"
|
|
4
|
+
#include "ExpoComponentDescriptorFactory.h"
|
|
4
5
|
#include "../types/FrontendConverterProvider.h"
|
|
6
|
+
#include <CoreComponentsRegistry.h>
|
|
7
|
+
#include <react/renderer/componentregistry/ComponentDescriptorProvider.h>
|
|
5
8
|
|
|
6
9
|
namespace jni = facebook::jni;
|
|
7
10
|
namespace react = facebook::react;
|
|
8
11
|
|
|
9
12
|
namespace expo {
|
|
10
13
|
|
|
11
|
-
#pragma clang diagnostic push
|
|
12
|
-
#pragma clang diagnostic ignored "-Wextern-initializer"
|
|
13
|
-
// Clang think that we don't need to initialize the extern variable here, but we do.
|
|
14
|
-
extern StatePropMapType statePropMap = {};
|
|
15
|
-
#pragma clang diagnostic pop
|
|
16
|
-
|
|
17
|
-
AndroidExpoViewComponentDescriptor::Unique concreteExpoComponentDescriptorConstructor(
|
|
18
|
-
const react::ComponentDescriptorParameters ¶meters
|
|
19
|
-
) {
|
|
20
|
-
auto descriptor = std::make_unique<AndroidExpoViewComponentDescriptor>(
|
|
21
|
-
parameters,
|
|
22
|
-
react::RawPropsParser(/*useRawPropsJsiValue=*/true)
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
if (statePropMap.contains(std::static_pointer_cast<std::string const>(parameters.flavor))) {
|
|
26
|
-
descriptor->setStateProps(
|
|
27
|
-
statePropMap.at(
|
|
28
|
-
std::static_pointer_cast<std::string const>(parameters.flavor)
|
|
29
|
-
)
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return descriptor;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
14
|
// static
|
|
37
15
|
void FabricComponentsRegistry::registerNatives() {
|
|
38
16
|
registerHybrid({
|
|
@@ -3,29 +3,12 @@
|
|
|
3
3
|
#pragma once
|
|
4
4
|
|
|
5
5
|
#include "../ExpoHeader.pch"
|
|
6
|
-
#include <CoreComponentsRegistry.h>
|
|
7
|
-
#include <react/renderer/componentregistry/ComponentDescriptorProvider.h>
|
|
8
|
-
|
|
9
6
|
#include "../types/ExpectedType.h"
|
|
10
|
-
#include "../types/FrontendConverter.h"
|
|
11
|
-
#include "AndroidExpoViewComponentDescriptor.h"
|
|
12
7
|
|
|
13
8
|
namespace jni = facebook::jni;
|
|
14
|
-
namespace react = facebook::react;
|
|
15
9
|
|
|
16
10
|
namespace expo {
|
|
17
11
|
|
|
18
|
-
typedef std::unordered_map<
|
|
19
|
-
AndroidExpoViewComponentDescriptor::Flavor,
|
|
20
|
-
std::unordered_map<std::string, std::shared_ptr<FrontendConverter>>
|
|
21
|
-
> StatePropMapType;
|
|
22
|
-
|
|
23
|
-
extern StatePropMapType statePropMap;
|
|
24
|
-
|
|
25
|
-
AndroidExpoViewComponentDescriptor::Unique concreteExpoComponentDescriptorConstructor(
|
|
26
|
-
const react::ComponentDescriptorParameters ¶meters
|
|
27
|
-
);
|
|
28
|
-
|
|
29
12
|
class FabricComponentsRegistry : public jni::HybridClass<FabricComponentsRegistry> {
|
|
30
13
|
public:
|
|
31
14
|
static auto constexpr
|
|
@@ -41,20 +41,42 @@ open class SharedObject(runtime: Runtime? = null) {
|
|
|
41
41
|
)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Emits an event with no payload to the associated JavaScript object.
|
|
46
|
+
*/
|
|
47
|
+
fun emit(event: String) {
|
|
48
|
+
emitInternal(event, emptyArray())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Emits an event with a single payload to the associated JavaScript object.
|
|
53
|
+
*/
|
|
54
|
+
fun emit(event: String, payload: Any?) {
|
|
55
|
+
emitInternal(event, arrayOf(payload))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Deprecated(
|
|
59
|
+
"Multi-argument event emission is deprecated. Use `emit(event)` or `emit(event, payload)` and pass a single payload (typically a Map/Bundle) instead.",
|
|
60
|
+
ReplaceWith("emit(event, args)")
|
|
61
|
+
)
|
|
62
|
+
fun emit(event: String, vararg args: Any?) {
|
|
63
|
+
emitInternal(event, args)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun emitInternal(event: String, payload: Array<out Any?>) {
|
|
45
67
|
val jsObject = getJavaScriptObject() ?: return
|
|
46
68
|
val jniInterop = runtime?.jsiContext ?: return
|
|
47
69
|
try {
|
|
48
70
|
JNIUtils.emitEvent(
|
|
49
71
|
jsObject,
|
|
50
72
|
jniInterop,
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
event,
|
|
74
|
+
payload
|
|
53
75
|
.map { JSTypeConverterProvider.convertToJSValue(it, useExperimentalConverter = true) }
|
|
54
76
|
.toTypedArray()
|
|
55
77
|
)
|
|
56
78
|
} catch (e: Throwable) {
|
|
57
|
-
logger.error("Unable to send event '$
|
|
79
|
+
logger.error("Unable to send event '$event' by shared object of type ${this::class.java.simpleName}", e)
|
|
58
80
|
}
|
|
59
81
|
}
|
|
60
82
|
|
|
@@ -208,7 +208,7 @@ private fun Project.expoPublishBody(publicationInfo: PublicationInfo, expoModule
|
|
|
208
208
|
providers.exec { env ->
|
|
209
209
|
env.workingDir(layout.projectDirectory.file(".."))
|
|
210
210
|
// TODO(@lukmccall): support other package managers
|
|
211
|
-
env.commandLine("
|
|
211
|
+
env.commandLine("pnpm", "prettier", "--write", "expo-module.config.json")
|
|
212
212
|
}.result.get()
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
A coding key carrying just a string and an integer index. Used to extend
|
|
5
|
+
`codingPath` with array indices in unkeyed containers, since unkeyed
|
|
6
|
+
containers have no associated `Key` type to draw from.
|
|
7
|
+
*/
|
|
8
|
+
internal struct AnyCodingKey: CodingKey {
|
|
9
|
+
let stringValue: String
|
|
10
|
+
let intValue: Int?
|
|
11
|
+
|
|
12
|
+
init(stringValue: String) {
|
|
13
|
+
self.stringValue = stringValue
|
|
14
|
+
self.intValue = Int(stringValue)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
init(intValue: Int) {
|
|
18
|
+
self.stringValue = String(intValue)
|
|
19
|
+
self.intValue = intValue
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -3,29 +3,33 @@
|
|
|
3
3
|
import ExpoModulesJSI
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
Dynamic type for values
|
|
7
|
-
|
|
6
|
+
Dynamic type for values that conform to `Encodable` and/or `Decodable`. Native→JS
|
|
7
|
+
goes through `JSValueEncoder` (requires `Encodable`); JS→native goes through
|
|
8
|
+
`JSValueDecoder` (requires `Decodable`). Either direction throws if the wrapped
|
|
9
|
+
type doesn't conform to the protocol it needs.
|
|
8
10
|
*/
|
|
9
|
-
internal struct
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
func wraps<InnerType>(_ type: InnerType.Type) -> Bool {
|
|
13
|
-
return type is Encodable
|
|
11
|
+
internal struct DynamicCodableType<InnerType>: AnyDynamicType {
|
|
12
|
+
func wraps<AnyInnerType>(_ type: AnyInnerType.Type) -> Bool {
|
|
13
|
+
return type == InnerType.self
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
func equals(_ type: any AnyDynamicType) -> Bool {
|
|
17
|
-
|
|
18
|
-
return false
|
|
17
|
+
return type is Self
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
func cast(jsValue: JavaScriptValue, appContext: AppContext) throws -> Any {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
guard let DecodableType = InnerType.self as? Decodable.Type else {
|
|
22
|
+
throw Conversions.CastingJSValueException<InnerType>(jsValue.kind)
|
|
23
|
+
}
|
|
24
|
+
let decoder = try JSValueDecoder(value: jsValue, appContext: appContext)
|
|
25
|
+
return try DecodableType.init(from: decoder)
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
func cast<ValueType>(_ value: ValueType, appContext: AppContext) throws -> Any {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
if let value = value as? InnerType {
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
throw Conversions.CastingException<InnerType>(value)
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
func castToJS<ValueType>(_ value: ValueType, appContext: AppContext) throws -> JavaScriptValue {
|
|
@@ -50,6 +54,6 @@ internal struct DynamicEncodableType: AnyDynamicType {
|
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
var description: String {
|
|
53
|
-
"
|
|
57
|
+
"Codable<\(InnerType.self)>"
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -19,10 +19,10 @@ private func DynamicType<T>(_ type: T.Type) -> AnyDynamicType {
|
|
|
19
19
|
if T.self == Void.self {
|
|
20
20
|
return DynamicVoidType.shared
|
|
21
21
|
}
|
|
22
|
-
if T.self is Encodable.Type {
|
|
23
|
-
// There is no dedicated `~` operator overload for
|
|
24
|
-
// when the type is both `AnyArgument` and `Encodable` (e.g. strings, numeric types).
|
|
25
|
-
return
|
|
22
|
+
if T.self is Encodable.Type || T.self is Decodable.Type {
|
|
23
|
+
// There is no dedicated `~` operator overload for Codable types to avoid ambiguity
|
|
24
|
+
// when the type is both `AnyArgument` and `Encodable`/`Decodable` (e.g. strings, numeric types).
|
|
25
|
+
return DynamicCodableType<T>()
|
|
26
26
|
}
|
|
27
27
|
return DynamicRawType(innerType: T.self)
|
|
28
28
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Keep in sync with `@expo/expo-modules-macros-plugin/apple/Sources/ExpoModulesOptimized/ExpoModulesOptimized.swift`.
|
|
5
|
-
*/
|
|
1
|
+
// Declares macro signatures whose implementations are provided by the `ExpoModulesMacros` compiler
|
|
2
|
+
// plugin shipped in the `@expo/expo-modules-macros-plugin` package. Keep the `#externalMacro`
|
|
3
|
+
// module/type names below in sync with the macro implementations in that package.
|
|
6
4
|
|
|
7
5
|
// MARK: - Macro declarations
|
|
8
6
|
|
|
@@ -25,3 +23,64 @@
|
|
|
25
23
|
@attached(peer, names: arbitrary)
|
|
26
24
|
public macro OptimizedFunction() =
|
|
27
25
|
#externalMacro(module: "ExpoModulesMacros", type: "OptimizedFunctionAttachedMacro")
|
|
26
|
+
|
|
27
|
+
/// Marker macro applied to module / shared-object members that should be exposed to JavaScript.
|
|
28
|
+
/// The accompanying `@ExpoModule` and `@SharedObject` macros discover `@JS`-marked declarations
|
|
29
|
+
/// and generate the matching `Function` / `AsyncFunction` / `Property` / `Constructor` registrations.
|
|
30
|
+
///
|
|
31
|
+
/// Usage:
|
|
32
|
+
///
|
|
33
|
+
/// @JS
|
|
34
|
+
/// func greet(name: String) -> String { ... }
|
|
35
|
+
///
|
|
36
|
+
/// @JS("doWork")
|
|
37
|
+
/// func performWork() async throws { ... }
|
|
38
|
+
///
|
|
39
|
+
/// @JS
|
|
40
|
+
/// var status: String { "ok" }
|
|
41
|
+
@attached(peer)
|
|
42
|
+
public macro JS(_ jsName: String? = nil) =
|
|
43
|
+
#externalMacro(module: "ExpoModulesMacros", type: "JSMacro")
|
|
44
|
+
|
|
45
|
+
/// Member macro applied to a `Module` subclass. Scans the class body for declarations
|
|
46
|
+
/// marked with `@JS` and synthesizes a framework-internal `_exposedDefinition()` method.
|
|
47
|
+
/// `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()`.
|
|
49
|
+
///
|
|
50
|
+
/// Usage:
|
|
51
|
+
///
|
|
52
|
+
/// @ExpoModule
|
|
53
|
+
/// public final class MyModule: Module {
|
|
54
|
+
/// public func definition() -> ModuleDefinition {
|
|
55
|
+
/// Name("MyModule")
|
|
56
|
+
/// }
|
|
57
|
+
///
|
|
58
|
+
/// @JS
|
|
59
|
+
/// func greet(name: String) -> String { "Hi, \(name)" }
|
|
60
|
+
/// }
|
|
61
|
+
@attached(member, names: named(_exposedDefinition), named(appContext), named(init))
|
|
62
|
+
public macro ExpoModule(_ name: String? = nil, classes: [Any.Type] = []) =
|
|
63
|
+
#externalMacro(module: "ExpoModulesMacros", type: "ExpoModuleMacro")
|
|
64
|
+
|
|
65
|
+
/// Member macro applied to a `SharedObject` subclass. Scans the class body for declarations
|
|
66
|
+
/// marked with `@JS` (including a single `@JS init(...)` for the JS constructor) and
|
|
67
|
+
/// synthesizes a `_exposedClassDefinition()` static method returning a `ClassDefinition`.
|
|
68
|
+
/// The companion `@ExpoModule(classes: [Foo.self])` wires the class into the module's
|
|
69
|
+
/// exposed surface.
|
|
70
|
+
///
|
|
71
|
+
/// Usage:
|
|
72
|
+
///
|
|
73
|
+
/// @SharedObject
|
|
74
|
+
/// final class Cache: SharedObject {
|
|
75
|
+
/// @JS
|
|
76
|
+
/// init(name: String) { self.name = name }
|
|
77
|
+
///
|
|
78
|
+
/// @JS
|
|
79
|
+
/// func get(_ key: String) -> String? { ... }
|
|
80
|
+
///
|
|
81
|
+
/// @JS
|
|
82
|
+
/// var size: Int { 42 }
|
|
83
|
+
/// }
|
|
84
|
+
@attached(member, names: named(_exposedClassDefinition))
|
|
85
|
+
public macro SharedObject(_ name: String? = nil) =
|
|
86
|
+
#externalMacro(module: "ExpoModulesMacros", type: "SharedObjectMacro")
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// Copyright 2026-present 650 Industries. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import ExpoModulesJSI
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Decodes `JavaScriptValue`s into `Decodable` Swift values.
|
|
7
|
+
|
|
8
|
+
For any property whose static type is known to the dynamic-type registry
|
|
9
|
+
(anything routed via the `~` operator: arrays, dictionaries, optionals,
|
|
10
|
+
`RawRepresentable` enums, `Convertible`s, `JavaScriptValue` and so on),
|
|
11
|
+
the decoder takes the fast path through `cast(jsValue:)`, preserving full
|
|
12
|
+
element-type metadata and picking up `Convertible` coercions for free.
|
|
13
|
+
|
|
14
|
+
Only types the registry doesn't recognize (i.e. plain `Decodable` structs and
|
|
15
|
+
classes) fall back to Swift's `Decodable` machinery via `T.init(from: decoder)`.
|
|
16
|
+
*/
|
|
17
|
+
internal final class JSValueDecoder: Decoder {
|
|
18
|
+
private let appContext: AppContext
|
|
19
|
+
private let runtime: JavaScriptRuntime
|
|
20
|
+
private let value: JavaScriptValue
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
Initializes the decoder with the given JS value and app context.
|
|
24
|
+
Throws if the runtime has been lost.
|
|
25
|
+
*/
|
|
26
|
+
convenience init(value: JavaScriptValue, appContext: AppContext) throws {
|
|
27
|
+
try self.init(value: value, appContext: appContext, runtime: appContext.runtime)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
Initializes the decoder with the given JS value, app context and an explicit runtime.
|
|
32
|
+
Use this when the source JS value lives in a runtime other than the one currently
|
|
33
|
+
held by the app context.
|
|
34
|
+
*/
|
|
35
|
+
convenience init(value: JavaScriptValue, appContext: AppContext, runtime: JavaScriptRuntime) {
|
|
36
|
+
self.init(value: value, appContext: appContext, runtime: runtime, codingPath: [])
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fileprivate init(
|
|
40
|
+
value: JavaScriptValue,
|
|
41
|
+
appContext: AppContext,
|
|
42
|
+
runtime: JavaScriptRuntime,
|
|
43
|
+
codingPath: [any CodingKey]
|
|
44
|
+
) {
|
|
45
|
+
self.value = value
|
|
46
|
+
self.appContext = appContext
|
|
47
|
+
self.runtime = runtime
|
|
48
|
+
self.codingPath = codingPath
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - Decoder
|
|
52
|
+
|
|
53
|
+
let codingPath: [any CodingKey]
|
|
54
|
+
let userInfo: [CodingUserInfoKey: Any] = [:]
|
|
55
|
+
|
|
56
|
+
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
|
|
57
|
+
guard value.isObject() else {
|
|
58
|
+
throw DecodingError.typeMismatch(
|
|
59
|
+
[String: Any].self,
|
|
60
|
+
DecodingError.Context(
|
|
61
|
+
codingPath: codingPath,
|
|
62
|
+
debugDescription: "Expected a JavaScript object to decode keyed container, got \(value.kind.rawValue)"
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
let container = JSObjectDecodingContainer<Key>(
|
|
67
|
+
object: value.getObject(),
|
|
68
|
+
appContext: appContext,
|
|
69
|
+
runtime: runtime,
|
|
70
|
+
codingPath: codingPath
|
|
71
|
+
)
|
|
72
|
+
return KeyedDecodingContainer(container)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
|
76
|
+
guard value.isArray() else {
|
|
77
|
+
throw DecodingError.typeMismatch(
|
|
78
|
+
[Any].self,
|
|
79
|
+
DecodingError.Context(
|
|
80
|
+
codingPath: codingPath,
|
|
81
|
+
debugDescription: "Expected a JavaScript array to decode unkeyed container, got \(value.kind.rawValue)"
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return JSArrayDecodingContainer(
|
|
86
|
+
array: value.getArray(),
|
|
87
|
+
appContext: appContext,
|
|
88
|
+
runtime: runtime,
|
|
89
|
+
codingPath: codingPath
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
|
94
|
+
return JSValueDecodingContainer(
|
|
95
|
+
value: value,
|
|
96
|
+
appContext: appContext,
|
|
97
|
+
runtime: runtime,
|
|
98
|
+
codingPath: codingPath
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: - Shared decoding logic
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
Decodes a single JS value into a typed Swift value, routing through the
|
|
107
|
+
dynamic-type registry whenever possible and falling back to `Decodable` only
|
|
108
|
+
for plain types.
|
|
109
|
+
*/
|
|
110
|
+
private func decodeUsingDynamicType<ValueType: Decodable>(
|
|
111
|
+
_ type: ValueType.Type,
|
|
112
|
+
from jsValue: JavaScriptValue,
|
|
113
|
+
appContext: AppContext,
|
|
114
|
+
runtime: JavaScriptRuntime,
|
|
115
|
+
codingPath: [any CodingKey]
|
|
116
|
+
) throws -> ValueType {
|
|
117
|
+
let dynamicType = ~ValueType.self
|
|
118
|
+
|
|
119
|
+
if !(dynamicType is DynamicCodableType<ValueType>) {
|
|
120
|
+
do {
|
|
121
|
+
let anyValue = try JavaScriptActor.assumeIsolated {
|
|
122
|
+
return try dynamicType.cast(jsValue: jsValue, appContext: appContext)
|
|
123
|
+
}
|
|
124
|
+
guard let typed = anyValue as? ValueType else {
|
|
125
|
+
throw DecodingError.typeMismatch(
|
|
126
|
+
ValueType.self,
|
|
127
|
+
DecodingError.Context(
|
|
128
|
+
codingPath: codingPath,
|
|
129
|
+
debugDescription: "Dynamic type \(dynamicType) produced \(Swift.type(of: anyValue)) which is not \(ValueType.self)"
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return typed
|
|
134
|
+
} catch let error as DecodingError {
|
|
135
|
+
throw error
|
|
136
|
+
} catch {
|
|
137
|
+
throw DecodingError.dataCorrupted(
|
|
138
|
+
DecodingError.Context(
|
|
139
|
+
codingPath: codingPath,
|
|
140
|
+
debugDescription: "Failed to cast JS value to \(ValueType.self)",
|
|
141
|
+
underlyingError: error
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Plain Decodable type the registry doesn't recognize — recurse via Decodable.
|
|
148
|
+
let decoder = JSValueDecoder(value: jsValue, appContext: appContext, runtime: runtime, codingPath: codingPath)
|
|
149
|
+
return try ValueType(from: decoder)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: - Containers
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
Single value container that reads a JS primitive (or unwraps an Optional).
|
|
156
|
+
*/
|
|
157
|
+
private struct JSValueDecodingContainer: SingleValueDecodingContainer {
|
|
158
|
+
private let appContext: AppContext
|
|
159
|
+
private let runtime: JavaScriptRuntime
|
|
160
|
+
private let value: JavaScriptValue
|
|
161
|
+
let codingPath: [any CodingKey]
|
|
162
|
+
|
|
163
|
+
init(value: JavaScriptValue, appContext: AppContext, runtime: JavaScriptRuntime, codingPath: [any CodingKey]) {
|
|
164
|
+
self.value = value
|
|
165
|
+
self.appContext = appContext
|
|
166
|
+
self.runtime = runtime
|
|
167
|
+
self.codingPath = codingPath
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func decodeNil() -> Bool {
|
|
171
|
+
return value.isNull() || value.isUndefined()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func decode<ValueType: Decodable>(_ type: ValueType.Type) throws -> ValueType {
|
|
175
|
+
return try decodeUsingDynamicType(
|
|
176
|
+
type,
|
|
177
|
+
from: value,
|
|
178
|
+
appContext: appContext,
|
|
179
|
+
runtime: runtime,
|
|
180
|
+
codingPath: codingPath
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
Keyed container that reads from a JS object.
|
|
187
|
+
|
|
188
|
+
- Note: This is a `class` (not a `struct`) for the same reason as the encoder's
|
|
189
|
+
keyed container: it holds a non-copyable `JavaScriptObject`. See
|
|
190
|
+
`JSObjectEncodingContainer` for the full rationale.
|
|
191
|
+
*/
|
|
192
|
+
private final class JSObjectDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
|
193
|
+
private let appContext: AppContext
|
|
194
|
+
private let runtime: JavaScriptRuntime
|
|
195
|
+
private let object: JavaScriptObject
|
|
196
|
+
let codingPath: [any CodingKey]
|
|
197
|
+
|
|
198
|
+
init(object: consuming JavaScriptObject, appContext: AppContext, runtime: JavaScriptRuntime, codingPath: [any CodingKey]) {
|
|
199
|
+
self.object = object
|
|
200
|
+
self.appContext = appContext
|
|
201
|
+
self.runtime = runtime
|
|
202
|
+
self.codingPath = codingPath
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
lazy var allKeys: [Key] = object.getPropertyNames().compactMap { Key(stringValue: $0) }
|
|
206
|
+
|
|
207
|
+
func contains(_ key: Key) -> Bool {
|
|
208
|
+
return object.hasProperty(key.stringValue)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func decodeNil(forKey key: Key) throws -> Bool {
|
|
212
|
+
let value = object.getProperty(key.stringValue)
|
|
213
|
+
return value.isNull() || value.isUndefined()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// JS doesn't meaningfully distinguish "key absent" from "value undefined" at the
|
|
217
|
+
// consumer level — a single `getProperty` fetch returns `undefined` in both cases.
|
|
218
|
+
// Collapse both into `keyNotFound` so the caller gets the more useful error.
|
|
219
|
+
func decode<ValueType: Decodable>(_ type: ValueType.Type, forKey key: Key) throws -> ValueType {
|
|
220
|
+
let jsValue = object.getProperty(key.stringValue)
|
|
221
|
+
if jsValue.isUndefined() {
|
|
222
|
+
throw DecodingError.keyNotFound(
|
|
223
|
+
key,
|
|
224
|
+
DecodingError.Context(
|
|
225
|
+
codingPath: codingPath,
|
|
226
|
+
debugDescription: "No value found for key \(key.stringValue)"
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
return try decodeUsingDynamicType(
|
|
231
|
+
type,
|
|
232
|
+
from: jsValue,
|
|
233
|
+
appContext: appContext,
|
|
234
|
+
runtime: runtime,
|
|
235
|
+
codingPath: codingPath + [key]
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Swift's default `decodeIfPresent` calls `contains(key)` and then `decodeNil(forKey:)`;
|
|
240
|
+
// for present-but-null JS keys this works, but it doesn't reach our dynamic-type
|
|
241
|
+
// fast path. Override to make optional decoding consistently route through it.
|
|
242
|
+
// `JavaScriptObject.getProperty` returns `undefined` for missing keys, so a single
|
|
243
|
+
// fetch covers both "absent" and "explicit null/undefined" cases.
|
|
244
|
+
func decodeIfPresent<ValueType: Decodable>(_ type: ValueType.Type, forKey key: Key) throws -> ValueType? {
|
|
245
|
+
let jsValue = object.getProperty(key.stringValue)
|
|
246
|
+
if jsValue.isNull() || jsValue.isUndefined() {
|
|
247
|
+
return nil
|
|
248
|
+
}
|
|
249
|
+
return try decodeUsingDynamicType(
|
|
250
|
+
type,
|
|
251
|
+
from: jsValue,
|
|
252
|
+
appContext: appContext,
|
|
253
|
+
runtime: runtime,
|
|
254
|
+
codingPath: codingPath + [key]
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
|
|
259
|
+
let jsValue = object.getProperty(key.stringValue)
|
|
260
|
+
guard jsValue.isObject() else {
|
|
261
|
+
throw DecodingError.typeMismatch(
|
|
262
|
+
[String: Any].self,
|
|
263
|
+
DecodingError.Context(
|
|
264
|
+
codingPath: codingPath + [key],
|
|
265
|
+
debugDescription: "Expected a JavaScript object for nested keyed container, got \(jsValue.kind.rawValue)"
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
let container = JSObjectDecodingContainer<NestedKey>(
|
|
270
|
+
object: jsValue.getObject(),
|
|
271
|
+
appContext: appContext,
|
|
272
|
+
runtime: runtime,
|
|
273
|
+
codingPath: codingPath + [key]
|
|
274
|
+
)
|
|
275
|
+
return KeyedDecodingContainer(container)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer {
|
|
279
|
+
let jsValue = object.getProperty(key.stringValue)
|
|
280
|
+
guard jsValue.isArray() else {
|
|
281
|
+
throw DecodingError.typeMismatch(
|
|
282
|
+
[Any].self,
|
|
283
|
+
DecodingError.Context(
|
|
284
|
+
codingPath: codingPath + [key],
|
|
285
|
+
debugDescription: "Expected a JavaScript array for nested unkeyed container, got \(jsValue.kind.rawValue)"
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
return JSArrayDecodingContainer(
|
|
290
|
+
array: jsValue.getArray(),
|
|
291
|
+
appContext: appContext,
|
|
292
|
+
runtime: runtime,
|
|
293
|
+
codingPath: codingPath + [key]
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func superDecoder() throws -> any Decoder {
|
|
298
|
+
throw DecodingError.dataCorrupted(
|
|
299
|
+
DecodingError.Context(
|
|
300
|
+
codingPath: codingPath,
|
|
301
|
+
debugDescription: "JSValueDecoder does not support superDecoder()"
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
func superDecoder(forKey key: Key) throws -> any Decoder {
|
|
307
|
+
throw DecodingError.dataCorrupted(
|
|
308
|
+
DecodingError.Context(
|
|
309
|
+
codingPath: codingPath + [key],
|
|
310
|
+
debugDescription: "JSValueDecoder does not support superDecoder(forKey:)"
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
Unkeyed container that reads from a JS array.
|
|
318
|
+
|
|
319
|
+
Like `JSObjectDecodingContainer`, this is a `class` so it can hold the
|
|
320
|
+
non-copyable `JavaScriptArray` directly.
|
|
321
|
+
*/
|
|
322
|
+
private final class JSArrayDecodingContainer: UnkeyedDecodingContainer {
|
|
323
|
+
private let appContext: AppContext
|
|
324
|
+
private let runtime: JavaScriptRuntime
|
|
325
|
+
private let array: JavaScriptArray
|
|
326
|
+
let codingPath: [any CodingKey]
|
|
327
|
+
var currentIndex: Int = 0
|
|
328
|
+
|
|
329
|
+
init(array: consuming JavaScriptArray, appContext: AppContext, runtime: JavaScriptRuntime, codingPath: [any CodingKey]) {
|
|
330
|
+
self.array = array
|
|
331
|
+
self.appContext = appContext
|
|
332
|
+
self.runtime = runtime
|
|
333
|
+
self.codingPath = codingPath
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
var count: Int? { array.length }
|
|
337
|
+
var isAtEnd: Bool { currentIndex >= array.length }
|
|
338
|
+
|
|
339
|
+
func decodeNil() throws -> Bool {
|
|
340
|
+
let jsValue = try nextValue(Any?.self)
|
|
341
|
+
if jsValue.isNull() || jsValue.isUndefined() {
|
|
342
|
+
currentIndex += 1
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
func decode<ValueType: Decodable>(_ type: ValueType.Type) throws -> ValueType {
|
|
349
|
+
let jsValue = try nextValue(type)
|
|
350
|
+
let key = AnyCodingKey(intValue: currentIndex)
|
|
351
|
+
let decoded = try decodeUsingDynamicType(
|
|
352
|
+
type,
|
|
353
|
+
from: jsValue,
|
|
354
|
+
appContext: appContext,
|
|
355
|
+
runtime: runtime,
|
|
356
|
+
codingPath: codingPath + [key]
|
|
357
|
+
)
|
|
358
|
+
currentIndex += 1
|
|
359
|
+
return decoded
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func decodeIfPresent<ValueType: Decodable>(_ type: ValueType.Type) throws -> ValueType? {
|
|
363
|
+
if isAtEnd {
|
|
364
|
+
return nil
|
|
365
|
+
}
|
|
366
|
+
let jsValue = try array.getValue(at: currentIndex)
|
|
367
|
+
if jsValue.isNull() || jsValue.isUndefined() {
|
|
368
|
+
currentIndex += 1
|
|
369
|
+
return nil
|
|
370
|
+
}
|
|
371
|
+
let key = AnyCodingKey(intValue: currentIndex)
|
|
372
|
+
let decoded = try decodeUsingDynamicType(
|
|
373
|
+
type,
|
|
374
|
+
from: jsValue,
|
|
375
|
+
appContext: appContext,
|
|
376
|
+
runtime: runtime,
|
|
377
|
+
codingPath: codingPath + [key]
|
|
378
|
+
)
|
|
379
|
+
currentIndex += 1
|
|
380
|
+
return decoded
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
|
|
384
|
+
let jsValue = try nextValue(KeyedDecodingContainer<NestedKey>.self)
|
|
385
|
+
let key = AnyCodingKey(intValue: currentIndex)
|
|
386
|
+
guard jsValue.isObject() else {
|
|
387
|
+
throw DecodingError.typeMismatch(
|
|
388
|
+
[String: Any].self,
|
|
389
|
+
DecodingError.Context(
|
|
390
|
+
codingPath: codingPath + [key],
|
|
391
|
+
debugDescription: "Expected a JavaScript object for nested keyed container, got \(jsValue.kind.rawValue)"
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
let container = JSObjectDecodingContainer<NestedKey>(
|
|
396
|
+
object: jsValue.getObject(),
|
|
397
|
+
appContext: appContext,
|
|
398
|
+
runtime: runtime,
|
|
399
|
+
codingPath: codingPath + [key]
|
|
400
|
+
)
|
|
401
|
+
currentIndex += 1
|
|
402
|
+
return KeyedDecodingContainer(container)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
|
406
|
+
let jsValue = try nextValue((any UnkeyedDecodingContainer).self)
|
|
407
|
+
let key = AnyCodingKey(intValue: currentIndex)
|
|
408
|
+
guard jsValue.isArray() else {
|
|
409
|
+
throw DecodingError.typeMismatch(
|
|
410
|
+
[Any].self,
|
|
411
|
+
DecodingError.Context(
|
|
412
|
+
codingPath: codingPath + [key],
|
|
413
|
+
debugDescription: "Expected a JavaScript array for nested unkeyed container, got \(jsValue.kind.rawValue)"
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
let container = JSArrayDecodingContainer(
|
|
418
|
+
array: jsValue.getArray(),
|
|
419
|
+
appContext: appContext,
|
|
420
|
+
runtime: runtime,
|
|
421
|
+
codingPath: codingPath + [key]
|
|
422
|
+
)
|
|
423
|
+
currentIndex += 1
|
|
424
|
+
return container
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Reads the element at `currentIndex`, throwing `valueNotFound` (rather than letting
|
|
428
|
+
// `JavaScriptArray.getValue(at:)` throw its non-`DecodingError` out-of-range error)
|
|
429
|
+
// when the unkeyed container has been exhausted.
|
|
430
|
+
private func nextValue<RequestedType>(_ expected: RequestedType.Type) throws -> JavaScriptValue {
|
|
431
|
+
if isAtEnd {
|
|
432
|
+
throw DecodingError.valueNotFound(
|
|
433
|
+
expected,
|
|
434
|
+
DecodingError.Context(
|
|
435
|
+
codingPath: codingPath + [AnyCodingKey(intValue: currentIndex)],
|
|
436
|
+
debugDescription: "Unkeyed container is at end — no value available at index \(currentIndex)"
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
return try array.getValue(at: currentIndex)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
func superDecoder() throws -> any Decoder {
|
|
444
|
+
throw DecodingError.dataCorrupted(
|
|
445
|
+
DecodingError.Context(
|
|
446
|
+
codingPath: codingPath,
|
|
447
|
+
debugDescription: "JSValueDecoder does not support superDecoder()"
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
@@ -107,7 +107,7 @@ private func encodeUsingDynamicType<ValueType: Encodable>(
|
|
|
107
107
|
) throws -> JavaScriptValue {
|
|
108
108
|
let dynamicType = ~ValueType.self
|
|
109
109
|
|
|
110
|
-
if !(dynamicType is
|
|
110
|
+
if !(dynamicType is DynamicCodableType<ValueType>) {
|
|
111
111
|
return try dynamicType.castToJS(value, appContext: appContext, in: runtime)
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -345,24 +345,3 @@ private final class JSArrayEncodingContainer: UnkeyedEncodingContainer {
|
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
-
// MARK: - Helpers
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
A coding key carrying just a string and an integer index. Used to extend
|
|
352
|
-
`codingPath` with array indices in the unkeyed container, since unkeyed
|
|
353
|
-
containers have no associated `Key` type to draw from.
|
|
354
|
-
*/
|
|
355
|
-
private struct AnyCodingKey: CodingKey {
|
|
356
|
-
let stringValue: String
|
|
357
|
-
let intValue: Int?
|
|
358
|
-
|
|
359
|
-
init(stringValue: String) {
|
|
360
|
-
self.stringValue = stringValue
|
|
361
|
-
self.intValue = Int(stringValue)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
init(intValue: Int) {
|
|
365
|
-
self.stringValue = String(intValue)
|
|
366
|
-
self.intValue = intValue
|
|
367
|
-
}
|
|
368
|
-
}
|
|
@@ -46,10 +46,28 @@ public final class ModuleHolder {
|
|
|
46
46
|
self.appContext = appContext
|
|
47
47
|
self._name = name
|
|
48
48
|
self.module = module
|
|
49
|
-
self.definition =
|
|
49
|
+
self.definition = ModuleHolder.buildDefinition(for: module)
|
|
50
50
|
post(event: .moduleCreate)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/// Combines the user-authored definition with the entries synthesized by the
|
|
54
|
+
/// `@ExpoModule` macro on this module's class (if any). The macro emits a
|
|
55
|
+
/// `_exposedDefinition()` method returning an `[AnyDefinition]` array of the
|
|
56
|
+
/// `Function` / `Property` / `Constructor` entries it generated from `@JS`
|
|
57
|
+
/// members. Those entries are prepended to the user's definitions and the
|
|
58
|
+
/// whole list is fed back through `ModuleDefinition.init` so the merged
|
|
59
|
+
/// result is rebucketed (into `functions`, `properties`, etc.) just like a
|
|
60
|
+
/// hand-written definition. Modules that don't use the macro fall through
|
|
61
|
+
/// the empty-exposed fast path and return the user's definition unchanged.
|
|
62
|
+
private static func buildDefinition(for module: AnyModule) -> ModuleDefinition {
|
|
63
|
+
let userDefinition = module.definition()
|
|
64
|
+
let exposed = module._exposedDefinition()
|
|
65
|
+
if exposed.isEmpty {
|
|
66
|
+
return userDefinition
|
|
67
|
+
}
|
|
68
|
+
return ModuleDefinition(definitions: exposed + userDefinition.rawDefinitions)
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
// MARK: Constants
|
|
54
72
|
|
|
55
73
|
/**
|
|
@@ -29,10 +29,17 @@ public final class ModuleDefinition: ObjectDefinition {
|
|
|
29
29
|
|
|
30
30
|
let eventObservers: [AnyEventObservingDefinition]
|
|
31
31
|
|
|
32
|
+
/// The raw list of definitions used to construct this module. Retained so
|
|
33
|
+
/// that `ModuleHolder` can merge in the entries synthesized by the
|
|
34
|
+
/// `@ExpoModule` macro without losing the user-authored ones.
|
|
35
|
+
let rawDefinitions: [AnyDefinition]
|
|
36
|
+
|
|
32
37
|
/**
|
|
33
38
|
Initializer that is called by the `ModuleDefinitionBuilder` results builder.
|
|
34
39
|
*/
|
|
35
40
|
override init(definitions: [AnyDefinition]) {
|
|
41
|
+
self.rawDefinitions = definitions
|
|
42
|
+
|
|
36
43
|
self.name = definitions
|
|
37
44
|
.compactMap { $0 as? ModuleNameDefinition }
|
|
38
45
|
.last?
|
|
@@ -14,4 +14,16 @@ public protocol AnyModule: AnyObject, AnyArgument {
|
|
|
14
14
|
*/
|
|
15
15
|
@ModuleDefinitionBuilder
|
|
16
16
|
func definition() -> ModuleDefinition
|
|
17
|
+
|
|
18
|
+
/// Returns definitions synthesized from `@JS`-annotated members by the `@ExpoModule` macro.
|
|
19
|
+
/// Framework-internal: the leading underscore signals this is not part of the public API and
|
|
20
|
+
/// should only be called by `expo-modules-core` itself. Modules that don't use the macro fall
|
|
21
|
+
/// back to the default empty implementation.
|
|
22
|
+
func _exposedDefinition() -> [AnyDefinition]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public extension AnyModule {
|
|
26
|
+
func _exposedDefinition() -> [AnyDefinition] {
|
|
27
|
+
return []
|
|
28
|
+
}
|
|
17
29
|
}
|
|
@@ -66,66 +66,87 @@ open class SharedObject: AnySharedObject {
|
|
|
66
66
|
public func getJavaScriptObject() -> JavaScriptObject? {
|
|
67
67
|
return appContext?.sharedObjectRegistry.toJavaScriptObject(self)
|
|
68
68
|
}
|
|
69
|
-
}
|
|
70
69
|
|
|
71
|
-
// Unfortunately the `emit` function needs to be defined in the extension.
|
|
72
|
-
// When put in the class, pack expansion is crashing with `EXC_BAD_ACCESS` code.
|
|
73
|
-
// See https://github.com/apple/swift/issues/72381 for more details.
|
|
74
|
-
public extension SharedObject { // swiftlint:disable:this no_grouping_extension
|
|
75
|
-
// Parameter packs feature requires Swift 5.9 (Xcode 15.0), but some CIs and EAS images may still use older versions.
|
|
76
|
-
// As of April 29, all submissions must be made with Xcode 15, so hopefully we can remove this condition soon.
|
|
77
|
-
// No one should use <15.0 these days.
|
|
78
|
-
#if swift(>=5.9)
|
|
79
70
|
/**
|
|
80
|
-
Schedules an event with the given name and
|
|
71
|
+
Schedules an event with the given name and a pre-converted JavaScript payload to be emitted
|
|
72
|
+
to the associated JavaScript object. This is the lowest-level emit overload — use it when the
|
|
73
|
+
value is already a `JavaScriptValue` to skip the native-to-JS conversion step.
|
|
81
74
|
*/
|
|
82
|
-
func emit
|
|
75
|
+
public func emit(event: String, payload: JavaScriptValue) {
|
|
83
76
|
guard let appContext, let runtime = try? appContext.runtime else {
|
|
84
77
|
log.warn("Trying to send event '\(event)' to \(type(of: self)), but the JS runtime has been lost")
|
|
85
78
|
return
|
|
86
79
|
}
|
|
80
|
+
guard let jsValue = getJavaScriptValue() else {
|
|
81
|
+
log.warn("Trying to send event '\(event)' to JS, but the JS object is no longer associated with the native instance")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
runtime.schedule {
|
|
85
|
+
dispatch(event: event, payload: payload, to: jsValue, in: runtime)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
/**
|
|
90
|
+
Schedules an event with the given name to be emitted to the associated JavaScript object.
|
|
91
|
+
*/
|
|
92
|
+
public func emit(event: String) {
|
|
93
|
+
emit(event: event, payload: .undefined)
|
|
94
|
+
}
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
/**
|
|
97
|
+
Schedules an event with the given name and payload to be emitted to the associated JavaScript object.
|
|
98
|
+
*/
|
|
99
|
+
public func emit<P: AnyArgument>(event: String, payload: sending P) {
|
|
100
|
+
guard let appContext, let runtime = try? appContext.runtime else {
|
|
101
|
+
log.warn("Trying to send event '\(event)' to \(type(of: self)), but the JS runtime has been lost")
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
guard let jsValue = getJavaScriptValue() else {
|
|
105
|
+
log.warn("Trying to send event '\(event)' to JS, but the JS object is no longer associated with the native instance")
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
runtime.schedule { [weak appContext] in
|
|
109
|
+
guard let appContext else {
|
|
96
110
|
return
|
|
97
111
|
}
|
|
98
|
-
|
|
99
|
-
// Convert native arguments to JS, just like function results
|
|
100
|
-
let jsArguments: [JavaScriptValue]
|
|
101
112
|
do {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
113
|
+
let jsPayload = try (~P.self).castToJS(payload, appContext: appContext, in: runtime)
|
|
114
|
+
dispatch(event: event, payload: jsPayload, to: jsValue, in: runtime)
|
|
105
115
|
} catch {
|
|
106
|
-
log.warn("Failed to convert
|
|
116
|
+
log.warn("Failed to convert payload for event '\(event)' on \(P.self); the event will not be emitted: \(error)")
|
|
107
117
|
return
|
|
108
118
|
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
/**
|
|
123
|
+
Backwards-compatible overload that forwards to `emit(event:payload:)`. Existing single-argument
|
|
124
|
+
call sites keep working unchanged; the parameter has been renamed to `payload` to make the
|
|
125
|
+
single-payload semantics explicit, so callers should migrate the label.
|
|
126
|
+
*/
|
|
127
|
+
@available(*, deprecated, renamed: "emit(event:payload:)", message: "Use `emit(event:payload:)` and pass a single value (typically a dictionary). Multi-argument event emission is no longer supported.")
|
|
128
|
+
public func emit<P: AnyArgument>(event: String, arguments: sending P) {
|
|
129
|
+
emit(event: event, payload: arguments)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
Sends a pre-converted event payload to the given JavaScript object via the JSI emitter helper.
|
|
135
|
+
Must run on the JS thread; the public `emit` overloads schedule onto the runtime before calling in.
|
|
136
|
+
*/
|
|
137
|
+
@JavaScriptActor
|
|
138
|
+
private func dispatch(event: String, payload: JavaScriptValue, to value: JavaScriptValue, in runtime: JavaScriptRuntime) {
|
|
139
|
+
runtime.withUnsafePointee { runtimePtr in
|
|
140
|
+
value.withUnsafePointee { objectPtr in
|
|
141
|
+
payload.withUnsafePointee { payloadPtr in
|
|
142
|
+
JSUtils.emitEvent(
|
|
143
|
+
event,
|
|
144
|
+
runtimePointer: runtimePtr,
|
|
145
|
+
objectPointer: objectPtr,
|
|
146
|
+
argumentsPointer: payloadPtr,
|
|
147
|
+
argumentCount: 1
|
|
148
|
+
)
|
|
122
149
|
}
|
|
123
150
|
}
|
|
124
151
|
}
|
|
125
|
-
#else // swift(>=5.9)
|
|
126
|
-
@available(*, unavailable, message: "Unavailable in Xcode <15.0")
|
|
127
|
-
public func emit(event: String, arguments: AnyArgument...) {
|
|
128
|
-
fatalError("Emitting events to JS requires at least Xcode 15.0")
|
|
129
|
-
}
|
|
130
|
-
#endif // swift(<5.9)
|
|
131
152
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-core",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.10",
|
|
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": "~0.0.
|
|
50
|
-
"expo-modules-jsi": "~56.0.
|
|
49
|
+
"@expo/expo-modules-macros-plugin": "~0.0.9",
|
|
50
|
+
"expo-modules-jsi": "~56.0.6",
|
|
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.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "290368bc41026449a05a4ebf991b85c3a2fb0e3a",
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "expo-module build",
|
|
72
72
|
"clean": "expo-module clean",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|