expo-modules-core 56.0.11 → 56.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,22 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.13 — 2026-05-26
14
+
15
+ ### 🎉 New features
16
+
17
+ - [Android] Create Compose props without View. ([#46256](https://github.com/expo/expo/pull/46256) by [@jakex7](https://github.com/jakex7))
18
+
19
+ ### 💡 Others
20
+
21
+ - 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))
22
+
23
+ ## 56.0.12 — 2026-05-21
24
+
25
+ ### 🐛 Bug fixes
26
+
27
+ - [Android] Suppress `-Wunused-result` compiler warning in `FrontendConverter.cpp`. ([#46073](https://github.com/expo/expo/pull/46073) by [@tomekzaw](https://github.com/tomekzaw))
28
+
13
29
  ## 56.0.11 — 2026-05-20
14
30
 
15
31
  ### 🐛 Bug fixes
@@ -27,7 +27,7 @@ if (shouldIncludeCompose) {
27
27
  }
28
28
 
29
29
  group = 'host.exp.exponent'
30
- version = '56.0.11'
30
+ version = '56.0.13'
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.11"
97
+ versionName "56.0.13"
98
98
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
99
99
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
100
100
 
@@ -1,7 +1,42 @@
1
1
  package expo.modules.kotlin.views
2
2
 
3
+ import com.facebook.react.bridge.ReadableMap
4
+ import expo.modules.kotlin.AppContext
5
+ import expo.modules.kotlin.recycle
6
+
3
7
  /**
4
8
  * A marker interface for props classes that are used to pass data to Compose views.
5
9
  * Needed for the R8 to not remove needed signatures that are used to receive prop types.
6
10
  */
7
11
  interface ComposeProps
12
+
13
+ inline fun <reified Props : ComposeProps> createComposeProps(
14
+ propsMap: ReadableMap?,
15
+ appContext: AppContext? = null
16
+ ): Props {
17
+ val propsParsingStrategy = toPropsParsingStrategy<Props>()
18
+ val propsInstance = propsParsingStrategy.createNewInstance()
19
+
20
+ if (propsMap == null) {
21
+ return propsInstance
22
+ }
23
+
24
+ val props = propsParsingStrategy.props()
25
+ val iterator = propsMap.keySetIterator()
26
+
27
+ while (iterator.hasNextKey()) {
28
+ val name = iterator.nextKey()
29
+ val prop = props[name] ?: continue
30
+
31
+ propsMap.getDynamic(name).recycle {
32
+ @Suppress("UNCHECKED_CAST")
33
+ prop.setPropDirectly(
34
+ prop = this,
35
+ currentProps = propsInstance,
36
+ appContext = appContext
37
+ ) as Props
38
+ }
39
+ }
40
+
41
+ return propsInstance
42
+ }
@@ -20,11 +20,16 @@ class ComposeViewProp(
20
20
 
21
21
  @Suppress("UNCHECKED_CAST")
22
22
  override fun set(prop: Dynamic, onView: View, appContext: AppContext?) {
23
- setPropDirectly(prop, onView, appContext)
23
+ setPropDirectly(prop = prop, onView = onView, appContext = appContext)
24
24
  }
25
25
 
26
26
  override fun set(prop: Any?, onView: View, appContext: AppContext?) {
27
- setPropDirectly(prop, onView, appContext)
27
+ setPropDirectly(prop = prop, onView = onView, appContext = appContext)
28
+ }
29
+
30
+ @PublishedApi
31
+ internal fun setPropDirectly(prop: Dynamic, currentProps: Any, appContext: AppContext?): Any {
32
+ return copyPropsWithNewValue(prop, currentProps, appContext) ?: currentProps
28
33
  }
29
34
 
30
35
  @Suppress("UNCHECKED_CAST")
@@ -37,15 +42,7 @@ class ComposeViewProp(
37
42
  if (onView is ComposeFunctionHolder<*>) {
38
43
  // Use current props state, not the initial props instance
39
44
  val currentProps = onView.propsMutableState.value
40
- // TODO(@lukmccall): We should remove the copy call
41
- val copy = currentProps::class.memberFunctions.firstOrNull { it.name == "copy" }
42
- if (copy == null) {
43
- logger.warn("⚠️ Props are not a data class with default values for all properties, cannot set prop $name dynamically.")
44
- return@exceptionDecorator
45
- }
46
- val instanceParam = copy.instanceParameter!!
47
- val newPropParam = copy.parameters.firstOrNull { it.name == name } ?: return@exceptionDecorator
48
- val result = copy.callBy(mapOf(instanceParam to currentProps, newPropParam to type.convert(prop, appContext)))
45
+ val result = copyPropsWithNewValue(prop, currentProps, appContext) ?: return@exceptionDecorator
49
46
  // Set the new props instance back to the onView
50
47
  (onView.propsMutableState as MutableState<Any?>).value = result
51
48
  return@exceptionDecorator
@@ -60,6 +57,18 @@ class ComposeViewProp(
60
57
  }
61
58
  }
62
59
 
60
+ private fun copyPropsWithNewValue(prop: Any?, currentProps: Any, appContext: AppContext?): Any? {
61
+ // TODO(@lukmccall): We should remove the copy call
62
+ val copy = currentProps::class.memberFunctions.firstOrNull { it.name == "copy" }
63
+ if (copy == null) {
64
+ logger.warn("⚠️ Props are not a data class with default values for all properties, cannot set prop $name dynamically.")
65
+ return null
66
+ }
67
+ val instanceParam = copy.instanceParameter!!
68
+ val newPropParam = copy.parameters.firstOrNull { it.name == name } ?: return null
69
+ return copy.callBy(mapOf(instanceParam to currentProps, newPropParam to type.convert(prop, appContext)))
70
+ }
71
+
63
72
  fun asStateProp(): ComposeViewProp {
64
73
  _isStateProp = true
65
74
  return this
@@ -775,7 +775,7 @@ jobject SynchronizableFrontendConverter::convert(
775
775
  bool SynchronizableFrontendConverter::canConvert(jsi::Runtime &rt, const jsi::Value &value) const {
776
776
  try {
777
777
  // TODO(@lukmccall): find a better way to check this without throwing exception
778
- worklets::extractSerializableOrThrow(rt, value);
778
+ (void)worklets::extractSerializableOrThrow(rt, value);
779
779
  return true;
780
780
  } catch (...) {
781
781
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeViewManagerAdapter.native.d.ts","sourceRoot":"","sources":["../src/NativeViewManagerAdapter.native.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAA4B,MAAM,OAAO,CAAC;AAwFrF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAAC,CAAC,CAAC,CAsClB"}
1
+ {"version":3,"file":"NativeViewManagerAdapter.native.d.ts","sourceRoot":"","sources":["../src/NativeViewManagerAdapter.native.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAA4B,MAAM,OAAO,CAAC;AA4HrF;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAAC,CAAC,CAAC,CAsClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "56.0.11",
3
+ "version": "56.0.13",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "main": "src/index.ts",
6
6
  "types": "build/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  "@types/invariant": "^2.2.33",
67
67
  "expo-module-scripts": "56.0.2"
68
68
  },
69
- "gitHead": "c4c9867a0bcbb188e55ecaec4998e38d33108a5d",
69
+ "gitHead": "f67a101bcbe56114e982184834b93da7bbed00af",
70
70
  "scripts": {
71
71
  "build": "expo-module build",
72
72
  "clean": "expo-module clean",
@@ -8,6 +8,7 @@ import { type Component, type ComponentType, createRef, PureComponent } from 're
8
8
  import { type ReactNativeElement, findNodeHandle, type HostComponent } from 'react-native';
9
9
  import { get as componentRegistryGet } from 'react-native/Libraries/NativeComponent/NativeComponentRegistry';
10
10
 
11
+ import { SharedObject } from './SharedObject';
11
12
  import { requireNativeModule } from './requireNativeModule';
12
13
 
13
14
  // To make the transition from React Native's `requireNativeComponent` to Expo's
@@ -63,15 +64,50 @@ function requireNativeComponent<Props>(
63
64
  viewName ?? 'default view',
64
65
  moduleName
65
66
  );
67
+ return { uiViewClassName: nativeViewName };
66
68
  }
67
69
 
68
70
  return {
69
71
  uiViewClassName: nativeViewName,
70
- ...expoViewConfig,
72
+ directEventTypes: expoViewConfig.directEventTypes,
73
+ validAttributes: addAttributeProcessing(expoViewConfig.validAttributes),
71
74
  };
72
75
  });
73
76
  }
74
77
 
78
+ /**
79
+ * Unwraps a shared object to its id so that native receives the registry id
80
+ * instead of the JS wrapper. Other values are passed through unchanged.
81
+ */
82
+ function processPropValue(value: unknown): unknown {
83
+ if (value != null && typeof value === 'object' && value instanceof SharedObject) {
84
+ // `__expo_shared_object_id__` is a hidden property installed by native and planned for
85
+ // removal; the `typeof` check guards against returning `undefined` once native stops
86
+ // setting the property.
87
+ // @ts-expect-error
88
+ const sharedObjectId = value.__expo_shared_object_id__;
89
+ if (typeof sharedObjectId === 'number') {
90
+ return sharedObjectId;
91
+ }
92
+ }
93
+ return value;
94
+ }
95
+
96
+ /**
97
+ * Wraps each attribute in the descriptor shape React Native expects and attaches `process`
98
+ * to unwrap shared objects to their registry id. `diff` is intentionally left unset so
99
+ * React Native falls back to its `deepDiffer` default, which does structural comparison
100
+ * for object/array props.
101
+ */
102
+ function addAttributeProcessing(validAttributes: Record<string, any>): Record<string, any> {
103
+ const descriptor = { process: processPropValue };
104
+ const attributes: Record<string, any> = {};
105
+ for (const key of Object.keys(validAttributes)) {
106
+ attributes[key] = descriptor;
107
+ }
108
+ return attributes;
109
+ }
110
+
75
111
  /**
76
112
  * Requires a React Native component from cache if possible. This prevents
77
113
  * "Tried to register two views with the same name" errors on fast refresh, but