expo-modules-jsi 58.0.7 → 58.0.8

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
  3. package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
  4. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
  5. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
  6. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
  7. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +75 -0
  8. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
  9. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
  10. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
  11. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +18 -13
  12. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
  13. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
  14. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +22 -0
  15. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
  16. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +54 -1
  18. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
  19. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +27 -0
  20. package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
  21. package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
  22. package/apple/Tests/JavaScriptRuntimeTests.swift +59 -0
  23. package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
  24. package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
  25. package/apple/scripts/build-xcframework.sh +15 -0
  26. package/package.json +1 -1
@@ -0,0 +1,117 @@
1
+ import ExpoModulesJSI
2
+ import Testing
3
+
4
+ private final class CachedBox {
5
+ let value: Int
6
+
7
+ init(_ value: Int) {
8
+ self.value = value
9
+ }
10
+ }
11
+
12
+ @Suite
13
+ @JavaScriptActor
14
+ struct JavaScriptRuntimeCacheTests {
15
+ let runtime = JavaScriptRuntime()
16
+
17
+ @Test
18
+ func `creates the value on first use and reuses it afterwards`() {
19
+ let key = JavaScriptRuntime.Cache.Key<CachedBox>()
20
+ var makeCount = 0
21
+
22
+ let first = runtime.cached(key) {
23
+ makeCount += 1
24
+ return CachedBox(1)
25
+ }
26
+ let second = runtime.cached(key) {
27
+ makeCount += 1
28
+ return CachedBox(2)
29
+ }
30
+
31
+ #expect(makeCount == 1)
32
+ #expect(first === second)
33
+ #expect(second.value == 1)
34
+ }
35
+
36
+ @Test
37
+ func `keys are independent`() {
38
+ let firstKey = JavaScriptRuntime.Cache.Key<CachedBox>()
39
+ let secondKey = JavaScriptRuntime.Cache.Key<CachedBox>()
40
+
41
+ #expect(runtime.cached(firstKey) { CachedBox(1) }.value == 1)
42
+ #expect(runtime.cached(secondKey) { CachedBox(2) }.value == 2)
43
+ #expect(runtime.cached(firstKey) { CachedBox(3) }.value == 1)
44
+ }
45
+
46
+ @Test
47
+ func `keys can be used in any order`() {
48
+ let keys = (0..<16).map { _ in JavaScriptRuntime.Cache.Key<CachedBox>() }
49
+
50
+ // Using the last key first grows the storage past all the others, which must then stay empty.
51
+ #expect(runtime.cached(keys[15]) { CachedBox(15) }.value == 15)
52
+ #expect(runtime.cached(keys[3]) { CachedBox(3) }.value == 3)
53
+ #expect(runtime.cached(keys[15]) { CachedBox(-1) }.value == 15)
54
+ #expect(runtime.cached(keys[0]) { CachedBox(0) }.value == 0)
55
+ }
56
+
57
+ @Test
58
+ func `each runtime has its own entries`() {
59
+ let key = JavaScriptRuntime.Cache.Key<CachedBox>()
60
+ let otherRuntime = JavaScriptRuntime()
61
+
62
+ #expect(runtime.cached(key) { CachedBox(1) }.value == 1)
63
+ #expect(otherRuntime.cached(key) { CachedBox(2) }.value == 2)
64
+ }
65
+
66
+ @Test
67
+ func `a value can be created from other cached values`() {
68
+ let baseKey = JavaScriptRuntime.Cache.Key<CachedBox>()
69
+ let derivedKey = JavaScriptRuntime.Cache.Key<CachedBox>()
70
+
71
+ // The nested lookup runs while the outer one is still creating its value.
72
+ let derived = runtime.cached(derivedKey) {
73
+ let base = runtime.cached(baseKey) { CachedBox(20) }
74
+ return CachedBox(base.value + 1)
75
+ }
76
+
77
+ #expect(derived.value == 21)
78
+ #expect(runtime.cached(baseKey) { CachedBox(0) }.value == 20)
79
+ }
80
+
81
+ @Test
82
+ func `a throwing make caches nothing`() throws {
83
+ struct MakeError: Error {}
84
+ let key = JavaScriptRuntime.Cache.Key<CachedBox>()
85
+
86
+ #expect(throws: MakeError.self) {
87
+ try runtime.cached(key) { throw MakeError() }
88
+ }
89
+ #expect(runtime.cached(key) { CachedBox(1) }.value == 1)
90
+ }
91
+
92
+ @Test
93
+ func `caches JavaScript values`() throws {
94
+ let key = JavaScriptRuntime.Cache.Key<JavaScriptValue>()
95
+
96
+ let first = try runtime.cached(key) { try runtime.eval("({ answer: 42 })") }
97
+ let second = try runtime.cached(key) { try runtime.eval("({ answer: 0 })") }
98
+
99
+ #expect(first === second)
100
+ #expect(second.getObject().getProperty("answer").getInt() == 42)
101
+ }
102
+
103
+ @Test
104
+ func `cached values are released with the runtime`() {
105
+ let key = JavaScriptRuntime.Cache.Key<CachedBox>()
106
+ weak var cachedValue: CachedBox?
107
+
108
+ do {
109
+ let runtime = JavaScriptRuntime()
110
+ cachedValue = runtime.cached(key) { CachedBox(1) }
111
+ #expect(cachedValue != nil)
112
+ withExtendedLifetime(runtime) {}
113
+ }
114
+
115
+ #expect(cachedValue == nil)
116
+ }
117
+ }
@@ -34,6 +34,40 @@ struct JavaScriptRuntimeTests {
34
34
  #expect(fn.asValue().isFunction() == true)
35
35
  }
36
36
 
37
+ @Test
38
+ func `host function returns a fresh string`() throws {
39
+ let fn = runtime.createFunction("greet") { [runtime] _, _ in
40
+ return JavaScriptValue(runtime, "hello")
41
+ }
42
+ runtime.global().setProperty("greet", value: fn)
43
+ #expect(try runtime.eval("greet() + ' ' + greet()").getString() == "hello hello")
44
+ }
45
+
46
+ @Test
47
+ func `host function returns a shared value repeatedly`() throws {
48
+ // The same instance is returned on every call, so the host call must copy the engine handle
49
+ // instead of moving it out: the captured value has to stay valid for the next call.
50
+ let shared = JavaScriptValue(runtime, "shared")
51
+ let fn = runtime.createFunction("shared") { _, _ in
52
+ return shared
53
+ }
54
+ runtime.global().setProperty("shared", value: fn)
55
+ #expect(try runtime.eval("shared() + ' ' + shared()").getString() == "shared shared")
56
+ #expect(shared.getString() == "shared")
57
+ }
58
+
59
+ @Test
60
+ func `host function returns an object it keeps`() throws {
61
+ let kept = runtime.createObject()
62
+ kept.setProperty("answer", value: 42)
63
+ let fn = runtime.createFunction("kept") { _, _ in
64
+ return kept.asValue()
65
+ }
66
+ runtime.global().setProperty("kept", value: fn)
67
+ #expect(try runtime.eval("kept().answer + kept().answer").getInt() == 84)
68
+ #expect(kept.getProperty("answer").getInt() == 42)
69
+ }
70
+
37
71
  @Test
38
72
  func `evaluate script`() throws {
39
73
  #expect(try runtime.eval("'hello' + ' ' + 'world'").getString() == "hello world")
@@ -217,6 +251,31 @@ struct JavaScriptRuntimeTests {
217
251
  #expect(hostObject.getProperty("unknown").isUndefined())
218
252
  }
219
253
 
254
+ @Test
255
+ func `host object getter returns strings`() throws {
256
+ // `fresh` is created per access and gets moved into the result; `shared` is the same instance on
257
+ // every access and has to be copied so it stays valid.
258
+ let shared = JavaScriptValue(runtime, "shared")
259
+ let hostObject = runtime.createHostObject(
260
+ get: { [runtime] name in
261
+ switch name {
262
+ case "fresh":
263
+ return JavaScriptValue(runtime, "fresh")
264
+ case "shared":
265
+ return shared
266
+ default:
267
+ return .undefined
268
+ }
269
+ },
270
+ getPropertyNames: { ["fresh", "shared"] }
271
+ )
272
+ runtime.global().setProperty("host", value: hostObject)
273
+
274
+ #expect(try runtime.eval("host.fresh + host.fresh").getString() == "freshfresh")
275
+ #expect(try runtime.eval("host.shared + host.shared").getString() == "sharedshared")
276
+ #expect(shared.getString() == "shared")
277
+ }
278
+
220
279
  @Test
221
280
  func `host object set property`() {
222
281
  var storedValue: Int?
@@ -0,0 +1,99 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Foundation
5
+ import Testing
6
+
7
+ /// Decodes through the `JavaScriptUnownedValue` overloads, and through `withUnownedValue(in:_:)` from an
8
+ /// owning value.
9
+ @Suite("JavaScriptDecodable unowned decode")
10
+ @JavaScriptActor
11
+ struct JavaScriptUnownedDecodeTests {
12
+ let runtime = JavaScriptRuntime()
13
+
14
+ private func unowned<T: JavaScriptDecodable>(_ type: T.Type, from source: String) throws -> T {
15
+ let value = try runtime.eval(source)
16
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: value)
17
+ return try T.decode(buffer.unownedValue(at: 0), in: runtime)
18
+ }
19
+
20
+ // MARK: - withUnownedValue
21
+
22
+ @Test
23
+ func `withUnownedValue borrows the value it is called on`() throws {
24
+ let number = try runtime.eval("42")
25
+ #expect(number.withUnownedValue(in: runtime) { $0.getDouble() } == 42)
26
+
27
+ let string = try runtime.eval("'text'")
28
+ #expect(string.withUnownedValue(in: runtime) { $0.getString() } == "text")
29
+
30
+ let object = try runtime.eval("({ answer: 42 })")
31
+ let answer = try object.withUnownedValue(in: runtime) { try $0.asObject(in: runtime).getProperty("answer").asInt() }
32
+ #expect(answer == 42)
33
+ }
34
+
35
+ @Test
36
+ func `withUnownedValue works for a value without a runtime`() {
37
+ let value = JavaScriptValue.number(1.5)
38
+ #expect(value.withUnownedValue(in: runtime) { $0.getDouble() } == 1.5)
39
+ }
40
+
41
+ // MARK: - Default owning decode
42
+
43
+ @Test
44
+ func `a type that only decodes unowned values also decodes owning ones`() throws {
45
+ let value = try runtime.eval("7")
46
+ #expect(try UnownedOnly.decode(value, in: runtime) == UnownedOnly(number: 7))
47
+ }
48
+
49
+ // MARK: - Containers
50
+
51
+ @Test
52
+ func `Array decodes an array and arrayizes a scalar`() throws {
53
+ #expect(try unowned([Int].self, from: "[1, 2, 3]") == [1, 2, 3])
54
+ #expect(try unowned([Int].self, from: "4") == [4])
55
+ }
56
+
57
+ @Test
58
+ func `Dictionary decodes an object and skips undefined properties`() throws {
59
+ #expect(try unowned([String: Int].self, from: "({ a: 1, b: undefined })") == ["a": 1])
60
+ #expect(throws: (any Error).self) {
61
+ _ = try unowned([String: Int].self, from: "42")
62
+ }
63
+ }
64
+
65
+ @Test
66
+ func `Date decodes a number, a string and a Date`() throws {
67
+ #expect(try unowned(Date.self, from: "1000") == Date(timeIntervalSince1970: 1))
68
+ #expect(try unowned(Date.self, from: "'1970-01-01T00:00:02Z'") == Date(timeIntervalSince1970: 2))
69
+ #expect(try unowned(Date.self, from: "new Date(3000)") == Date(timeIntervalSince1970: 3))
70
+ #expect(throws: (any Error).self) {
71
+ _ = try unowned(Date.self, from: "true")
72
+ }
73
+ #expect(throws: (any Error).self) {
74
+ _ = try unowned(Date.self, from: "({ getTime() { return 0 } })")
75
+ }
76
+ #expect(throws: (any Error).self) {
77
+ _ = try unowned(Date.self, from: "'not a date'")
78
+ }
79
+ }
80
+
81
+ @Test
82
+ func `Date decodes the same from an owning value`() throws {
83
+ #expect(try Date.decode(runtime.eval("1000"), in: runtime) == Date(timeIntervalSince1970: 1))
84
+ #expect(try Date.decode(runtime.eval("'1970-01-01T00:00:02Z'"), in: runtime) == Date(timeIntervalSince1970: 2))
85
+ #expect(try Date.decode(runtime.eval("new Date(3000)"), in: runtime) == Date(timeIntervalSince1970: 3))
86
+ }
87
+ }
88
+
89
+ /// Implements only the unowned decode, so its owning decode is the protocol default.
90
+ private struct UnownedOnly: JavaScriptDecodable, Equatable {
91
+ let number: Double
92
+
93
+ @JavaScriptActor
94
+ static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws
95
+ -> UnownedOnly
96
+ {
97
+ return UnownedOnly(number: try value.asDouble())
98
+ }
99
+ }
@@ -0,0 +1,76 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Testing
5
+
6
+ /// Lending array elements and object properties out as `JavaScriptUnownedValue`s.
7
+ @Suite("Unowned elements and properties")
8
+ @JavaScriptActor
9
+ struct JavaScriptUnownedElementsTests {
10
+ let runtime = JavaScriptRuntime()
11
+
12
+ @Test
13
+ func `mapUnowned transforms every element in order`() throws {
14
+ let array = try runtime.eval("[1, 'two', null, { four: 4 }]").getArray()
15
+ let kinds = array.mapUnowned { element -> String in
16
+ if element.isNumber() {
17
+ return "number \(element.getDouble())"
18
+ }
19
+ if element.isString() {
20
+ return "string \(element.getString())"
21
+ }
22
+ if element.isNull() {
23
+ return "null"
24
+ }
25
+ return "object"
26
+ }
27
+ #expect(kinds == ["number 1.0", "string two", "null", "object"])
28
+ }
29
+
30
+ @Test
31
+ func `mapUnowned of an empty array is empty`() throws {
32
+ let array = try runtime.eval("[]").getArray()
33
+ #expect(array.mapUnowned { $0.isNumber() }.isEmpty)
34
+ }
35
+
36
+ @Test
37
+ func `mapUnowned rethrows the transform's error`() throws {
38
+ let array = try runtime.eval("[1, 'two']").getArray()
39
+ #expect(throws: (any Error).self) {
40
+ _ = try array.mapUnowned { try $0.asDouble() }
41
+ }
42
+ }
43
+
44
+ @Test
45
+ func `withUnownedProperty lends the property and undefined for a missing one`() throws {
46
+ let object = try runtime.eval("({ answer: 42, name: 'expo' })").getObject()
47
+ #expect(object.withUnownedProperty("answer") { $0.getDouble() } == 42)
48
+ #expect(object.withUnownedProperty("name") { $0.getString() } == "expo")
49
+ #expect(object.withUnownedProperty("missing") { $0.isUndefined() })
50
+ }
51
+
52
+ @Test
53
+ func `containers decode nested values through the unowned path`() throws {
54
+ let value = try runtime.eval("[[1, 2], [3]]")
55
+ #expect(try [[Int]].decode(value, in: runtime) == [[1, 2], [3]])
56
+ let dictionary = try runtime.eval("({ a: [1], b: undefined, c: [2, 3] })")
57
+ #expect(try [String: [Int]].decode(dictionary, in: runtime) == ["a": [1], "c": [2, 3]])
58
+ }
59
+
60
+ @Test
61
+ func `an unowned value tells arrays apart and reads them`() throws {
62
+ let buffer = JavaScriptValuesBuffer.allocate(
63
+ in: runtime,
64
+ with: try runtime.eval("[1, 2, 3]"),
65
+ try runtime.eval("({ length: 3 })"),
66
+ try runtime.eval("new Uint8Array(3)"),
67
+ try runtime.eval("3")
68
+ )
69
+ // `#expect` can't capture the `~Copyable` value, so compare the results instead.
70
+ let isArray = (0..<4).map { buffer.unownedValue(at: $0).isArray() }
71
+ #expect(isArray == [true, false, false, false])
72
+ let array = buffer.unownedValue(at: 0).getArray(in: runtime)
73
+ #expect(array.length == 3)
74
+ #expect(array.mapUnowned { $0.getDouble() } == [1, 2, 3])
75
+ }
76
+ }
@@ -208,6 +208,18 @@ build_slice() {
208
208
  # `___profc_*` symbols and `__llvm_prf_*` sections) plus ~40% extra binary size. Setting
209
209
  # CLANG_COVERAGE_MAPPING=NO is what removes the flags; CLANG_ENABLE_CODE_COVERAGE=NO alone
210
210
  # does not, and `-enableCodeCoverage NO` is rejected outside of `test`.
211
+ #
212
+ # With a dSYM, Xcode's default STRIP_SWIFT_SYMBOLS=YES runs `strip -T`, which removes or renames to
213
+ # `<redacted>` every Swift symbol that isn't exported. Crash reports symbolicated on the device then
214
+ # show no function name for any frame in this framework. Keeping the symbols adds about 250 KB of
215
+ # symbol table to the device slice; the code itself doesn't change.
216
+ #
217
+ # The binary .swiftmodule must not embed this checkout's absolute paths: consumers key
218
+ # the Xcode compilation cache on its content, so any PACKAGE_DIR or PODS_ROOT path inside
219
+ # it makes every module that imports ExpoModulesJSI a cache miss in another checkout or
220
+ # worktree. SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO drops the serialized search paths and
221
+ # SWIFT_ENABLE_EXPLICIT_MODULES=NO the explicit-module cache directories under
222
+ # .DerivedData. The .swiftinterface and the dSYM are unaffected.
211
223
  (cd "$PACKAGE_DIR" && env -i PATH="$PATH" HOME="$HOME" PODS_ROOT="$PODS_ROOT" RN_ROOT="$RN_ROOT" \
212
224
  xcodebuild \
213
225
  build \
@@ -226,10 +238,13 @@ build_slice() {
226
238
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
227
239
  SKIP_INSTALL=NO \
228
240
  DEBUG_INFORMATION_FORMAT=dwarf-with-dsym \
241
+ STRIP_SWIFT_SYMBOLS=NO \
229
242
  COMPILER_INDEX_STORE_ENABLE=NO \
230
243
  SWIFT_COMPILATION_MODE=wholemodule \
231
244
  CLANG_ENABLE_CODE_COVERAGE=NO \
232
245
  CLANG_COVERAGE_MAPPING=NO \
246
+ SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO \
247
+ SWIFT_ENABLE_EXPLICIT_MODULES=NO \
233
248
  )
234
249
 
235
250
  local product_path="${BUILD_PRODUCTS_PATH}/${build_dir_name}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-jsi",
3
- "version": "58.0.7",
3
+ "version": "58.0.8",
4
4
  "description": "The JavaScript Interface for Expo Modules",
5
5
  "main": "index.js",
6
6
  "sideEffects": [],