expo-modules-jsi 58.0.6 → 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 (29) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/apple/Benchmarks/FreeFormBenchmarks.swift +61 -0
  3. package/apple/Benchmarks/RuntimeCacheBenchmarks.swift +71 -0
  4. package/apple/Benchmarks/UnownedDecodeBenchmarks.swift +121 -0
  5. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Any.swift +500 -0
  6. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Containers.swift +52 -10
  7. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Data.swift +5 -0
  8. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Date.swift +27 -5
  9. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptCodable+Primitives.swift +75 -0
  10. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptDecodable.swift +34 -5
  11. package/apple/Sources/ExpoModulesJSI/Coding/JavaScriptValueKinds.swift +73 -0
  12. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +4 -2
  13. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +18 -13
  14. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntimeCache.swift +74 -0
  15. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +26 -0
  16. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +30 -0
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptPromise.swift +14 -14
  18. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +17 -0
  19. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +64 -2
  20. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +4 -3
  21. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +27 -0
  22. package/apple/Tests/JavaScriptCodableAnyTests.swift +531 -0
  23. package/apple/Tests/JavaScriptDecodableKindsTests.swift +132 -0
  24. package/apple/Tests/JavaScriptRuntimeCacheTests.swift +117 -0
  25. package/apple/Tests/JavaScriptRuntimeTests.swift +59 -0
  26. package/apple/Tests/JavaScriptUnownedDecodeTests.swift +99 -0
  27. package/apple/Tests/JavaScriptUnownedElementsTests.swift +76 -0
  28. package/apple/scripts/build-xcframework.sh +15 -0
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 58.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [iOS] Add `decodableKinds` to `JavaScriptDecodable`: the kinds of JavaScript value (`JavaScriptValueKinds`) that `decode` can accept, so code that picks between several types can skip the ones that can't match. ([#50905](https://github.com/expo/expo/pull/50905) by [@tsapeta](https://github.com/tsapeta))
8
+ - [iOS] Fix crash reports symbolicated on the device showing no function names for `ExpoModulesJSI` frames. ([#50698](https://github.com/expo/expo/pull/50698) by [@tsapeta](https://github.com/tsapeta))
9
+ - Return strings, objects and arrays from host functions and host object getters without cloning the engine handle, and build short ASCII strings from JS inline. ([#50937](https://github.com/expo/expo/pull/50937) by [@tsapeta](https://github.com/tsapeta))
10
+ - [iOS] Add `JavaScriptRuntime.cached(_:_:)` with typed `JavaScriptRuntime.Cache.Key`s, to create a value once per runtime and reuse it, for example a JavaScript constructor or a property name. A lookup reads one array slot, about 5× faster than the string-keyed `JavaScriptPropNameID.cached(_:_:)`. ([#50888](https://github.com/expo/expo/pull/50888) by [@tsapeta](https://github.com/tsapeta))
11
+ - [iOS] Add `JavaScriptValue.withUnownedValue(in:_:)`, and give the owning `JavaScriptDecodable.decode` a default that borrows the value and decodes it through the `JavaScriptUnownedValue` overload, so a conformer can implement only that one. Arrays, dictionaries, dates, records and enums now decode unowned values without copying them first. ([#50960](https://github.com/expo/expo/pull/50960) by [@tsapeta](https://github.com/tsapeta))
12
+ - [iOS] Add `JavaScriptArray.mapUnowned(_:)`, `JavaScriptObject.withUnownedProperty(_:_:)`, and `isArray()` and `getArray(in:)` on `JavaScriptUnownedValue`. Arrays, dictionaries and dates now decode through their unowned overload without copying the value or wrapping each element in a `JavaScriptValue`, and their owning decodes forward to it. ([#50980](https://github.com/expo/expo/pull/50980) by [@tsapeta](https://github.com/tsapeta))
13
+ - [iOS] Keep checkout paths out of the Swift compilation cache key so modules importing ExpoModulesCore can reuse cached compilation results across checkouts and worktrees. ([#50354](https://github.com/expo/expo/pull/50354) by [@janicduplessis](https://github.com/janicduplessis))
14
+
15
+ ## 58.0.7
16
+
17
+ ### Patch Changes
18
+
19
+ - Bump precompiled artifacts due to ABI breakage in expo-modules-jsi@58.0.6. ([#50881](https://github.com/expo/expo/pull/50881) by [@kitten](https://github.com/kitten))
20
+ - [iOS] Add conversions between JavaScript values and free-form `Any`, `[Any]` and `[String: Any]` values. ([#50660](https://github.com/expo/expo/pull/50660) by [@tsapeta](https://github.com/tsapeta))
21
+
3
22
  ## 58.0.6
4
23
 
5
24
  ### Patch Changes
@@ -0,0 +1,61 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Testing
5
+
6
+ /// Benchmarks for the free-form (`Any`) conversions. Each pair compares the dedicated fast path for
7
+ /// `[String: Any]` against the generic path that converts through `Any` and casts, on the same
8
+ /// attributes-like object of string, number and boolean values.
9
+ extension JSIBenchmarks {
10
+ private static let attributesSource = """
11
+ ({ screen: 'home', build: 'release', region: 'eu', retries: 3, latency: 12.5, cached: true, visible: false, count: 42 })
12
+ """
13
+
14
+ @Test
15
+ func `decode free-form dictionary`() async throws {
16
+ try await benchmarkCase { runtime in
17
+ let value = try runtime.eval(Self.attributesSource)
18
+ try benchmark("JavaScriptValue.decodeAnyDictionary(_:in:)", runtime: runtime) { iterations in
19
+ for _ in 0..<iterations {
20
+ _ = try JavaScriptValue.decodeAnyDictionary(value, in: runtime)
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ @Test
27
+ func `decode free-form dictionary through the generic path`() async throws {
28
+ try await benchmarkCase { runtime in
29
+ let value = try runtime.eval(Self.attributesSource)
30
+ try benchmark("JavaScriptValue.decodeAny(_:as: [String: Any].self, in:)", runtime: runtime) { iterations in
31
+ for _ in 0..<iterations {
32
+ _ = try JavaScriptValue.decodeAny(value, as: [String: Any].self, in: runtime)
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ @Test
39
+ func `encode free-form dictionary`() async throws {
40
+ try await benchmarkCase { runtime in
41
+ let attributes = try JavaScriptValue.decodeAnyDictionary(runtime.eval(Self.attributesSource), in: runtime)
42
+ try benchmark("JavaScriptValue.encodeAnyDictionary(_:in:)", runtime: runtime) { iterations in
43
+ for _ in 0..<iterations {
44
+ _ = try JavaScriptValue.encodeAnyDictionary(attributes, in: runtime)
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ @Test
51
+ func `encode free-form dictionary through the generic path`() async throws {
52
+ try await benchmarkCase { runtime in
53
+ let attributes: Any = try JavaScriptValue.decodeAnyDictionary(runtime.eval(Self.attributesSource), in: runtime)
54
+ try benchmark("JavaScriptValue.encodeAny(_:in:): [String: Any]", runtime: runtime) { iterations in
55
+ for _ in 0..<iterations {
56
+ _ = try JavaScriptValue.encodeAny(attributes, in: runtime)
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,71 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Testing
5
+
6
+ private let answerPropNameKey = JavaScriptRuntime.Cache.Key<JavaScriptPropNameID>()
7
+
8
+ /// Benchmarks for values cached per runtime with ``JavaScriptRuntime/cached(_:_:)``, read against
9
+ /// the string-keyed ``JavaScriptPropNameID/cached(_:_:)`` registry.
10
+ extension JSIBenchmarks {
11
+ @Test
12
+ func `runtime cache hit`() async throws {
13
+ try await benchmarkCase { runtime in
14
+ _ = runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") }
15
+ try benchmark("JavaScriptRuntime.cached(_:_:): hit", runtime: runtime) { iterations in
16
+ for _ in 0..<iterations {
17
+ _ = runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") }
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ @Test
24
+ func `object property by runtime-cached PropNameID`() async throws {
25
+ try await benchmarkCase { runtime in
26
+ let object = try runtime.eval("({ answer: 42 })").getObject()
27
+ try benchmark("JavaScriptObject.getProperty(_:): runtime-cached PropNameID", runtime: runtime) { iterations in
28
+ for _ in 0..<iterations {
29
+ _ = object.getProperty(runtime.cached(answerPropNameKey) { JavaScriptPropNameID(runtime, string: "answer") })
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ @Test
36
+ func `string-keyed PropNameID cache hit`() async throws {
37
+ try await benchmarkCase { runtime in
38
+ try benchmark("JavaScriptPropNameID.cached(_:_:): hit", runtime: runtime) { iterations in
39
+ for _ in 0..<iterations {
40
+ _ = JavaScriptPropNameID.cached(runtime, "answer")
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ @Test
47
+ func `object property by string-keyed cached PropNameID`() async throws {
48
+ try await benchmarkCase { runtime in
49
+ let object = try runtime.eval("({ answer: 42 })").getObject()
50
+ try benchmark("JavaScriptObject.getProperty(_:): string-cached PropNameID", runtime: runtime) { iterations in
51
+ for _ in 0..<iterations {
52
+ _ = object.getProperty(.cached(runtime, "answer"))
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ @Test
59
+ func `object property by prebuilt PropNameID`() async throws {
60
+ try await benchmarkCase { runtime in
61
+ let object = try runtime.eval("({ answer: 42 })").getObject()
62
+ let propName = JavaScriptPropNameID(runtime, string: "answer")
63
+ try benchmark("JavaScriptObject.getProperty(_:): prebuilt PropNameID", runtime: runtime) { iterations in
64
+ for _ in 0..<iterations {
65
+ _ = object.getProperty(propName)
66
+ }
67
+ }
68
+ withExtendedLifetime(propName) {}
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,121 @@
1
+ // Copyright 2026-present 650 Industries. All rights reserved.
2
+
3
+ import ExpoModulesJSI
4
+ import Foundation
5
+ import Testing
6
+
7
+ /// Decoding from a borrowed `JavaScriptUnownedValue`, the path a `@JS` argument takes, and from an owning
8
+ /// `JavaScriptValue`, the path a nested value takes. A type without its own unowned decode copies the
9
+ /// value into an owning `JavaScriptValue` first.
10
+ extension JSIBenchmarks {
11
+ @Test
12
+ func `unowned decode of an array`() async throws {
13
+ try await benchmarkCase { runtime in
14
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("[1, 2, 3, 4, 5, 6, 7, 8]"))
15
+ try benchmark("[Double].decode(unowned): 8 elements", runtime: runtime) { iterations in
16
+ for _ in 0..<iterations {
17
+ _ = try [Double].decode(buffer.unownedValue(at: 0), in: runtime)
18
+ }
19
+ }
20
+ }
21
+ }
22
+
23
+ @Test
24
+ func `unowned decode of a dictionary`() async throws {
25
+ try await benchmarkCase { runtime in
26
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("({ a: 1, b: 2, c: 3, d: 4 })"))
27
+ try benchmark("[String: Double].decode(unowned): 4 entries", runtime: runtime) { iterations in
28
+ for _ in 0..<iterations {
29
+ _ = try [String: Double].decode(buffer.unownedValue(at: 0), in: runtime)
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ @Test
36
+ func `unowned decode of a date`() async throws {
37
+ try await benchmarkCase { runtime in
38
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("1700000000000"))
39
+ try benchmark("Date.decode(unowned): number", runtime: runtime) { iterations in
40
+ for _ in 0..<iterations {
41
+ _ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ @Test
48
+ func `owning decode of an array`() async throws {
49
+ try await benchmarkCase { runtime in
50
+ let value = try runtime.eval("[1, 2, 3, 4, 5, 6, 7, 8]")
51
+ try benchmark("[Double].decode(owning): 8 elements", runtime: runtime) { iterations in
52
+ for _ in 0..<iterations {
53
+ _ = try [Double].decode(value, in: runtime)
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ @Test
60
+ func `owning decode of a dictionary`() async throws {
61
+ try await benchmarkCase { runtime in
62
+ let value = try runtime.eval("({ a: 1, b: 2, c: 3, d: 4 })")
63
+ try benchmark("[String: Double].decode(owning): 4 entries", runtime: runtime) { iterations in
64
+ for _ in 0..<iterations {
65
+ _ = try [String: Double].decode(value, in: runtime)
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ @Test
72
+ func `owning decode of nested arrays`() async throws {
73
+ try await benchmarkCase { runtime in
74
+ let value = try runtime.eval("[[1, 2], [3, 4], [5, 6], [7, 8]]")
75
+ try benchmark("[[Double]].decode(owning): 4 x 2 elements", runtime: runtime) { iterations in
76
+ for _ in 0..<iterations {
77
+ _ = try [[Double]].decode(value, in: runtime)
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ @Test
84
+ func `unowned decode of nested arrays`() async throws {
85
+ try await benchmarkCase { runtime in
86
+ let buffer = JavaScriptValuesBuffer.allocate(
87
+ in: runtime,
88
+ with: try runtime.eval("[[1, 2], [3, 4], [5, 6], [7, 8]]")
89
+ )
90
+ try benchmark("[[Double]].decode(unowned): 4 x 2 elements", runtime: runtime) { iterations in
91
+ for _ in 0..<iterations {
92
+ _ = try [[Double]].decode(buffer.unownedValue(at: 0), in: runtime)
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ @Test
99
+ func `unowned decode of a date string`() async throws {
100
+ try await benchmarkCase { runtime in
101
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("'2026-10-02T12:00:00Z'"))
102
+ try benchmark("Date.decode(unowned): ISO string", runtime: runtime) { iterations in
103
+ for _ in 0..<iterations {
104
+ _ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ @Test
111
+ func `unowned decode of a date object`() async throws {
112
+ try await benchmarkCase { runtime in
113
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: try runtime.eval("new Date(1700000000000)"))
114
+ try benchmark("Date.decode(unowned): Date object", runtime: runtime) { iterations in
115
+ for _ in 0..<iterations {
116
+ _ = try Date.decode(buffer.unownedValue(at: 0), in: runtime)
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }