@depup/react-native 0.87.0-depup.0 → 0.87.1-depup.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 (37) 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 +1 -1
  6. package/README.md +3 -3
  7. package/React/Base/RCTVersion.m +1 -1
  8. package/ReactAndroid/gradle.properties +1 -1
  9. package/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt +7 -3
  10. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  11. package/ReactCommon/cxxreact/ReactNativeVersion.h +2 -2
  12. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm +1 -1
  13. package/changes.json +2 -2
  14. package/package.json +12 -12
  15. package/scripts/cocoapods/rndependencies.rb +31 -25
  16. package/scripts/replace-rncore-version.js +18 -2
  17. package/scripts/setup-apple-spm.js +28 -12
  18. package/scripts/spm/autolinking-plugins.js +80 -0
  19. package/scripts/spm/download-spm-artifacts.js +64 -6
  20. package/scripts/spm/expand-spm-dependencies.js +249 -30
  21. package/scripts/spm/generate-spm-autolinking.js +157 -40
  22. package/scripts/spm/generate-spm-package.js +30 -18
  23. package/scripts/spm/generate-spm-xcodeproj.js +595 -71
  24. package/scripts/spm/scaffold-package-swift.js +65 -24
  25. package/scripts/spm/spm-pbxproj.js +158 -28
  26. package/scripts/spm/spm-types.js +29 -3
  27. package/scripts/spm/spm-utils.js +117 -2
  28. package/sdks/.hermesv1version +1 -1
  29. package/sdks/hermes-engine/version.properties +1 -1
  30. package/types_generated/Libraries/Animated/AnimatedImplementation.d.ts +2 -2
  31. package/types_generated/Libraries/Animated/nodes/AnimatedNode.d.ts +4 -1
  32. package/types_generated/Libraries/Animated/nodes/AnimatedValue.d.ts +3 -3
  33. package/scripts/spm/__doc__/rfc-spm-xcframework.md +0 -707
  34. package/scripts/spm/__doc__/spm-autolinking-plugins.md +0 -244
  35. package/scripts/spm/__doc__/spm-header-paths-contract.md +0 -97
  36. package/scripts/spm/__doc__/spm-plugins-assessment.md +0 -128
  37. 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,7 +28,7 @@
28
28
  export default class ReactNativeVersion {
29
29
  static major: number = 0;
30
30
  static minor: number = 87;
31
- static patch: number = 0;
31
+ static patch: number = 1;
32
32
  static prerelease: string | null = null;
33
33
 
34
34
  static getVersionString(): string {
package/README.md CHANGED
@@ -13,8 +13,8 @@ npm install @depup/react-native
13
13
 
14
14
  | Field | Value |
15
15
  |-------|-------|
16
- | Original | [react-native](https://www.npmjs.com/package/react-native) @ 0.87.0 |
17
- | Processed | 2026-08-16 |
16
+ | Original | [react-native](https://www.npmjs.com/package/react-native) @ 0.87.1 |
17
+ | Processed | 2026-08-30 |
18
18
  | Smoke test | failed |
19
19
  | Deps updated | 15 |
20
20
 
@@ -27,7 +27,7 @@ npm install @depup/react-native
27
27
  | babel-plugin-syntax-hermes-parser | 0.36.1 | ^0.37.0 |
28
28
  | commander | ^12.0.0 | ^15.0.0 |
29
29
  | memoize-one | ^5.0.0 | ^6.0.0 |
30
- | pretty-format | ^29.7.0 | ^30.4.1 |
30
+ | pretty-format | ^29.7.0 | ^30.5.0 |
31
31
  | react-devtools-core | ^6.1.5 | ^7.0.1 |
32
32
  | react-refresh | ^0.14.0 | ^0.18.0 |
33
33
  | regenerator-runtime | ^0.13.2 | ^0.14.1 |
@@ -23,7 +23,7 @@ NSDictionary* RCTGetReactNativeVersion(void)
23
23
  __rnVersion = @{
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(87),
26
- RCTVersionPatch: @(0),
26
+ RCTVersionPatch: @(1),
27
27
  RCTVersionPrerelease: [NSNull null],
28
28
  };
29
29
  });
@@ -1,4 +1,4 @@
1
- VERSION_NAME=0.87.0
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,
17
+ "patch" to 1,
18
18
  "prerelease" to null
19
19
  )
20
20
  }
@@ -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 = "";
26
26
  };
27
27
 
@@ -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/changes.json CHANGED
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "pretty-format": {
24
24
  "from": "^29.7.0",
25
- "to": "^30.4.1"
25
+ "to": "^30.5.0"
26
26
  },
27
27
  "react-devtools-core": {
28
28
  "from": "^6.1.5",
@@ -61,6 +61,6 @@
61
61
  "to": "^18.1.0"
62
62
  }
63
63
  },
64
- "timestamp": "2026-08-16T00:27:34.139Z",
64
+ "timestamp": "2026-08-30T00:58:15.370Z",
65
65
  "totalUpdated": 15
66
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@depup/react-native",
3
- "version": "0.87.0-depup.0",
3
+ "version": "0.87.1-depup.0",
4
4
  "description": "A framework for building native apps using React (with updated dependencies)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -155,25 +155,25 @@
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",
163
- "@react-native/virtualized-lists": "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
+ "@react-native/virtualized-lists": "0.87.1",
164
164
  "anser": "^2.3.5",
165
165
  "ansi-regex": "^6.3.0",
166
166
  "babel-plugin-syntax-hermes-parser": "^0.37.0",
167
167
  "base64-js": "^1.5.1",
168
168
  "commander": "^15.0.0",
169
169
  "flow-enums-runtime": "^0.0.6",
170
- "hermes-compiler": "250829098.0.16",
170
+ "hermes-compiler": "250829098.0.17",
171
171
  "invariant": "^2.2.4",
172
172
  "memoize-one": "^6.0.0",
173
173
  "metro-runtime": "^0.87.0",
174
174
  "metro-source-map": "^0.87.0",
175
175
  "nullthrows": "^1.1.1",
176
- "pretty-format": "^30.4.1",
176
+ "pretty-format": "^30.5.0",
177
177
  "promise": "^8.3.0",
178
178
  "react-devtools-core": "^7.0.1",
179
179
  "react-refresh": "^0.18.0",
@@ -245,7 +245,7 @@
245
245
  },
246
246
  "pretty-format": {
247
247
  "from": "^29.7.0",
248
- "to": "^30.4.1"
248
+ "to": "^30.5.0"
249
249
  },
250
250
  "react-devtools-core": {
251
251
  "from": "^6.1.5",
@@ -286,8 +286,8 @@
286
286
  },
287
287
  "depsUpdated": 15,
288
288
  "originalPackage": "react-native",
289
- "originalVersion": "0.87.0",
290
- "processedAt": "2026-08-16T00:27:44.731Z",
289
+ "originalVersion": "0.87.1",
290
+ "processedAt": "2026-08-30T00:58:26.000Z",
291
291
  "smokeTest": "failed"
292
292
  }
293
293
  }
@@ -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