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.
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +35 -0
- package/apple/Benchmarks/BenchmarkRunner.swift +115 -0
- package/apple/Benchmarks/FunctionCallBenchmarks.swift +45 -0
- package/apple/Benchmarks/HostFunctionBenchmarks.swift +217 -0
- package/apple/Benchmarks/StringConversionBenchmarks.swift +245 -0
- package/apple/Benchmarks/ValueAccessBenchmarks.swift +132 -0
- package/apple/Package.swift +10 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostCallbackContext.swift +28 -0
- package/apple/Sources/ExpoModulesJSI/Contexts/HostFunctionContext.swift +18 -6
- package/apple/Sources/ExpoModulesJSI/Contexts/HostObjectContext.swift +7 -3
- package/apple/Sources/ExpoModulesJSI/Protocols/JSIRepresentable.swift +20 -13
- package/apple/Sources/ExpoModulesJSI/Protocols/JavaScriptRepresentable.swift +15 -3
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptPropNameID.swift +11 -7
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +80 -58
- package/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptValuesBuffer.swift +7 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptArray.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptBigInt.swift +1 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptObject.swift +18 -8
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptUnownedValue.swift +2 -2
- package/apple/Sources/ExpoModulesJSI/Runtime/Values/JavaScriptValue.swift +18 -7
- package/apple/Sources/ExpoModulesJSI/Utilities/String+JSI.swift +130 -0
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostFunctionClosure.h +6 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObject.h +2 -1
- package/apple/Sources/ExpoModulesJSI-Cxx/include/HostObjectCallbacks.h +5 -3
- package/apple/Sources/ExpoModulesJSI-Cxx/include/JSIUtils.h +14 -3
- package/apple/Tests/JavaScriptArrayBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptNativeStateTests.swift +10 -10
- package/apple/Tests/JavaScriptObjectTests.swift +79 -0
- package/apple/Tests/JavaScriptPropNameIDTests.swift +123 -0
- package/apple/Tests/JavaScriptRuntimeTests.swift +29 -0
- package/apple/Tests/JavaScriptTypedArrayTests.swift +1 -1
- package/apple/Tests/JavaScriptUnownedValueTests.swift +9 -0
- package/apple/Tests/JavaScriptValueTests.swift +102 -0
- package/apple/Tests/JavaScriptValuesBufferTests.swift +2 -2
- package/apple/Tests/JavaScriptWeakObjectTests.swift +2 -2
- package/apple/scripts/build-xcframework.sh +10 -0
- package/apple/scripts/generate-modulemap.sh +10 -1
- 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
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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",
|