packages/react-native/scripts/spm/__doc__/spm-scripts.md
Preview. SwiftPM support is an early preview: the commands, flags, generated layout, and distribution model may change in future releases, and it is not yet recommended for production. Feedback is welcome. CocoaPods remains the supported default.
Build React Native iOS apps using Swift Package Manager with prebuilt
XCFrameworks, as an alternative to CocoaPods. It is opt-in and additive —
CocoaPods remains the default; spm injects into your existing .xcodeproj
in place and is fully reversible.
cd ios
# First-time setup: injects SwiftPM packages into your existing MyApp.xcodeproj,
# in place. `npx react-native spm` with no action auto-resolves to `add` (or
# `update` once injected); on a fresh CocoaPods app it converts in one command
# (implies --deintegrate). To do it explicitly:
npx react-native spm add --deintegrate
# Open in Xcode (or `npm run ios`). Incremental dep changes auto-sync on build.
open MyApp.xcodeproj
After the initial run, the .xcodeproj includes an auto-sync build phase
that detects dependency changes and re-runs autolinking before compilation
(see Auto-Sync) — you don't re-invoke
react-native spm manually for day-to-day dependency changes. On a fresh
clone or CI checkout, run npx react-native spm once before building (see
Fresh clones & CI).
Note:
react-native spmis a thin wrapper overnode node_modules/react-native/scripts/setup-apple-spm.js. If the CLI alias is unavailable in your environment, invoke the script directly with the same actions and the kebab-case flag equivalents (e.g.--skip-codegen).
spm add injects into a project that is not CocoaPods-integrated. On a
CocoaPods app it fails loud and points you at --deintegrate, which:
pod deintegrate — removes CocoaPods integration from the
.xcodeproj (Pods references, [CP] build phases, xcconfig links). Your
Podfile is left on disk.use_react_native!,
use_native_modules!, prepare_react_native_project!) from the Podfile —
every other line, including your own pod '…' entries, is preserved..xcodeproj.React Native now comes from SwiftPM; no pods are linked yet (deintegrate removed the integration).
Non-RN pods can stay side-by-side. After spm add --deintegrate your Podfile
still lists them (only the RN directives were removed) — re-integrate them
with a normal install:
pod install # re-integrates the remaining (non-RN) pods; (re)creates the .xcworkspace
Then open the .xcworkspace (not the .xcodeproj): the workspace includes
the SwiftPM-injected project, so React Native resolves through SwiftPM and your
other pods through CocoaPods, together.
Do not re-add
use_react_native!. React Native must be provided by either SwiftPM or CocoaPods, never both — they sharebuild/generated/, so a dual-managed RN does not build.spm addrefuses to run while the Podfile still declaresuse_react_native!.
The migration is fully reversible — see Removing / resetting.
spm add injects into your existing .xcodeproj in place, so an app that
embeds React Native works the same way — point it at the right project and
target:
npx react-native spm add --xcodeproj MyApp.xcodeproj --productName MyApp
Requirement: the .xcodeproj must live inside the React Native JS tree
— i.e. the app's package.json is a parent directory of the project. Both
setup and the build-time sync locate React Native by walking up from the
project to the nearest package.json. The common "native project at the repo
root with the RN JS in a sibling/child subfolder" layout is not supported
yet — there is no way to point at a JS root outside the project's ancestors.
Brownfield apps that keep CocoaPods for their other native dependencies follow
the coexistence rules above: React Native from SwiftPM,
everything else from CocoaPods, and no use_react_native! in the Podfile.
react-native spm [action] [options]
With no action, the command auto-resolves: if SwiftPM has been injected
(.spm-injected.json marker present) it routes to update; otherwise add.
On a freshly-scaffolded CocoaPods project (clean git tree, stock Podfile) the
zero-arg path additionally implies --deintegrate (the safe-gate), so
npx react-native spm converts a brand-new app to SwiftPM in one command.
When invoked from the JS root of a standard RN app (sibling ios/ subdir),
the command auto-redirects into ios/ with a banner.
| Action | Description |
|---|---|
add | Inject SwiftPM packages (package refs, build settings, the Sync build phase) into the existing .xcodeproj, in place. Idempotent. Default on first run. --deintegrate first runs pod deintegrate + strips React Native from the Podfile. |
update | Re-run the pipeline and refresh the existing injection. Default once a project is injected. |
deinit | The exact inverse of add: surgically remove only what add injected (recorded in .spm-injected.json) and drop the marker. Git-recoverable; no prompt. |
scaffold | Generate Package.swift into node_modules/<dep>/ for community RN libraries that ship only a podspec. |
sync (advanced) | Lightweight resync invoked by the Xcode auto-sync build phase. Regenerates invariant codegen and autolinking output only. Not for humans. |
codegen (advanced) | Run codegen and install the SwiftPM codegen template only. |
download (advanced) | Download/check xcframework artifacts only. |
Flags below use the react-native spm (camelCase) form. The raw script
accepts kebab-case equivalents (e.g. --skip-codegen).
| Option | Description |
|---|---|
--version <ver> | RN version (default: from package.json) |
--yes | Skip the dirty-pbxproj confirmation prompt |
--xcodeproj <path> | [add] Which .xcodeproj to inject into (when several exist) |
--productName <name> | [add] Which app target to inject into (when several exist) |
--deintegrate | [add] Run pod deintegrate + strip React Native from the Podfile before injecting |
--artifacts <path> | [advanced] Local artifact root containing complete debug/ and release/ cache slots |
--download <auto|skip|force> | [advanced] Artifact download policy (default: auto) |
--skipCodegen | [advanced] Skip the codegen step |
--configCommand <json> | [advanced] JSON array of the argv used to generate autolinking.json, overriding the default @react-native-community/cli config command. Also settable via the RCT_SPM_AUTOLINKING_CONFIG_COMMAND env var. Either way the value is remembered, so you pass it once. Example: '["npx","expo-modules-autolinking","react-native-config","--json","--platform","ios"]' |
An app that replaces @react-native-community/cli autolinking (an Expo app,
for example) has to tell spm how to produce autolinking.json. Pass the
command once, on add or update:
npx react-native spm add --configCommand '["npx","expo-modules-autolinking","react-native-config","--json","--platform","ios"]'
Every action that needs autolinking.json — add, update, scaffold, and
the build-time sync — resolves the command in this order:
--configCommandRCT_SPM_AUTOLINKING_CONFIG_COMMANDconfigCommand pinned in MyApp.xcodeproj/.spm-injected.json by an
earlier add/update@react-native-community/cli configadd/update pin whichever of the first two routes supplied the command,
validated as an argv array; a later run that passes neither keeps the existing
pin, and passing --configCommand again replaces it. The pin exists because
the Sync SPM Autolinking build phase inherits neither your flag nor the
shell that exported the env var — without it, a successful add is followed by
failing builds, because the phase re-derives autolinking.json with the
default command. A pin never shadows the env var, so an override in your shell
still takes effect, and a pin that no longer parses is ignored in favor of the
default.
deinit deletes .spm-injected.json, and the pin with it. A later add
therefore falls back to the default command unless you pass --configCommand
(or export the env var) again.
React Native ships flavored prebuilt binaries: the debug React.framework
(and hermesvm / ReactNativeDependencies) carry the dev experience — dev menu,
assertions, RN_DEBUG_STRING_CONVERTIBLE — while release strips them for
production. A Debug build must embed the debug binaries and a Release/archive the
release ones.
SwiftPM binaryTargets can't branch on the build configuration, so runtime
frameworks are deliberately kept out of the package graph. spm add downloads
and validates both flavors into immutable app-local slots. It injects
SDK/architecture-qualified Xcode settings that link the exact selected binaries,
plus one phase that copies and signs the selected frameworks into the app.
Configurations containing debug or development select Debug; every other
configuration selects Release. Selection uses only generated build settings and
standard macOS tools: builds do not run Node, mutate symlinks, regenerate the
package graph, or require a second build.
| Path | Commit? | Why |
|---|---|---|
MyApp.xcodeproj/ | Yes | Your project, with SwiftPM injected in place. Holds your signing, capabilities, Build Phases — add only adds SwiftPM refs/settings, additively. |
MyApp.xcodeproj/.spm-injected.json | Yes | Marker recording every edit add made, so deinit can surgically reverse it and re-runs stay idempotent. Also pins settings later runs and Xcode builds must reuse, such as the autolinking config command. |
build/generated/ | No | Codegen/autolinking output; regenerated |
build/xcframeworks/ | No | Symlinks to the machine-local artifact cache |
Package.resolved | No | SwiftPM resolution file; machine-specific |
Injection is purely additive and idempotent: add/update insert only
SwiftPM package refs, the React build settings, the Sync build phase, and a scheme
pre-action — every other byte (your signing / capabilities / Build Phases)
stays untouched, and a re-run is a no-op. The injected refs point at three
stable sub-package paths under build/; adding or removing community deps
changes the sub-package contents (gitignored) and never re-injects. deinit
removes exactly what was injected (using the marker), leaving the project
byte-identical to its pre-add state.
Because everything under build/ is gitignored, a clean checkout has no
resolvable Swift packages until they are regenerated — see the next section.
Xcode resolves the Swift package graph before any build phase runs, so on a
clean checkout (where the gitignored build/ packages don't exist yet) the
auto-sync build phase can't regenerate them in time — a bare xcodebuild
fails at "Resolve Package Graph … build/generated/autolinking doesn't exist".
Run the setup command once after cloning, before building — the SwiftPM analog
of pod install:
npx react-native spm # downloads artifacts (if missing) + regenerates build/
On an already-injected project this routes to update: it fetches the
xcframework artifacts into the shared cache if they aren't present and
regenerates build/xcframeworks + build/generated. After this first run,
incremental dependency changes are picked up automatically by the auto-sync
build phase.
Automate it so nobody has to remember — add a postinstall hook, which
runs as part of the npm install / yarn install your CI already does before
xcodebuild:
{
"scripts": {
"postinstall": "react-native spm"
}
}
npx react-native spm auto-redirects from the JS root into ios/, so the hook
works from the app root; in CI (non-interactive) it proceeds without prompting.
It re-runs the full pipeline (codegen + an idempotent re-inject that is a no-op
when nothing changed), so it is slightly heavier than the internal sync the
build phase calls — a fine trade for not having to remember a command.
A future remote-package distribution (a tagged
Package.swiftrepo +binaryTarget(url:checksum:)) removes this step entirely: SwiftPM resolves and fetches the artifacts itself during normal package resolution. Until then, the one-time setup run is required on clean machines.
Modules not discovered via autolinking can be declared in react-native.config.js:
module.exports = {
spm: {
modules: [
{
name: 'MyNativeModule',
path: 'ios/MyNativeModule', // relative to app root
exclude: ['*.podspec'], // optional
publicHeadersPath: '.', // optional
},
],
},
};
Each entry becomes a target in build/generated/autolinking/Package.swift.
Sources outside build/generated/autolinking/ are automatically mirrored with
file-level symlinks.
A community library that ships its own Package.swift is referenced
directly by the autolinker instead of being wrapped. To keep SwiftPM's
package identity (which it derives from the path basename) unique across
deps — even when several libs put their manifest inside an ios/ subdir
— each self-managed dep is exposed through a uniquely-named symlink at
build/generated/autolinking/libs/<SwiftName>/. The aggregator
Package.swift references that path, so two libs both shipping
<dep>/ios/Package.swift never collide on identity "ios".
The libs/ directory is wiped and recreated on every autolinker run,
so deleting a dep via npm uninstall cleans up the alias automatically
on the next build.
If an autolinked library ships no Package.swift, the build fails with a
clear per-dep error (Package.swift is missing for library "<name>"). Generate
one from the library's podspec:
npx react-native spm scaffold # writes Package.swift into node_modules/<dep>/
Because node_modules/ isn't committed, persist it so it survives the next
install:
npx patch-package <dep> # then commit the generated patch
Better: contribute the manifest upstream. The generated Package.swift is
a normal, committable manifest — the ideal fix is for the library to ship it
itself, so every consumer gets SwiftPM support without a local patch. Please
file an issue or open a PR on the library with the scaffolded
Package.swift (mention it was generated by react-native spm scaffold for
React Native SwiftPM support). Until it lands upstream, the patch-package
workaround keeps your app building.
A library whose sources mix Swift and Objective-C/C++ in one target, or that ships neither a
Package.swiftnor a podspec, can't be scaffolded automatically — the error says so. Opt it out viareact-native.config.js(platforms.ios = null) or ask the maintainer for a prebuilt xcframework.
Frameworks with their own module system (e.g. Expo) contribute to the autolinking graph through a plugin — a function invoked on every regeneration (including the build-time sync) that adds SwiftPM package refs, product dependencies, and generated sources. Discovery is transitive (installing the framework is enough), and the plugin returns data that RN merges idempotently.
See spm-autolinking-plugins.md for the discovery mechanism, the full context/return contract, lifecycle, and failure behavior.
To remove SwiftPM entirely, use deinit (the inverse of add):
react-native spm deinit # surgically removes everything `add` injected
pod install # then, to restore CocoaPods
To reset the regenerable build state (without un-injecting), just delete the gitignored dirs and re-run:
rm -rf build/xcframeworks build/generated .build
react-native spm update
Xcode's "Clean Build Folder" (Cmd+Shift+K) only removes DerivedData — it does
not touch SwiftPM-generated directories. The cached xcframework slot is shared
across apps; refresh it with react-native spm update --download force.
| Problem | Fix |
|---|---|
xcodebuild fails: "Could not resolve package dependencies … build/generated/autolinking doesn't exist" | Fresh clone — run npx react-native spm once before building (see Fresh clones & CI) |
spm add fails: "CocoaPods-integrated project" | Re-run spm add --deintegrate (runs pod deintegrate + strips RN from the Podfile), or pod deintegrate yourself first. |
spm add fails: "no .xcodeproj found" | Create an app first (npx @react-native-community/cli init) or make a project in Xcode, then spm add. |
spm add fails: "multiple .xcodeproj found" | Pass --xcodeproj <path> (and --product-name <target> if multiple app targets). |
| Missing headers | Re-run react-native spm |
| "not contained in target" | Re-run setup (regenerates file-level symlinks) |
| Codegen fails | Use --skipCodegen to iterate on other parts |
| "SPM sync failed" warning | Check Xcode build log for details; node may not be in PATH — ensure with-environment.sh is present |
"Sync SPM Autolinking" build phase fails: 'npx --no-install @react-native-community/cli config' exited with status 1 | This app replaces @react-native-community/cli autolinking (e.g. an Expo app). Re-run spm add/update with --configCommand (or with RCT_SPM_AUTOLINKING_CONFIG_COMMAND exported) so the working command is pinned for the build phase to reuse — see The autolinking config command is remembered. |
| Autolinking not updating on build | Touch package.json to force a sync, or delete build/generated/autolinking/.spm-sync-stamp |
| Stale SwiftPM state or corrupted build | rm -rf build/ .build/, then react-native spm update, then reopen Xcode |
| Want to revert to CocoaPods | react-native spm deinit, then pod install |
react-native spm add and react-native spm update orchestrate these steps:
| Step | Script | Output |
|---|---|---|
| 1. CLI config | spm/generate-spm-autolinking-config.js | build/generated/autolinking/autolinking.json |
| 2. Codegen | generate-codegen-artifacts.js | build/generated/ios/ |
| 3. Autolinking | spm/generate-spm-autolinking.js | build/generated/autolinking/Package.swift |
| 4. Download | spm/download-spm-artifacts.js | Complete Debug and Release cache slots |
| 5. Package | spm/generate-spm-package.js | Immutable flavor slots, central manifest, canonical ReactHeaders, and invariant Package.swift |
| 6. Inject | spm/generate-spm-xcodeproj.js | Invariant SwiftPM products plus configuration-qualified linker settings and the embed/sign phase |
| Auto-sync | spm/sync-spm-autolinking.js | Re-runs invariant codegen/autolinking output only at Xcode build time |
my-app/ios/
MyApp.xcodeproj/ <-- committed (your project; SwiftPM injected in place, carries .spm-injected.json)
Podfile <-- present until `pod deintegrate` (CocoaPods coexistence is best-effort)
build/
generated/
autolinking/ <-- gitignored (regenerated at build time)
Package.swift
autolinking.json
packages/ <-- synth wrappers for autolinker-managed deps
libs/ <-- symlinks to self-managed deps' Package.swift
dirs, named by Swift module so SwiftPM
package identity stays unique
headers/ <-- generated header symlinks
ios/ <-- gitignored, codegen output
xcframeworks/ <-- gitignored, immutable runtime flavor slots + invariant package
debug/
React.xcframework -> ~/Library/Caches/.../debug/React.xcframework
ReactNativeDependencies.xcframework -> ...
hermes-engine.xcframework -> ...
release/
React.xcframework -> ~/Library/Caches/.../release/React.xcframework
ReactNativeDependencies.xcframework -> ...
hermes-engine.xcframework -> ...
ReactHeadersTarget/ <-- canonical Objective-C React headers + module map
ReactNativeHeaders.xcframework -> ...
ReactNativeDependenciesHeaders.xcframework -> ...
flavored-frameworks.json
.artifact-stamp
React Native uses CocoaPods-style imports (#import <React/RCTBridge.h>) that
SwiftPM doesn't natively support. The prebuilt artifacts serve them through SwiftPM
package products — no -I search-path flags, and no clang VFS overlay:
<React/…> and import React resolve through the invariant
ReactHeaders Clang target. It stages one canonical header copy after
proving Debug and Release expose identical public headers, and uses a plain
module React module map with React/-prefixed paths.react/ and every other RN namespace (yoga/, jsi/,
jsinspector-modern, …) comes from ReactNativeHeaders.xcframework, a
headers-only (LIBRARY-type) binaryTarget whose per-slice Headers/ SwiftPM
auto-serves to dependents.folly/, glog/, boost/, fmt/,
double-conversion/, fast_float/, SocketRocket/) come from
ReactNativeDependenciesHeaders.xcframework, the deps headers-only
sidecar (same mechanism — the binary ReactNativeDependencies.xcframework
is framework-type and can't expose those headers to SwiftPM).Targets that compile against React take these as product dependencies
(ReactHeaders, ReactNativeHeaders, ReactNativeDependenciesHeaders, plus the
app's ReactAppHeaders), so all of the above resolve with zero search-path
flags.
The generated .xcodeproj includes a Sync SPM Autolinking shell script
build phase. It keeps build/generated/autolinking/Package.swift up to date
without requiring manual re-runs of react-native spm for incremental
dependency changes. (It cannot bootstrap a fresh clone — Xcode resolves the
package graph before any phase runs; see Fresh clones & CI.)
How it works:
build/generated/autolinking/.spm-sync-stamp:
package.json — dependency declarationsreact-native.config.js — spm.modules confignode_modules/ directory mtime — updated by any package manager (npm, yarn, pnpm, bun); also checks parent node_modules for monorepo setupsbuild/xcframeworks/ (e.g. after a manual clean) also marks stalenpx react-native spm sync,
which re-executes autolinking + package generation (downloading artifacts if
the cache slot is incomplete) and writes the stamp file.Build phase ordering:
| # | Phase |
|---|---|
| 0 | Resolve Package Graph (Xcode — runs before all build phases) |
| 1 | Sync SPM Autolinking |
| 2 | Sources (compile) |
| 3 | Frameworks (link) |
| 4 | Embed React Native Flavored Frameworks |
| 5 | Resources (copy) |
| 6 | Build JS Bundle |
Failures in the sync phase are non-fatal — it emits a warning: and exits 0,
so an already-generated package graph can still produce a successful build.