Skip to content

Commit 233fb49

Browse files
committed
feat(spm) updated docs.
1 parent 23c6e63 commit 233fb49

2 files changed

Lines changed: 202 additions & 132 deletions

File tree

packages/react-native/scripts/spm/__doc__/rfc-spm-xcframework.md

Lines changed: 121 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,24 @@ of React Native internals and enabling fast, reproducible builds.
2020

2121
```bash
2222
npx react-native init MyApp
23-
cd MyApp/ios
24-
open MyApp-SPM.xcodeproj
25-
# Build and run from Xcode — no pod install, no Ruby toolchain
23+
cd MyApp
24+
npx react-native spm # auto-detects first-run → init; prompts to rename legacy CocoaPods xcodeproj
25+
npm run ios
2626
```
2727

2828
A future CLI integration (e.g., an `--ios-build-system spm` flag on
2929
`react-native init`) could run `react-native spm init` automatically as part
30-
of project creation.
30+
of project creation, eliminating the manual step.
3131

3232
### Existing project
3333

3434
```bash
35-
cd MyApp/ios
36-
react-native spm init
37-
# CocoaPods setup continues to work — both can coexist
38-
open MyApp-SPM.xcodeproj
35+
cd MyApp
36+
npx react-native spm
37+
# Prompted: rename CocoaPods MyApp.xcodeproj → MyApp.xcodeproj.legacy?
38+
# Accept (Y) — the SPM xcodeproj writes to the now-free MyApp.xcodeproj slot,
39+
# `npm run ios` resolves to it unambiguously. The legacy stays on disk
40+
# (git mv tracks the rename cleanly) for rollback via `spm clean --project`.
3941
```
4042

4143
After initial setup, day-to-day development requires no extra commands. Adding
@@ -75,10 +77,14 @@ custom workarounds. First-class SPM support might help overcoming this barrier.
7577

7678
### Compatibility
7779

78-
CocoaPods continues to work during the transition period. The SPM workflow
79-
generates a separate `AppName-SPM.xcodeproj` that coexists with the existing
80-
Pods-based project. Teams can migrate at their own pace before CocoaPods trunk
81-
goes read-only in December 2026.
80+
The SPM workflow generates an `AppName.xcodeproj` that takes the same
81+
filename slot as the legacy CocoaPods xcodeproj. On `init`, the script
82+
prompts to rename the existing CocoaPods project to `AppName.xcodeproj.legacy`
83+
— preserving it for rollback while letting the community CLI's
84+
`findXcodeProject` resolve `npm run ios` to the SPM project unambiguously.
85+
Teams can migrate at their own pace before CocoaPods trunk goes read-only
86+
in December 2026, and `spm clean --project` reverses the migration when
87+
needed.
8288

8389
## Detailed design
8490

@@ -99,42 +105,48 @@ goes read-only in December 2026.
99105
└──────────────────┬──────────────────────────────┘
100106
│ symlink
101107
102-
┌─────────────────────────────────────────────────┐
103-
│ App Root │
104-
│ ├── Package.swift (committed) │
105-
│ ├── AppName-SPM.xcodeproj/ (committed) │
106-
│ ├── autolinked/ (generated) │
107-
│ │ ├── Package.swift │
108-
│ │ └── sources/ (symlinks) │
109-
│ └── build/ │
110-
│ ├── generated/ios/ (codegen) │
111-
│ └── xcframeworks/ (symlinks) │
112-
│ ├── Package.swift │
108+
┌──────────────────────────────────────────────────┐
109+
│ App ios/ │
110+
│ ├── AppName.xcodeproj/ (committed) │
111+
│ │ └── .spm-managed (marker file) │
112+
│ ├── AppName.xcodeproj.legacy/ (committed if │
113+
│ │ rename was │
114+
│ │ accepted) │
115+
│ └── build/ │
116+
│ ├── generated/ │
117+
│ │ ├── autolinking/ (generated) │
118+
│ │ │ ├── Package.swift │
119+
│ │ │ ├── autolinking.json │
120+
│ │ │ └── packages/ (synth wrappers) │
121+
│ │ └── ios/ (codegen) │
122+
│ └── xcframeworks/ (symlinks) │
123+
│ ├── Package.swift │
113124
│ ├── React.xcframework -> cache │
114125
│ ├── ReactNativeDependencies.xcframework │
115126
│ └── hermes-engine.xcframework │
116-
└─────────────────────────────────────────────────┘
127+
└─────────────────────────────────────────────────
117128
```
118129

119130
### Pipeline
120131

121-
`react-native spm` orchestrates five steps (the underlying script is
132+
`react-native spm` orchestrates six steps (the underlying script is
122133
`scripts/setup-apple-spm.js`):
123134

124135
| # | Step | Script | Output |
125136
|---|------|--------|--------|
126-
| 1 | Codegen | `generate-codegen-artifacts.js` | `build/generated/ios/` |
127-
| 2 | Autolinking | `spm/generate-spm-autolinking.js` | `autolinked/Package.swift` + source symlinks |
128-
| 3 | Download | `spm/download-spm-artifacts.js` | Cached xcframeworks |
129-
| 4 | Package | `spm/generate-spm-package.js` | `build/xcframeworks/Package.swift` + symlinks |
130-
| 5 | Xcodeproj | `spm/generate-spm-xcodeproj.js` | `AppName-SPM.xcodeproj` |
131-
|| Sync (build-time) | `spm/sync-spm-autolinking.js` | Re-runs steps 1–4 when inputs change (downloads artifacts if missing) |
132-
133-
When run with the `init` action, the script also appends SPM-specific entries to the
134-
project's `.gitignore` (e.g., `autolinked/`, `build/generated/ios/`,
135-
`build/xcframeworks/`, `.build/`, `Package.resolved`). Entries that already
136-
exist are not duplicated. This ensures generated artifacts are not accidentally
137-
committed.
137+
| 1 | CLI config | `spm/generate-spm-autolinking-config.js` | `build/generated/autolinking/autolinking.json` |
138+
| 2 | Codegen | `generate-codegen-artifacts.js` | `build/generated/ios/` |
139+
| 3 | Autolinking | `spm/generate-spm-autolinking.js` | `build/generated/autolinking/Package.swift` + source symlinks |
140+
| 4 | Download | `spm/download-spm-artifacts.js` | Cached xcframeworks |
141+
| 5 | Package | `spm/generate-spm-package.js` | `build/xcframeworks/Package.swift` + symlinks |
142+
| 6 | Xcodeproj | `spm/generate-spm-xcodeproj.js` | `AppName.xcodeproj` + `.spm-managed` marker (`init` only; create-if-missing on subsequent runs) |
143+
|| Sync (build-time) | `spm/sync-spm-autolinking.js` | Re-runs steps 1–5 when inputs change (downloads artifacts if missing) |
144+
145+
The `init` action additionally (a) prompts to rename any existing
146+
CocoaPods `<App>.xcodeproj` to `<App>.xcodeproj.legacy` before step 6, and
147+
(b) appends SPM-specific entries to `.gitignore`
148+
(`build/generated/`, `build/xcframeworks/`, `.build/`, `Package.resolved`).
149+
Existing entries are not duplicated.
138150

139151
### Auto-sync build phase
140152

@@ -173,17 +185,26 @@ builds recover automatically.
173185
### Cleaning generated SPM state
174186

175187
Xcode's "Clean Build Folder" (Cmd+Shift+K) only removes DerivedData — it does
176-
not touch the project's `build/`, `autolinked/`, or `.build/` directories.
177-
Xcode provides no hook to run custom scripts during GUI clean actions.
188+
not touch the project's `build/` or `.build/` directories. Xcode provides no
189+
hook to run custom scripts during GUI clean actions.
178190

179-
To fully reset SPM state, run:
191+
`react-native spm clean` is scoped by opt-in flags. The default removes only
192+
generated dirs under `appRoot`:
180193

181194
```bash
182-
react-native spm clean
195+
react-native spm clean # build/xcframeworks/, build/generated/, .build/
196+
react-native spm clean --project # also: delete SPM xcodeproj, restore .legacy backup
197+
react-native spm clean --derived-data # also: this app's Xcode DerivedData entries
198+
react-native spm clean --cache # also: cached xcframework slot for current version
199+
react-native spm clean --all # = --project --derived-data --cache
183200
```
184201

185-
This removes `build/xcframeworks/`, `build/generated/ios/`, `autolinked/`, and
186-
`.build/`. Then run `react-native spm update` (or open the checked-in
202+
Destructive scopes (`--project`, `--derived-data`, `--cache`, `--all`) prompt
203+
for confirmation (bypass with `--yes`). `--project` is the reverse of the
204+
init-time rename migration — deleting the SPM xcodeproj and restoring
205+
`<App>.xcodeproj.legacy` to its original filename if a backup exists.
206+
207+
After a plain `clean`, run `react-native spm update` (or open the checked-in
187208
`.xcodeproj` and build) to regenerate state. SPM package resolution is locked
188209
for the duration of a build — if only stubs were left in place, Xcode would
189210
resolve stubs and never pick up the real packages generated by the sync build
@@ -223,37 +244,34 @@ for CI environments where a specific path must be persisted across builds.
223244

224245
### Package graph
225246

226-
SPM resolves three local packages as dependencies of the app target:
247+
The generated `<App>.xcodeproj` references three local packages directly
248+
via `XCLocalSwiftPackageReference` — no app-level `Package.swift` is required:
227249

228250
```
229-
AppName-SPM.xcodeproj
230-
└── Package.swift (app root — committed, user-owned)
231-
├── build/xcframeworks/Package.swift
232-
│ ├── ReactNative (product, wraps React binaryTarget)
233-
│ ├── ReactNativeDependencies (binaryTarget)
234-
│ └── hermes-engine (binaryTarget)
235-
├── build/generated/ios/Package.swift
236-
│ ├── ReactCodegen (target — codegen output)
237-
│ └── ReactAppDependencyProvider (target)
238-
├── autolinked/Package.swift
239-
│ └── <AutolinkedModule>... (targets — symlinked sources)
240-
└── build/generated/ios/ (codegen source path)
251+
AppName.xcodeproj
252+
├── XCLocalSwiftPackageReference → build/xcframeworks/Package.swift
253+
│ ├── ReactNative (product, wraps React binaryTarget)
254+
│ ├── ReactNativeDependencies (binaryTarget)
255+
│ └── hermes-engine (binaryTarget)
256+
├── XCLocalSwiftPackageReference → build/generated/ios/Package.swift
257+
│ ├── ReactCodegen (target — codegen output)
258+
│ └── ReactAppDependencyProvider (target)
259+
└── XCLocalSwiftPackageReference → build/generated/autolinking/Package.swift
260+
└── <AutolinkedModule>... (targets — symlinked sources)
241261
```
242262

243-
The root `Package.swift` is generated once (`react-native spm init`) and committed. Developers
244-
can customize it — add dependencies, adjust settings, or add new targets.
245-
Subsequent runs only regenerate the sub-packages (`autolinked/` and
246-
`build/xcframeworks/`).
247-
248-
Both `Package.swift` and `AppName-SPM.xcodeproj` are generated once during
249-
`react-native spm init` and committed. This is typically done by project
250-
scaffolding tools (`react-native init`, `expo init`) rather than by developers
251-
manually. After initial generation, these files are user-owned — subsequent
252-
`react-native spm update` runs only regenerate the sub-packages, not the root
253-
`Package.swift` or `.xcodeproj`. Teammates can clone the repo and open Xcode
254-
immediately — stub packages allow package resolution to succeed, and the
255-
auto-sync build phase downloads artifacts and handles autolinking on the first
256-
build.
263+
These three sub-package paths are **stable**: adding or removing community
264+
deps changes the contents of `build/generated/autolinking/Package.swift`
265+
(gitignored) but never the xcodeproj's references. That's why the
266+
`.xcodeproj` is committed once and not regenerated on subsequent runs.
267+
268+
The xcodeproj generation is **create-if-missing** on `update` (use
269+
`--force-xcodeproj` for an explicit overwrite). This protects user-side
270+
Xcode edits — signing, capabilities, Build Phases, scheme settings — from
271+
being clobbered. Teammates can clone the repo and open Xcode immediately:
272+
stub `Package.swift` files in each sub-package directory let SPM resolution
273+
succeed before the first build, and the auto-sync build phase downloads
274+
artifacts and writes the real sub-packages on first compile.
257275

258276
### Header resolution
259277

@@ -458,14 +476,16 @@ and network access, the `.xcodeproj` could be eliminated in favor of a purely
458476
SPM-native workflow — but this is a future direction that depends on Apple's
459477
roadmap, not something this proposal can resolve.
460478

461-
### User-owned `Package.swift`
479+
### Committed xcodeproj edits
462480

463-
The root `Package.swift` is generated once and committed. It only references
464-
sub-packages by relative path, so React Native upgrades do not require changes
465-
to it. However, developers who customize it (adding dependencies or targets)
466-
must understand the generated package structure. In rare cases where the
467-
sub-package layout changes across a major version, the root file may need
468-
manual updates.
481+
The generated `<App>.xcodeproj` is committed and may carry user edits —
482+
signing, capabilities, Build Phases, custom schemes. The `update` action is
483+
**create-if-missing** to protect those edits, which means the script does
484+
not propagate generator improvements into existing projects automatically.
485+
Bug fixes that change the emitted pbxproj need an explicit
486+
`--force-xcodeproj` run to take effect. A future improvement could
487+
preserve user-side edits through a merge step rather than full overwrite —
488+
see "Hardening `update --force-xcodeproj`" in unresolved questions.
469489

470490
## Alternatives
471491

@@ -541,11 +561,18 @@ required; libraries that don't provide them fall back to source compilation.
541561

542562
### Migration path for existing apps
543563

544-
1. Run `react-native spm init` in the `ios/` directory.
545-
2. Commit the generated `Package.swift` and `AppName-SPM.xcodeproj`.
546-
3. Open the new `.xcodeproj` and build.
547-
4. Once validated, optionally remove CocoaPods files (`Podfile`, `Pods/`,
548-
`.xcworkspace`).
564+
1. Run `npx react-native spm` from the project root (auto-redirects into
565+
`ios/`).
566+
2. Accept the rename prompt — your existing `AppName.xcodeproj` becomes
567+
`AppName.xcodeproj.legacy` (preserved for rollback).
568+
3. Commit the new `AppName.xcodeproj/` (SPM-managed) and the renamed
569+
`AppName.xcodeproj.legacy/`. `git mv` tracks the rename cleanly.
570+
4. Run `npm run ios` and verify the SPM build.
571+
5. Once validated, optionally delete the `.legacy` backup, `Podfile`,
572+
`Pods/`, and `.xcworkspace`.
573+
574+
To roll back: `npx react-native spm clean --project` deletes the SPM
575+
xcodeproj and renames `.legacy` back to the canonical filename.
549576

550577
No changes to JavaScript code, Metro configuration, or Android setup are
551578
required.
@@ -556,10 +583,10 @@ After upgrading `react-native` in `package.json` and running `npm install`,
556583
the auto-sync build phase detects the `node_modules` mtime change on the next
557584
Xcode build and re-runs the sync step automatically. This downloads the new
558585
version's xcframeworks, regenerates the sub-packages, and updates autolinking.
559-
No manual edits to `Package.swift` are needed — the root `Package.swift` only
560-
references sub-packages by relative path, and those sub-packages are fully
561-
regenerated each run. Developers can also run `react-native spm` manually to
562-
trigger the update before building.
586+
No manual edits are needed — the xcodeproj's sub-package references are
587+
stable, and those sub-packages are fully regenerated each run. Developers
588+
can also run `react-native spm` manually to trigger the update before
589+
building.
563590

564591
## How we teach this
565592

@@ -622,14 +649,14 @@ trigger the update before building.
622649
sources compile as an SPM target without producing a full release artifact.
623650
This would be useful for CI checks on pull requests.
624651

625-
5. **Hardening `init` for existing projects.** Running `init` on a project
626-
that already has `Package.swift` or `.xcodeproj` should detect existing
627-
files and avoid overwriting user modifications. The current `.xcodeproj`
628-
generation uses a simple template-based approach; adopting a proper Xcode
629-
project parser/generator (e.g., `@bacons/xcode`) would enable safer
630-
incremental updates — reading the existing project, merging changes, and
631-
writing back without losing manual edits. This is planned work for
632-
production readiness.
652+
5. **Hardening `update --force-xcodeproj`.** The default `update` is
653+
create-if-missing, which preserves user edits but means generator
654+
improvements don't propagate to existing projects automatically. Passing
655+
`--force-xcodeproj` clobbers everything. A future improvement could read
656+
the existing pbxproj, merge changes (signing, capabilities, custom Build
657+
Phases), and write back — likely via a proper Xcode project
658+
parser/generator (e.g., `@bacons/xcode`) rather than the current
659+
template-based approach. Planned work for production readiness.
633660

634661
6. **Auto-sync failure visibility.** The sync build phase currently emits
635662
`warning:` and exits 0 on failure, which means a broken autolinking state

0 commit comments

Comments
 (0)