react-native 0.87.0-rc.4 → 0.87.1

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 (35) 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/Core/ReactNativeVersion.js +2 -2
  6. package/React/Base/RCTVersion.m +2 -2
  7. package/ReactAndroid/gradle.properties +1 -1
  8. package/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt +7 -3
  9. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +2 -2
  10. package/ReactCommon/cxxreact/ReactNativeVersion.h +3 -3
  11. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm +1 -1
  12. package/package.json +8 -8
  13. package/scripts/cocoapods/rndependencies.rb +31 -25
  14. package/scripts/replace-rncore-version.js +18 -2
  15. package/scripts/setup-apple-spm.js +28 -12
  16. package/scripts/spm/autolinking-plugins.js +80 -0
  17. package/scripts/spm/download-spm-artifacts.js +64 -6
  18. package/scripts/spm/expand-spm-dependencies.js +249 -30
  19. package/scripts/spm/generate-spm-autolinking.js +157 -40
  20. package/scripts/spm/generate-spm-package.js +30 -18
  21. package/scripts/spm/generate-spm-xcodeproj.js +595 -71
  22. package/scripts/spm/scaffold-package-swift.js +65 -24
  23. package/scripts/spm/spm-pbxproj.js +158 -28
  24. package/scripts/spm/spm-types.js +29 -3
  25. package/scripts/spm/spm-utils.js +117 -2
  26. package/sdks/.hermesv1version +1 -1
  27. package/sdks/hermes-engine/version.properties +1 -1
  28. package/types_generated/Libraries/Animated/AnimatedImplementation.d.ts +2 -2
  29. package/types_generated/Libraries/Animated/nodes/AnimatedNode.d.ts +4 -1
  30. package/types_generated/Libraries/Animated/nodes/AnimatedValue.d.ts +3 -3
  31. package/scripts/spm/__doc__/rfc-spm-xcframework.md +0 -707
  32. package/scripts/spm/__doc__/spm-autolinking-plugins.md +0 -244
  33. package/scripts/spm/__doc__/spm-header-paths-contract.md +0 -97
  34. package/scripts/spm/__doc__/spm-plugins-assessment.md +0 -128
  35. package/scripts/spm/__doc__/spm-scripts.md +0 -486
@@ -199,7 +199,7 @@ export class AnimatedEvent {
199
199
  this._attachedEvent && this._attachedEvent.detach();
200
200
  }
201
201
 
202
- __getHandler(): any | ((...args: any) => void) {
202
+ __getHandler(): (...args: any) => void {
203
203
  if (this.__isNative) {
204
204
  if (__DEV__) {
205
205
  let validatedMapping = false;
@@ -576,10 +576,14 @@ function unforkEventImpl(
576
576
  }
577
577
  }
578
578
 
579
- const eventImpl = function <T>(
579
+ // NOTE: With `useNativeDriver: true` this returns an `AnimatedEvent` instance
580
+ // rather than a callable handler. That object is only ever meant to be handed
581
+ // straight back to an animated component's event prop, so the declared type
582
+ // describes the handler shape both branches are consumed as.
583
+ const eventImpl: <T>(
580
584
  argMapping: ReadonlyArray<?Mapping>,
581
585
  config: EventConfig<T>,
582
- ): any {
586
+ ) => (...args: Array<any>) => void = function (argMapping, config): any {
583
587
  const animatedEvent = new AnimatedEvent(argMapping, config);
584
588
  if (animatedEvent.__isNative) {
585
589
  return animatedEvent;
@@ -13,7 +13,7 @@ import type {PlatformConfig} from '../AnimatedPlatformConfig';
13
13
  import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
14
14
  import invariant from 'invariant';
15
15
 
16
- type ValueListenerCallback = (state: {value: number, ...}) => unknown;
16
+ export type ValueListenerCallback = (state: {value: number}) => unknown;
17
17
 
18
18
  export type AnimatedNodeConfig = Readonly<{
19
19
  debugID?: string,
@@ -17,7 +17,7 @@ import type {
17
17
  InterpolationConfigType,
18
18
  } from './AnimatedInterpolation';
19
19
  import type AnimatedNode from './AnimatedNode';
20
- import type {AnimatedNodeConfig} from './AnimatedNode';
20
+ import type {AnimatedNodeConfig, ValueListenerCallback} from './AnimatedNode';
21
21
  import type AnimatedTracking from './AnimatedTracking';
22
22
 
23
23
  import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
@@ -138,7 +138,7 @@ export default class AnimatedValue extends AnimatedWithChildren {
138
138
  }
139
139
  }
140
140
 
141
- addListener(callback: (value: any) => unknown): string {
141
+ addListener(callback: ValueListenerCallback): string {
142
142
  const id = super.addListener(callback);
143
143
  this._listenerCount++;
144
144
  if (this.__isNative) {
@@ -28,8 +28,8 @@
28
28
  export default class ReactNativeVersion {
29
29
  static major: number = 0;
30
30
  static minor: number = 87;
31
- static patch: number = 0;
32
- static prerelease: string | null = 'rc.4';
31
+ static patch: number = 1;
32
+ static prerelease: string | null = null;
33
33
 
34
34
  static getVersionString(): string {
35
35
  return `${this.major}.${this.minor}.${this.patch}${this.prerelease != null ? `-${this.prerelease}` : ''}`;
@@ -23,8 +23,8 @@ NSDictionary* RCTGetReactNativeVersion(void)
23
23
  __rnVersion = @{
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(87),
26
- RCTVersionPatch: @(0),
27
- RCTVersionPrerelease: @"rc.4",
26
+ RCTVersionPatch: @(1),
27
+ RCTVersionPrerelease: [NSNull null],
28
28
  };
29
29
  });
30
30
  return __rnVersion;
@@ -1,4 +1,4 @@
1
- VERSION_NAME=0.87.0-rc.4
1
+ VERSION_NAME=0.87.1
2
2
  react.internal.publishingGroup=com.facebook.react
3
3
  react.internal.hermesPublishingGroup=com.facebook.hermes
4
4
 
@@ -87,11 +87,15 @@ public open class IntentModule(reactContext: ReactApplicationContext) :
87
87
  override fun onHostResume() {
88
88
  reactApplicationContext.removeLifecycleEventListener(this)
89
89
  synchronized(this@IntentModule) {
90
- for (pendingPromise in pendingOpenURLPromises) {
90
+ // getInitialURL can re-enter and re-add to pendingOpenURLPromises when the activity
91
+ // is still null at resume, so drain a snapshot (after clearing the list and listener)
92
+ // to avoid mutating the list being iterated (ConcurrentModificationException).
93
+ val pendingPromises = ArrayList(pendingOpenURLPromises)
94
+ pendingOpenURLPromises.clear()
95
+ initialURLListener = null
96
+ for (pendingPromise in pendingPromises) {
91
97
  getInitialURL(pendingPromise)
92
98
  }
93
- initialURLListener = null
94
- pendingOpenURLPromises.clear()
95
99
  }
96
100
  }
97
101
 
@@ -14,7 +14,7 @@ public object ReactNativeVersion {
14
14
  public val VERSION: Map<String, Any?> = mapOf(
15
15
  "major" to 0,
16
16
  "minor" to 87,
17
- "patch" to 0,
18
- "prerelease" to "rc.4"
17
+ "patch" to 1,
18
+ "prerelease" to null
19
19
  )
20
20
  }
@@ -14,15 +14,15 @@
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;
25
- std::string_view Prerelease = "rc.4";
24
+ int32_t Patch = 1;
25
+ std::string_view Prerelease = "";
26
26
  };
27
27
 
28
28
  constexpr ReactNativeVersionType ReactNativeVersion;
@@ -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",
3
- "version": "0.87.0-rc.4",
3
+ "version": "0.87.1",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -150,19 +150,19 @@
150
150
  }
151
151
  },
152
152
  "dependencies": {
153
- "@react-native/asset-utils": "0.87.0-rc.4",
154
- "@react-native/codegen": "0.87.0-rc.4",
155
- "@react-native/community-cli-plugin": "0.87.0-rc.4",
156
- "@react-native/gradle-plugin": "0.87.0-rc.4",
157
- "@react-native/normalize-colors": "0.87.0-rc.4",
158
- "@react-native/virtualized-lists": "0.87.0-rc.4",
153
+ "@react-native/asset-utils": "0.87.1",
154
+ "@react-native/codegen": "0.87.1",
155
+ "@react-native/community-cli-plugin": "0.87.1",
156
+ "@react-native/gradle-plugin": "0.87.1",
157
+ "@react-native/normalize-colors": "0.87.1",
158
+ "@react-native/virtualized-lists": "0.87.1",
159
159
  "anser": "^1.4.9",
160
160
  "ansi-regex": "^5.0.0",
161
161
  "babel-plugin-syntax-hermes-parser": "0.36.1",
162
162
  "base64-js": "^1.5.1",
163
163
  "commander": "^12.0.0",
164
164
  "flow-enums-runtime": "^0.0.6",
165
- "hermes-compiler": "250829098.0.16",
165
+ "hermes-compiler": "250829098.0.17",
166
166
  "invariant": "^2.2.4",
167
167
  "memoize-one": "^5.0.0",
168
168
  "metro-runtime": "^0.87.0",
@@ -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,
@@ -69,6 +69,14 @@
69
69
  * // dir add/remove there re-triggers the sync. Relative/empty/non-string
70
70
  * // entries are dropped with a warning; a non-array is ignored.
71
71
  * watchPaths: ['/abs/path/Package.swift', '/abs/dir'],
72
+ * // Build-time shell phases on the app target — SwiftPM has no
73
+ * // `script_phase`. Only id/name/script are required; `id` is the
74
+ * // ledger key and UUID seed, `position` defaults to 'end'. Recorded
75
+ * // to `.spm-plugin-script-phases.json`, from which the `spm add`/
76
+ * // `update` injector emits a PBXShellScriptBuildPhase per entry.
77
+ * // Malformed entries and duplicate ids are fatal.
78
+ * scriptPhases: [{id, name, script, position, inputPaths, outputPaths,
79
+ * alwaysOutOfDate}],
72
80
  * };
73
81
  * };
74
82
  *
@@ -76,12 +84,14 @@
76
84
  * the merge, so regeneration stays deterministic and idempotent.
77
85
  */
78
86
 
87
+ const {isValidScriptPhaseId, isValidScriptPhaseName} = require('./spm-utils');
79
88
  const path = require('path');
80
89
 
81
90
  /*:: import type {
82
91
  AutolinkedDep,
83
92
  PluginContext,
84
93
  PluginResult,
94
+ PluginScriptPhase,
85
95
  DiscoveredPlugin,
86
96
  } from './spm-types';
87
97
  */
@@ -149,6 +159,12 @@ function discoverPlugins(
149
159
  return found;
150
160
  }
151
161
 
162
+ const isNonEmptyString = (value /*: unknown */) /*: boolean */ =>
163
+ typeof value === 'string' && value.length > 0;
164
+
165
+ const isOptionalStringList = (value /*: unknown */) /*: boolean */ =>
166
+ value == null || (Array.isArray(value) && value.every(isNonEmptyString));
167
+
152
168
  /**
153
169
  * Invoke discovered plugins and merge their results. Each plugin gets the same
154
170
  * context. Fail-closed: a throwing plugin aborts (named), and a malformed
@@ -169,10 +185,12 @@ function invokePlugins(
169
185
  const flavoredFrameworks /*: Array<{id: string, frameworkName: string, linkage: 'dynamic', flavors: {debug: string, release: string}}> */ =
170
186
  [];
171
187
  const watchPaths /*: Array<string> */ = [];
188
+ const scriptPhases /*: Array<PluginScriptPhase> */ = [];
172
189
  const seenPackages /*: Set<string> */ = new Set();
173
190
  const seenProducts /*: Set<string> */ = new Set();
174
191
  const seenFrameworkIds /*: Set<string> */ = new Set();
175
192
  const seenFrameworkNames /*: Set<string> */ = new Set();
193
+ const seenScriptPhaseIds /*: Set<string> */ = new Set();
176
194
 
177
195
  for (const {depName, pluginPath, plugin} of plugins) {
178
196
  let result /*: unknown */ = null;
@@ -203,11 +221,18 @@ function invokePlugins(
203
221
  const rawFrameworks = result.flavoredFrameworks ?? [];
204
222
  // $FlowFixMe[incompatible-use]
205
223
  const rawWatch = result.watchPaths ?? [];
224
+ // $FlowFixMe[incompatible-use]
225
+ const rawScriptPhases = result.scriptPhases ?? [];
206
226
  if (!Array.isArray(rawFrameworks)) {
207
227
  throw new Error(
208
228
  `react-native spm: '${depName}' returned a non-array flavoredFrameworks.`,
209
229
  );
210
230
  }
231
+ if (!Array.isArray(rawScriptPhases)) {
232
+ throw new Error(
233
+ `react-native spm: '${depName}' returned a non-array scriptPhases.`,
234
+ );
235
+ }
211
236
  // Watch paths are best-effort staleness hints, so
212
237
  // a non-array is ignored (warn, never fatal).
213
238
  if (!Array.isArray(rawWatch)) {
@@ -314,6 +339,60 @@ function invokePlugins(
314
339
  }
315
340
  watchPaths.push(w);
316
341
  }
342
+ // Fatal like flavoredFrameworks, not dropped like watchPaths: a missing
343
+ // phase produces a GREEN build whose generated content was never written.
344
+ for (const phase of rawScriptPhases) {
345
+ if (
346
+ phase == null ||
347
+ !isValidScriptPhaseId(phase.id) ||
348
+ !isNonEmptyString(phase.script) ||
349
+ (phase.position != null &&
350
+ phase.position !== 'beforeCompile' &&
351
+ phase.position !== 'end') ||
352
+ !isOptionalStringList(phase.inputPaths) ||
353
+ !isOptionalStringList(phase.outputPaths) ||
354
+ (phase.alwaysOutOfDate != null &&
355
+ typeof phase.alwaysOutOfDate !== 'boolean')
356
+ ) {
357
+ const named = typeof phase?.id === 'string' ? ` '${phase.id}'` : '';
358
+ throw new Error(
359
+ `react-native spm: '${depName}' returned an invalid scriptPhase${named} ` +
360
+ '(need {id (matching /^[@A-Za-z0-9_./-]+$/, and not __proto__, ' +
361
+ 'constructor or prototype), name, script} plus optional ' +
362
+ '{position: "beforeCompile" | "end", inputPaths, outputPaths, ' +
363
+ 'alwaysOutOfDate}).',
364
+ );
365
+ }
366
+ if (!isValidScriptPhaseName(phase.name)) {
367
+ throw new Error(
368
+ `react-native spm: '${depName}' returned an invalid scriptPhase name for ` +
369
+ `'${phase.id}' (a name must be a non-empty single-line string — it is ` +
370
+ "the phase's display name in Xcode).",
371
+ );
372
+ }
373
+ if (seenScriptPhaseIds.has(phase.id)) {
374
+ throw new Error(
375
+ `react-native spm: duplicate script phase id '${phase.id}'.`,
376
+ );
377
+ }
378
+ seenScriptPhaseIds.add(phase.id);
379
+ const normalized /*: PluginScriptPhase */ = {
380
+ id: phase.id,
381
+ name: phase.name,
382
+ script: phase.script,
383
+ position: phase.position ?? 'end',
384
+ };
385
+ if (phase.inputPaths != null) {
386
+ normalized.inputPaths = [...phase.inputPaths];
387
+ }
388
+ if (phase.outputPaths != null) {
389
+ normalized.outputPaths = [...phase.outputPaths];
390
+ }
391
+ if (phase.alwaysOutOfDate != null) {
392
+ normalized.alwaysOutOfDate = phase.alwaysOutOfDate;
393
+ }
394
+ scriptPhases.push(normalized);
395
+ }
317
396
  }
318
397
 
319
398
  return {
@@ -322,6 +401,7 @@ function invokePlugins(
322
401
  generatedSources,
323
402
  flavoredFrameworks,
324
403
  watchPaths,
404
+ scriptPhases,
325
405
  };
326
406
  }
327
407
 
@@ -393,15 +393,63 @@ async function resolveRNDepsArtifact(
393
393
  return {url: snapshotUrl, version};
394
394
  }
395
395
 
396
+ /**
397
+ * Resolves the `hermes-compiler` npm package's version from THIS project's own
398
+ * node_modules — the exact same lookup generate-spm-xcodeproj.js's
399
+ * resolveHermesCliPathSetting() uses to find the hermesc binary that will
400
+ * compile the JS bundle, and the same one react-native-xcode.sh falls back to
401
+ * for SwiftPM builds. Returns null when the package isn't resolvable (e.g.
402
+ * USE_HERMES=false apps that never installed it) so the caller can fall back
403
+ * to the npm dist-tag lookup.
404
+ */
405
+ function resolveLocalHermesCompilerVersion(
406
+ rnRoot /*: string */,
407
+ ) /*: string | null */ {
408
+ try {
409
+ const pkgPath = require.resolve('hermes-compiler/package.json', {
410
+ paths: [rnRoot],
411
+ });
412
+ // $FlowFixMe[incompatible-type] JSON.parse returns any
413
+ const pkg /*: {version: string} */ = JSON.parse(
414
+ fs.readFileSync(pkgPath, 'utf8'),
415
+ );
416
+ assertSafeVersion(pkg.version, 'local hermes-compiler/package.json');
417
+ return pkg.version;
418
+ } catch (error) {
419
+ // A MODULE_NOT_FOUND resolution failure is the expected case (e.g.
420
+ // USE_HERMES=false apps that never installed hermes-compiler) — fall back
421
+ // silently. Any other failure means hermes-compiler IS installed but its
422
+ // package.json is unreadable/malformed or carries an unsafe version; warn
423
+ // loudly rather than silently regressing to the live latest-v1 dist-tag,
424
+ // which would re-introduce the version-skew crash this resolves (#57917).
425
+ if (error.code !== 'MODULE_NOT_FOUND') {
426
+ log(
427
+ ` WARNING: hermes-compiler is installed but its version could not be resolved (${error.message}); falling back to the latest-v1 dist-tag, which may not match the pinned hermesc and can crash at launch with "Wrong bytecode version".`,
428
+ );
429
+ }
430
+ return null;
431
+ }
432
+ }
433
+
396
434
  /**
397
435
  * Returns {url, version} for Hermes. Hermes uses its own version space
398
436
  * decoupled from React Native's nightly cadence — RN's `hermes-compiler`
399
437
  * npm package publishes a `latest-v1` dist-tag that always resolves to a
400
- * binary that's been built and uploaded to Maven. Our default mirrors RN's
401
- * CocoaPods prebuild path (see scripts/ios-prebuild/hermes.js):
438
+ * binary that's been built and uploaded to Maven.
402
439
  *
403
- * HERMES_VERSION unset → 'latest-v1' dist-tag
404
- * HERMES_VERSION=latest-v1 → same (explicit)
440
+ * HERMES_VERSION unset → version pinned by the locally installed
441
+ * hermes-compiler package (node_modules).
442
+ * This is the SAME source
443
+ * resolveHermesCliPathSetting() reads for
444
+ * HERMES_CLI_PATH, so the downloaded VM and
445
+ * the hermesc that compiles the JS bundle
446
+ * always agree — a mismatched pair crashes at
447
+ * launch with "Wrong bytecode version" (#57917).
448
+ * Falls back to the 'latest-v1' npm dist-tag
449
+ * (RN's CocoaPods prebuild default; see
450
+ * scripts/ios-prebuild/hermes.js) only when
451
+ * hermes-compiler isn't locally resolvable.
452
+ * HERMES_VERSION=latest-v1 → 'latest-v1' dist-tag (explicit)
405
453
  * HERMES_VERSION=nightly → hermes-compiler@nightly dist-tag
406
454
  * HERMES_VERSION=<literal> → use that version verbatim
407
455
  *
@@ -413,8 +461,17 @@ async function resolveHermesArtifact(
413
461
  rnVersion /*: string */,
414
462
  flavor /*: string */,
415
463
  rawVersion /*: string | null */,
464
+ rnRoot /*: string */,
416
465
  ) /*: Promise<ResolvedArtifact> */ {
417
- let version = process.env.HERMES_VERSION ?? 'latest-v1';
466
+ let version = process.env.HERMES_VERSION;
467
+
468
+ if (version == null) {
469
+ const localVersion = resolveLocalHermesCompilerVersion(rnRoot);
470
+ if (localVersion != null) {
471
+ log(` Using locally pinned hermes-compiler: ${localVersion}`);
472
+ }
473
+ version = localVersion ?? 'latest-v1';
474
+ }
418
475
 
419
476
  if (version === 'nightly') {
420
477
  version = await resolveNightlyVersion('hermes-compiler');
@@ -1119,7 +1176,7 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
1119
1176
  label: 'hermes',
1120
1177
  name: 'hermes-engine',
1121
1178
  resolve: () =>
1122
- resolveHermesArtifact(resolvedRnVersion, flavor, rawVersion),
1179
+ resolveHermesArtifact(resolvedRnVersion, flavor, rawVersion, rnRoot),
1123
1180
  sharedName: (v /*: string */) => `hermes-ios-${v}-${flavor}.tar.gz`,
1124
1181
  },
1125
1182
  ];
@@ -1390,6 +1447,7 @@ module.exports = {
1390
1447
  main,
1391
1448
  resolveCacheSlotVersion,
1392
1449
  resolveHermesArtifact,
1450
+ resolveLocalHermesCompilerVersion,
1393
1451
  REQUIRED_ARTIFACTS,
1394
1452
  validateArtifactsCache,
1395
1453
  // Exposed for unit tests (pure / fetch-stubbable helpers).