expo-modules-core 58.0.1 → 58.0.2

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,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 58.0.2 — 2026-09-15
14
+
15
+ ### 🛠 Breaking changes
16
+
17
+ - [Android] Compile `expo-module-gradle-plugin` against Android Gradle Plugin 9.2.1 and drop the Android Gradle Plugin 8 compatibility code. ([#50114](https://github.com/expo/expo/pull/50114) by [@lukmccall](https://github.com/lukmccall))
18
+
19
+ ### 🐛 Bug fixes
20
+
21
+ - [Android] Fixed native-backed `ArrayBuffer`s and `NativeArrayBuffer`s being copied instead of shared when passed back from JS on Hermes V1. ([#50132](https://github.com/expo/expo/pull/50132) by [@lukmccall](https://github.com/lukmccall))
22
+
13
23
  ## 58.0.1 — 2026-09-14
14
24
 
15
25
  ### 🎉 New features
@@ -34,6 +44,7 @@
34
44
 
35
45
  ### 🎉 New features
36
46
 
47
+ - [macOS] Added `UIColor`, `UIGestureRecognizer` and `UITextContentType` to the AppKit compatibility aliases in `Platform.swift`. ([#50110](https://github.com/expo/expo/pull/50110) by [@gabrieldonadel](https://github.com/gabrieldonadel))
37
48
  - [Android] Added a `loadImageForManipulationFromURL` overload to `ImageLoaderInterface` that decodes the image within the given `maxWidth`/`maxHeight` bounds. ([#47877](https://github.com/expo/expo/pull/47877) by [@jiunshinn](https://github.com/jiunshinn))
38
49
  - Add `useReleasingSharedObjectWithLifecycle` hook. ([#46494](https://github.com/expo/expo/pull/46494) by [@behenate](https://github.com/behenate))
39
50
  - Added `ArrayBuffer` as the preferred safe native module argument and return type, and deprecated `NativeArrayBuffer` in favor of it. ([#47106](https://github.com/expo/expo/pull/47106) by [@barthap](https://github.com/barthap))
@@ -7,11 +7,21 @@ project(expo-modules-core)
7
7
  include(${CMAKE_SOURCE_DIR}/cmake/variables.cmake)
8
8
  include(${CMAKE_SOURCE_DIR}/cmake/common.cmake)
9
9
 
10
- # defines expo-modules-jsi target
11
- include(${CMAKE_SOURCE_DIR}/cmake/jsi.cmake)
10
+ # When prebuilt artifacts are shipped in the package, imports expo-modules-jsi / expo-modules-core
11
+ # as IMPORTED targets (and sets EXPO_JSI_PREBUILT / EXPO_CORE_PREBUILT) so they are not recompiled.
12
+ if (EXPO_PREBUILT_NATIVE_LIBS_DIR)
13
+ include(${CMAKE_SOURCE_DIR}/cmake/prebuilt.cmake)
14
+ endif ()
15
+
16
+ # defines expo-modules-jsi target, unless it was imported from a prebuilt artifact above
17
+ if (NOT EXPO_JSI_PREBUILT)
18
+ include(${CMAKE_SOURCE_DIR}/cmake/jsi.cmake)
19
+ endif ()
12
20
 
13
- # defines expo-modules-core target
14
- include(${CMAKE_SOURCE_DIR}/cmake/main.cmake)
21
+ # defines expo-modules-core target, unless it was imported from a prebuilt artifact above
22
+ if (NOT EXPO_CORE_PREBUILT)
23
+ include(${CMAKE_SOURCE_DIR}/cmake/main.cmake)
24
+ endif ()
15
25
 
16
26
  # defines expo-modules-worklets target (worklets integration), only when react-native-worklets is installed
17
27
  if (REACT_NATIVE_WORKLETS_DIR)
@@ -1,6 +1,18 @@
1
- import groovy.json.JsonSlurper
2
1
  import expo.modules.plugin.gradle.ExpoModuleExtension
2
+ import groovy.json.JsonOutput
3
+ import groovy.json.JsonSlurper
4
+ import org.apache.commons.compress.archivers.tar.TarArchiveEntry
5
+ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
6
+ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
3
7
  import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
8
+ import org.tukaani.xz.LZMA2Options
9
+ import org.tukaani.xz.XZInputStream
10
+ import org.tukaani.xz.XZOutputStream
11
+
12
+ import java.nio.file.Files
13
+ import java.nio.file.StandardCopyOption
14
+ import java.time.Instant
15
+ import java.util.regex.Matcher
4
16
 
5
17
  buildscript {
6
18
  // List of features that are required by linked modules
@@ -16,6 +28,8 @@ buildscript {
16
28
  classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${kotlinVersion}")
17
29
  }
18
30
 
31
+ classpath "org.tukaani:xz:1.12"
32
+ classpath "org.apache.commons:commons-compress:1.28.0"
19
33
  }
20
34
  }
21
35
 
@@ -27,7 +41,7 @@ if (shouldIncludeCompose) {
27
41
  }
28
42
 
29
43
  group = 'host.exp.exponent'
30
- version = '58.0.1'
44
+ version = '58.0.2'
31
45
 
32
46
  def isExpoModulesCoreTests = {
33
47
  Gradle gradle = getGradle()
@@ -78,7 +92,7 @@ if (workletsProject != null) {
78
92
 
79
93
  def reactNativeWorkletsRootDir = workletsProject?.projectDir?.parentFile
80
94
 
81
- def expoModulesV2Version = "0.1.2"
95
+ def expoModulesV2Version = "0.1.5"
82
96
 
83
97
  configurations {
84
98
  // Artifact-only: the C++ sources, not something to put on any classpath.
@@ -99,6 +113,36 @@ tasks.matching {
99
113
  it.dependsOn(unpackExpoModulesV2Cpp)
100
114
  }
101
115
 
116
+ def reactNativeVersionString = expoModuleExtension.reactNativeVersion.toString()
117
+
118
+ def prebuiltNativeLibsDir = project.file("prebuilt")
119
+ def prebuiltNativeLibsArchive = project.file("prebuilt/native-libs.tar.xz")
120
+ def prebuiltMetadataFile = project.file("prebuilt/metadata.json")
121
+
122
+ def decompressedNativeLibsDir = layout.buildDirectory.dir("expo-prebuilt-natives").get().asFile
123
+
124
+ def prebuiltVariants = ["debug", "release"]
125
+
126
+ def isPrebuiltCachingRun = gradle.startParameter.taskNames.any { it.contains("cachePrebuiltNativeLibs") }
127
+
128
+ def isPrebuiltUsable = {
129
+ if (!prebuiltMetadataFile.exists() || !prebuiltNativeLibsArchive.exists()) {
130
+ return false
131
+ }
132
+ def metadata = new JsonSlurper().parseText(prebuiltMetadataFile.text)
133
+ if (metadata.libraryVersion != version) {
134
+ logger.warn("[expo-modules-core] Ignoring prebuilt libs: built for v${metadata.libraryVersion}, package is v${version} - building from source")
135
+ return false
136
+ }
137
+ if (metadata.reactNativeVersion != reactNativeVersionString) {
138
+ logger.warn("[expo-modules-core] Ignoring prebuilt libs: built for RN ${metadata.reactNativeVersion}, installed RN ${reactNativeVersionString} - building from source")
139
+ return false
140
+ }
141
+ return true
142
+ }
143
+
144
+ def usePrebuiltNativeLibs = !isPrebuiltCachingRun && !isTests && isPrebuiltUsable()
145
+
102
146
  expoModule {
103
147
  canBePublished false
104
148
  }
@@ -115,7 +159,7 @@ android {
115
159
  defaultConfig {
116
160
  consumerProguardFiles 'proguard-rules.pro'
117
161
  versionCode 1
118
- versionName "58.0.1"
162
+ versionName "58.0.2"
119
163
  buildConfigField "String", "EXPO_MODULES_CORE_VERSION", "\"${versionName}\""
120
164
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true"
121
165
 
@@ -137,6 +181,10 @@ android {
137
181
  cppArguments += "-DREACT_NATIVE_WORKLETS_DIR=${reactNativeWorkletsRootDir.absolutePath}"
138
182
  }
139
183
 
184
+ if (usePrebuiltNativeLibs) {
185
+ cppArguments += "-DEXPO_PREBUILT_NATIVE_LIBS_DIR=${decompressedNativeLibsDir.absolutePath}"
186
+ }
187
+
140
188
  abiFilters(*reactNativeArchitectures())
141
189
  arguments(*cppArguments)
142
190
  }
@@ -210,6 +258,14 @@ android {
210
258
  }
211
259
  }
212
260
  }
261
+
262
+ // Package the decompressed prebuilt .so into each variant. The layout `<variant>/<abi>/lib*.so`
263
+ // matches what AGP expects from a jniLibs source set.
264
+ if (usePrebuiltNativeLibs) {
265
+ prebuiltVariants.each { variant ->
266
+ getByName(variant).jniLibs.srcDir(new File(decompressedNativeLibsDir, variant))
267
+ }
268
+ }
213
269
  }
214
270
 
215
271
  testOptions {
@@ -320,10 +376,10 @@ def generateStubPCHTask = tasks.register("generateStubPCH") {
320
376
  def stubHeaderPath = stubHeader.absolutePath.replace('\\', '/')
321
377
 
322
378
  def cmd = entry.command
323
- // Replace the forced-include path: `-Xclang -include -Xclang <path>/cmake_pch.hxx`
324
- .replaceAll(/-Xclang -include -Xclang [^\s]+cmake_pch\.hxx(?=\s)/, java.util.regex.Matcher.quoteReplacement("-Xclang -include -Xclang ${stubHeaderPath}"))
325
- // Replace the source file operand: `<path>/cmake_pch.hxx.cxx`
326
- .replaceAll(/[^\s]+cmake_pch\.hxx\.cxx/, java.util.regex.Matcher.quoteReplacement(stubHeaderPath))
379
+ // Replace the forced-include path: `-Xclang -include -Xclang <path>/cmake_pch.hxx`
380
+ .replaceAll(/-Xclang -include -Xclang [^\s]+cmake_pch\.hxx(?=\s)/, Matcher.quoteReplacement("-Xclang -include -Xclang ${stubHeaderPath}"))
381
+ // Replace the source file operand: `<path>/cmake_pch.hxx.cxx`
382
+ .replaceAll(/[^\s]+cmake_pch\.hxx\.cxx/, Matcher.quoteReplacement(stubHeaderPath))
327
383
 
328
384
  def process = new ProcessBuilder(cmd.split(" ").toList())
329
385
  .directory(new File(entry.directory))
@@ -344,3 +400,206 @@ def generateStubPCHTask = tasks.register("generateStubPCH") {
344
400
  tasks.register("prepareKotlinBuildScriptModel") {
345
401
  dependsOn(generateStubPCHTask)
346
402
  }
403
+
404
+ def prebuiltNativeLibNames = ["libexpo-modules-core.so", "libexpo-modules-jsi.so"]
405
+
406
+ // Helpers for the cachePrebuiltNativeLibs task. They are `static` so the task action can call them
407
+ // without capturing the build script, which keeps the task configuration-cache compatible.
408
+ class PrebuiltNativeLibs {
409
+ /**
410
+ * Returns host tag based on the OS name.
411
+ */
412
+ private static String hostTag() {
413
+ def osName = System.getProperty("os.name").toLowerCase()
414
+ if (osName.contains("mac")) {
415
+ return "darwin-x86_64"
416
+ }
417
+
418
+ if (osName.contains("win")) {
419
+ return "windows-x86_64"
420
+ }
421
+
422
+ return "linux-x86_64"
423
+ }
424
+
425
+ private static void strippedLib(File stripBinary, File source, File dest) {
426
+ def proc = new ProcessBuilder(
427
+ stripBinary.absolutePath,
428
+ "--strip-all",
429
+ source.absolutePath,
430
+ "-o",
431
+ dest.absolutePath
432
+ )
433
+ .redirectErrorStream(true)
434
+ .start()
435
+
436
+ if (proc.waitFor() != 0) {
437
+ throw new GradleException("[expo-modules-core] llvm-strip failed for ${source}: ${proc.inputStream.text}")
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Locate the NDK's `llvm-strip`, or null when the NDK/binary can't be found.
443
+ */
444
+ static File findLlvmStrip(File ndkDir) {
445
+ if (ndkDir == null || !ndkDir.exists()) {
446
+ return null
447
+ }
448
+ def hostTag = hostTag()
449
+ def strip = new File(ndkDir, "toolchains/llvm/prebuilt/${hostTag}/bin/llvm-strip")
450
+ return strip.exists() ? strip : null
451
+ }
452
+
453
+ /**
454
+ * Copy each of `soNames` from `objDir/<abi>/` into `stagingVariantDir/<abi>/`, stripping debug
455
+ * symbols. Returns the set of ABIs that were staged.
456
+ */
457
+ static Set<String> stageStrippedLibs(File objDir, List<String> soNames, File stagingVariantDir, File stripBinary) {
458
+ def abis = new TreeSet<String>()
459
+ objDir
460
+ .listFiles({ it.isDirectory() } as FileFilter)
461
+ ?.each { abiDir ->
462
+ soNames.each { soName ->
463
+ def source = new File(abiDir, soName)
464
+ if (!source.exists()) {
465
+ return
466
+ }
467
+
468
+ def dest = new File(stagingVariantDir, "${abiDir.name}/${soName}")
469
+ dest.parentFile.mkdirs()
470
+
471
+ if (stripBinary != null) {
472
+ strippedLib(stripBinary, source, dest)
473
+ } else {
474
+ Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING)
475
+ }
476
+
477
+ abis << abiDir.name
478
+ }
479
+ }
480
+ return abis
481
+ }
482
+
483
+ /**
484
+ * Bundle every file under `stagingDir` into a single solid tar.xz
485
+ */
486
+ static void bundleToTarXz(File stagingDir, File archive) {
487
+ archive.parentFile.mkdirs()
488
+ def stagingPath = stagingDir.toPath()
489
+
490
+ def files = []
491
+ stagingDir.eachFileRecurse {
492
+ if (it.isFile()) {
493
+ files << it
494
+ }
495
+ }
496
+ files.sort { it.absolutePath }
497
+
498
+ new TarArchiveOutputStream(
499
+ new XZOutputStream(archive.newOutputStream(), new LZMA2Options(9))
500
+ ).withCloseable { tar ->
501
+ files.each { file ->
502
+ def archiveEntry = new TarArchiveEntry(file, stagingPath.relativize(file.toPath()).toString())
503
+ tar.putArchiveEntry(archiveEntry)
504
+ Files.copy(file.toPath(), tar)
505
+ tar.closeArchiveEntry()
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ def stripBinary = PrebuiltNativeLibs.findLlvmStrip(android.ndkDirectory)
512
+ def libraryVersion = version.toString()
513
+ def cacheStagingDir = layout.buildDirectory.dir("tmp/cache-prebuilt-natives").get().asFile
514
+ def variantDescriptors = prebuiltVariants.collect { variant ->
515
+ def variantCap = variant.capitalize()
516
+ def libsDir = layout.buildDirectory.dir("intermediates/merged_native_libs/${variant}/merge${variantCap}NativeLibs/out/lib").get().asFile
517
+ def outDir = layout.buildDirectory.dir("tmp/cache-prebuilt-natives/${variant}").get().asFile
518
+ [
519
+ name : variant,
520
+ mergeTask: "merge${variantCap}NativeLibs",
521
+ libsDir : libsDir,
522
+ outDir : outDir,
523
+ ]
524
+ }
525
+
526
+ tasks.register("cachePrebuiltNativeLibs") {
527
+ group = "prebuilt natives"
528
+ description = "Build expo-modules-core from source (debug + release), strip and cache the shippable .so files into prebuilt/"
529
+ dependsOn(variantDescriptors.collect { it.mergeTask })
530
+
531
+ doLast {
532
+ if (stripBinary == null) {
533
+ logger.warn("[expo-modules-core] NDK llvm-strip not found - .so files will be cached unstripped")
534
+ }
535
+
536
+ if (prebuiltNativeLibsDir.exists()) {
537
+ prebuiltNativeLibsDir.deleteDir()
538
+ }
539
+
540
+ if (cacheStagingDir.exists()) {
541
+ cacheStagingDir.deleteDir()
542
+ }
543
+
544
+ def allAbis = new TreeSet<String>()
545
+ variantDescriptors.each { vd ->
546
+ logger.lifecycle("[expo-modules-core] Caching ${vd.name} from ${vd.libsDir}")
547
+ allAbis.addAll(PrebuiltNativeLibs.stageStrippedLibs(vd.libsDir, prebuiltNativeLibNames, vd.outDir, stripBinary))
548
+ }
549
+ if (allAbis.isEmpty()) {
550
+ throw new GradleException("[expo-modules-core] No ${prebuiltNativeLibNames} were cached")
551
+ }
552
+
553
+ PrebuiltNativeLibs.bundleToTarXz(cacheStagingDir, prebuiltNativeLibsArchive)
554
+ cacheStagingDir.deleteDir()
555
+
556
+ prebuiltMetadataFile.text = JsonOutput.prettyPrint(JsonOutput.toJson([
557
+ libraryVersion : libraryVersion,
558
+ reactNativeVersion: reactNativeVersionString,
559
+ variants : variantDescriptors.collect { it.name },
560
+ abis : allAbis as List,
561
+ soFiles : prebuiltNativeLibNames as List,
562
+ stripped : stripBinary != null,
563
+ createdAt : Instant.now().toString(),
564
+ ])) + "\n"
565
+
566
+ def archiveSizeMb = prebuiltNativeLibsArchive.length() / (1024 * 1024)
567
+ logger.lifecycle(
568
+ "[expo-modules-core] Create ${prebuiltNativeLibNames.join(", ")} for ${allAbis.size()} ABIs " +
569
+ "(${allAbis.join(", ")}) x ${variantDescriptors.size()} variants into " +
570
+ "${prebuiltNativeLibsArchive.name} (~${archiveSizeMb}MB, stripped, solid tar.xz)"
571
+ )
572
+ }
573
+ }
574
+
575
+ // Unpacks the shipped solid `prebuilt/native-libs.tar.xz` into the build dir so CMake can import the
576
+ // libs and AGP can package them via the jniLibs source sets
577
+ if (usePrebuiltNativeLibs) {
578
+ def archiveFile = prebuiltNativeLibsArchive
579
+ def outputDir = decompressedNativeLibsDir
580
+ def decompressPrebuiltNativeLibs = tasks.register("decompressPrebuiltNativeLibs") {
581
+ description = "Unpack the shipped native-libs.tar.xz into the build dir for CMake import and packaging"
582
+ inputs.file(archiveFile)
583
+ outputs.dir(outputDir)
584
+
585
+ doLast {
586
+ outputDir.deleteDir()
587
+ new TarArchiveInputStream(
588
+ new XZInputStream(archiveFile.newInputStream())
589
+ ).withCloseable { tar ->
590
+ def entry
591
+ while ((entry = tar.nextEntry) != null) {
592
+ if (entry.isDirectory()) {
593
+ continue
594
+ }
595
+ // entries are `<variant>/<abi>/lib*.so`
596
+ def dest = new File(outputDir, entry.name)
597
+ dest.parentFile.mkdirs()
598
+ dest.withOutputStream { out -> tar.transferTo(out) }
599
+ }
600
+ }
601
+ }
602
+ }
603
+
604
+ tasks.named("preBuild").configure { dependsOn(decompressPrebuiltNativeLibs) }
605
+ }
@@ -3,7 +3,7 @@ file(GLOB android_sources_jsi "${ANDROID_SRC_DIR}/jsi/*.cpp")
3
3
 
4
4
  add_library(
5
5
  expo-modules-jsi
6
- STATIC
6
+ SHARED
7
7
  ${common_sources_jsi}
8
8
  ${android_sources_jsi}
9
9
  )
@@ -0,0 +1,34 @@
1
+ if (${CMAKE_BUILD_TYPE} MATCHES "Debug")
2
+ set(EXPO_PREBUILT_VARIANT "debug")
3
+ else ()
4
+ set(EXPO_PREBUILT_VARIANT "release")
5
+ endif ()
6
+
7
+ set(EXPO_PREBUILT_ABI_DIR "${EXPO_PREBUILT_NATIVE_LIBS_DIR}/${EXPO_PREBUILT_VARIANT}/${ANDROID_ABI}")
8
+
9
+ # expo-modules-jsi
10
+ set(EXPO_PREBUILT_JSI_SO "${EXPO_PREBUILT_ABI_DIR}/libexpo-modules-jsi.so")
11
+ if (EXISTS "${EXPO_PREBUILT_JSI_SO}")
12
+ add_library(expo-modules-jsi SHARED IMPORTED)
13
+ set_target_properties(
14
+ expo-modules-jsi
15
+ PROPERTIES
16
+ IMPORTED_LOCATION "${EXPO_PREBUILT_JSI_SO}"
17
+ INTERFACE_INCLUDE_DIRECTORIES "${COMMON_DIR}/JSI;${ANDROID_SRC_DIR}/jsi"
18
+ )
19
+ set(EXPO_JSI_PREBUILT TRUE)
20
+ message(STATUS "[expo-modules-core] Using prebuilt libexpo-modules-jsi.so (${EXPO_PREBUILT_VARIANT}/${ANDROID_ABI})")
21
+ endif ()
22
+
23
+ # expo-modules-core
24
+ set(EXPO_PREBUILT_CORE_SO "${EXPO_PREBUILT_ABI_DIR}/libexpo-modules-core.so")
25
+ if (EXISTS "${EXPO_PREBUILT_CORE_SO}")
26
+ add_library(expo-modules-core SHARED IMPORTED)
27
+ set_target_properties(
28
+ expo-modules-core
29
+ PROPERTIES
30
+ IMPORTED_LOCATION "${EXPO_PREBUILT_CORE_SO}"
31
+ )
32
+ set(EXPO_CORE_PREBUILT TRUE)
33
+ message(STATUS "[expo-modules-core] Using prebuilt libexpo-modules-core.so (${EXPO_PREBUILT_VARIANT}/${ANDROID_ABI})")
34
+ endif ()
@@ -0,0 +1,20 @@
1
+ {
2
+ "libraryVersion": "58.0.1",
3
+ "reactNativeVersion": "0.88.0",
4
+ "variants": [
5
+ "debug",
6
+ "release"
7
+ ],
8
+ "abis": [
9
+ "arm64-v8a",
10
+ "armeabi-v7a",
11
+ "x86",
12
+ "x86_64"
13
+ ],
14
+ "soFiles": [
15
+ "libexpo-modules-core.so",
16
+ "libexpo-modules-jsi.so"
17
+ ],
18
+ "stripped": true,
19
+ "createdAt": "2026-09-15T14:36:32.485597Z"
20
+ }
@@ -1,4 +1,5 @@
1
1
  #include "ArrayBuffer.h"
2
+ #include "MutableBufferNativeState.h"
2
3
 
3
4
  #include "Exceptions.h"
4
5
  #include "JavaReferencesCache.h"
@@ -184,7 +185,7 @@ jni::local_ref<ArrayBuffer::javaobject> makeArrayBufferFromJSIArrayBuffer(
184
185
  size_t length,
185
186
  const std::function<uint8_t *(jsi::ArrayBuffer &)> &copySourceProvider
186
187
  ) {
187
- auto mutableBuffer = arrayBuffer.tryGetMutableBuffer(runtime);
188
+ auto mutableBuffer = expo::tryGetMutableBuffer(runtime, arrayBuffer);
188
189
  if (mutableBuffer) {
189
190
  return makeArrayBufferObject(
190
191
  jsiContext,
@@ -285,7 +286,7 @@ std::shared_ptr<jsi::MutableBuffer> ByteBufferArrayBufferStorage::jsiMutableBuff
285
286
  }
286
287
 
287
288
  jsi::Value ByteBufferArrayBufferStorage::toJSIValue(jsi::Runtime &runtime) {
288
- return jsi::Value(runtime, runtime.createArrayBuffer(jsiMutableBuffer()));
289
+ return jsi::Value(runtime, createNativeBackedArrayBuffer(runtime, jsiMutableBuffer()));
289
290
  }
290
291
 
291
292
  jni::local_ref<jni::JObject> ByteBufferArrayBufferStorage::withJSBytes(
@@ -328,7 +329,7 @@ std::shared_ptr<jsi::MutableBuffer> MutableBufferViewArrayBufferStorage::jsiMuta
328
329
  }
329
330
 
330
331
  jsi::Value MutableBufferViewArrayBufferStorage::toJSIValue(jsi::Runtime &runtime) {
331
- return jsi::Value(runtime, runtime.createArrayBuffer(jsiMutableBuffer()));
332
+ return jsi::Value(runtime, createNativeBackedArrayBuffer(runtime, jsiMutableBuffer()));
332
333
  }
333
334
 
334
335
  jni::local_ref<jni::JObject> MutableBufferViewArrayBufferStorage::withJSBytes(
@@ -446,11 +447,11 @@ jsi::Value JavaScriptBackedArrayBufferStorage::toJSIValue(jsi::Runtime &runtime)
446
447
  }
447
448
  auto byteBuffer = copyToDirectBuffer(_arrayBuffer->data(runtime) + _offset, _length);
448
449
  auto mutableBuffer = std::make_shared<ByteBufferJSIMutableBuffer>(byteBuffer);
449
- return jsi::Value(runtime, runtime.createArrayBuffer(std::move(mutableBuffer)));
450
+ return jsi::Value(runtime, createNativeBackedArrayBuffer(runtime, std::move(mutableBuffer)));
450
451
  }
451
452
  }
452
453
 
453
- return jsi::Value(runtime, runtime.createArrayBuffer(jsiMutableBuffer()));
454
+ return jsi::Value(runtime, createNativeBackedArrayBuffer(runtime, jsiMutableBuffer()));
454
455
  }
455
456
 
456
457
  jni::local_ref<jni::JObject> JavaScriptBackedArrayBufferStorage::withJSBytes(
@@ -0,0 +1,36 @@
1
+ #include "MutableBufferNativeState.h"
2
+
3
+ namespace expo {
4
+
5
+ MutableBufferNativeState::MutableBufferNativeState(std::shared_ptr<jsi::MutableBuffer> buffer)
6
+ : buffer(std::move(buffer)) {}
7
+
8
+ jsi::ArrayBuffer createNativeBackedArrayBuffer(
9
+ jsi::Runtime &runtime,
10
+ std::shared_ptr<jsi::MutableBuffer> buffer
11
+ ) {
12
+ auto arrayBuffer = runtime.createArrayBuffer(buffer);
13
+ arrayBuffer.setNativeState(runtime, std::make_shared<MutableBufferNativeState>(std::move(buffer)));
14
+ return arrayBuffer;
15
+ }
16
+
17
+ std::shared_ptr<jsi::MutableBuffer> tryGetMutableBuffer(
18
+ jsi::Runtime &runtime,
19
+ const jsi::ArrayBuffer &arrayBuffer
20
+ ) {
21
+ if (auto buffer = arrayBuffer.tryGetMutableBuffer(runtime)) {
22
+ return buffer;
23
+ }
24
+
25
+ if (!arrayBuffer.hasNativeState<MutableBufferNativeState>(runtime)) {
26
+ return nullptr;
27
+ }
28
+
29
+ auto buffer = arrayBuffer.getNativeState<MutableBufferNativeState>(runtime)->buffer;
30
+ if (!buffer || arrayBuffer.data(runtime) != buffer->data()) {
31
+ return nullptr;
32
+ }
33
+ return buffer;
34
+ }
35
+
36
+ } // namespace expo
@@ -0,0 +1,43 @@
1
+ #pragma once
2
+
3
+ #include "ExpoHeader.pch"
4
+
5
+ namespace expo {
6
+
7
+ namespace jsi = facebook::jsi;
8
+
9
+ /**
10
+ * Native state attached to every native-backed `jsi::ArrayBuffer` created by Expo.
11
+ * It retains the `jsi::MutableBuffer` that owns the bytes, so the buffer can be recovered
12
+ * later with `expo::tryGetMutableBuffer` even when the runtime doesn't implement
13
+ * `jsi::Runtime::tryGetMutableBuffer`. Hermes V1 returns `nullptr` from that method,
14
+ * which would otherwise force a copy and break the zero-copy contract.
15
+ */
16
+ class MutableBufferNativeState final : public jsi::NativeState {
17
+ public:
18
+ explicit MutableBufferNativeState(std::shared_ptr<jsi::MutableBuffer> buffer);
19
+
20
+ std::shared_ptr<jsi::MutableBuffer> buffer;
21
+ };
22
+
23
+ /**
24
+ * Creates a JS `ArrayBuffer` that shares the bytes of `buffer` and tags it with
25
+ * `MutableBufferNativeState`, so `expo::tryGetMutableBuffer` can find the buffer again.
26
+ */
27
+ jsi::ArrayBuffer createNativeBackedArrayBuffer(
28
+ jsi::Runtime &runtime,
29
+ std::shared_ptr<jsi::MutableBuffer> buffer
30
+ );
31
+
32
+ /**
33
+ * Returns the `jsi::MutableBuffer` backing `arrayBuffer`, or `nullptr` when the bytes live
34
+ * on the JS heap. Asks the runtime first and falls back to the native state attached by
35
+ * `createNativeBackedArrayBuffer`. The fallback is used only when the JS object still points
36
+ * at the retained buffer's memory, so a detached or re-attached `ArrayBuffer` is never shared.
37
+ */
38
+ std::shared_ptr<jsi::MutableBuffer> tryGetMutableBuffer(
39
+ jsi::Runtime &runtime,
40
+ const jsi::ArrayBuffer &arrayBuffer
41
+ );
42
+
43
+ } // namespace expo
@@ -1,4 +1,5 @@
1
1
  #include "NativeArrayBuffer.h"
2
+ #include "MutableBufferNativeState.h"
2
3
 
3
4
  #include "JavaScriptRuntime.h"
4
5
  #include "JSIContext.h"
@@ -57,7 +58,7 @@ NativeArrayBuffer::initHybrid(jni::alias_ref<JavaPart::javaobject>,
57
58
  jni::local_ref<NativeArrayBuffer::javaobject>
58
59
  NativeArrayBuffer::newInstance(JSIContext *jsiContext, jsi::Runtime &runtime,
59
60
  jsi::ArrayBuffer &arrayBuffer) {
60
- auto mutableBuffer = arrayBuffer.tryGetMutableBuffer(runtime);
61
+ auto mutableBuffer = expo::tryGetMutableBuffer(runtime, arrayBuffer);
61
62
  if (mutableBuffer) {
62
63
  auto byteBuffer = jni::JByteBuffer::wrapBytes(mutableBuffer->data(), mutableBuffer->size());
63
64
  byteBuffer->order(jni::JByteOrder::nativeOrder());
@@ -82,7 +83,7 @@ NativeArrayBuffer::newInstance(JSIContext *jsiContext, jsi::Runtime &runtime,
82
83
  size_t size = typedArray.byteLength(runtime);
83
84
 
84
85
  auto backingBuffer = typedArray.getBuffer(runtime);
85
- auto mutableBuffer = backingBuffer.tryGetMutableBuffer(runtime);
86
+ auto mutableBuffer = expo::tryGetMutableBuffer(runtime, backingBuffer);
86
87
  if (mutableBuffer) {
87
88
  size_t offset = typedArray.byteOffset(runtime);
88
89
  auto byteBuffer = jni::JByteBuffer::wrapBytes(
@@ -56,7 +56,7 @@ public:
56
56
 
57
57
  /**
58
58
  * Creates a NativeArrayBuffer from the given ArrayBuffer. Uses zero-copy when the
59
- * buffer is native-backed (tryGetMutableBuffer), otherwise copies the data.
59
+ * buffer is native-backed (`expo::tryGetMutableBuffer`), otherwise copies the data.
60
60
  */
61
61
  static jni::local_ref<NativeArrayBuffer::javaobject> newInstance(
62
62
  JSIContext *jsiContext,
@@ -10,6 +10,7 @@
10
10
  #include "ObjectDeallocator.h"
11
11
  #include "../JavaScriptArrayBuffer.h"
12
12
  #include "../NativeArrayBuffer.h"
13
+ #include "../MutableBufferNativeState.h"
13
14
  #include "../concepts/jni_deref.h"
14
15
  #include "../concepts/jni.h"
15
16
  #include "../concepts/jsi.h"
@@ -182,7 +183,7 @@ struct JNIToJSIConverter<JavaScriptArrayBuffer *> {
182
183
  template<>
183
184
  struct JNIToJSIConverter<NativeArrayBuffer *> {
184
185
  static jsi::Value convert(JNIEnv *, jsi::Runtime &rt, NativeArrayBuffer *value) {
185
- jsi::ArrayBuffer arrayBuffer(rt, value->jsiMutableBuffer());
186
+ auto arrayBuffer = createNativeBackedArrayBuffer(rt, value->jsiMutableBuffer());
186
187
  return jsi::Value{rt, arrayBuffer};
187
188
  }
188
189
  };
@@ -2,7 +2,7 @@ package expo.modules.v2
2
2
 
3
3
  import com.facebook.react.bridge.ReactApplicationContext
4
4
  import expo.modules.kotlin.logger
5
- import io.github.expo.modules.v2.modules.Module
5
+ import io.github.expo.modules.v2.Module
6
6
  import io.github.expo.modules.v2.modules.ModuleRegistry
7
7
  import io.github.expo.modules.v2.react.ReactRuntime
8
8
  import java.lang.reflect.Modifier
@@ -1,6 +1,6 @@
1
1
  package expo.modules.v2
2
2
 
3
- import io.github.expo.modules.v2.modules.Module
3
+ import io.github.expo.modules.v2.Module
4
4
 
5
5
  interface ExpoModulesV2Provider {
6
6
  fun getModules(): List<Class<out Module>>
@@ -29,11 +29,11 @@ val isExpoAutolinkingSettingsPluginAvailable = expoAutolinkingSettingsPlugin ==
29
29
 
30
30
  dependencies {
31
31
  implementation(gradleApi())
32
- compileOnly("com.android.tools.build:gradle:8.5.0")
32
+ compileOnly("com.android.tools.build:gradle:9.2.1")
33
33
  implementation("com.facebook.react:react-native-gradle-plugin")
34
34
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
35
35
  implementation("io.github.expo.pika:pika-gradle:1.0.0")
36
- implementation("io.github.expo:expo-modules-v2-gradle-plugin:0.1.2")
36
+ implementation("io.github.expo:expo-modules-v2-gradle-plugin:0.1.5")
37
37
 
38
38
  if (isExpoAutolinkingSettingsPluginAvailable) {
39
39
  implementation("expo.modules:expo-autolinking-plugin-shared")
@@ -3,7 +3,6 @@
3
3
  package expo.modules.plugin
4
4
 
5
5
  import com.android.build.api.dsl.LibraryExtension
6
- import com.android.build.api.variant.AndroidComponentsExtension
7
6
  import expo.modules.plugin.android.PublicationInfo
8
7
  import expo.modules.plugin.android.applyLinterOptions
9
8
  import expo.modules.plugin.android.applyPublishingVariant
@@ -132,18 +131,11 @@ internal fun Project.applyPublishing(expoModulesExtension: ExpoModuleExtension)
132
131
  }
133
132
  }
134
133
 
135
- private const val AGP_BUILT_IN_KOTLIN_MAJOR = 9
136
-
137
134
  /**
138
135
  * Whether AGP's built-in Kotlin support is active, meaning the `kotlin-android` plugin must not be
139
- * applied. True on AGP 9+ unless the project explicitly opts out with `android.builtInKotlin=false`.
136
+ * applied. AGP 9 enables it by default unless the project explicitly opts out with `android.builtInKotlin=false`.
140
137
  */
141
138
  internal fun Project.hasBuiltInKotlinSupport(): Boolean {
142
- val androidComponents = extensions.findByType(AndroidComponentsExtension::class.java)
143
- ?: return false
144
- if (androidComponents.pluginVersion.major < AGP_BUILT_IN_KOTLIN_MAJOR) {
145
- return false
146
- }
147
139
  return findProperty("android.builtInKotlin")?.toString()?.toBoolean() ?: true
148
140
  }
149
141
 
@@ -19,6 +19,10 @@ data class Version(
19
19
  return major * 10000 + minor * 100 + patch
20
20
  }
21
21
 
22
+ override fun toString(): String {
23
+ return "${major}.${minor}.${patch}"
24
+ }
25
+
22
26
  companion object {
23
27
  fun fromString(value: String): Version {
24
28
  // strip pre-release
@@ -16,6 +16,9 @@ public typealias UILabel = NSLabel
16
16
  public typealias UIImage = NSImage
17
17
  public typealias UIImageView = NSImageView
18
18
  public typealias UIPasteboard = NSPasteboard
19
+ public typealias UIColor = NSColor
20
+ public typealias UIGestureRecognizer = NSGestureRecognizer
21
+ public typealias UITextContentType = NSTextContentType
19
22
 
20
23
  extension UIApplication {
21
24
  public typealias LaunchOptionsKey = String
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-core",
3
- "version": "58.0.1",
3
+ "version": "58.0.2",
4
4
  "description": "The core of Expo Modules architecture",
5
5
  "keywords": [
6
6
  "expo",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@expo/expo-modules-macros-plugin": "0.10.0",
68
- "expo-modules-jsi": "~58.0.1",
68
+ "expo-modules-jsi": "~58.0.2",
69
69
  "invariant": "^2.2.4"
70
70
  },
71
71
  "devDependencies": {
@@ -84,7 +84,7 @@
84
84
  "optional": true
85
85
  }
86
86
  },
87
- "gitHead": "40afe3afa42f1dcca3dcba7ffbb3641859bdf2f0",
87
+ "gitHead": "313e67f5c9dfb16f9deca74a271cd1d4f33a6f82",
88
88
  "scripts": {
89
89
  "build": "expo-build src",
90
90
  "precompile-ios": "et prebuild-package-for-publish",