react-native 0.87.0-rc.0 → 0.87.0-rc.2

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 (70) hide show
  1. package/Libraries/Core/InitializeCore.js +9 -1
  2. package/Libraries/Core/ReactNativeVersion.js +1 -1
  3. package/Libraries/ReactNative/AppRegistry.flow.js +1 -0
  4. package/Libraries/ReactNative/AppRegistryImpl.js +2 -3
  5. package/Libraries/ReactPrivate/ReactNativePrivateInterface.js +5 -116
  6. package/Libraries/ReactPrivate/ReactNativePrivateInterface.js.flow +5 -31
  7. package/React/Base/RCTVersion.m +1 -1
  8. package/React/I18n/RCTLocalizedString.mm +38 -2
  9. package/React-Core-prebuilt.podspec +45 -17
  10. package/React-Core.podspec +9 -2
  11. package/ReactAndroid/external-artifacts/build.gradle.kts +49 -0
  12. package/ReactAndroid/gradle.properties +1 -1
  13. package/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SynchronousMountItem.kt +8 -2
  14. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  15. package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
  16. package/index.js +15 -3
  17. package/index.js.flow +0 -2
  18. package/package.json +27 -29
  19. package/react-native.config.js +81 -0
  20. package/scripts/cocoapods/fabric.rb +1 -1
  21. package/scripts/cocoapods/rncore.rb +35 -78
  22. package/scripts/cocoapods/rncore_facades.rb +232 -0
  23. package/scripts/cocoapods/rndependencies.rb +64 -3
  24. package/scripts/cocoapods/rndeps_facades.rb +193 -0
  25. package/scripts/cocoapods/spm.rb +78 -11
  26. package/scripts/codegen/templates/Package.swift.spm-template +97 -0
  27. package/scripts/react-native-xcode.sh +20 -0
  28. package/scripts/react_native_pods.rb +65 -16
  29. package/scripts/replace-rncore-version.js +53 -6
  30. package/scripts/setup-apple-spm.js +1165 -0
  31. package/scripts/spm/__doc__/rfc-spm-xcframework.md +707 -0
  32. package/scripts/spm/__doc__/spm-autolinking-plugins.md +244 -0
  33. package/scripts/spm/__doc__/spm-header-paths-contract.md +97 -0
  34. package/scripts/spm/__doc__/spm-plugins-assessment.md +128 -0
  35. package/scripts/spm/__doc__/spm-scripts.md +451 -0
  36. package/scripts/spm/autolinking-plugins.js +331 -0
  37. package/scripts/spm/download-spm-artifacts.js +1409 -0
  38. package/scripts/spm/expand-spm-dependencies.js +216 -0
  39. package/scripts/spm/flavored-frameworks.js +1008 -0
  40. package/scripts/spm/generate-spm-autolinking-config.js +161 -0
  41. package/scripts/spm/generate-spm-autolinking.js +1888 -0
  42. package/scripts/spm/generate-spm-package.js +302 -0
  43. package/scripts/spm/generate-spm-xcodeproj.js +2224 -0
  44. package/scripts/spm/read-podspec.js +695 -0
  45. package/scripts/spm/scaffold-package-swift.js +1206 -0
  46. package/scripts/spm/spm-pbxproj.js +654 -0
  47. package/scripts/spm/spm-types.js +517 -0
  48. package/scripts/spm/spm-utils.js +645 -0
  49. package/scripts/spm/sync-spm-autolinking.js +160 -0
  50. package/sdks/.hermesv1version +1 -0
  51. package/sdks/hermes-engine/utils/replace_hermes_version.js +18 -4
  52. package/sdks/hermes-engine/version.properties +1 -1
  53. package/src/asset-registry.js +1 -1
  54. package/src/react-private-interface.js +145 -0
  55. package/src/react-private-interface.js.flow +48 -0
  56. package/src/setup-env.js +22 -0
  57. package/src/unstable-internals-do-not-use.d.ts +214 -0
  58. package/src/unstable-internals-do-not-use.js +76 -0
  59. package/third-party-podspecs/ReactNativeDependencies.podspec +2 -2
  60. package/types_generated/Libraries/ReactNative/AppRegistry.flow.d.ts +2 -2
  61. package/types_generated/Libraries/ReactPrivate/ReactNativePrivateInterface.d.ts +6 -21
  62. package/types_generated/index.d.ts +1 -2
  63. package/types_generated/src/private/renderer/events/dispatchNativeEvent.d.ts +26 -0
  64. package/types_generated/src/react-private-interface.d.ts +33 -0
  65. package/Libraries/Utilities/SceneTracker.js +0 -42
  66. package/jest-preset.js +0 -26
  67. package/rn-get-polyfills.js +0 -13
  68. package/types/tsconfig.json +0 -17
  69. package/types_generated/Libraries/Components/Touchable/Touchable.d.ts +0 -261
  70. package/types_generated/tsconfig.test.json +0 -17
@@ -0,0 +1,1165 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ /*:: import type {CliConfigJson, SetupArgs} from './spm/spm-types'; */
14
+
15
+ /**
16
+ * setup-apple-spm.js – Entry point for setting up Swift Package Manager support
17
+ * in a React Native app using prebuilt XCFrameworks.
18
+ *
19
+ * Usage (from your app directory):
20
+ * node node_modules/react-native/scripts/setup-apple-spm.js [action] [options]
21
+ * (or `npx react-native spm [action]`)
22
+ *
23
+ * Actions:
24
+ * add Inject SPM packages (package refs, build
25
+ * settings, the Sync build phase) into your
26
+ * existing .xcodeproj, in place. Idempotent.
27
+ * Default on first run. `--deintegrate` first
28
+ * runs `pod deintegrate` + strips React Native
29
+ * from the Podfile (CocoaPods → SPM migration).
30
+ * update Re-run the pipeline and refresh the existing
31
+ * injection. Default once a project is injected.
32
+ * deinit The exact inverse of `add`: surgically remove
33
+ * only what `add` injected (recorded in
34
+ * .spm-injected.json) and drop the marker.
35
+ * scaffold Generate Package.swift for community deps that
36
+ * lack SPM support.
37
+ * sync / codegen / download Advanced/internal: `sync` is invoked by the
38
+ * generated Xcode build phase; `codegen` and
39
+ * `download` run a single pipeline step.
40
+ *
41
+ * Zero-arg `npx react-native spm` auto-detects: a freshly-scaffolded CocoaPods
42
+ * project (clean tree, stock Podfile) → `add --deintegrate`; an injected
43
+ * project → `update`; otherwise → `add` (fails loud on a CocoaPods project,
44
+ * directing you to `--deintegrate`).
45
+ *
46
+ * Options:
47
+ * --version <ver> React Native version (default: the resolved
48
+ * node_modules/react-native version).
49
+ * --yes Skip the dirty-pbxproj confirmation prompt.
50
+ * [add] --xcodeproj <path> Which .xcodeproj to inject into (when several).
51
+ * [add] --product-name <name> Which app target to inject into (when several).
52
+ * [add] --deintegrate Run `pod deintegrate` + strip RN from the
53
+ * Podfile before injecting.
54
+ * [advanced] --artifacts <path> Local two-flavor artifact root;
55
+ * must contain debug/ and release/ cache slots.
56
+ * [advanced] --download <auto|skip|force> Artifact policy (default: auto).
57
+ * [advanced] --skip-codegen Skip the react-native codegen step.
58
+ *
59
+ * Steps performed (add/update):
60
+ * 1. react-native codegen → build/generated/ios/ + install SPM codegen template
61
+ * 2. generate-spm-autolinking-config.js → build/generated/autolinking/autolinking.json
62
+ * 3. generate-spm-autolinking.js → build/generated/autolinking/Package.swift
63
+ * 4. download-spm-artifacts.js → cache dir (per --download policy)
64
+ * 5. generate-spm-package.js → build/xcframeworks/Package.swift + symlinks
65
+ * 6. inject SPM packages into the existing .xcodeproj (in place)
66
+ *
67
+ * The injection is committed with your project; its XCLocalSwiftPackageReference
68
+ * entries point at stable sub-package paths under build/ (xcframeworks,
69
+ * generated/autolinking, generated/ios), so adding/removing community deps
70
+ * changes those sub-packages (gitignored) and never re-injects. No app-level
71
+ * Package.swift is generated or required.
72
+ */
73
+
74
+ const {
75
+ main: downloadArtifacts,
76
+ resolveCacheSlotVersion,
77
+ validateArtifactsCache,
78
+ } = require('./spm/download-spm-artifacts');
79
+ const {finalizeArtifactPublication} = require('./spm/flavored-frameworks');
80
+ const {
81
+ MissingManifestError,
82
+ main: generateAutolinking,
83
+ } = require('./spm/generate-spm-autolinking');
84
+ const {
85
+ generateAutolinkingConfig,
86
+ } = require('./spm/generate-spm-autolinking-config');
87
+ const {main: generatePackage} = require('./spm/generate-spm-package');
88
+ const {findSourcePath} = require('./spm/generate-spm-package');
89
+ const {
90
+ cleanupDanglingJavaScriptCoreRef,
91
+ cleanupLeftoverPodsGroup,
92
+ findInjectedXcodeproj,
93
+ injectSpmIntoExistingXcodeproj,
94
+ removeSpmInjection,
95
+ } = require('./spm/generate-spm-xcodeproj');
96
+ const {scaffoldAll} = require('./spm/scaffold-package-swift');
97
+ const {
98
+ RemoteVersionError,
99
+ buildPerAppHeaderTree,
100
+ defaultCacheDir,
101
+ deriveAppName,
102
+ displayPath,
103
+ findProjectRoot,
104
+ installSpmCodegenTemplate,
105
+ makeLogger,
106
+ readPackageJson,
107
+ remotePackageConfig,
108
+ runCodegenAndInstallTemplate,
109
+ } = require('./spm/spm-utils');
110
+ const {execFileSync} = require('child_process');
111
+ const fs = require('fs');
112
+ const path = require('path');
113
+ const readline = require('readline');
114
+ const yargs = require('yargs');
115
+
116
+ const {log, warn: logError} = makeLogger('setup-apple-spm');
117
+
118
+ const VALID_ACTIONS = new Set([
119
+ 'add',
120
+ 'update',
121
+ 'deinit',
122
+ 'sync',
123
+ 'codegen',
124
+ 'download',
125
+ 'scaffold',
126
+ ]);
127
+
128
+ /*::
129
+ type AutolinkingConfigResult = {
130
+ config: CliConfigJson,
131
+ outputPath: string,
132
+ rawJson: string,
133
+ };
134
+ */
135
+
136
+ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
137
+ const parsed = yargs(argv)
138
+ .version(false)
139
+ .command('$0 [action]', 'Set up Apple SPM support')
140
+ .positional('action', {
141
+ type: 'string',
142
+ choices: Array.from(VALID_ACTIONS),
143
+ describe:
144
+ 'Action to run: add, update, deinit, or scaffold. ' +
145
+ 'Defaults to add (or update if SPM is already set up).',
146
+ })
147
+ .option('version', {
148
+ type: 'string',
149
+ describe:
150
+ 'React Native version (e.g. 0.80.0). Defaults to the version in node_modules/react-native/package.json',
151
+ })
152
+ .option('yes', {
153
+ type: 'boolean',
154
+ default: false,
155
+ describe: 'Skip the dirty-pbxproj confirmation prompt',
156
+ })
157
+ .option('xcodeproj', {
158
+ type: 'string',
159
+ describe:
160
+ '[add] Path to the .xcodeproj to inject SPM packages into (disambiguates when several exist).',
161
+ })
162
+ .option('product-name', {
163
+ type: 'string',
164
+ describe:
165
+ '[add] App target to inject into (disambiguates when several exist).',
166
+ })
167
+ .option('deintegrate', {
168
+ type: 'boolean',
169
+ default: false,
170
+ describe:
171
+ '[add] Run `pod deintegrate` + strip React Native from the Podfile before injecting (CocoaPods → SPM migration).',
172
+ })
173
+ .option('artifacts', {
174
+ type: 'string',
175
+ describe:
176
+ '[advanced] Local artifact root containing complete debug/ and release/ cache slots.',
177
+ })
178
+ .option('download', {
179
+ type: 'string',
180
+ choices: ['auto', 'skip', 'force'],
181
+ default: 'auto',
182
+ describe:
183
+ '[advanced] Artifact download policy: auto (fetch if missing), skip (never fetch), force (clear cache + refetch).',
184
+ })
185
+ .option('skip-codegen', {
186
+ type: 'boolean',
187
+ default: false,
188
+ describe: '[advanced] Skip the react-native codegen step',
189
+ })
190
+ .usage(
191
+ 'Usage: $0 [action] [options]\n\nSets up Swift Package Manager support in a React Native app.',
192
+ )
193
+ .strictOptions()
194
+ .help()
195
+ .parseSync();
196
+
197
+ const positional = parsed._.map(String);
198
+ const requestedAction = parsed.action ?? positional[0] ?? null;
199
+ if (positional.length > 1) {
200
+ throw new Error(
201
+ `Expected at most one action, got: ${positional.join(', ')}`,
202
+ );
203
+ }
204
+ if (requestedAction != null && !VALID_ACTIONS.has(requestedAction)) {
205
+ throw new Error(
206
+ `Unknown action "${requestedAction}". Expected one of: ${Array.from(
207
+ VALID_ACTIONS,
208
+ ).join(', ')}`,
209
+ );
210
+ }
211
+
212
+ return {
213
+ action: requestedAction,
214
+ version: parsed.version ?? null,
215
+ artifacts: parsed.artifacts ?? null,
216
+ skipCodegen: parsed['skip-codegen'],
217
+ downloadPolicy: parsed.download,
218
+ productName: parsed['product-name'] ?? null,
219
+ xcodeprojPath: parsed.xcodeproj ?? null,
220
+ deintegrate: parsed.deintegrate,
221
+ yes: parsed.yes,
222
+ };
223
+ }
224
+
225
+ const SPM_GITIGNORE_ENTRIES = [
226
+ 'Package.resolved',
227
+ 'build/generated/',
228
+ 'build/xcframeworks/',
229
+ '.build/',
230
+ ];
231
+
232
+ /**
233
+ * Ensure the project's .gitignore contains entries for SPM-generated
234
+ * directories. Called during init so that generated artifacts are not
235
+ * accidentally committed.
236
+ */
237
+ function ensureGitignoreSpmEntries(appRoot /*: string */) {
238
+ const gitignorePath = path.join(appRoot, '.gitignore');
239
+ let content = '';
240
+ if (fs.existsSync(gitignorePath)) {
241
+ content = fs.readFileSync(gitignorePath, 'utf8');
242
+ }
243
+
244
+ const existingEntries = new Set(content.split('\n').map(l => l.trim()));
245
+ const missing = SPM_GITIGNORE_ENTRIES.filter(e => !existingEntries.has(e));
246
+
247
+ if (missing.length === 0) {
248
+ return;
249
+ }
250
+
251
+ const block = [
252
+ '',
253
+ '# SPM – auto-generated at build time (do not commit)',
254
+ ...missing,
255
+ ].join('\n');
256
+
257
+ // Append, ensuring we start on a fresh line
258
+ const separator = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
259
+ fs.writeFileSync(gitignorePath, content + separator + block + '\n', 'utf8');
260
+ log(`Updated .gitignore with SPM entries: ${missing.join(', ')}`);
261
+ }
262
+
263
+ /**
264
+ * Single TTY-gated Y/N prompt helper used by every interactive confirmation
265
+ * in this file. Non-TTY (CI / piped stdin) auto-confirms — every callsite
266
+ * either opted into the action explicitly or is downstream of an opt-in.
267
+ */
268
+ function promptYesNo(
269
+ question /*: string */,
270
+ defaultYes /*: boolean */,
271
+ ) /*: Promise<boolean> */ {
272
+ // $FlowFixMe[prop-missing] process.stdin.isTTY not in Flow stubs
273
+ if (process.stdin.isTTY !== true) {
274
+ return Promise.resolve(true);
275
+ }
276
+ const suffix = defaultYes ? '[Y/n]' : '[y/N]';
277
+ return new Promise(resolve => {
278
+ const rl = readline.createInterface({
279
+ input: process.stdin,
280
+ output: process.stdout,
281
+ });
282
+ rl.question(`${question} ${suffix} `, answer => {
283
+ rl.close();
284
+ const a = answer.trim().toLowerCase();
285
+ const yes = a === 'y' || a === 'yes';
286
+ resolve(defaultYes ? a === '' || yes : yes);
287
+ });
288
+ });
289
+ }
290
+
291
+ function resolveAction(
292
+ requestedAction /*: SetupArgs['action'] */,
293
+ appRoot /*: string */,
294
+ ) /*: 'add' | 'update' | 'deinit' | 'sync' | 'codegen' | 'download' | 'scaffold' */ {
295
+ if (requestedAction != null) {
296
+ return requestedAction;
297
+ }
298
+ // Zero-arg default. Once SPM has been injected, default to `update` (regen +
299
+ // refresh). Otherwise it's a first run → `add`. Whether `add` should imply
300
+ // `--deintegrate` (fresh CocoaPods project) is decided by the safe-gate in
301
+ // main(), which only applies on this implicit path.
302
+ return findInjectedXcodeproj(appRoot) != null ? 'update' : 'add';
303
+ }
304
+
305
+ /**
306
+ * Detects the JS-root-vs-ios-dir mismatch that produces silently-broken
307
+ * builds for standard RN apps. The community CLI writes
308
+ * `autolinking.json` under `<project.ios.sourceDir>/build/generated/autolinking/`
309
+ * (i.e. `<projectRoot>/ios/...`), while every SPM script anchors its
310
+ * inputs/outputs on `process.cwd()`. Running from the JS root therefore
311
+ * (a) writes outputs at `<projectRoot>/build/...` — away from the iOS
312
+ * project, and (b) makes the autolinker miss `autolinking.json` and
313
+ * silently skip every npm native dep. The build "succeeds" but anything
314
+ * touching a native module crashes at runtime.
315
+ *
316
+ * Returns the absolute path to the redirected app root (`<projectRoot>/ios`)
317
+ * when the redirect heuristic applies, else null. Pure: no side effects.
318
+ * The caller decides whether to auto-redirect (non-destructive actions) or
319
+ * refuse (destructive actions like `clean`).
320
+ */
321
+ function detectStandardRnLayoutRedirect(
322
+ appRoot /*: string */,
323
+ projectRoot /*: string */,
324
+ ) /*: string | null */ {
325
+ // Only relevant when cwd === projectRoot (i.e. user is at the JS root
326
+ // of their RN app). If they've already cd'd into a subdir, projectRoot
327
+ // walks up to find package.json and the two paths differ — leave alone.
328
+ if (path.resolve(appRoot) !== path.resolve(projectRoot)) {
329
+ return null;
330
+ }
331
+ // Standard RN layout has an `ios/` subdir holding the native project.
332
+ // Without it (e.g. rn-tester's flat layout), no mismatch to flag.
333
+ const iosSubdir = path.join(projectRoot, 'ios');
334
+ try {
335
+ if (!fs.statSync(iosSubdir).isDirectory()) {
336
+ return null;
337
+ }
338
+ } catch {
339
+ return null;
340
+ }
341
+ return iosSubdir;
342
+ }
343
+
344
+ function resolveReactNativeRoot(
345
+ autolinkingConfigResult /*: ?AutolinkingConfigResult */,
346
+ projectRoot /*: string */,
347
+ ) /*: string */ {
348
+ // Prefer the React Native path resolved by the CLI config we already run for
349
+ // autolinking. Fall back to this script's package root for direct repo usage.
350
+ let reactNativeRoot = path.resolve(__dirname, '..');
351
+ const cliConfig = autolinkingConfigResult?.config;
352
+ const cliReactNativePath = cliConfig?.reactNativePath;
353
+ const cliConfigRoot = cliConfig?.root;
354
+ if (typeof cliReactNativePath === 'string' && cliReactNativePath.length > 0) {
355
+ reactNativeRoot = path.resolve(
356
+ typeof cliConfigRoot === 'string' && cliConfigRoot.length > 0
357
+ ? cliConfigRoot
358
+ : projectRoot,
359
+ cliReactNativePath,
360
+ );
361
+ }
362
+ return reactNativeRoot;
363
+ }
364
+
365
+ function determineVersion(
366
+ args /*: SetupArgs */,
367
+ reactNativeRoot /*: string */,
368
+ ) /*: string */ {
369
+ let version = args.version;
370
+ if (version == null) {
371
+ // $FlowFixMe[incompatible-type] JSON.parse returns any
372
+ const pkgJson /*: {version: string} */ = JSON.parse(
373
+ fs.readFileSync(path.join(reactNativeRoot, 'package.json'), 'utf8'),
374
+ );
375
+ version = pkgJson.version;
376
+ }
377
+ return version;
378
+ }
379
+
380
+ function runCodegenStep(
381
+ projectRoot /*: string */,
382
+ appRoot /*: string */,
383
+ reactNativeRoot /*: string */,
384
+ skipCodegen /*: boolean */,
385
+ ) /*: void */ {
386
+ if (skipCodegen) {
387
+ // Output dir may already exist from a previous run; still refresh the
388
+ // SPM template so cache-slot changes propagate.
389
+ log('Skipping codegen (--skip-codegen)');
390
+ installSpmCodegenTemplate(appRoot, reactNativeRoot, {log});
391
+ return;
392
+ }
393
+ log('Running react-native codegen...');
394
+ try {
395
+ runCodegenAndInstallTemplate(projectRoot, appRoot, reactNativeRoot, {log});
396
+ } catch {
397
+ logError('Codegen failed. Continuing anyway...');
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Walks autolinking.json and writes a Package.swift into each community RN
403
+ * package that ships a podspec but no SPM manifest. Reuses the dep's
404
+ * podspec (via `pod ipc spec` when available) so the scaffolded file
405
+ * captures the dep's actual sources, header search paths, frameworks, and
406
+ * dependencies. Files carrying the scaffolder's own marker are regenerated
407
+ * when the cache slot changes (manifest-hash bump); files without the
408
+ * marker are left alone (upstream-shipped or user-managed).
409
+ *
410
+ * Runs as part of `init` / `update` / `scaffold` actions. Each invocation
411
+ * is a no-op for deps already in a clean state.
412
+ */
413
+ // Scaffolding is an EXPLICIT, manual step (`npx react-native spm scaffold`) and
414
+ // is NEVER run automatically by init/update/sync. A missing Package.swift is a
415
+ // real gap that must surface as a hard build error (see reportMissingManifests)
416
+ // so the user fixes it deliberately: scaffold, then persist with patch-package
417
+ // (node_modules is not committed), and ideally get it fixed upstream. There is
418
+ // intentionally no prompt and no auto-restore — auto-scaffolding would hide the
419
+ // error, and a wiped scaffold SHOULD re-surface it.
420
+ async function runScaffold(
421
+ args /*: SetupArgs */,
422
+ appRoot /*: string */,
423
+ projectRoot /*: string */,
424
+ reactNativeRoot /*: string */,
425
+ ) /*: Promise<void> */ {
426
+ // Resolve the cache slot identifier so the scaffolded files carry it as
427
+ // a comment — that's how SPM's manifest hash bumps on slot transitions.
428
+ let cacheSlotLabel /*: ?string */ = null;
429
+ try {
430
+ const rawVersion = args.version ?? determineVersion(args, reactNativeRoot);
431
+ const slotVersion = await resolveCacheSlotVersion(rawVersion);
432
+ cacheSlotLabel = `${slotVersion}/dual-flavor`;
433
+ } catch {
434
+ // Without a slot label the scaffolder still works; the file just
435
+ // doesn't get the slot-bump comment.
436
+ }
437
+
438
+ let results;
439
+ try {
440
+ results = scaffoldAll({
441
+ appRoot,
442
+ projectRoot,
443
+ reactNativeRoot,
444
+ cacheSlotLabel,
445
+ // Always force a re-render so re-running after editing a podspec picks
446
+ // up the new content.
447
+ force: true,
448
+ });
449
+ } catch (e) {
450
+ logError(`scaffold failed: ${e.message}.`);
451
+ process.exitCode = 1;
452
+ return;
453
+ }
454
+
455
+ const written = results.filter(r => r.status === 'written');
456
+ const errored = results.filter(r => r.status === 'error');
457
+ const warned = results.filter(
458
+ r => r.status === 'written' && r.warnings && r.warnings.length > 0,
459
+ );
460
+
461
+ if (written.length > 0) {
462
+ log(`Scaffolded Package.swift for ${written.length} dep(s):`);
463
+ for (const r of written) {
464
+ log(` • ${r.depName}`);
465
+ }
466
+ log('');
467
+ log(
468
+ 'node_modules is NOT committed and is wiped by `npm install`. To keep\n' +
469
+ 'these manifests, create and commit a patch with a tool like patch-package:\n' +
470
+ ' • `npx patch-package <dep>` for each scaffolded dep, then commit the patch.\n' +
471
+ 'Also consider asking the maintainer to ship a Package.swift upstream.\n' +
472
+ 'Without a committed patch the build will hard-error again after a fresh install.',
473
+ );
474
+ log('');
475
+ }
476
+
477
+ for (const r of warned) {
478
+ if (r.status !== 'written') continue;
479
+ for (const w of r.warnings) {
480
+ log(` ! ${r.depName}: ${w}`);
481
+ }
482
+ }
483
+
484
+ for (const r of errored) {
485
+ if (r.status !== 'error') continue;
486
+ logError(` ! ${r.depName}: ${r.reason}`);
487
+ }
488
+ }
489
+
490
+ async function ensureArtifactFlavor(
491
+ args /*: SetupArgs */,
492
+ rawVersion /*: string */,
493
+ slotVersion /*: string */,
494
+ flavor /*: 'debug' | 'release' */,
495
+ ) /*: Promise<string> */ {
496
+ if (args.artifacts != null && args.artifacts.endsWith('.xcframework')) {
497
+ throw new Error(
498
+ '--artifacts must be a directory containing debug/ and release/ slots; ' +
499
+ 'a single XCFramework cannot satisfy automatic switching',
500
+ );
501
+ }
502
+ const resolvedArtifactsDir =
503
+ args.artifacts != null
504
+ ? path.resolve(args.artifacts, flavor)
505
+ : defaultCacheDir(slotVersion, flavor);
506
+
507
+ if (args.downloadPolicy === 'force' && resolvedArtifactsDir != null) {
508
+ log('Clearing cached artifacts (--download force)...');
509
+ fs.rmSync(resolvedArtifactsDir, {recursive: true, force: true});
510
+ }
511
+
512
+ if (args.downloadPolicy === 'skip') {
513
+ const error = validateArtifactsCache(resolvedArtifactsDir);
514
+ if (error != null) {
515
+ throw new Error(
516
+ `--download skip requires a complete ${flavor} slot: ${error}`,
517
+ );
518
+ }
519
+ log(`Using ${flavor} artifacts (--download skip)`);
520
+ return resolvedArtifactsDir;
521
+ }
522
+
523
+ // Validate the cache before trusting it. A bare existsSync(artifacts.json)
524
+ // check would accept a partial write from a prior failed download (e.g.
525
+ // hermes-engine 404 on a not-yet-published nightly) and silently propagate
526
+ // the gap into the xcodeproj, surfacing only as "Missing package product"
527
+ // in Xcode. validateArtifactsCache reads the JSON and confirms every
528
+ // REQUIRED_ARTIFACT has a present xcframework on disk.
529
+ const cacheError = validateArtifactsCache(resolvedArtifactsDir);
530
+ if (cacheError == null) {
531
+ log(
532
+ `${flavor} artifacts already present in ${displayPath(resolvedArtifactsDir)}`,
533
+ );
534
+ return resolvedArtifactsDir;
535
+ }
536
+ log(`Cache incomplete (${cacheError}); re-downloading...`);
537
+ log(`Downloading ${flavor} xcframework artifacts (slot: ${slotVersion})...`);
538
+ await downloadArtifacts([
539
+ '--version',
540
+ rawVersion,
541
+ '--flavor',
542
+ flavor,
543
+ '--output',
544
+ resolvedArtifactsDir,
545
+ ]);
546
+ const downloadedError = validateArtifactsCache(resolvedArtifactsDir);
547
+ if (downloadedError != null) {
548
+ throw new Error(
549
+ `downloaded ${flavor} slot is incomplete: ${downloadedError}`,
550
+ );
551
+ }
552
+ return resolvedArtifactsDir;
553
+ }
554
+
555
+ async function ensureBothArtifactFlavors(
556
+ args /*: SetupArgs */,
557
+ version /*: string */,
558
+ ) /*: Promise<{debug: string, release: string}> */ {
559
+ // Resolve aliases/nightly labels exactly once so both flavors are guaranteed
560
+ // to come from the same selected artifact version.
561
+ const rawVersion = args.version ?? version;
562
+ const slotVersion = await resolveCacheSlotVersion(rawVersion);
563
+ const debug = await ensureArtifactFlavor(
564
+ args,
565
+ rawVersion,
566
+ slotVersion,
567
+ 'debug',
568
+ );
569
+ const release = await ensureArtifactFlavor(
570
+ args,
571
+ rawVersion,
572
+ slotVersion,
573
+ 'release',
574
+ );
575
+ return {debug, release};
576
+ }
577
+
578
+ function generateXcframeworksPackage(
579
+ args /*: SetupArgs */,
580
+ appRoot /*: string */,
581
+ reactNativeRoot /*: string */,
582
+ version /*: string */,
583
+ artifactDirs /*: {debug: string, release: string} */,
584
+ ) {
585
+ log('Generating xcframeworks sub-package...');
586
+ const packageArgs = [
587
+ '--app-root',
588
+ appRoot,
589
+ '--react-native-root',
590
+ reactNativeRoot,
591
+ '--version',
592
+ version,
593
+ ];
594
+ packageArgs.push('--debug-artifacts-dir', artifactDirs.debug);
595
+ packageArgs.push('--release-artifacts-dir', artifactDirs.release);
596
+ generatePackage(packageArgs);
597
+ }
598
+
599
+ // True when the chosen pbxproj is still CocoaPods-integrated (its build configs
600
+ // layer a `Pods-*.xcconfig`) — the real blocker for SPM injection.
601
+ function pbxprojUsesCocoaPods(xcodeprojPath /*: string */) /*: boolean */ {
602
+ try {
603
+ const t = fs.readFileSync(
604
+ path.join(xcodeprojPath, 'project.pbxproj'),
605
+ 'utf8',
606
+ );
607
+ return /\bPods[-/][^\n]*\.xcconfig\b/.test(t);
608
+ } catch {
609
+ return false;
610
+ }
611
+ }
612
+
613
+ // True when the Podfile still declares React Native integration — a latent
614
+ // landmine even after `pod deintegrate` (a future `pod install` re-breaks the
615
+ // SPM graph). Warned about (not refused) once the pbxproj itself is clean.
616
+ function podfileHasRnIntegration(appRoot /*: string */) /*: boolean */ {
617
+ const podfilePath = path.join(appRoot, 'Podfile');
618
+ if (!fs.existsSync(podfilePath)) {
619
+ return false;
620
+ }
621
+ return /use_react_native!|use_native_modules!|prepare_react_native_project!/.test(
622
+ fs.readFileSync(podfilePath, 'utf8'),
623
+ );
624
+ }
625
+
626
+ // True when the Podfile declares any explicit `pod '...'` (third-party pods).
627
+ function podfileHasThirdPartyPods(appRoot /*: string */) /*: boolean */ {
628
+ const podfilePath = path.join(appRoot, 'Podfile');
629
+ if (!fs.existsSync(podfilePath)) {
630
+ return false;
631
+ }
632
+ return /^\s*pod\s+['"]/m.test(fs.readFileSync(podfilePath, 'utf8'));
633
+ }
634
+
635
+ // The zero-arg "fresh project" safe-gate: auto-`add --deintegrate` ONLY when it
636
+ // is provably safe — a first-run CocoaPods RN project with a stock Podfile (no
637
+ // third-party pods) whose pbxproj AND Podfile are git-tracked and clean, so the
638
+ // conversion is fully revertible. We check only the two files `deintegrate`
639
+ // mutates (not the whole tree) — a fresh app typically has a dirty
640
+ // node_modules/lockfile/patches after `npm install` + `spm scaffold`, none of
641
+ // which affect the revertibility of the CocoaPods → SwiftPM conversion.
642
+ // Otherwise false → strict `add` (which fails loud on a CocoaPods project).
643
+ function shouldAutoDeintegrate(
644
+ appRoot /*: string */,
645
+ xcodeprojPath /*: string | null */,
646
+ ) /*: boolean */ {
647
+ if (xcodeprojPath == null || !pbxprojUsesCocoaPods(xcodeprojPath)) {
648
+ return false;
649
+ }
650
+ if (podfileHasThirdPartyPods(appRoot)) {
651
+ return false;
652
+ }
653
+ const pbxprojPath = path.join(xcodeprojPath, 'project.pbxproj');
654
+ if (gitTrackedAndClean(appRoot, pbxprojPath) !== true) {
655
+ return false;
656
+ }
657
+ const podfilePath = path.join(appRoot, 'Podfile');
658
+ if (
659
+ fs.existsSync(podfilePath) &&
660
+ gitTrackedAndClean(appRoot, podfilePath) !== true
661
+ ) {
662
+ return false;
663
+ }
664
+ return true;
665
+ }
666
+
667
+ // Run `pod deintegrate` then strip React Native from the Podfile (leaving any
668
+ // non-RN pods). Requires CocoaPods on PATH (fail-loud otherwise). Flag-gated ⇒
669
+ // no prompt ⇒ CI-safe. Does NOT touch the .xcworkspace.
670
+ function runDeintegrate(appRoot /*: string */) /*: void */ {
671
+ try {
672
+ execFileSync('pod', ['--version'], {stdio: 'ignore'});
673
+ } catch {
674
+ logError(
675
+ '`--deintegrate` needs CocoaPods (`pod`) on PATH. Remove the React ' +
676
+ 'Native integration from your project manually, then run `spm add`.',
677
+ );
678
+ process.exitCode = 1;
679
+ throw new Error('pod not found');
680
+ }
681
+ log('Running `pod deintegrate`...');
682
+ execFileSync('pod', ['deintegrate'], {cwd: appRoot, stdio: 'inherit'});
683
+
684
+ const podfilePath = path.join(appRoot, 'Podfile');
685
+ if (fs.existsSync(podfilePath)) {
686
+ const orig = fs.readFileSync(podfilePath, 'utf8');
687
+ const stripped = orig
688
+ .split('\n')
689
+ .filter(
690
+ l =>
691
+ !/use_react_native!|use_native_modules!|prepare_react_native_project!/.test(
692
+ l,
693
+ ),
694
+ )
695
+ .join('\n');
696
+ if (stripped !== orig) {
697
+ fs.writeFileSync(podfilePath, stripped, 'utf8');
698
+ log('Stripped React Native integration from Podfile.');
699
+ }
700
+ }
701
+ }
702
+
703
+ // Pick the .xcodeproj to inject into: --xcodeproj override > a prior in-place
704
+ // target (re-run) > the single .xcodeproj in appRoot. Returns an error string
705
+ // (ambiguous / none) so the caller fails loud.
706
+ function resolveInjectionTarget(
707
+ args /*: SetupArgs */,
708
+ appRoot /*: string */,
709
+ ) /*: {path: string, error?: void} | {error: string, path?: void} */ {
710
+ if (args.xcodeprojPath != null) {
711
+ const p = path.resolve(appRoot, args.xcodeprojPath);
712
+ return fs.existsSync(p)
713
+ ? {path: p}
714
+ : {error: `--xcodeproj not found: ${p}`};
715
+ }
716
+ const injected = findInjectedXcodeproj(appRoot);
717
+ if (injected != null) {
718
+ return {path: injected};
719
+ }
720
+ const names /*: Array<string> */ = [];
721
+ let entries /*: Array<{name: string, isDirectory(): boolean}> */ = [];
722
+ try {
723
+ // $FlowFixMe[incompatible-type] Dirent typing
724
+ entries = fs.readdirSync(appRoot, {withFileTypes: true});
725
+ } catch {}
726
+ for (const entry of entries) {
727
+ if (!entry.isDirectory()) continue;
728
+ // $FlowFixMe[incompatible-type] Dirent.name is string|Buffer in Flow stubs
729
+ const name /*: string */ = entry.name;
730
+ if (name.endsWith('.xcodeproj')) {
731
+ names.push(name);
732
+ }
733
+ }
734
+ if (names.length === 0) {
735
+ return {
736
+ error:
737
+ 'no .xcodeproj found. Create an app first (e.g. `npx ' +
738
+ '@react-native-community/cli init`) or make one in Xcode, then `spm add`.',
739
+ };
740
+ }
741
+ if (names.length > 1) {
742
+ return {
743
+ error: `multiple .xcodeproj found (${names.join(', ')}); pass --xcodeproj to pick one.`,
744
+ };
745
+ }
746
+ return {path: path.join(appRoot, names[0])};
747
+ }
748
+
749
+ /**
750
+ * Inject SPM packages into the user's existing .xcodeproj, in place — the only
751
+ * xcodeproj strategy (`add` and `update` both run this; there is no
752
+ * from-scratch generation). Fails loud rather than silently retargeting. With
753
+ * `--deintegrate`, removes CocoaPods first.
754
+ */
755
+ async function setupXcodeproj(
756
+ args /*: SetupArgs */,
757
+ appRoot /*: string */,
758
+ reactNativeRoot /*: string */,
759
+ action /*: string */,
760
+ ) /*: Promise<void> */ {
761
+ const target = resolveInjectionTarget(args, appRoot);
762
+ if (target.error != null) {
763
+ logError(`Cannot set up SPM: ${target.error}`);
764
+ process.exitCode = 1;
765
+ throw new Error(target.error);
766
+ }
767
+ const xcodeprojPath = target.path;
768
+ const pbxprojPath = path.join(xcodeprojPath, 'project.pbxproj');
769
+
770
+ // Snapshot the pbxproj's git state BEFORE deintegrate — `pod deintegrate`
771
+ // rewrites it (removing the Pods xcconfig layering), so checking afterward
772
+ // would always look dirty and trigger a spurious confirmation prompt.
773
+ const cleanBeforeEdits = gitTrackedAndClean(appRoot, pbxprojPath);
774
+
775
+ if (args.deintegrate) {
776
+ runDeintegrate(appRoot);
777
+ // `pod deintegrate` strips the build integration but can leave an empty
778
+ // `Pods` group in the navigator — remove it so the converted project is
779
+ // visually clean.
780
+ if (cleanupLeftoverPodsGroup(xcodeprojPath)) {
781
+ log('Removed the leftover empty `Pods` group from the project.');
782
+ }
783
+ }
784
+
785
+ // Preflight: a still-CocoaPods-integrated pbxproj is the real build-breaker.
786
+ if (pbxprojUsesCocoaPods(xcodeprojPath)) {
787
+ logError(
788
+ `${path.basename(xcodeprojPath)} is CocoaPods-integrated. Re-run ` +
789
+ '`spm add --deintegrate` to convert it (runs `pod deintegrate` + ' +
790
+ 'strips React Native from the Podfile), or run `pod deintegrate` ' +
791
+ 'yourself first. Side-by-side non-RN pods are fine.',
792
+ );
793
+ process.exitCode = 1;
794
+ throw new Error('CocoaPods-integrated project');
795
+ }
796
+ if (podfileHasRnIntegration(appRoot)) {
797
+ log(
798
+ '\x1b[33mNote: your Podfile still declares React Native integration. ' +
799
+ 'Remove it and avoid `pod install`, or it will re-break the SPM ' +
800
+ 'package graph.\x1b[0m',
801
+ );
802
+ }
803
+
804
+ // No backup is made — git is the safety net. Refuse on a dirty/untracked
805
+ // pbxproj (as it was BEFORE any deintegrate edits) unless --yes, so a bad
806
+ // inject is always `git checkout`-able.
807
+ const clean = cleanBeforeEdits;
808
+ if (clean === false && !args.yes) {
809
+ const proceed = await promptYesNo(
810
+ `${path.basename(xcodeprojPath)} has uncommitted changes and no ` +
811
+ `backup is made (git is the only undo). Inject SPM packages anyway?`,
812
+ false,
813
+ );
814
+ if (!proceed) {
815
+ log('Aborted. Commit or stash the project, then re-run `spm add`.');
816
+ process.exitCode = 1;
817
+ throw new Error('In-place injection declined (dirty working tree)');
818
+ }
819
+ } else if (clean === null) {
820
+ log(
821
+ `\x1b[33mNote: ${path.basename(xcodeprojPath)} is not in a git ` +
822
+ `repo — no backup is made before in-place injection.\x1b[0m`,
823
+ );
824
+ }
825
+
826
+ const result = injectSpmIntoExistingXcodeproj({
827
+ appRoot,
828
+ reactNativeRoot,
829
+ xcodeprojPath,
830
+ appName: args.productName,
831
+ // Only an EXPLICIT `--version` pins the artifacts-cache slot in the
832
+ // marker; omitting it (null) leaves any previously-recorded pin alone
833
+ // (injectSpmIntoExistingXcodeproj preserves it — see
834
+ // generate-spm-xcodeproj.js).
835
+ artifactsVersionOverride: args.version ?? null,
836
+ });
837
+ if (result.status !== 'injected') {
838
+ logError(`SPM injection failed: ${result.reason}`);
839
+ process.exitCode = 1;
840
+ throw new Error(result.reason);
841
+ }
842
+
843
+ // The community template has carried a dangling `JavaScriptCore.framework`
844
+ // reference since RN 0.60 (navigator-only, unlinked, meaningless under
845
+ // Hermes) — one-time migration hygiene at conversion time, like the empty
846
+ // Pods group above. `add` only; `update` stays a minimal re-sync.
847
+ if (action === 'add' && cleanupDanglingJavaScriptCoreRef(xcodeprojPath)) {
848
+ log(
849
+ "Removed the template's dangling JavaScriptCore.framework reference " +
850
+ '(unused; React Native uses Hermes).',
851
+ );
852
+ }
853
+ }
854
+
855
+ // True when `git status --porcelain` reports the path dirty/untracked. Returns
856
+ // null when git is unavailable or the path is outside a repo (no safety net).
857
+ function gitTrackedAndClean(
858
+ appRoot /*: string */,
859
+ targetPath /*: string */,
860
+ ) /*: boolean | null */ {
861
+ try {
862
+ const out = execFileSync(
863
+ 'git',
864
+ ['status', '--porcelain', '--', targetPath],
865
+ {cwd: appRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']},
866
+ );
867
+ return out.trim() === '';
868
+ } catch {
869
+ return null; // not a git repo / git missing
870
+ }
871
+ }
872
+
873
+ function logNextSteps(
874
+ projectRoot /*: string */,
875
+ appRoot /*: string */,
876
+ productName /*: string | null */,
877
+ ) {
878
+ const appPkgJson = readPackageJson(projectRoot);
879
+ const rawName =
880
+ (appPkgJson != null ? appPkgJson.name : null) ?? path.basename(projectRoot);
881
+ const sourcePath = findSourcePath(appRoot, rawName);
882
+ const appDisplayName = productName ?? deriveAppName(rawName, sourcePath);
883
+
884
+ log('');
885
+ log('SPM setup complete!');
886
+ log('');
887
+ log('Next steps:');
888
+ log(` • Open ${appDisplayName}.xcodeproj in Xcode (or \`npm run ios\`)`);
889
+ log(' • Set your Development Team in Signing & Capabilities');
890
+ log(' • Build and run on Simulator or device');
891
+ log('');
892
+ log('To remove SPM later: `npx react-native spm deinit`');
893
+ }
894
+
895
+ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
896
+ let appRoot = process.cwd();
897
+ const projectRoot = findProjectRoot(appRoot);
898
+ const args = parseArgs(argv ?? process.argv.slice(2));
899
+
900
+ // Standard-RN-layout redirect: if invoked from the JS root and there's an
901
+ // `ios/` subdir, route the run there. Runs BEFORE resolveAction so the
902
+ // first-run heuristic checks the correct directory.
903
+ const redirectTo = detectStandardRnLayoutRedirect(appRoot, projectRoot);
904
+ if (redirectTo != null) {
905
+ const redirectAction = args.action ?? 'add';
906
+ log(
907
+ `\x1b[33mDetected standard RN layout — running ${redirectAction} in ${displayPath(redirectTo)} ` +
908
+ `instead of ${displayPath(appRoot)}.\x1b[0m`,
909
+ );
910
+ appRoot = redirectTo;
911
+ }
912
+
913
+ const action = resolveAction(args.action, appRoot);
914
+
915
+ // Zero-arg safe-gate: when `add` was resolved implicitly (no action typed)
916
+ // on a freshly-scaffolded CocoaPods project, imply `--deintegrate` so the
917
+ // common "new app → SPM" path is one command. Never on an explicit `spm add`
918
+ // (that stays strict and fails loud on CocoaPods).
919
+ if (
920
+ action === 'add' &&
921
+ args.action == null &&
922
+ !args.deintegrate &&
923
+ shouldAutoDeintegrate(
924
+ appRoot,
925
+ resolveInjectionTarget(args, appRoot).path ?? null,
926
+ )
927
+ ) {
928
+ log(
929
+ 'Detected a freshly-scaffolded CocoaPods project — converting to SwiftPM ' +
930
+ '(running `pod deintegrate`). Revert with `git` or `spm deinit`.',
931
+ );
932
+ args.deintegrate = true;
933
+ }
934
+
935
+ log(`Running SPM ${action} in: ${displayPath(appRoot)}`);
936
+ if (projectRoot !== appRoot) {
937
+ log(`Project root (package.json): ${displayPath(projectRoot)}`);
938
+ }
939
+
940
+ if (action === 'deinit') {
941
+ const xcodeprojPath =
942
+ args.xcodeprojPath != null
943
+ ? path.resolve(appRoot, args.xcodeprojPath)
944
+ : findInjectedXcodeproj(appRoot);
945
+ if (xcodeprojPath == null) {
946
+ log('No SPM injection found — nothing to remove.');
947
+ return;
948
+ }
949
+ const result = removeSpmInjection({appRoot, xcodeprojPath});
950
+ log(
951
+ result.status === 'removed'
952
+ ? `Removed SPM packages from ${path.basename(xcodeprojPath)}.`
953
+ : 'No SPM injection found — nothing to remove.',
954
+ );
955
+ return;
956
+ }
957
+
958
+ // Fail fast on a CocoaPods-integrated target BEFORE the (expensive) pipeline,
959
+ // so the user isn't made to wait through codegen + artifact download only to
960
+ // be told to re-run with --deintegrate. Skipped when --deintegrate is set
961
+ // (explicitly or via the safe-gate) — deintegration happens in setupXcodeproj.
962
+ if ((action === 'add' || action === 'update') && !args.deintegrate) {
963
+ const target = resolveInjectionTarget(args, appRoot);
964
+ if (target.path != null && pbxprojUsesCocoaPods(target.path)) {
965
+ logError(
966
+ `${path.basename(target.path)} is CocoaPods-integrated. Re-run ` +
967
+ '`spm add --deintegrate` to convert it (runs `pod deintegrate` + ' +
968
+ 'strips React Native from the Podfile), or run `pod deintegrate` ' +
969
+ 'yourself first. Side-by-side non-RN pods are fine.',
970
+ );
971
+ process.exitCode = 1;
972
+ return;
973
+ }
974
+ }
975
+
976
+ const needsCliConfig =
977
+ action === 'add' ||
978
+ action === 'update' ||
979
+ action === 'sync' ||
980
+ action === 'scaffold';
981
+ let autolinkingConfigResult /*: ?AutolinkingConfigResult */ = null;
982
+ if (needsCliConfig) {
983
+ log('Generating autolinking.json (CLI config)...');
984
+ try {
985
+ autolinkingConfigResult = generateAutolinkingConfig({projectRoot});
986
+ log(
987
+ `Wrote ${path.relative(appRoot, autolinkingConfigResult.outputPath)}`,
988
+ );
989
+ } catch (e) {
990
+ logError(
991
+ `generate-spm-autolinking-config failed: ${e.message}. External native modules may not be discovered.`,
992
+ );
993
+ }
994
+ }
995
+ const reactNativeRoot = resolveReactNativeRoot(
996
+ autolinkingConfigResult,
997
+ projectRoot,
998
+ );
999
+ const version = determineVersion(args, reactNativeRoot);
1000
+ log(`React Native version: ${version}`);
1001
+
1002
+ // Resolve remote SPM mode ONCE up front. remotePackageConfig throws
1003
+ // RemoteVersionError when remote mode is active but no usable RN version can
1004
+ // be derived (e.g. the monorepo '1000.0.0' placeholder with no override).
1005
+ // The downstream scaffold/autolinker/package steps all call it internally;
1006
+ // surfacing it here gives a single, predictable failure point before any of
1007
+ // them run. Exit 2 (same as a missing manifest) so the Xcode build phase
1008
+ // turns it into a hard build error while staying lenient on transient sync
1009
+ // failures. No-op in local mode (returns null).
1010
+ try {
1011
+ remotePackageConfig(appRoot);
1012
+ } catch (e) {
1013
+ if (e instanceof RemoteVersionError) {
1014
+ logError(e.message);
1015
+ process.exitCode = 2;
1016
+ return;
1017
+ }
1018
+ throw e;
1019
+ }
1020
+ // The artifact cache directory is resolved later in ensureArtifacts so the
1021
+ // nightly hash can be folded in for dev / nightly labels. That branch logs
1022
+ // either "Downloading xcframework artifacts (slot: ...)" or
1023
+ // "Artifacts already present in ...".
1024
+
1025
+ if (action === 'codegen') {
1026
+ runCodegenStep(projectRoot, appRoot, reactNativeRoot, false);
1027
+ return;
1028
+ }
1029
+
1030
+ if (action === 'sync') {
1031
+ const {main: runSync} = require('./spm/sync-spm-autolinking');
1032
+ try {
1033
+ await runSync([
1034
+ '--app-root',
1035
+ appRoot,
1036
+ '--react-native-root',
1037
+ reactNativeRoot,
1038
+ ]);
1039
+ } catch (e) {
1040
+ if (e instanceof MissingManifestError) {
1041
+ // The per-dep `error:` lines were already printed by the autolinker.
1042
+ // Exit 2 (distinct from generic failure) so the Xcode build phase can
1043
+ // turn this into a hard build error while staying lenient on transient
1044
+ // sync failures.
1045
+ process.exitCode = 2;
1046
+ } else {
1047
+ logError(`SPM sync failed: ${e.message}`);
1048
+ process.exitCode = 1;
1049
+ }
1050
+ }
1051
+ return;
1052
+ }
1053
+
1054
+ if (action === 'download') {
1055
+ try {
1056
+ await ensureBothArtifactFlavors(args, version);
1057
+ } catch (e) {
1058
+ logError(`Artifact setup failed: ${e.message}`);
1059
+ process.exitCode = 1;
1060
+ }
1061
+ return;
1062
+ }
1063
+
1064
+ // Scaffold Package.swift for community RN packages that don't ship SPM
1065
+ // support — ONLY for the explicit `scaffold` action. init/update never
1066
+ // auto-scaffold: a missing manifest must surface as a hard error (the
1067
+ // autolinker below throws MissingManifestError → exit 2) so the gap is
1068
+ // visible and fixed deliberately (scaffold + patch-package, or upstream).
1069
+ // Auto-scaffolding would silently hide that real error.
1070
+ if (action === 'scaffold') {
1071
+ await runScaffold(args, appRoot, projectRoot, reactNativeRoot);
1072
+ }
1073
+
1074
+ runCodegenStep(projectRoot, appRoot, reactNativeRoot, args.skipCodegen);
1075
+ log('Generating build/generated/autolinking/Package.swift...');
1076
+ try {
1077
+ generateAutolinking([
1078
+ '--app-root',
1079
+ appRoot,
1080
+ '--react-native-root',
1081
+ reactNativeRoot,
1082
+ ]);
1083
+ } catch (e) {
1084
+ if (e instanceof MissingManifestError) {
1085
+ // Per-dep `error:` lines already printed by the autolinker. This happens
1086
+ // on init/update when the user declined the scaffold prompt — surface it
1087
+ // as a hard failure (exit 2) directing them to scaffold.
1088
+ process.exitCode = 2;
1089
+ } else {
1090
+ logError(`generate-spm-autolinking.js failed: ${e.message}`);
1091
+ process.exitCode = 1;
1092
+ }
1093
+ return;
1094
+ }
1095
+
1096
+ const remote = remotePackageConfig(appRoot);
1097
+ if (remote != null) {
1098
+ log(`Remote ReactNative package: ${remote.url} @ ${remote.version}`);
1099
+ }
1100
+ let artifactDirs;
1101
+ try {
1102
+ artifactDirs = await ensureBothArtifactFlavors(args, version);
1103
+ } catch (e) {
1104
+ logError(`Artifact setup failed: ${e.message}`);
1105
+ process.exitCode = 1;
1106
+ return;
1107
+ }
1108
+ try {
1109
+ generateXcframeworksPackage(
1110
+ args,
1111
+ appRoot,
1112
+ reactNativeRoot,
1113
+ version,
1114
+ artifactDirs,
1115
+ );
1116
+ } catch (e) {
1117
+ logError(`generate-spm-package.js failed: ${e.message}`);
1118
+ process.exitCode = 1;
1119
+ return;
1120
+ }
1121
+
1122
+ // (Re)install the static codegen Package.swift template once build/generated/ios exists.
1123
+ installSpmCodegenTemplate(appRoot, reactNativeRoot, {log});
1124
+
1125
+ // Build the per-app generated-headers farm (vended as the ReactAppHeaders
1126
+ // SPM target inside the codegen package). React core headers need no trees
1127
+ // — they are vended by invariant SwiftPM compile products. The
1128
+ // generated manifests are fully declarative (fixed-relative package paths),
1129
+ // so no path-locator JSON is written.
1130
+ buildPerAppHeaderTree(appRoot, {log});
1131
+
1132
+ // First-time setup only adds the gitignore entries on `add`.
1133
+ if (action === 'add') {
1134
+ ensureGitignoreSpmEntries(appRoot);
1135
+ }
1136
+
1137
+ // Xcodeproj setup: in-place injection into the existing project (the only
1138
+ // strategy — no rename, no from-scratch; git is the safety net).
1139
+ try {
1140
+ await setupXcodeproj(args, appRoot, reactNativeRoot, action);
1141
+ } catch (e) {
1142
+ logError(`xcodeproj setup failed: ${e.message}`);
1143
+ if (process.exitCode == null) {
1144
+ process.exitCode = 1;
1145
+ }
1146
+ return;
1147
+ }
1148
+
1149
+ finalizeArtifactPublication(appRoot);
1150
+
1151
+ logNextSteps(projectRoot, appRoot, args.productName);
1152
+ }
1153
+
1154
+ if (require.main === module) {
1155
+ void main();
1156
+ }
1157
+
1158
+ module.exports = {
1159
+ main,
1160
+ detectStandardRnLayoutRedirect,
1161
+ findInjectedXcodeproj,
1162
+ resolveAction,
1163
+ shouldAutoDeintegrate,
1164
+ ensureBothArtifactFlavors,
1165
+ };