Repository navigation
feat: add experimental SPM support #955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fdea071
74973d1
64a0b36
f7ee559
045c48c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -280,3 +280,106 @@ jobs: | |
| rm -rf lib/* | ||
|
|
||
| yarn example build:web | ||
|
|
||
| build-spm: | ||
| env: | ||
| XCODE_VERSION: 26 | ||
|
|
||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| type: | ||
| - name: turbo-module | ||
| language: kotlin-objc | ||
| - name: turbo-module | ||
| language: cpp | ||
| - name: fabric-view | ||
| language: kotlin-objc | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }}-spm-${{ matrix.type.name }}-${{ matrix.type.language }} | ||
| cancel-in-progress: true | ||
|
|
||
| runs-on: macos-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
|
|
||
| - name: Setup | ||
| uses: ./.github/actions/setup | ||
|
|
||
| - name: Build package | ||
| run: | | ||
| yarn workspace create-react-native-library prepare | ||
|
|
||
| - name: Get working directory | ||
| shell: bash | ||
| run: | | ||
| echo "work_dir=spm-${{ matrix.type.name }}-${{ matrix.type.language }}" >> $GITHUB_ENV | ||
|
|
||
| - name: Create library | ||
| shell: bash | ||
| run: | | ||
| rm -rf ${{ env.work_dir }} | ||
|
|
||
| ./packages/create-react-native-library/bin/create-react-native-library ${{ env.work_dir }} \ | ||
| --slug @bob/react-native-test \ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: With a scoped slug, both naming schemes give |
||
| --description test \ | ||
| --author-name test \ | ||
| --author-email test@test \ | ||
| --author-url https://test.test \ | ||
| --repo-url https://test.test \ | ||
| --type ${{ matrix.type.name }} \ | ||
| --languages ${{ matrix.type.language }} \ | ||
| --example vanilla \ | ||
| --tools eslint \ | ||
| --experimental-spm | ||
|
|
||
| - name: Install dependencies of library | ||
| working-directory: ${{ env.work_dir }} | ||
| shell: bash | ||
| run: | | ||
| touch yarn.lock # Without this Yarn will fail due to the parent directory being a Yarn workspace | ||
| yarn install --no-immutable | ||
|
|
||
| - name: Use local version of react-native-builder-bob | ||
| shell: bash | ||
| run: | | ||
| cd packages/react-native-builder-bob | ||
| npm pack | ||
| tgz=$(ls react-native-builder-bob-*.tgz) | ||
| cd - | ||
| cd ${{ env.work_dir }} | ||
|
|
||
| export YARN_CHECKSUM_BEHAVIOR=ignore | ||
|
|
||
| yarn add --dev ../packages/react-native-builder-bob/$tgz | ||
| yarn --cwd example add --dev ../../packages/react-native-builder-bob/$tgz | ||
|
|
||
| - name: Generate codegen native code | ||
| if: matrix.type.language == 'cpp' | ||
| working-directory: ${{ env.work_dir }} | ||
| run: yarn bob build --target codegen | ||
|
|
||
| - name: Use appropriate Xcode version | ||
| uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 | ||
| with: | ||
| xcode-version: ${{ env.XCODE_VERSION }} | ||
|
|
||
| - name: Set up SPM | ||
| working-directory: ${{ env.work_dir }}/example | ||
| run: | | ||
| npx react-native spm --deintegrate --yes | ||
|
|
||
| - name: Build example (iOS, Debug) | ||
| working-directory: ${{ env.work_dir }} | ||
| run: | | ||
| yarn example build:ios | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
|
||
| - name: Build example (iOS, Release) | ||
| working-directory: ${{ env.work_dir }}/example/ios | ||
| run: | | ||
| # The CLI builds Release for a device, which requires code signing | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: Calling # Release catches DEBUG/NDEBUG mismatches with the prebuilt React framework |
||
| project=$(ls -d *.xcodeproj) | ||
| xcodebuild -project "$project" -scheme "${project%.xcodeproj}" -configuration Release -sdk iphonesimulator -destination "generic/platform=iOS Simulator" build | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: This step skips turbo completely. The generated |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,3 +90,6 @@ lib/ | |
| typescript/ | ||
| doc_build/ | ||
| *.tsbuildinfo | ||
|
|
||
| # Swift Package Manager | ||
| .build/ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: This is needed. A |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,20 @@ To create new project, run the following: | |
| npx create-react-native-library@latest awesome-library | ||
| ``` | ||
|
|
||
| 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: The generated |
||
|
|
||
| ### Limitations | ||
|
|
||
| - Requires React Native 0.87 or newer | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| - 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. | ||
|
Comment on lines
+28
to
+40
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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". |
||
|
|
||
| This will ask you a few questions about your project and generate a new project in a folder named `awesome-library`. | ||
|
|
||
|  | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,17 @@ export function printNonLocalLibNextSteps(config: TemplateConfiguration) { | |
| `${kleur.bold('Get started')} with the project` | ||
| )}${kleur.gray(':')} | ||
|
|
||
| ${kleur.gray('$')} yarn | ||
| ${kleur.gray('$')} yarn${ | ||
| config.experimentalSpm && config.example === 'vanilla' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: With |
||
| ? ` | ||
|
|
||
| ${kleur.magenta( | ||
| `Set up ${kleur.bold('Swift Package Manager')} for the example app (React Native 0.87+)` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| )}${kleur.gray(':')} | ||
|
|
||
| ${kleur.gray('$')} cd example && npx react-native spm --deintegrate` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: After yarn example react-native spm --deintegrate |
||
| : '' | ||
| } | ||
| ${Object.entries(platforms) | ||
| .map( | ||
| ([script, { name, color }]) => ` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,6 +143,13 @@ const isInPackage = (): boolean => { | |
| }; | ||
|
|
||
| export const prompt = create(['[name]'], { | ||
| 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'), | ||
|
Comment on lines
+146
to
+151
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: Let's use |
||
| }, | ||
| local: { | ||
| type: 'confirm', | ||
| description: 'Whether to create a local library', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ export type ViewConfig = 'fabric-view' | 'nitro-view' | null; | |
| // Please think at least 5 times before introducing a new config key | ||
| // You can just reuse the existing ones most of the time | ||
| export type TemplateConfiguration = { | ||
| experimentalSpm: boolean; | ||
| versions: TemplateVersions; | ||
| project: { | ||
| slug: string; | ||
|
|
@@ -101,6 +102,7 @@ const CPP_FILES = path.resolve( | |
| import.meta.dirname, | ||
| '../../templates/cpp-library' | ||
| ); | ||
| const SPM_FILES = path.resolve(import.meta.dirname, '../../templates/spm'); | ||
|
|
||
| const NATIVE_FILES = { | ||
| module_new: path.resolve( | ||
|
|
@@ -191,6 +193,11 @@ export function generateTemplateConfiguration({ | |
| .toLowerCase()}`; | ||
|
|
||
| return { | ||
| experimentalSpm: | ||
| answers.experimentalSpm === true && | ||
| languages !== 'js' && | ||
| getModuleConfig(type) !== 'nitro-modules' && | ||
| getViewConfig(type) !== 'nitro-view', | ||
| versions, | ||
| project: { | ||
| slug, | ||
|
|
@@ -261,6 +268,14 @@ export async function applyTemplates( | |
| ) { | ||
| const { local } = answers; | ||
|
|
||
| if (config.experimentalSpm) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: This also runs for
Local libraries need the same setup and instructions as published ones. |
||
| await applyTemplate(config, SPM_FILES, folder); | ||
| } 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." | ||
| ); | ||
|
Comment on lines
+273
to
+276
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: The other warnings (in |
||
| } | ||
|
|
||
| if (local) { | ||
| await applyTemplate(config, COMMON_LOCAL_FILES, folder); | ||
| } else { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ export function createMetadata(answers: Partial<Answers>) { | |
| 'example', | ||
| 'reactNativeVersion', | ||
| 'local', | ||
| 'experimentalSpm', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ]; | ||
|
|
||
| const libraryMetadata = Object.fromEntries( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ module.exports = { | |
| <% } else if (example === 'vanilla') { -%> | ||
| project: { | ||
| ios: { | ||
| automaticPodsInstallation: true, | ||
| automaticPodsInstallation: <%- experimentalSpm ? 'false' : 'true' %>, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
The CI and turbo templates need to handle the SwiftPM case too. |
||
| }, | ||
| }, | ||
| <% } -%> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // swift-tools-version: 6.0 | ||
| <% | ||
| // 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(''); | ||
|
Comment on lines
+3
to
+9
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 We can use |
||
| -%> | ||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "<%- spmName -%>", | ||
| platforms: [.iOS(.v15)], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: The podspec uses |
||
| products: [ | ||
| .library(name: "<%- spmName -%>", targets: ["<%- spmName -%>"]), | ||
| ], | ||
| dependencies: [ | ||
| .package(name: "ReactNative", path: "../../../../xcframeworks"), | ||
| .package(name: "React-GeneratedCode", path: "../../../ios"), | ||
|
Comment on lines
+20
to
+21
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: React Native's contract for libraries that ship their own The contract asks for two things:
One catch is that the docs only show the remote form for the first one, and the second one goes against #819, where we stopped shipping generated code. It's worth asking in RFC 0994 how libraries should reference React Native in local mode. |
||
| ], | ||
| targets: [ | ||
| .target( | ||
| name: "<%- spmName -%>", | ||
| dependencies: [ | ||
| .product(name: "ReactHeaders", package: "ReactNative"), | ||
| .product(name: "ReactNativeHeaders", package: "ReactNative"), | ||
| .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"), | ||
| .product(name: "ReactAppHeaders", package: "React-GeneratedCode"), | ||
| ], | ||
| path: ".", | ||
| // SwiftPM scans the whole path for resources, even outside `sources` | ||
| // Without this, the example app in the library's repo fails with "multiple resources named ..." | ||
| exclude: ["example", "node_modules"], | ||
| <% if (project.cpp) { -%> | ||
| sources: ["ios", "cpp"], | ||
| <% } else { -%> | ||
| sources: ["ios"], | ||
| <% } -%> | ||
| publicHeadersPath: "ios", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: With |
||
| cxxSettings: [ | ||
| <% if (project.cpp) { -%> | ||
| .headerSearchPath("cpp"), | ||
| .headerSearchPath("ios/generated/ReactCodegen"), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Via Claude: |
||
| <% } -%> | ||
| .define("DEBUG", .when(configuration: .debug)), | ||
| .define("NDEBUG", .when(configuration: .release)), | ||
| ] | ||
| ), | ||
| ], | ||
| cxxLanguageStandard: .cxx20 | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Via Claude: Let's add this to the existing
buildmatrix instead of a separate job so it doesn't drift from the rest. It has already drifted after mergingmain:actions/checkoutis on v5.0.0 here and v7.0.1 inbuild.setup-xcodeis on v1.6.0 here and v1.7.0 inbuild.--react-native-version latestrun.The "Generate codegen native code" step can go too. The C++ example builds without it, and nothing uses
ios/generatedanymore.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Via Claude: One more difference: this job only passes
--tools eslint, whilebuildalso passesvite,jest,lefthookandrelease-it. Moving it into the matrix fixes this too.