Skip to content

feat: add experimental SPM support - #955

Open
krozniata wants to merge 5 commits into
mainfrom
fet/experimental-spm-support
Open

krozniata wants to merge 5 commits into
mainfrom
fet/experimental-spm-support

Conversation

@krozniata

@krozniata krozniata commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR adds an experimental --experimental-spm flag that generates a Package.swift for Turbo Modules and Fabric views, targeting React Native 0.87's Swift Package Manager support

When passing flag it adds Package.swift file to library to support SPM and also disables automatic pod installation in example app

Also adds CI workflows that create such library and build app that uses SPM package ('npx react-native spm --deintegrate`)

image

Test plan

  1. Generate example lib
  2. connect to RN87 app e.g. via tarball
  3. Run npx react-native spm --deintegrate
  4. Build app

Also CI GREEN

@krozniata
krozniata marked this pull request as ready for review October 6, 2026 07:01
@satya164

satya164 commented Oct 6, 2026

Copy link
Copy Markdown
Member

@krozniata regarding nitro, can you reach out to @mrousavy on slack to see what's the plan?

@mrousavy

mrousavy commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

I'm already actively working on it :)

@krozniata
krozniata requested a review from atlj October 9, 2026 09:09
# Conflicts:
#	packages/create-react-native-library/templates/objc-view-new/ios/{%- project.name %}View.mm
Comment on lines +3 to +9
// Must match the name React Native's SPM autolinker derives from the package name
const spmName = project.slug
.replace(/^@[^/]+\//, '')
.split(/[^a-zA-Z0-9]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: This name doesn't match what React Native 0.88 expects. The SwiftPM name now comes from swiftpmConfig.name, then the podspec name, and only then the npm name (docs). Our podspec uses project.name, so for an unscoped package like react-native-awesome-module, the app looks for AwesomeModule while we declare ReactNativeAwesomeModule. It fails in both the example app and a standalone 0.88 app with the packed library:

product 'AwesomeModule' required by package 'autolinking' target 'AutolinkedAggregate' not found in package 'AwesomeModule'.

We can use project.name here and add "swiftpmConfig": { "name": "<%- project.name -%>" } to package.json. With that, both apps build and run. Then we don't need to compute the name in the template either.

rm -rf ${{ env.work_dir }}

./packages/create-react-native-library/bin/create-react-native-library ${{ env.work_dir }} \
--slug @bob/react-native-test \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: With a scoped slug, both naming schemes give ReactNativeTest, so this job passes even though unscoped libraries fail to build (see the comment on Package.swift). Let's use an unscoped slug here so the job catches name mismatches.

`Set up ${kleur.bold('Swift Package Manager')} for the example app (React Native 0.87+)`
)}${kleur.gray(':')}

${kleur.gray('$')} cd example && npx react-native spm --deintegrate`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: After cd example, the next step (yarn example ios) fails with Couldn't find a script named "example". We can run it through the workspace script instead, so the user stays in the root:

yarn example react-native spm --deintegrate

project: {
ios: {
automaticPodsInstallation: true,
automaticPodsInstallation: <%- experimentalSpm ? 'false' : 'true' %>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: Once the example is moved to SwiftPM, the generated CI and turbo config still assume CocoaPods:

  • ci.yml runs pod install, but after --deintegrate the Podfile doesn't parse anymore (React Native 0.88.0 removes use_react_native!( and leaves its arguments behind).
  • A fresh checkout needs npx react-native spm before building, since everything it generates is gitignored (docs). React Native suggests a postinstall script for this.
  • turbo.json lists *.podspec in the build:ios inputs but not Package.swift, so changes to the manifest don't invalidate the cache.

The CI and turbo templates need to handle the SwiftPM case too.

Comment thread docs/pages/create.md
npx create-react-native-library@latest awesome-library --experimental-spm
```

The podspec is still generated, so CocoaPods keeps working. The example app is set up for SPM instead of CocoaPods: run `npx react-native spm --deintegrate` in `example` once to set it up, and `npx react-native spm` after a fresh install.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: The generated CONTRIBUTING.md still describes the CocoaPods setup. It says to open the .xcworkspace and find the sources under Pods > Development Pods, and SwiftPM creates neither. It needs SwiftPM instructions too, including running npx react-native spm after a fresh clone.

cxxSettings: [
<% if (project.cpp) { -%>
.headerSearchPath("cpp"),
.headerSearchPath("ios/generated/ReactCodegen"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: ios/generated/ReactCodegen doesn't exist since #819, and the spec header comes from the app's ReactAppHeaders now. The C++ example builds without it, so we can remove this line.

Comment on lines +146 to +151
experimentalSpm: {
type: 'confirm',
description: 'Whether to generate an experimental Swift package manifest',
message: 'Generate a Swift Package Manager manifest?',
default: false,
skip: () => !process.argv.includes('--experimental-spm'),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: Let's use skip: true like reactNativeVersion, so it's only set from the CLI and never prompted. It should also go last, or at least before reactNativeVersion, instead of first.

Comment on lines +273 to +276
} else if (answers.experimentalSpm === true && config.project.native) {
console.warn(
"Swift Package Manager manifest was not generated: Nitro libraries mix Swift and C++ sources, which Swift Package Manager can't compile in a single target."
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: The other warnings (in index.ts and generateExampleApp.ts) have a kleur.yellow('⚠') prefix and are printed from there, not from applyTemplates. This also repeats the condition from generateTemplateConfiguration, and JS-only libraries get no warning. Let's do the check and print the warning once, next to the --react-native-version warning in index.ts.

? `

${kleur.magenta(
`Set up ${kleur.bold('Swift Package Manager')} for the example app (React Native 0.87+)`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: We mix "SPM", "SwiftPM" and "Swift Package Manager" across the docs, CLI output and comments. React Native uses "Swift Package Manager (SwiftPM)" on first mention and "SwiftPM" after that, and keeps spm for the command. Let's use the same terms. This should also say 0.88+, since the name fix relies on swiftpmConfig, which 0.87 doesn't read.

Comment thread docs/pages/create.md
Comment on lines +28 to +40
To also generate a `Package.swift` for React Native experimental [Swift Package Manager support](https://reactnative.dev/blog/2026/08/11/react-native-0.87#experimental-swift-package-manager-support-for-ios), pass `--experimental-spm`:

```sh
npx create-react-native-library@latest awesome-library --experimental-spm
```

The podspec is still generated, so CocoaPods keeps working. The example app is set up for SPM instead of CocoaPods: run `npx react-native spm --deintegrate` in `example` once to set it up, and `npx react-native spm` after a fresh install.

### Limitations

- Requires React Native 0.87 or newer
- Nitro libraries aren't supported, since SwiftPM can't compile Swift and C++ sources in the same target.
- Expo and react-native-test-app examples can't use SPM yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: This splits the usage section, so "This will ask you a few questions..." now reads like it's about the SwiftPM command. Let's move it to its own section, like "Local library".

- name: Build example (iOS, Debug)
working-directory: ${{ env.work_dir }}
run: |
yarn example build:ios

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: This job also bypasses the existing turbo setup. It builds with yarn example build:ios and has no turbo cache, so every run does a full build. Moving it to yarn turbo run build:ios needs two more changes:

  • Package.swift isn't in the build:ios inputs in the turbo.json template, so editing it keeps the same hash and turbo reuses the cached build.
  • npx react-native spm rewrites tracked files under example/ios (project.pbxproj, the scheme, Podfile, .spm-injected.json), so the hash changes after setup. The cache check needs to run after setup, or it won't match what the build saved.

run: |
# The CLI builds Release for a device, which requires code signing
project=$(ls -d *.xcodeproj)
xcodebuild -project "$project" -scheme "${project%.xcodeproj}" -configuration Release -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: This step skips turbo completely. The generated turbo.json only has a Debug build:ios task, so there's nothing for a Release build to go through, and every run does two uncached Xcode builds. If we want to keep the Release build, the example needs a Release script and turbo.json needs a task for it.


${kleur.gray('$')} yarn
${kleur.gray('$')} yarn${
config.experimentalSpm && config.example === 'vanilla'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: With --example expo or --example test-app, the library still gets a Package.swift, but the example stays on CocoaPods and nothing tells the user. So the manifest gets published without being built anywhere. Let's warn about this the same way as for Nitro, or skip the manifest for these examples.

Comment thread docs/pages/create.md

### Limitations

- Requires React Native 0.87 or newer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: This should say 0.88, since the name fix relies on swiftpmConfig in package.json, which 0.87 doesn't read.

'example',
'reactNativeVersion',
'local',
'experimentalSpm',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: This looks fine. The list is for answers we can derive from the generated package.json, and we can tell this one from Package.swift in files (and from swiftpmConfig after the name fix). Nothing in the repo reads this metadata either.

Comment thread .gitignore
*.tsbuildinfo

# Swift Package Manager
.build/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via Claude: This is needed. A .build/ folder showed up at the repo root while working on this branch, even though nothing ran swift build there. Its marker file says SwiftPM's build system wrote it. The exact trigger isn't clear, but it's likely editor tooling picking up templates/spm/Package.swift.

@mrousavy

Copy link
Copy Markdown
Contributor

JFYI Nitro V1 is now on SPM. It dropped CocoaPods support.
V1 is still in alpha (v1.0.0-alpha.nightly.84.gc5ad5050ce94 is out now) but I think it'd be fair to support that in Bob

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants