react-native-tvos 0.87.0-0 → 0.87.1-0

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 (50) hide show
  1. package/Libraries/Animated/AnimatedEvent.js +1 -1
  2. package/Libraries/Animated/AnimatedImplementation.js +6 -2
  3. package/Libraries/Animated/nodes/AnimatedNode.js +1 -1
  4. package/Libraries/Animated/nodes/AnimatedValue.js +2 -2
  5. package/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +2 -0
  6. package/Libraries/Components/ScrollView/ScrollView.d.ts +26 -0
  7. package/Libraries/Components/ScrollView/ScrollView.js +32 -0
  8. package/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +2 -0
  9. package/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +11 -0
  10. package/Libraries/Components/ScrollView/processScrollAnimationEasing.js +40 -0
  11. package/Libraries/Components/TV/TVFocusGuideView.js +31 -0
  12. package/Libraries/Components/TV/useFocusEnterLeave.js +112 -0
  13. package/Libraries/Core/ReactNativeVersion.js +1 -1
  14. package/README.md +17 -0
  15. package/React/Base/RCTVersion.m +1 -1
  16. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +30 -0
  17. package/ReactAndroid/gradle.properties +1 -1
  18. package/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt +7 -3
  19. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  20. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt +36 -13
  21. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt +13 -0
  22. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt +25 -3
  23. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +62 -0
  24. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt +13 -0
  25. package/ReactCommon/cxxreact/ReactNativeVersion.h +2 -2
  26. package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp +26 -0
  27. package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h +3 -0
  28. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm +1 -1
  29. package/package.json +8 -8
  30. package/scripts/cocoapods/rndependencies.rb +31 -25
  31. package/scripts/replace-rncore-version.js +18 -2
  32. package/scripts/setup-apple-spm.js +28 -12
  33. package/scripts/spm/autolinking-plugins.js +80 -0
  34. package/scripts/spm/download-spm-artifacts.js +64 -6
  35. package/scripts/spm/expand-spm-dependencies.js +249 -30
  36. package/scripts/spm/generate-spm-autolinking.js +157 -40
  37. package/scripts/spm/generate-spm-package.js +30 -18
  38. package/scripts/spm/generate-spm-xcodeproj.js +595 -71
  39. package/scripts/spm/scaffold-package-swift.js +65 -24
  40. package/scripts/spm/spm-pbxproj.js +158 -28
  41. package/scripts/spm/spm-types.js +29 -3
  42. package/scripts/spm/spm-utils.js +117 -2
  43. package/sdks/.hermesv1version +1 -1
  44. package/sdks/hermes-engine/version.properties +1 -1
  45. package/types/public/ReactNativeTVTypes.d.ts +40 -1
  46. package/scripts/spm/__doc__/rfc-spm-xcframework.md +0 -707
  47. package/scripts/spm/__doc__/spm-autolinking-plugins.md +0 -244
  48. package/scripts/spm/__doc__/spm-header-paths-contract.md +0 -97
  49. package/scripts/spm/__doc__/spm-plugins-assessment.md +0 -128
  50. package/scripts/spm/__doc__/spm-scripts.md +0 -486
@@ -42,6 +42,7 @@ import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.Companion.re
42
42
  import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollCommandHandler
43
43
  import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollToCommandData
44
44
  import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollToEndCommandData
45
+ import com.facebook.react.views.scroll.ReactScrollViewHelper.createScrollAnimationInterpolator
45
46
  import com.facebook.react.views.scroll.ReactScrollViewHelper.parseOverScrollMode
46
47
  import com.facebook.react.views.scroll.ReactScrollViewHelper.parseSnapToAlignment
47
48
 
@@ -174,6 +175,18 @@ constructor(private val fpsListener: FpsListener? = null) :
174
175
  view.scrollAnimationEnabled = value
175
176
  }
176
177
 
178
+ @ReactProp(name = "scrollAnimationDuration")
179
+ public fun setScrollAnimationDuration(view: ReactHorizontalScrollView, value: Float) {
180
+ // Exposed as a float because of the JavaScript interface, see `snapToInterval`.
181
+ view.scrollAnimationDuration = value.toInt()
182
+ }
183
+
184
+ @ReactProp(name = "scrollAnimationEasing")
185
+ public fun setScrollAnimationEasing(view: ReactHorizontalScrollView, value: ReadableArray?) {
186
+ // Resolved here rather than per frame: the curve only changes when the prop does.
187
+ view.scrollAnimationInterpolator = createScrollAnimationInterpolator(value)
188
+ }
189
+
177
190
  @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
178
191
  public fun setRemoveClippedSubviews(
179
192
  view: ReactHorizontalScrollView,
@@ -8,6 +8,7 @@
8
8
  package com.facebook.react.views.scroll
9
9
 
10
10
  import android.animation.ObjectAnimator
11
+ import android.animation.TimeInterpolator
11
12
  import android.animation.ValueAnimator
12
13
  import android.content.Context
13
14
  import android.graphics.Canvas
@@ -207,6 +208,10 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
207
208
  private var scrollsChildToFocus = true
208
209
  internal var snapToItemPadding: Int = 0
209
210
  internal var scrollAnimationEnabled: Boolean = true
211
+ /** Values <= 0 keep the platform default duration. */
212
+ internal var scrollAnimationDuration: Int = 0
213
+ /** `null` keeps the platform default curve. */
214
+ internal var scrollAnimationInterpolator: TimeInterpolator? = null
210
215
  private var blockScrollDelta: Boolean = false
211
216
 
212
217
  init {
@@ -264,6 +269,8 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
264
269
  scrollsChildToFocus = true
265
270
  snapToItemPadding = 0
266
271
  scrollAnimationEnabled = true
272
+ scrollAnimationDuration = 0
273
+ scrollAnimationInterpolator = null
267
274
  blockScrollDelta = false
268
275
  }
269
276
 
@@ -505,12 +512,21 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
505
512
  val currentFocused = findFocus()
506
513
  val nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction)
507
514
  if (nextFocused != null && nextFocused !== currentFocused && nextFocused !== this) {
508
- nextFocused.requestFocus(direction)
515
+ if (nextFocused.requestFocus(direction)) {
516
+ ReactScrollViewHelper.playFocusNavigationSoundEffect(this, direction)
517
+ }
509
518
  return true
510
519
  }
511
520
  return false
512
521
  }
513
- return super.arrowScroll(direction)
522
+
523
+ val focusedBeforeScroll = findFocus()
524
+ val handled = super.arrowScroll(direction)
525
+ // super.arrowScroll() can scroll without moving focus, so only click when focus moved.
526
+ if (handled && findFocus() !== focusedBeforeScroll) {
527
+ ReactScrollViewHelper.playFocusNavigationSoundEffect(this, direction)
528
+ }
529
+ return handled
514
530
  }
515
531
 
516
532
  /**
@@ -1374,7 +1390,13 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
1374
1390
 
1375
1391
  override fun startFlingAnimator(start: Int, end: Int) {
1376
1392
  defaultFlingAnimator.cancel()
1377
- val duration = ReactScrollViewHelper.getDefaultScrollAnimationDuration(context)
1393
+ // The animator is reused, so the interpolator must be re-applied on every call to undo whatever
1394
+ // a previous `scrollAnimationEasing` left on it.
1395
+ val duration =
1396
+ if (scrollAnimationDuration > 0) scrollAnimationDuration
1397
+ else ReactScrollViewHelper.getDefaultScrollAnimationDuration(context)
1398
+ defaultFlingAnimator.interpolator =
1399
+ scrollAnimationInterpolator ?: ReactScrollViewHelper.getDefaultScrollAnimationInterpolator()
1378
1400
  defaultFlingAnimator.setDuration(duration.toLong()).setIntValues(start, end)
1379
1401
  defaultFlingAnimator.start()
1380
1402
 
@@ -8,19 +8,24 @@
8
8
  package com.facebook.react.views.scroll
9
9
 
10
10
  import android.animation.Animator
11
+ import android.animation.TimeInterpolator
11
12
  import android.animation.ValueAnimator
12
13
  import android.content.Context
13
14
  import android.graphics.Point
14
15
  import android.graphics.Rect
15
16
  import android.os.Build
17
+ import android.view.SoundEffectConstants
16
18
  import android.view.View
17
19
  import android.view.ViewGroup
20
+ import android.view.animation.AccelerateDecelerateInterpolator
21
+ import android.view.animation.PathInterpolator
18
22
  import android.widget.OverScroller
19
23
  import androidx.annotation.RequiresApi
20
24
  import androidx.core.view.ViewCompat.FocusDirection
21
25
  import androidx.core.view.ViewCompat.FocusRealDirection
22
26
  import com.facebook.common.logging.FLog
23
27
  import com.facebook.react.bridge.ReactContext
28
+ import com.facebook.react.bridge.ReadableArray
24
29
  import com.facebook.react.bridge.WritableMap
25
30
  import com.facebook.react.bridge.WritableNativeMap
26
31
  import com.facebook.react.common.ReactConstants
@@ -67,6 +72,11 @@ public object ReactScrollViewHelper {
67
72
  private var SMOOTH_SCROLL_DURATION = 250
68
73
  private var smoothScrollDurationInitialized = false
69
74
 
75
+ // The curve `ValueAnimator` applies when no interpolator is set, kept here so the reused fling
76
+ // animator can be reset to it once a custom `scrollAnimationEasing` is cleared.
77
+ private val defaultScrollAnimationInterpolator: TimeInterpolator =
78
+ AccelerateDecelerateInterpolator()
79
+
70
80
  /** Shared by [ReactScrollView] and [ReactHorizontalScrollView]. */
71
81
  @JvmStatic
72
82
  public fun <T> emitScrollEvent(scrollView: T, xVelocity: Float, yVelocity: Float)
@@ -225,6 +235,35 @@ public object ReactScrollViewHelper {
225
235
  return SMOOTH_SCROLL_DURATION
226
236
  }
227
237
 
238
+ /** Curve used by the fling animator when `scrollAnimationEasing` is not set. */
239
+ @JvmStatic
240
+ public fun getDefaultScrollAnimationInterpolator(): TimeInterpolator =
241
+ defaultScrollAnimationInterpolator
242
+
243
+ /**
244
+ * Builds the `scrollAnimationEasing` curve from the CSS cubic-bezier control points `[x1, y1, x2,
245
+ * y2]` sent by JS. Returns `null` when the value is missing or does not describe a usable curve,
246
+ * so callers fall back to [getDefaultScrollAnimationInterpolator].
247
+ */
248
+ @JvmStatic
249
+ public fun createScrollAnimationInterpolator(controlPoints: ReadableArray?): TimeInterpolator? {
250
+ if (controlPoints == null || controlPoints.size() != 4) {
251
+ return null
252
+ }
253
+ return try {
254
+ PathInterpolator(
255
+ controlPoints.getDouble(0).toFloat(),
256
+ controlPoints.getDouble(1).toFloat(),
257
+ controlPoints.getDouble(2).toFloat(),
258
+ controlPoints.getDouble(3).toFloat(),
259
+ )
260
+ } catch (e: IllegalArgumentException) {
261
+ // PathInterpolator rejects curves that leave 0..1 on x or loop back on themselves.
262
+ FLog.w(ReactConstants.TAG, e, "Invalid scrollAnimationEasing control points")
263
+ null
264
+ }
265
+ }
266
+
228
267
  /**
229
268
  * Adds a scroll listener.
230
269
  *
@@ -574,6 +613,29 @@ public object ReactScrollViewHelper {
574
613
  return host.findViewById(nextFocusableViewId)
575
614
  }
576
615
 
616
+ /**
617
+ * Plays the system focus navigation sound for a D-pad move the scroll view handled itself.
618
+ *
619
+ * ViewRootImpl only sonifies focus moves it performs, so any key event a scroll view consumes
620
+ * (which it does as soon as its content is scrollable) navigates silently. Call this after a
621
+ * successful requestFocus to keep the click consistent. It is a no-op when the user has system
622
+ * sounds off.
623
+ */
624
+ @JvmStatic
625
+ public fun playFocusNavigationSoundEffect(view: View, @FocusDirection direction: Int) {
626
+ val soundConstant =
627
+ when (direction) {
628
+ View.FOCUS_UP,
629
+ View.FOCUS_BACKWARD -> SoundEffectConstants.NAVIGATION_UP
630
+ View.FOCUS_DOWN,
631
+ View.FOCUS_FORWARD -> SoundEffectConstants.NAVIGATION_DOWN
632
+ View.FOCUS_LEFT -> SoundEffectConstants.NAVIGATION_LEFT
633
+ View.FOCUS_RIGHT -> SoundEffectConstants.NAVIGATION_RIGHT
634
+ else -> return
635
+ }
636
+ view.playSoundEffect(soundConstant)
637
+ }
638
+
577
639
  /**
578
640
  * Walks up the view hierarchy from the focused view to find a ReactViewGroup with
579
641
  * either scrollSnapAlign or scrollSnapOffset set. Returns a Triple of (snapTarget,
@@ -41,6 +41,7 @@ import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.Companion.re
41
41
  import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollCommandHandler
42
42
  import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollToCommandData
43
43
  import com.facebook.react.views.scroll.ReactScrollViewCommandHelper.ScrollToEndCommandData
44
+ import com.facebook.react.views.scroll.ReactScrollViewHelper.createScrollAnimationInterpolator
44
45
  import com.facebook.react.views.scroll.ReactScrollViewHelper.parseOverScrollMode
45
46
  import com.facebook.react.views.scroll.ReactScrollViewHelper.parseSnapToAlignment
46
47
  import com.facebook.react.views.scroll.ScrollEventType.Companion.getJSEventName
@@ -158,6 +159,18 @@ constructor(private val fpsListener: FpsListener? = null) :
158
159
  view.scrollAnimationEnabled = value
159
160
  }
160
161
 
162
+ @ReactProp(name = "scrollAnimationDuration")
163
+ public fun setScrollAnimationDuration(view: ReactScrollView, value: Float) {
164
+ // Exposed as a float because of the JavaScript interface, see `snapToInterval`.
165
+ view.scrollAnimationDuration = value.toInt()
166
+ }
167
+
168
+ @ReactProp(name = "scrollAnimationEasing")
169
+ public fun setScrollAnimationEasing(view: ReactScrollView, value: ReadableArray?) {
170
+ // Resolved here rather than per frame: the curve only changes when the prop does.
171
+ view.scrollAnimationInterpolator = createScrollAnimationInterpolator(value)
172
+ }
173
+
161
174
  @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
162
175
  public fun setRemoveClippedSubviews(view: ReactScrollView, removeClippedSubviews: Boolean) {
163
176
  view.removeClippedSubviews = removeClippedSubviews
@@ -14,14 +14,14 @@
14
14
 
15
15
  #define REACT_NATIVE_VERSION_MAJOR 0
16
16
  #define REACT_NATIVE_VERSION_MINOR 87
17
- #define REACT_NATIVE_VERSION_PATCH 0
17
+ #define REACT_NATIVE_VERSION_PATCH 1
18
18
 
19
19
  namespace facebook::react {
20
20
 
21
21
  struct ReactNativeVersionType {
22
22
  int32_t Major = 0;
23
23
  int32_t Minor = 87;
24
- int32_t Patch = 0;
24
+ int32_t Patch = 1;
25
25
  std::string_view Prerelease = "0";
26
26
  };
27
27
 
@@ -55,6 +55,18 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps(
55
55
  rawProps,
56
56
  "snapToItemPadding",
57
57
  sourceProps.snapToItemPadding,
58
+ {})),
59
+ scrollAnimationDuration(convertRawProp(
60
+ context,
61
+ rawProps,
62
+ "scrollAnimationDuration",
63
+ sourceProps.scrollAnimationDuration,
64
+ {})),
65
+ scrollAnimationEasing(convertRawProp(
66
+ context,
67
+ rawProps,
68
+ "scrollAnimationEasing",
69
+ sourceProps.scrollAnimationEasing,
58
70
  {})) {}
59
71
 
60
72
  void HostPlatformScrollViewProps::setProp(
@@ -76,6 +88,8 @@ void HostPlatformScrollViewProps::setProp(
76
88
  RAW_SET_PROP_SWITCH_CASE_BASIC(overScrollMode);
77
89
  RAW_SET_PROP_SWITCH_CASE_BASIC(endFillColor);
78
90
  RAW_SET_PROP_SWITCH_CASE_BASIC(snapToItemPadding);
91
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollAnimationDuration);
92
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollAnimationEasing);
79
93
  }
80
94
  }
81
95
 
@@ -394,6 +408,18 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(
394
408
  result["snapToItemPadding"] = snapToItemPadding;
395
409
  }
396
410
 
411
+ if (scrollAnimationDuration != oldProps->scrollAnimationDuration) {
412
+ result["scrollAnimationDuration"] = scrollAnimationDuration;
413
+ }
414
+
415
+ if (scrollAnimationEasing != oldProps->scrollAnimationEasing) {
416
+ auto scrollAnimationEasingArray = folly::dynamic::array();
417
+ for (const auto& controlPoint : scrollAnimationEasing) {
418
+ scrollAnimationEasingArray.push_back(controlPoint);
419
+ }
420
+ result["scrollAnimationEasing"] = scrollAnimationEasingArray;
421
+ }
422
+
397
423
  return result;
398
424
  }
399
425
 
@@ -35,6 +35,9 @@ class HostPlatformScrollViewProps : public BaseScrollViewProps {
35
35
 
36
36
  Float snapToItemPadding{0};
37
37
 
38
+ Float scrollAnimationDuration{0};
39
+ std::vector<Float> scrollAnimationEasing{};
40
+
38
41
  #pragma mark - DebugStringConvertible
39
42
 
40
43
  #if RN_DEBUG_STRING_CONVERTIBLE
@@ -361,7 +361,7 @@ UIFont *RCTFontWithFontProperties(RCTFontProperties fontProperties)
361
361
  font = [UIFont fontWithName:fontProperties.family size:effectiveFontSize];
362
362
  if (font != nullptr) {
363
363
  fontNames = [UIFont fontNamesForFamilyName:font.familyName];
364
- fontWeight = (fontWeight != 0.0) ?: RCTGetFontWeight(font);
364
+ fontWeight = (fontWeight != 0.0) ? fontWeight : RCTGetFontWeight(font);
365
365
  } else {
366
366
  // Failback to system font.
367
367
  font = RCTDefaultFontWithFontProperties(fontProperties);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tvos",
3
- "version": "0.87.0-0",
3
+ "version": "0.87.1-0",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -155,18 +155,18 @@
155
155
  }
156
156
  },
157
157
  "dependencies": {
158
- "@react-native/asset-utils": "0.87.0",
159
- "@react-native/codegen": "0.87.0",
160
- "@react-native/community-cli-plugin": "0.87.0",
161
- "@react-native/gradle-plugin": "0.87.0",
162
- "@react-native/normalize-colors": "0.87.0",
158
+ "@react-native/asset-utils": "0.87.1",
159
+ "@react-native/codegen": "0.87.1",
160
+ "@react-native/community-cli-plugin": "0.87.1",
161
+ "@react-native/gradle-plugin": "0.87.1",
162
+ "@react-native/normalize-colors": "0.87.1",
163
163
  "anser": "^1.4.9",
164
164
  "ansi-regex": "^5.0.0",
165
165
  "babel-plugin-syntax-hermes-parser": "0.36.1",
166
166
  "base64-js": "^1.5.1",
167
167
  "commander": "^12.0.0",
168
168
  "flow-enums-runtime": "^0.0.6",
169
- "hermes-compiler": "250829098.0.16",
169
+ "hermes-compiler": "250829098.0.17",
170
170
  "invariant": "^2.2.4",
171
171
  "memoize-one": "^5.0.0",
172
172
  "metro-runtime": "^0.87.0",
@@ -184,7 +184,7 @@
184
184
  "whatwg-fetch": "^3.0.0",
185
185
  "ws": "^7.5.10",
186
186
  "yargs": "^17.6.2",
187
- "@react-native-tvos/virtualized-lists": "0.87.0-0"
187
+ "@react-native-tvos/virtualized-lists": "0.87.1-0"
188
188
  },
189
189
  "codegenConfig": {
190
190
  "libraries": [
@@ -6,7 +6,6 @@
6
6
  require "json"
7
7
  require 'net/http'
8
8
  require 'rexml/document'
9
- require 'shellwords'
10
9
 
11
10
  require_relative './utils.rb'
12
11
 
@@ -34,38 +33,26 @@ def add_rn_third_party_dependencies(s)
34
33
  s.dependency "RCT-Folly/Fabric"
35
34
  end
36
35
 
37
- header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || []
38
-
39
- if header_search_paths.is_a?(String)
40
- header_search_paths = Shellwords.shellsplit(header_search_paths)
41
- end
42
-
43
- header_search_paths << "$(PODS_ROOT)/glog"
44
- header_search_paths << "$(PODS_ROOT)/boost"
45
- header_search_paths << "$(PODS_ROOT)/DoubleConversion"
46
- header_search_paths << "$(PODS_ROOT)/fast_float/include"
47
- header_search_paths << "$(PODS_ROOT)/fmt/include"
48
- header_search_paths << "$(PODS_ROOT)/SocketRocket"
49
- header_search_paths << "$(PODS_ROOT)/RCT-Folly"
50
-
51
- # uniq so a second call on the same spec can't duplicate entries.
52
- current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths.uniq
36
+ ReactNativeDependenciesUtils.append_header_search_paths(current_pod_target_xcconfig, [
37
+ "$(PODS_ROOT)/glog",
38
+ "$(PODS_ROOT)/boost",
39
+ "$(PODS_ROOT)/DoubleConversion",
40
+ "$(PODS_ROOT)/fast_float/include",
41
+ "$(PODS_ROOT)/fmt/include",
42
+ "$(PODS_ROOT)/SocketRocket",
43
+ "$(PODS_ROOT)/RCT-Folly",
44
+ ])
53
45
  else
54
46
  # Prebuilt-deps mode: this pod SELF-SERVES the third-party headers from its
55
47
  # own xcframework (incl. SocketRocket - sole supplier in this mode). See
56
48
  # scripts/cocoapods/__docs__/prebuilt-deps.md for the full contract.
57
49
  s.dependency "ReactNativeDependencies"
58
50
 
59
- header_search_paths = current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] || []
60
- if header_search_paths.is_a?(String)
61
- header_search_paths = Shellwords.shellsplit(header_search_paths)
62
- end
63
51
  # Artifact headers are flattened into the pod-local Headers/ by the podspec
64
52
  # prepare_command (see __docs__/prebuilt-deps.md).
65
- header_search_paths << "$(PODS_ROOT)/ReactNativeDependencies/Headers"
66
-
67
- # uniq so a second call on the same spec can't duplicate entries.
68
- current_pod_target_xcconfig["HEADER_SEARCH_PATHS"] = header_search_paths.uniq
53
+ ReactNativeDependenciesUtils.append_header_search_paths(current_pod_target_xcconfig, [
54
+ "$(PODS_ROOT)/ReactNativeDependencies/Headers",
55
+ ])
69
56
  end
70
57
 
71
58
  s.pod_target_xcconfig = current_pod_target_xcconfig
@@ -148,6 +135,25 @@ class ReactNativeDependenciesUtils
148
135
  end
149
136
  end
150
137
 
138
+ # Xcode splits HEADER_SEARCH_PATHS on whitespace, so every path we add is
139
+ # quoted - PODS_ROOT can expand to a directory with spaces in its name.
140
+ # Paths already in the xcconfig are left untouched: they carry the podspec
141
+ # author's own quoting, and re-quoting them would break it.
142
+ def self.append_header_search_paths(xcconfig, paths)
143
+ quoted = paths.map { |path| "\"#{path}\"" }
144
+ existing = xcconfig["HEADER_SEARCH_PATHS"]
145
+
146
+ # reject so a second call on the same spec can't duplicate entries.
147
+ case existing
148
+ when nil
149
+ xcconfig["HEADER_SEARCH_PATHS"] = quoted
150
+ when Array
151
+ xcconfig["HEADER_SEARCH_PATHS"] = existing + quoted.reject { |path| existing.include?(path) }
152
+ else
153
+ xcconfig["HEADER_SEARCH_PATHS"] = ([existing] + quoted.reject { |path| existing.include?(path) }).join(" ")
154
+ end
155
+ end
156
+
151
157
  def self.abort_if_use_local_rndeps_with_no_file()
152
158
  if !File.exist?(ENV["RCT_USE_LOCAL_RN_DEP"])
153
159
  abort("RCT_USE_LOCAL_RN_DEP is set to #{ENV["RCT_USE_LOCAL_RN_DEP"]} but the file does not exist!")
@@ -18,6 +18,9 @@ const yargs = require('yargs');
18
18
 
19
19
  const LAST_BUILD_FILENAME = 'React-Core-prebuilt/.last_build_configuration';
20
20
 
21
+ // Not a valid configuration, so finding it means the swap did not finish.
22
+ const REPLACEMENT_IN_PROGRESS = 'in-progress';
23
+
21
24
  function validateBuildConfiguration(configuration /*: string */) {
22
25
  if (!['Debug', 'Release'].includes(configuration)) {
23
26
  throw new Error(`Invalid configuration ${configuration}`);
@@ -42,10 +45,12 @@ function shouldReplaceRnCoreConfiguration(configuration /*: string */) {
42
45
  );
43
46
  return false;
44
47
  }
48
+ return true;
45
49
  }
46
50
 
47
- // Assumption: if there is no stored last build, we assume that it was build for debug.
48
- if (!fileExists && configuration === 'Debug') {
51
+ // With no marker the on-disk flavor is Debug: the podspec installs the debug
52
+ // tarball (see resolve_podspec_source in scripts/cocoapods/rncore.rb).
53
+ if (configuration === 'Debug') {
49
54
  console.log(
50
55
  'No previous build detected, but Debug Configuration. No need to replace React-Core-prebuilt',
51
56
  );
@@ -130,6 +135,10 @@ function updateLastBuildConfiguration(configuration /*: string */) {
130
135
  fs.writeFileSync(LAST_BUILD_FILENAME, configuration);
131
136
  }
132
137
 
138
+ function markReplacementInProgress() /*: void */ {
139
+ fs.writeFileSync(LAST_BUILD_FILENAME, REPLACEMENT_IN_PROGRESS);
140
+ }
141
+
133
142
  function main(
134
143
  configuration /*: string */,
135
144
  version /*: string */,
@@ -139,9 +148,16 @@ function main(
139
148
  validateVersion(version);
140
149
 
141
150
  if (!shouldReplaceRnCoreConfiguration(configuration)) {
151
+ // A fresh install leaves no marker; record the flavor we skipped on.
152
+ if (!fs.existsSync(LAST_BUILD_FILENAME)) {
153
+ updateLastBuildConfiguration(configuration);
154
+ }
142
155
  return;
143
156
  }
144
157
 
158
+ // Invalidate before touching the framework so an interrupted swap is
159
+ // detectable on the next run.
160
+ markReplacementInProgress();
145
161
  replaceRNCoreConfiguration(configuration, version, podsRoot);
146
162
  updateLastBuildConfiguration(configuration);
147
163
  console.log('Done replacing React Native prebuilt');
@@ -44,8 +44,10 @@
44
44
  * directing you to `--deintegrate`).
45
45
  *
46
46
  * Options:
47
- * --version <ver> React Native version (default: the resolved
48
- * node_modules/react-native version).
47
+ * --version <ver> React Native version. Pinned into
48
+ * .spm-injected.json and reused by later runs
49
+ * until a new one is passed (default: the
50
+ * resolved node_modules/react-native version).
49
51
  * --yes Skip the dirty-pbxproj confirmation prompt.
50
52
  * [add] --xcodeproj <path> Which .xcodeproj to inject into (when several).
51
53
  * [add] --product-name <name> Which app target to inject into (when several).
@@ -96,6 +98,7 @@ const {
96
98
  cleanupLeftoverPodsGroup,
97
99
  findInjectedXcodeproj,
98
100
  injectSpmIntoExistingXcodeproj,
101
+ readArtifactsVersionOverride,
99
102
  readPinnedConfigCommand,
100
103
  removeSpmInjection,
101
104
  } = require('./spm/generate-spm-xcodeproj');
@@ -153,7 +156,7 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
153
156
  .option('version', {
154
157
  type: 'string',
155
158
  describe:
156
- 'React Native version (e.g. 0.80.0). Defaults to the version in node_modules/react-native/package.json',
159
+ 'React Native version (e.g. 0.80.0). Sticks: later runs reuse it until you pass a new one. Defaults to the version in node_modules/react-native/package.json',
157
160
  })
158
161
  .option('yes', {
159
162
  type: 'boolean',
@@ -377,19 +380,31 @@ function resolveReactNativeRoot(
377
380
  return reactNativeRoot;
378
381
  }
379
382
 
383
+ // Explicit `--version` → the version an earlier `--version` pinned into the
384
+ // injection marker → node_modules/react-native/package.json. The pin makes
385
+ // `--version` stick for later flagless runs, which would otherwise re-point the
386
+ // project at a different artifact slot than the one it was wired to.
380
387
  function determineVersion(
381
388
  args /*: SetupArgs */,
382
389
  reactNativeRoot /*: string */,
390
+ appRoot /*: string */,
383
391
  ) /*: string */ {
384
- let version = args.version;
385
- if (version == null) {
386
- // $FlowFixMe[incompatible-type] JSON.parse returns any
387
- const pkgJson /*: {version: string} */ = JSON.parse(
388
- fs.readFileSync(path.join(reactNativeRoot, 'package.json'), 'utf8'),
392
+ if (args.version != null) {
393
+ return args.version;
394
+ }
395
+ const pinned = readArtifactsVersionOverride(appRoot);
396
+ if (pinned != null) {
397
+ log(
398
+ `Using version ${pinned} pinned in ${SPM_INJECTED_MARKER} by an earlier ` +
399
+ '--version. Pass --version to change it.',
389
400
  );
390
- version = pkgJson.version;
401
+ return pinned;
391
402
  }
392
- return version;
403
+ // $FlowFixMe[incompatible-type] JSON.parse returns any
404
+ const pkgJson /*: {version: string} */ = JSON.parse(
405
+ fs.readFileSync(path.join(reactNativeRoot, 'package.json'), 'utf8'),
406
+ );
407
+ return pkgJson.version;
393
408
  }
394
409
 
395
410
  function runCodegenStep(
@@ -442,7 +457,7 @@ async function runScaffold(
442
457
  // a comment — that's how SPM's manifest hash bumps on slot transitions.
443
458
  let cacheSlotLabel /*: ?string */ = null;
444
459
  try {
445
- const rawVersion = args.version ?? determineVersion(args, reactNativeRoot);
460
+ const rawVersion = determineVersion(args, reactNativeRoot, appRoot);
446
461
  const slotVersion = await resolveCacheSlotVersion(rawVersion);
447
462
  cacheSlotLabel = `${slotVersion}/dual-flavor`;
448
463
  } catch {
@@ -1094,7 +1109,7 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
1094
1109
  autolinkingConfigResult,
1095
1110
  projectRoot,
1096
1111
  );
1097
- const version = determineVersion(args, reactNativeRoot);
1112
+ const version = determineVersion(args, reactNativeRoot, appRoot);
1098
1113
  log(`React Native version: ${version}`);
1099
1114
 
1100
1115
  // Resolve remote SPM mode ONCE up front. remotePackageConfig throws
@@ -1256,6 +1271,7 @@ if (require.main === module) {
1256
1271
  module.exports = {
1257
1272
  main,
1258
1273
  detectStandardRnLayoutRedirect,
1274
+ determineVersion,
1259
1275
  findInjectedXcodeproj,
1260
1276
  generateAutolinkingConfigOrFailClosed,
1261
1277
  parseArgs,