expo-modules-jsi 57.0.7 → 57.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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/CLAUDE.md +35 -0
  3. package/apple/Benchmarks/BenchmarkRunner.swift +115 -0
  4. package/apple/Benchmarks/FunctionCallBenchmarks.swift +45 -0
  5. package/apple/Benchmarks/HostFunctionBenchmarks.swift +217 -0
  6. package/apple/Benchmarks/StringConversionBenchmarks.swift +245 -0
  7. package/apple/Benchmarks/ValueAccessBenchmarks.swift +132 -0
  8. package/apple/Package.swift +10 -0
  9. package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +28 -0
  10. package/apple/Sources/ExpoModulesJSI/Contexts/HostFunctionContext.swift +18 -6
  11. package/apple/Sources/ExpoModulesJSI/Contexts/HostObjectContext.swift +7 -3
  12. package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +20 -13
  13. package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +15 -3
  14. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptPropNameID.swift +11 -7
  15. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +80 -58
  16. package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +7 -2
  17. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +2 -2
  18. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptBigInt.swift +1 -2
  19. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +18 -8
  20. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +2 -2
  21. package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +18 -7
  22. package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +130 -0
  23. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +6 -3
  24. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -1
  25. package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +5 -3
  26. package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +14 -3
  27. package/apple/Tests/JavaScriptArrayBufferTests.swift +2 -2
  28. package/apple/Tests/JavaScriptNativeStateTests.swift +10 -10
  29. package/apple/Tests/JavaScriptObjectTests.swift +79 -0
  30. package/apple/Tests/JavaScriptPropNameIDTests.swift +123 -0
  31. package/apple/Tests/JavaScriptRuntimeTests.swift +29 -0
  32. package/apple/Tests/JavaScriptTypedArrayTests.swift +1 -1
  33. package/apple/Tests/JavaScriptUnownedValueTests.swift +9 -0
  34. package/apple/Tests/JavaScriptValueTests.swift +102 -0
  35. package/apple/Tests/JavaScriptValuesBufferTests.swift +2 -2
  36. package/apple/Tests/JavaScriptWeakObjectTests.swift +2 -2
  37. package/apple/scripts/build-xcframework.sh +10 -0
  38. package/apple/scripts/generate-modulemap.sh +10 -1
  39. package/package.json +3 -2
@@ -182,8 +182,8 @@ struct JavaScriptArrayBufferTests {
182
182
  // Buffer goes out of scope here
183
183
  }
184
184
 
185
- // Hermes gc() is synchronous, so the buffer should be collected immediately.
186
- try runtime.eval("gc()")
185
+ // Hermes collects synchronously, so the buffer should be collected immediately.
186
+ runtime.collectGarbage()
187
187
 
188
188
  cleanupCalled = flag.pointee
189
189
  flag.deallocate()
@@ -78,7 +78,7 @@ struct JavaScriptNativeStateTests {
78
78
  object1.unsetNativeState()
79
79
 
80
80
  // Force garbage collection so the previous C++ pointee is dropped.
81
- try runtime.eval("gc() && gc() && gc()")
81
+ runtime.collectGarbage()
82
82
 
83
83
  // Reattaching transparently materializes a fresh pointee.
84
84
  object2.setNativeState(nativeState)
@@ -106,7 +106,7 @@ struct JavaScriptNativeStateTests {
106
106
  object.unsetNativeState()
107
107
 
108
108
  // Force garbage collection
109
- try runtime.eval("gc() && gc() && gc()")
109
+ runtime.collectGarbage()
110
110
 
111
111
  #expect(deallocatorCalled == true)
112
112
  }
@@ -125,12 +125,12 @@ struct JavaScriptNativeStateTests {
125
125
 
126
126
  // Unset from the first object — deallocator should not fire yet.
127
127
  object1.unsetNativeState()
128
- try runtime.eval("gc() && gc() && gc()")
128
+ runtime.collectGarbage()
129
129
  #expect(deallocatorCallCount == 0)
130
130
 
131
131
  // Unset from the second object — now the deallocator should fire exactly once.
132
132
  object2.unsetNativeState()
133
- try runtime.eval("gc() && gc() && gc()")
133
+ runtime.collectGarbage()
134
134
  #expect(deallocatorCallCount == 1)
135
135
  }
136
136
 
@@ -141,7 +141,7 @@ struct JavaScriptNativeStateTests {
141
141
  object.setNativeState(nativeState)
142
142
 
143
143
  // GC shouldn't release the underlying C++ pointee while the JS object is reachable.
144
- try runtime.eval("gc() && gc() && gc()")
144
+ runtime.collectGarbage()
145
145
 
146
146
  #expect(object.getNativeState() === nativeState)
147
147
  }
@@ -183,7 +183,7 @@ struct JavaScriptNativeStateTests {
183
183
 
184
184
  object1.setNativeState(nativeState)
185
185
  object1.unsetNativeState()
186
- try runtime.eval("gc() && gc() && gc()")
186
+ runtime.collectGarbage()
187
187
  #expect(deallocatorCallCount == 1)
188
188
 
189
189
  // Re-attaching builds a new C++ pointee; the wrapper-level deallocator
@@ -192,7 +192,7 @@ struct JavaScriptNativeStateTests {
192
192
  #expect(object2.getNativeState() === nativeState)
193
193
 
194
194
  object2.unsetNativeState()
195
- try runtime.eval("gc() && gc() && gc()")
195
+ runtime.collectGarbage()
196
196
  #expect(deallocatorCallCount == 2)
197
197
  }
198
198
 
@@ -209,7 +209,7 @@ struct JavaScriptNativeStateTests {
209
209
  // baked into the C++ pointee is the only Swift-side strong reference.
210
210
  }
211
211
 
212
- try runtime.eval("gc() && gc() && gc()")
212
+ runtime.collectGarbage()
213
213
 
214
214
  // The wrapper is still alive because the C++ pointee (held by JSI's slot)
215
215
  // retains it via Unmanaged. `getNativeState` recovers it.
@@ -219,7 +219,7 @@ struct JavaScriptNativeStateTests {
219
219
  // Once detached and GC'd, the C++ pointee dies, releases the Unmanaged,
220
220
  // and the Swift wrapper finally deallocates.
221
221
  object.unsetNativeState()
222
- try runtime.eval("gc() && gc() && gc()")
222
+ runtime.collectGarbage()
223
223
  #expect(weakWrapper == nil)
224
224
  }
225
225
 
@@ -232,7 +232,7 @@ struct JavaScriptNativeStateTests {
232
232
  object2.setNativeState(nativeState)
233
233
 
234
234
  object1.unsetNativeState()
235
- try runtime.eval("gc() && gc() && gc()")
235
+ runtime.collectGarbage()
236
236
 
237
237
  #expect(object1.hasNativeState() == false)
238
238
  #expect(object2.hasNativeState() == true)
@@ -32,6 +32,71 @@ struct JavaScriptObjectTests {
32
32
  #expect(object.getProperty("score").getInt() == 100)
33
33
  }
34
34
 
35
+ @Test
36
+ func `get property names preserves non-ASCII and empty keys`() throws {
37
+ let object = try runtime.eval("({ 'café': 1, '日本語': 2, '🎉': 3, '': 4 })").getObject()
38
+ #expect(object.getPropertyNames() == ["café", "日本語", "🎉", ""])
39
+ }
40
+
41
+ // MARK: - Non-ASCII property names
42
+
43
+ @Test
44
+ func `getProperty finds non-ASCII and empty names`() throws {
45
+ let object = try runtime.eval("({ 'café': 42, '日本語': 'ok', '🎉': true, '': 'empty' })").getObject()
46
+ #expect(object.getProperty("café").isNumber() == true)
47
+ #expect(object.getProperty("日本語").isString() == true)
48
+ #expect(object.getProperty("🎉").isBool() == true)
49
+ #expect(object.getProperty("").isString() == true)
50
+ #expect(object.getProperty("cafe").isUndefined() == true)
51
+ }
52
+
53
+ @Test
54
+ func `hasProperty finds non-ASCII and empty names`() throws {
55
+ let object = try runtime.eval("({ 'café': 42, '': 'empty' })").getObject()
56
+ #expect(object.hasProperty("café") == true)
57
+ #expect(object.hasProperty("") == true)
58
+ #expect(object.hasProperty("cafe") == false)
59
+ #expect(object.hasProperty("naïve") == false)
60
+ }
61
+
62
+ @Test
63
+ func `setProperty with a non-ASCII name is visible to JavaScript`() throws {
64
+ let object = JavaScriptObject(runtime)
65
+ object.setProperty("café", value: 42.0)
66
+ object.setProperty("日本語", value: JavaScriptValue(runtime, "ok"))
67
+ object.setProperty("🎉", JavaScriptObject(runtime))
68
+ object.setProperty("", value: true)
69
+ runtime.global().setProperty("obj", object)
70
+ #expect(try runtime.eval("obj['café'] === 42 && obj['日本語'] === 'ok' && obj[''] === true").getBool() == true)
71
+ #expect(try runtime.eval("typeof obj['🎉']").getString() == "object")
72
+ #expect(try runtime.eval("Object.keys(obj).join(',')").getString() == "café,日本語,🎉,")
73
+ }
74
+
75
+ @Test
76
+ func `deleteProperty removes a non-ASCII name`() throws {
77
+ let object = try runtime.eval("({ 'café': 42, 'cafe': 1 })").getObject()
78
+ object.deleteProperty("café")
79
+ #expect(object.hasProperty("café") == false)
80
+ #expect(object.hasProperty("cafe") == true)
81
+ runtime.global().setProperty("obj", object)
82
+ #expect(try runtime.eval("'café' in obj").getBool() == false)
83
+ }
84
+
85
+ @Test
86
+ func `defineProperty with a non-ASCII name is visible to JavaScript`() throws {
87
+ let object = JavaScriptObject(runtime)
88
+ object.defineProperty("café", value: 42.0, options: [.enumerable])
89
+ runtime.global().setProperty("obj", object)
90
+ #expect(try runtime.eval("obj['café']").getInt() == 42)
91
+ }
92
+
93
+ @Test
94
+ func `getPropertyAsFunction finds a non-ASCII name`() throws {
95
+ let object = try runtime.eval("({ 'zażółć': () => 'gęślą jaźń' })").getObject()
96
+ let function = try object.getPropertyAsFunction("zażółć")
97
+ #expect(try function.call().getString() == "gęślą jaźń")
98
+ }
99
+
35
100
  // MARK: - Property Access Tests
36
101
 
37
102
  @Suite("Property Access")
@@ -199,6 +264,20 @@ struct JavaScriptObjectTests {
199
264
  #expect(object.getProperty("pi").getDouble() == 3.14159)
200
265
  }
201
266
 
267
+ @Test
268
+ func `set property with non-ASCII string`() {
269
+ let object = JavaScriptObject(runtime)
270
+ object.setProperty("greeting", value: "żółć 世界 🎉")
271
+ #expect(object.getProperty("greeting").getString() == "żółć 世界 🎉")
272
+ }
273
+
274
+ @Test
275
+ func `set property with empty string`() {
276
+ let object = JavaScriptObject(runtime)
277
+ object.setProperty("empty", value: "")
278
+ #expect(object.getProperty("empty").getString() == "")
279
+ }
280
+
202
281
  @Test
203
282
  func `set property with JavaScriptValue`() {
204
283
  let object = JavaScriptObject(runtime)
@@ -0,0 +1,123 @@
1
+ import ExpoModulesJSI
2
+ import Testing
3
+
4
+ @Suite
5
+ @JavaScriptActor
6
+ struct JavaScriptPropNameIDTests {
7
+ let runtime = JavaScriptRuntime()
8
+
9
+ // MARK: - UTF-8 round-trip
10
+
11
+ @Test
12
+ func `round-trips an ASCII string`() {
13
+ let propName = JavaScriptPropNameID(runtime, string: "length")
14
+ #expect(propName.utf8() == "length")
15
+ }
16
+
17
+ // Regression test for the `forUtf8(pointer, length)` byte-count bug: `String.count` is the
18
+ // grapheme-cluster count, not the UTF-8 byte count, so non-ASCII keys used to be truncated
19
+ // (e.g. `"café"` reported 4 for its 5 UTF-8 bytes) and the PropNameID held mangled bytes.
20
+ @Test(arguments: ["café", "naïve", "Ωμέγα", "日本語", "Grüße", "emoji 🎉", "over\u{2028}line"])
21
+ func `round-trips a non-ASCII string via utf8`(key: String) {
22
+ let propName = JavaScriptPropNameID(runtime, string: key)
23
+ #expect(propName.utf8() == key)
24
+ }
25
+
26
+ // Multi-scalar grapheme clusters (ZWJ sequences, combining marks) have a `count` of 1 but many
27
+ // UTF-8 bytes — the worst case for the old truncation.
28
+ @Test(arguments: ["👨‍👩‍👧", "e\u{0301}", "🇵🇱"])
29
+ func `round-trips a multi-scalar grapheme via utf8`(key: String) {
30
+ let propName = JavaScriptPropNameID(runtime, string: key)
31
+ #expect(propName.utf8() == key)
32
+ }
33
+
34
+ @Test(arguments: ["café", "日本語", "🎉"])
35
+ func `round-trips a non-ASCII string via utf16`(key: String) {
36
+ let propName = JavaScriptPropNameID(runtime, string: key)
37
+ #expect(propName.utf16() == key)
38
+ }
39
+
40
+ @Test
41
+ func `round-trips an empty string`() {
42
+ let propName = JavaScriptPropNameID(runtime, string: "")
43
+ #expect(propName.utf8() == "")
44
+ #expect(propName.utf16() == "")
45
+ }
46
+
47
+ @Test
48
+ func `round-trips a long non-ASCII string`() {
49
+ let key = String(repeating: "ą", count: 2000)
50
+ let propName = JavaScriptPropNameID(runtime, string: key)
51
+ #expect(propName.utf8() == key)
52
+ #expect(propName.utf16() == key)
53
+ }
54
+
55
+ // MARK: - Caching
56
+
57
+ @Test
58
+ func `cached returns the same instance for the same string`() {
59
+ let first = JavaScriptPropNameID.cached(runtime, "café")
60
+ let second = JavaScriptPropNameID.cached(runtime, "café")
61
+ #expect(first === second)
62
+ }
63
+
64
+ @Test
65
+ func `cached round-trips a non-ASCII string`() {
66
+ let propName = JavaScriptPropNameID.cached(runtime, "café")
67
+ #expect(propName.utf8() == "café")
68
+ }
69
+
70
+ // MARK: - Property resolution
71
+
72
+ // End-to-end: a PropNameID built from a non-ASCII key must resolve the matching JS property.
73
+ // With the truncation bug the mangled key missed the real property and returned `undefined`.
74
+ @Test
75
+ func `resolves a non-ASCII property by PropNameID`() throws {
76
+ let object = try runtime.eval("({ 'café': 42, '日本語': 'ok', '🎉': true })").getObject()
77
+
78
+ #expect(object.getProperty(JavaScriptPropNameID(runtime, string: "café")).getInt() == 42)
79
+ #expect(object.getProperty(JavaScriptPropNameID(runtime, string: "日本語")).getString() == "ok")
80
+ #expect(object.getProperty(JavaScriptPropNameID(runtime, string: "🎉")).getBool() == true)
81
+ }
82
+
83
+ @Test
84
+ func `resolves a non-ASCII property that does not exist as undefined`() throws {
85
+ let object = try runtime.eval("({ 'café': 42 })").getObject()
86
+
87
+ // A different non-ASCII key must not collide with `café` via truncated bytes.
88
+ #expect(object.getProperty(JavaScriptPropNameID(runtime, string: "caff")).isUndefined() == true)
89
+ #expect(object.getProperty(JavaScriptPropNameID(runtime, string: "naïve")).isUndefined() == true)
90
+ }
91
+
92
+ @Test
93
+ func `property type errors report the non-ASCII property name`() throws {
94
+ let object = try runtime.eval("({ 'café': 42 })").getObject()
95
+ let propName = JavaScriptPropNameID(runtime, string: "café")
96
+ let notObject = #expect(throws: JavaScriptObject.PropertyNotObjectError.self) {
97
+ try object.getPropertyAsObject(propName)
98
+ }
99
+ #expect(notObject?.description == "Property 'café' is not an object")
100
+ let notFunction = #expect(throws: JavaScriptObject.PropertyNotFunctionError.self) {
101
+ try object.getPropertyAsFunction(propName)
102
+ }
103
+ #expect(notFunction?.description == "Property 'café' is not a function")
104
+ }
105
+
106
+ // Second `forUtf8(pointer, length)` call site: the array's string-keyed subscript getter.
107
+ @Test
108
+ func `array string subscript reads a non-ASCII custom property`() throws {
109
+ let array = try runtime.eval("(() => { const a = [10, 20]; a['café'] = 42; a['🎉'] = 'party'; return a; })()")
110
+ .getArray()
111
+
112
+ #expect(array["café"].getInt() == 42)
113
+ #expect(array["🎉"].getString() == "party")
114
+ }
115
+
116
+ @Test
117
+ func `array string subscript writes a non-ASCII custom property`() throws {
118
+ let array = try runtime.eval("[1, 2]").getArray()
119
+ array["café"] = JavaScriptValue(runtime, "party")
120
+ runtime.global().setProperty("arr", value: array.asValue())
121
+ #expect(try runtime.eval("arr['café'] === 'party'").getBool() == true)
122
+ }
123
+ }
@@ -857,6 +857,35 @@ struct JavaScriptRuntimeTests {
857
857
  #expect(tracked.released == false)
858
858
  #expect(runtime.longLivedObjects.count == 1)
859
859
  }
860
+
861
+ // MARK: - Garbage collection
862
+
863
+ @Test
864
+ func `collecting garbage releases an unreachable object`() {
865
+ var weakObject: JavaScriptWeakObject? = nil
866
+
867
+ do {
868
+ let object = runtime.createObject()
869
+ weakObject = JavaScriptWeakObject(runtime, object)
870
+ #expect((weakObject?.lock() != nil) == true)
871
+ }
872
+
873
+ runtime.collectGarbage()
874
+
875
+ #expect((weakObject?.lock() == nil) == true)
876
+ }
877
+
878
+ @Test
879
+ func `collecting garbage keeps a reachable object alive`() {
880
+ let object = runtime.createObject()
881
+ object.setProperty("survives", value: true)
882
+ let weakObject = JavaScriptWeakObject(runtime, object)
883
+
884
+ runtime.collectGarbage(cause: "test")
885
+
886
+ let survives = weakObject.lock()?.getProperty("survives").getBool()
887
+ #expect(survives == true)
888
+ }
860
889
  }
861
890
 
862
891
  /// Runs `body` on a freshly spawned synchronous thread and bridges the result back into the
@@ -172,7 +172,7 @@ struct JavaScriptTypedArrayTests {
172
172
  let typedArray = try runtime.eval("(() => new Uint8Array([1, 2, 3, 4]))()").getTypedArray()
173
173
 
174
174
  // The backing ArrayBuffer should remain alive because `JavaScriptTypedArray` holds it.
175
- try runtime.eval("gc() && gc() && gc()")
175
+ runtime.collectGarbage()
176
176
 
177
177
  typedArray.withUnsafeBytes { bytes in
178
178
  #expect(bytes[0] == 1)
@@ -26,6 +26,15 @@ struct JavaScriptUnownedValueTests {
26
26
  #expect(bool.getBool() == true)
27
27
  }
28
28
 
29
+ @Test
30
+ func `getString preserves non-ASCII, empty and long strings`() {
31
+ let long = String(repeating: "ą", count: 2000)
32
+ let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: "żółć 世界 🎉", "", long)
33
+ #expect(buffer.unownedValue(at: 0).getString() == "żółć 世界 🎉")
34
+ #expect(buffer.unownedValue(at: 1).getString() == "")
35
+ #expect(buffer.unownedValue(at: 2).getString() == long)
36
+ }
37
+
29
38
  @Test
30
39
  func `recognizes null and undefined`() {
31
40
  let buffer = JavaScriptValuesBuffer.allocate(in: runtime, with: JavaScriptValue.null, JavaScriptValue.undefined)
@@ -1,4 +1,5 @@
1
1
  import ExpoModulesJSI
2
+ import Foundation
2
3
  import Testing
3
4
 
4
5
  @Suite
@@ -156,6 +157,107 @@ struct JavaScriptValueTests {
156
157
  #expect(result == "\u{FFFD}")
157
158
  }
158
159
 
160
+ @Test
161
+ func `getString repairs lone surrogates the same way as the standard library`() throws {
162
+ // High surrogate followed by a non-surrogate, a stray low surrogate, and a high surrogate at the
163
+ // very end each become one U+FFFD, exactly like `String(decoding:as: UTF16.self)`.
164
+ let value = try runtime.eval("'a\\uD800b\\uDC00c\\uD83C\\uDF89\\uD800'")
165
+ let units: [UInt16] = [0x61, 0xD800, 0x62, 0xDC00, 0x63, 0xD83C, 0xDF89, 0xD800]
166
+ #expect(value.getString() == String(decoding: units, as: UTF16.self))
167
+ #expect(value.getString() == "a\u{FFFD}b\u{FFFD}c🎉\u{FFFD}")
168
+ }
169
+
170
+ @Test
171
+ func `getString covers every UTF-8 width below the bulk threshold`() throws {
172
+ let value = try runtime.eval("'a\\u00E9\\u4E2D\\uD83C\\uDF89'.repeat(100)")
173
+ #expect(value.getString() == String(repeating: "aé中🎉", count: 100))
174
+ #expect(value.getString().utf8.count == 1000)
175
+ }
176
+
177
+ @Test
178
+ func `getString handles long non-ASCII string`() throws {
179
+ let value = try runtime.eval("'ą'.repeat(2000)")
180
+ #expect(value.getString() == String(repeating: "ą", count: 2000))
181
+ }
182
+
183
+ @Test
184
+ func `getString handles string built by concatenation`() throws {
185
+ let value = try runtime.eval(
186
+ "(() => { let s = ''; for (let i = 0; i < 300; i++) s += i % 2 ? 'abc' : 'żółć🎉'; return s; })()"
187
+ )
188
+ let expected = (0..<300).map { $0 % 2 == 1 ? "abc" : "żółć🎉" }.joined()
189
+ #expect(value.getString() == expected)
190
+ }
191
+
192
+ @Test
193
+ func `String(runtime, ...) round-trips an empty string`() {
194
+ let value = JavaScriptValue(runtime, "")
195
+ #expect(value.isString() == true)
196
+ #expect(value.getString() == "")
197
+ }
198
+
199
+ @Test
200
+ func `String(runtime, ...) round-trips a bridged string`() {
201
+ let bridged = NSString(string: "café 世界 🎉") as String
202
+ let value = JavaScriptValue(runtime, bridged)
203
+ #expect(value.getString() == "café 世界 🎉")
204
+ }
205
+
206
+ @Test
207
+ func `String(runtime, ...) round-trips a long non-ASCII string`() {
208
+ let long = String(repeating: "ż", count: 5_000)
209
+ let value = JavaScriptValue(runtime, long)
210
+ #expect(value.getString() == long)
211
+ }
212
+
213
+ // MARK: - String as JavaScriptRepresentable
214
+
215
+ @Test
216
+ func `String.fromJavaScriptValue preserves non-ASCII`() throws {
217
+ let value = try runtime.eval("'żółć 世界 🎉'")
218
+ #expect(String.fromJavaScriptValue(value) == "żółć 世界 🎉")
219
+ }
220
+
221
+ @Test
222
+ func `String.toJavaScriptValue preserves non-ASCII`() {
223
+ let value = "żółć 世界 🎉".toJavaScriptValue(in: runtime)
224
+ #expect(value.getString() == "żółć 世界 🎉")
225
+ }
226
+
227
+ @Test
228
+ func `dictionary with ASCII keys and non-ASCII values round-trips through JavaScriptRepresentable`() throws {
229
+ let value = try runtime.eval("({ a: 'żółć', b: '🎉' })")
230
+ let dictionary = [String: String].fromJavaScriptValue(value)
231
+ #expect(dictionary == ["a": "żółć", "b": "🎉"])
232
+ }
233
+
234
+ @Test
235
+ func `dictionary with non-ASCII keys round-trips through JavaScriptRepresentable`() throws {
236
+ let value = try runtime.eval("({ 'café': 'one', '日本語': 'two' })")
237
+ let dictionary = [String: String].fromJavaScriptValue(value)
238
+ #expect(dictionary == ["café": "one", "日本語": "two"])
239
+ }
240
+
241
+ @Test
242
+ func `dictionary with non-ASCII keys encodes to JavaScript`() throws {
243
+ let value = ["café": "one", "": "empty"].toJavaScriptValue(in: runtime)
244
+ runtime.global().setProperty("dict", value: value)
245
+ #expect(try runtime.eval("dict['café'] === 'one' && dict[''] === 'empty'").getBool() == true)
246
+ }
247
+
248
+ @Test
249
+ func `array of non-ASCII strings round-trips through JavaScriptRepresentable`() throws {
250
+ let value = try runtime.eval("['café', '日本語', '']")
251
+ #expect([String].fromJavaScriptValue(value) == ["café", "日本語", ""])
252
+ }
253
+
254
+ @Test
255
+ func `dictionary with an empty key round-trips through JavaScriptRepresentable`() throws {
256
+ let value = try runtime.eval("({ '': 'empty' })")
257
+ let dictionary = [String: String].fromJavaScriptValue(value)
258
+ #expect(dictionary == ["": "empty"])
259
+ }
260
+
159
261
  // MARK: - Type Checking Tests
160
262
 
161
263
  @Test
@@ -51,11 +51,11 @@ struct JavaScriptValuesBufferTests {
51
51
  // Allocate a buffer that captures the JS object as an argument; the result is
52
52
  // discarded and `deinit` runs at the end of this statement. If `deinit` skipped
53
53
  // `deinitialize`, the contained `jsi::Value` would still hold a strong ref and
54
- // the weak handle would survive `gc()`.
54
+ // the weak handle would survive the collection below.
55
55
  _ = JavaScriptValuesBuffer.allocate(in: runtime, with: object.asValue())
56
56
  _ = consume object
57
57
 
58
- try runtime.eval("gc() && gc() && gc()")
58
+ runtime.collectGarbage()
59
59
 
60
60
  let stillAlive = weak.lock() != nil
61
61
  #expect(stillAlive == false)
@@ -57,7 +57,7 @@ struct JavaScriptWeakObjectTests {
57
57
  #expect((weakObject?.lock() != nil) == true)
58
58
 
59
59
  // Force garbage collection
60
- _ = try! runtime.eval("gc() && gc() && gc()")
60
+ runtime.collectGarbage()
61
61
 
62
62
  // Object should be collected now
63
63
  // Note: This test may be flaky depending on GC behavior
@@ -79,7 +79,7 @@ struct JavaScriptWeakObjectTests {
79
79
  }
80
80
 
81
81
  // Force garbage collection
82
- _ = try! runtime.eval("gc() && gc() && gc()")
82
+ runtime.collectGarbage()
83
83
 
84
84
  // After collection, asValue should return undefined
85
85
  // Note: This test may be flaky depending on GC behavior
@@ -204,6 +204,14 @@ build_slice() {
204
204
  # SYMROOT/OBJROOT to the paths this script reads from forces products and the
205
205
  # generated module maps back into DERIVED_DATA_PATH. On Xcode versions that
206
206
  # honor -derivedDataPath these point at the same locations, so it's a no-op.
207
+ #
208
+ # Xcode turns coverage mapping on for auto-generated SwiftPM schemes, and that leaks into a
209
+ # plain `build` of a Release configuration: swiftc gets `-profile-generate
210
+ # -profile-coverage-mapping` and clang gets `-fprofile-instr-generate`. The result is a counter
211
+ # increment in every function and branch region of the shipped framework (visible as
212
+ # `___profc_*` symbols and `__llvm_prf_*` sections) plus ~40% extra binary size. Setting
213
+ # CLANG_COVERAGE_MAPPING=NO is what removes the flags; CLANG_ENABLE_CODE_COVERAGE=NO alone
214
+ # does not, and `-enableCodeCoverage NO` is rejected outside of `test`.
207
215
  local env_args=(PATH="$PATH" HOME="$HOME" PODS_ROOT="$PODS_ROOT" RN_ROOT="$RN_ROOT")
208
216
  if [[ -n "${DEVELOPER_DIR:-}" ]]; then
209
217
  env_args+=(DEVELOPER_DIR="$DEVELOPER_DIR")
@@ -229,6 +237,8 @@ build_slice() {
229
237
  DEBUG_INFORMATION_FORMAT=dwarf-with-dsym \
230
238
  COMPILER_INDEX_STORE_ENABLE=NO \
231
239
  SWIFT_COMPILATION_MODE=wholemodule \
240
+ CLANG_ENABLE_CODE_COVERAGE=NO \
241
+ CLANG_COVERAGE_MAPPING=NO \
232
242
  )
233
243
 
234
244
  local product_path="${BUILD_PRODUCTS_PATH}/${build_dir_name}"
@@ -46,10 +46,19 @@ if [[ ! -f "$JSI_UMBRELLA" ]]; then
46
46
  fi
47
47
  [[ -f "$JSI_UMBRELLA" ]] || { echo "error: cannot locate jsi.h" >&2; exit 1; }
48
48
 
49
+ # The umbrella header doesn't include `instrumentation.h`, so declare it explicitly.
50
+ # Otherwise including it warns about a missing `jsi.instrumentation` submodule.
51
+ JSI_INSTRUMENTATION="$(dirname "$JSI_UMBRELLA")/instrumentation.h"
52
+ JSI_EXTRA_HEADERS=""
53
+ if [[ -f "$JSI_INSTRUMENTATION" ]]; then
54
+ JSI_EXTRA_HEADERS="
55
+ header \"${JSI_INSTRUMENTATION}\""
56
+ fi
57
+
49
58
  # Avoid touching the file when contents would be identical, so the xcframework
50
59
  # hash cache and Xcode don't see a spurious change when inputs are unchanged.
51
60
  NEW_CONTENT="module jsi {
52
- umbrella header \"${JSI_UMBRELLA}\"
61
+ umbrella header \"${JSI_UMBRELLA}\"${JSI_EXTRA_HEADERS}
53
62
 
54
63
  export *
55
64
  module * { export * }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-jsi",
3
- "version": "57.0.7",
3
+ "version": "57.0.8",
4
4
  "description": "The JavaScript Interface for Expo Modules",
5
5
  "main": "index.js",
6
6
  "sideEffects": [],
@@ -41,8 +41,9 @@
41
41
  "./apple/scripts/test.sh"
42
42
  ]
43
43
  },
44
- "gitHead": "b76ecf192c5325793bcea31dd9bb8efd91f9ad7f",
44
+ "gitHead": "d36f125067e9c6e67689f2b75cf4fb19254c3c59",
45
45
  "scripts": {
46
+ "benchmark": "TEST_RUNNER_EXPO_BENCHMARK=1 DESTINATION=\"${DESTINATION:-platform=macOS,variant=Mac Catalyst}\" apple/scripts/test.sh -configuration Release -only-testing Benchmarks -enableCodeCoverage NO",
46
47
  "build": "apple/scripts/build-xcframework.sh",
47
48
  "clean": "apple/scripts/clear-caches.sh",
48
49
  "swift:format": "../../scripts/swift-format.sh",