expo-modules-jsi 58.0.1 → 58.0.3

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,18 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 58.0.3 — 2026-09-21
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Fix `pod install` failing when the Command Line Tools SDK is newer than the selected Xcode. ([#50316](https://github.com/expo/expo/pull/50316) by [@alanjhughes](https://github.com/alanjhughes))
18
+
19
+ ## 58.0.2 — 2026-09-15
20
+
21
+ ### 🎉 New features
22
+
23
+ - [iOS] Add a `JavaScriptRuntime.collectGarbage(passes:cause:until:)` overload that collects repeatedly until the given condition holds, for tests asserting on a release that a single collection doesn't always complete. ([#50188](https://github.com/expo/expo/pull/50188) by [@tsapeta](https://github.com/tsapeta))
24
+
13
25
  ## 58.0.1 — 2026-09-14
14
26
 
15
27
  _This version does not introduce any user-facing changes._
@@ -691,10 +691,15 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
691
691
 
692
692
  // MARK: - Garbage collection
693
693
 
694
+ // Both `collectGarbage` overloads below are for tests and memory diagnostics only. Don't call
695
+ // either from production code: a forced collection stops the world for as long as the heap takes
696
+ // to trace, and the engine already collects on its own schedule with far better information about
697
+ // when that is worth paying for.
698
+
694
699
  /// Requests a full, synchronous garbage collection of the JavaScript heap.
695
700
  ///
696
- /// Intended for tests and memory diagnostics. The engine collects on its own, so calling this in
697
- /// production code usually costs more than it saves.
701
+ /// - Important: For tests and memory diagnostics only. This blocks the JavaScript thread for the
702
+ /// length of a full collection, so calling it in production code costs more than it saves.
698
703
  ///
699
704
  /// - Note: This is a no-op on engines whose runtime doesn't implement GC instrumentation. JSI's
700
705
  /// default implementation does nothing; Hermes overrides it with a real collection.
@@ -705,6 +710,49 @@ open class JavaScriptRuntime: Equatable, Identifiable, @unchecked Sendable {
705
710
  expo.collectGarbage(pointee, std.string(cause))
706
711
  }
707
712
 
713
+ /// Collects garbage repeatedly until `condition` holds, or until the pass budget runs out.
714
+ ///
715
+ /// - Important: For tests and memory diagnostics only, and more so than the single-pass overload:
716
+ /// this runs up to `passes` full collections back to back, blocking the JavaScript thread for
717
+ /// all of them. A production caller that reaches for this wants a weak reference or an explicit
718
+ /// release hook, not a forced collection.
719
+ ///
720
+ /// One collection does not always finish the job. Releasing a detached native state means
721
+ /// finalizing the decoration that owns its `shared_ptr`, and a single pass does not always get
722
+ /// there: measured over 3000 attempts against a populated heap, 4.4% needed a second pass and a
723
+ /// handful needed a third. Asserting on a release after exactly one collection therefore tests
724
+ /// the engine's scheduling as much as the code under test, which is what made several suites
725
+ /// flaky on busy CI machines.
726
+ ///
727
+ /// Collecting again is what makes progress here; waiting does not substitute for it. The same
728
+ /// measurement with a microtask drain and a millisecond of sleep in place of the extra passes
729
+ /// left 18 times as many unreleased.
730
+ ///
731
+ /// This does not weaken the assertion that follows it: a value that is genuinely leaked never
732
+ /// satisfies `condition`, exhausts the budget, and still fails.
733
+ ///
734
+ /// - Note: This is a no-op on engines whose runtime doesn't implement GC instrumentation, in
735
+ /// which case `condition` is evaluated once per pass and the budget is spent in full.
736
+ /// - Parameters:
737
+ /// - passes: Maximum number of collections to run. The default sits far above what an
738
+ /// unleaked value needs, so exhausting it means the value is leaked, not merely unlucky.
739
+ /// - cause: Reason for the collection, as the engine should report it in its logs.
740
+ /// Defaults to the calling function's name.
741
+ /// - condition: Evaluated after each collection. Collecting stops as soon as it returns `true`.
742
+ @JavaScriptActor
743
+ public func collectGarbage(
744
+ passes: Int = 10,
745
+ cause: String = #function,
746
+ until condition: () -> Bool
747
+ ) {
748
+ for _ in 0..<passes {
749
+ collectGarbage(cause: cause)
750
+ if condition() {
751
+ return
752
+ }
753
+ }
754
+ }
755
+
708
756
  // MARK: - Equatable
709
757
 
710
758
  public static func == (lhs: JavaScriptRuntime, rhs: JavaScriptRuntime) -> Bool {
@@ -182,8 +182,7 @@ struct JavaScriptArrayBufferTests {
182
182
  // Buffer goes out of scope here
183
183
  }
184
184
 
185
- // Hermes collects synchronously, so the buffer should be collected immediately.
186
- runtime.collectGarbage()
185
+ runtime.collectGarbage { flag.pointee }
187
186
 
188
187
  cleanupCalled = flag.pointee
189
188
  flag.deallocate()
@@ -73,12 +73,18 @@ struct JavaScriptNativeStateTests {
73
73
  func `reattaches after the previous pointee was released`() throws {
74
74
  let object1 = runtime.createObject()
75
75
  let object2 = runtime.createObject()
76
+ var previousPointeeReleased = false
76
77
  let nativeState = CustomNativeState()
78
+ nativeState.setDeallocator { _ in
79
+ previousPointeeReleased = true
80
+ }
77
81
  object1.setNativeState(nativeState)
78
82
  object1.unsetNativeState()
79
83
 
80
- // Force garbage collection so the previous C++ pointee is dropped.
81
- runtime.collectGarbage()
84
+ // Collect until the previous C++ pointee is dropped, so the reattach below exercises the
85
+ // expired-weak-pointer path rather than reusing a pointee that happens to still be alive.
86
+ runtime.collectGarbage { previousPointeeReleased }
87
+ #expect(previousPointeeReleased == true)
82
88
 
83
89
  // Reattaching transparently materializes a fresh pointee.
84
90
  object2.setNativeState(nativeState)
@@ -105,8 +111,7 @@ struct JavaScriptNativeStateTests {
105
111
  object.setNativeState(nativeState)
106
112
  object.unsetNativeState()
107
113
 
108
- // Force garbage collection
109
- runtime.collectGarbage()
114
+ runtime.collectGarbage { deallocatorCalled }
110
115
 
111
116
  #expect(deallocatorCalled == true)
112
117
  }
@@ -123,14 +128,17 @@ struct JavaScriptNativeStateTests {
123
128
  object1.setNativeState(nativeState)
124
129
  object2.setNativeState(nativeState)
125
130
 
126
- // Unset from the first object — deallocator should not fire yet.
131
+ // Unset from the first object — deallocator should not fire yet. `object2` still holds a
132
+ // strong ref, so no number of collections may release the pointee; this assertion keeps a
133
+ // fixed collection rather than a retry loop, because it waits for something that must
134
+ // never happen.
127
135
  object1.unsetNativeState()
128
136
  runtime.collectGarbage()
129
137
  #expect(deallocatorCallCount == 0)
130
138
 
131
139
  // Unset from the second object — now the deallocator should fire exactly once.
132
140
  object2.unsetNativeState()
133
- runtime.collectGarbage()
141
+ runtime.collectGarbage { deallocatorCallCount == 1 }
134
142
  #expect(deallocatorCallCount == 1)
135
143
  }
136
144
 
@@ -183,7 +191,7 @@ struct JavaScriptNativeStateTests {
183
191
 
184
192
  object1.setNativeState(nativeState)
185
193
  object1.unsetNativeState()
186
- runtime.collectGarbage()
194
+ runtime.collectGarbage { deallocatorCallCount == 1 }
187
195
  #expect(deallocatorCallCount == 1)
188
196
 
189
197
  // Re-attaching builds a new C++ pointee; the wrapper-level deallocator
@@ -192,7 +200,7 @@ struct JavaScriptNativeStateTests {
192
200
  #expect(object2.getNativeState() === nativeState)
193
201
 
194
202
  object2.unsetNativeState()
195
- runtime.collectGarbage()
203
+ runtime.collectGarbage { deallocatorCallCount == 2 }
196
204
  #expect(deallocatorCallCount == 2)
197
205
  }
198
206
 
@@ -219,7 +227,7 @@ struct JavaScriptNativeStateTests {
219
227
  // Once detached and GC'd, the C++ pointee dies, releases the Unmanaged,
220
228
  // and the Swift wrapper finally deallocates.
221
229
  object.unsetNativeState()
222
- runtime.collectGarbage()
230
+ runtime.collectGarbage { weakWrapper == nil }
223
231
  #expect(weakWrapper == nil)
224
232
  }
225
233
 
@@ -1031,7 +1031,7 @@ struct JavaScriptRuntimeTests {
1031
1031
  #expect((weakObject?.lock() != nil) == true)
1032
1032
  }
1033
1033
 
1034
- runtime.collectGarbage()
1034
+ runtime.collectGarbage { weakObject?.lock() == nil }
1035
1035
 
1036
1036
  #expect((weakObject?.lock() == nil) == true)
1037
1037
  }
@@ -55,7 +55,7 @@ struct JavaScriptValuesBufferTests {
55
55
  _ = JavaScriptValuesBuffer.allocate(in: runtime, with: object.asValue())
56
56
  _ = consume object
57
57
 
58
- runtime.collectGarbage()
58
+ runtime.collectGarbage { weak.lock() == nil }
59
59
 
60
60
  let stillAlive = weak.lock() != nil
61
61
  #expect(stillAlive == false)
@@ -56,12 +56,8 @@ struct JavaScriptWeakObjectTests {
56
56
 
57
57
  #expect((weakObject?.lock() != nil) == true)
58
58
 
59
- // Force garbage collection
60
- runtime.collectGarbage()
59
+ runtime.collectGarbage { weakObject?.lock() == nil }
61
60
 
62
- // Object should be collected now
63
- // Note: This test may be flaky depending on GC behavior
64
- // In practice, lock() should return nil after GC, but timing may vary
65
61
  #expect((weakObject?.lock() == nil) == true)
66
62
  }
67
63
 
@@ -78,11 +74,8 @@ struct JavaScriptWeakObjectTests {
78
74
  #expect(value?.isObject() == true)
79
75
  }
80
76
 
81
- // Force garbage collection
82
- runtime.collectGarbage()
77
+ runtime.collectGarbage { weakObject?.asValue().isUndefined() == true }
83
78
 
84
- // After collection, asValue should return undefined
85
- // Note: This test may be flaky depending on GC behavior
86
79
  #expect(weakObject?.asValue().isUndefined() == true)
87
80
  }
88
81
 
@@ -31,7 +31,7 @@ echo -e "${BLUE}[Expo]${RESET} Ensuring required slices in ${PACKAGE_NAME}.xcfra
31
31
  # reuse the same binary for every slice that needs stamping.
32
32
  mkdir -p "$XCFRAMEWORK_PATH"
33
33
  STUB_SOURCE="${XCFRAMEWORK_PATH}/.stub-binary"
34
- echo "" | clang -x c - -dynamiclib \
34
+ echo "" | xcrun --sdk macosx clang -x c - -dynamiclib \
35
35
  -o "$STUB_SOURCE" \
36
36
  -install_name "@rpath/${PACKAGE_NAME}.framework/${PACKAGE_NAME}"
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-jsi",
3
- "version": "58.0.1",
3
+ "version": "58.0.3",
4
4
  "description": "The JavaScript Interface for Expo Modules",
5
5
  "main": "index.js",
6
6
  "sideEffects": [],
@@ -41,7 +41,7 @@
41
41
  "./apple/scripts/test.sh"
42
42
  ]
43
43
  },
44
- "gitHead": "e4b50c311af4e076b49a532ba2dca72546783e46",
44
+ "gitHead": "ebb1fd7569c9c9e172cb9abd73dfede027c1d674",
45
45
  "scripts": {
46
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",
47
47
  "build:xcframework": "apple/scripts/build-xcframework.sh",
package/.npmignore DELETED
@@ -1,19 +0,0 @@
1
- # @generated by expo-module-scripts
2
-
3
- # Exclude all top-level hidden directories by convention
4
- /.*/
5
-
6
- # Exclude tarballs generated by `npm pack`
7
- /*.tgz
8
-
9
- __mocks__
10
- __tests__
11
- __rsc_tests__
12
-
13
- /e2e
14
- /e2e-cli
15
- /babel.config.js
16
- /oxlint.config.mjs
17
- /android/src/androidTest/
18
- /android/src/test/
19
- /ios/Tests
package/apple/.gitignore DELETED
@@ -1,16 +0,0 @@
1
- .DS_Store
2
- .build
3
- .DerivedData
4
- .swiftpm
5
-
6
- # Generated module map for the `jsi` Clang module (written by build-xcframework.sh)
7
- .generated
8
-
9
- # Staged slices for incremental xcframework builds
10
- .xcframework-slices
11
-
12
- # Symlinks to host-app xcframeworks used by the test target (written by scripts/test.sh)
13
- .test-frameworks
14
-
15
- # Built xcframework output
16
- Products
package/turbo.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "$schema": "https://turborepo.dev/schema.json",
3
- "extends": ["//"],
4
- "tasks": {
5
- "precompile-ios": {
6
- "inputs": [
7
- "spm.config.json",
8
- "*.podspec",
9
- "apple/**",
10
- "!apple/.DerivedData/**",
11
- "!apple/.build/**",
12
- "!apple/.swiftpm/**",
13
- "!apple/.generated/**",
14
- "!apple/Products/**",
15
- "node_modules/react-native/package.json",
16
- "node_modules/react-native/sdks/hermes-engine/version.properties",
17
- "$TURBO_ROOT$/tools/src/prebuilds/**"
18
- ],
19
- "dependsOn": []
20
- }
21
- }
22
- }