From fdea071fd6b5ca8050a68232d5517a62e2fee01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Roz=CC=87niata?= <56474758+krozniata@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:12:00 +0200 Subject: [PATCH 1/5] feat: add experimental SPM support --- .gitignore | 3 ++ .../create-react-native-library/src/inform.ts | 13 ++++- .../create-react-native-library/src/prompt.ts | 7 +++ .../src/template.ts | 16 ++++++ .../src/utils/createMetadata.ts | 1 + .../templates/common/$.gitignore | 7 +++ .../templates/common/$package.json | 3 ++ .../example/react-native.config.js | 4 +- .../ios/{%- project.name %}View.mm | 2 +- .../templates/spm/Package.swift | 53 +++++++++++++++++++ 10 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 packages/create-react-native-library/templates/spm/Package.swift diff --git a/.gitignore b/.gitignore index c55006938..34e489ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ lib/ typescript/ doc_build/ *.tsbuildinfo + +# Swift Package Manager +.build/ diff --git a/packages/create-react-native-library/src/inform.ts b/packages/create-react-native-library/src/inform.ts index 23877f4ce..8d253c6f7 100644 --- a/packages/create-react-native-library/src/inform.ts +++ b/packages/create-react-native-library/src/inform.ts @@ -18,7 +18,18 @@ 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' || config.example === 'test-app') + ? ` + + ${kleur.magenta( + `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` + : '' + } ${Object.entries(platforms) .map( ([script, { name, color }]) => ` diff --git a/packages/create-react-native-library/src/prompt.ts b/packages/create-react-native-library/src/prompt.ts index 1dee17b57..77c39a299 100644 --- a/packages/create-react-native-library/src/prompt.ts +++ b/packages/create-react-native-library/src/prompt.ts @@ -147,6 +147,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'), + }, local: { type: 'confirm', description: 'Whether to create a local library', diff --git a/packages/create-react-native-library/src/template.ts b/packages/create-react-native-library/src/template.ts index b0734dafb..2ae370385 100644 --- a/packages/create-react-native-library/src/template.ts +++ b/packages/create-react-native-library/src/template.ts @@ -19,6 +19,7 @@ export type ViewConfig = 'paper-view' | '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; @@ -105,6 +106,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( @@ -195,6 +197,12 @@ export function generateTemplateConfiguration({ .toLowerCase()}`; return { + // Nitro requires Swift/C++ interoperability that's not supported by SPM + experimentalSpm: + answers.experimentalSpm === true && + languages !== 'js' && + getModuleConfig(type) !== 'nitro-modules' && + getViewConfig(type) !== 'nitro-view', versions, project: { slug, @@ -264,6 +272,14 @@ export async function applyTemplates( ) { const { local } = answers; + if (config.experimentalSpm) { + 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." + ); + } + if (local) { await applyTemplate(config, COMMON_LOCAL_FILES, folder); } else { diff --git a/packages/create-react-native-library/src/utils/createMetadata.ts b/packages/create-react-native-library/src/utils/createMetadata.ts index f8b8f0e18..cb2478a99 100644 --- a/packages/create-react-native-library/src/utils/createMetadata.ts +++ b/packages/create-react-native-library/src/utils/createMetadata.ts @@ -15,6 +15,7 @@ export function createMetadata(answers: Partial) { 'example', 'reactNativeVersion', 'local', + 'experimentalSpm', ]; const libraryMetadata = Object.fromEntries( diff --git a/packages/create-react-native-library/templates/common/$.gitignore b/packages/create-react-native-library/templates/common/$.gitignore index 67f32126d..d432cea73 100644 --- a/packages/create-react-native-library/templates/common/$.gitignore +++ b/packages/create-react-native-library/templates/common/$.gitignore @@ -29,6 +29,13 @@ DerivedData *.xcuserstate project.xcworkspace **/.xcode.env.local +<% if (experimentalSpm) { -%> + +# Swift Package Manager +# +.build/ +.swiftpm/ +<% } -%> # Android/IJ # diff --git a/packages/create-react-native-library/templates/common/$package.json b/packages/create-react-native-library/templates/common/$package.json index 2b5124c73..29054deff 100644 --- a/packages/create-react-native-library/templates/common/$package.json +++ b/packages/create-react-native-library/templates/common/$package.json @@ -23,6 +23,9 @@ "nitro.json", <% } -%> "*.podspec", +<% if (experimentalSpm) { -%> + "Package.swift", +<% } -%> "react-native.config.js", "!ios/build", "!android/build", diff --git a/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js b/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js index 00c6117c6..e68d4d1ac 100644 --- a/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js +++ b/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js @@ -12,13 +12,13 @@ module.exports = { }, ios: { sourceDir: 'ios', - automaticPodsInstallation: true, + automaticPodsInstallation: <%- experimentalSpm ? 'false' : 'true' %>, }, }), <% } else if (example === 'vanilla') { -%> project: { ios: { - automaticPodsInstallation: true, + automaticPodsInstallation: <%- experimentalSpm ? 'false' : 'true' %>, }, }, <% } -%> diff --git a/packages/create-react-native-library/templates/objc-view-new/ios/{%- project.name %}View.mm b/packages/create-react-native-library/templates/objc-view-new/ios/{%- project.name %}View.mm index c9b518d79..33e50750a 100644 --- a/packages/create-react-native-library/templates/objc-view-new/ios/{%- project.name %}View.mm +++ b/packages/create-react-native-library/templates/objc-view-new/ios/{%- project.name %}View.mm @@ -6,7 +6,7 @@ #import ViewSpec/Props.h> #import ViewSpec/RCTComponentViewHelpers.h> -#import "RCTFabricComponentsPlugins.h" +#import using namespace facebook::react; diff --git a/packages/create-react-native-library/templates/spm/Package.swift b/packages/create-react-native-library/templates/spm/Package.swift new file mode 100644 index 000000000..2b131b5b7 --- /dev/null +++ b/packages/create-react-native-library/templates/spm/Package.swift @@ -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(''); +-%> +import PackageDescription + +let package = Package( + name: "<%- spmName -%>", + platforms: [.iOS(.v15)], + products: [ + .library(name: "<%- spmName -%>", targets: ["<%- spmName -%>"]), + ], + dependencies: [ + .package(name: "ReactNative", path: "../../../../xcframeworks"), + .package(name: "React-GeneratedCode", path: "../../../ios"), + ], + 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", + cxxSettings: [ +<% if (project.cpp) { -%> + .headerSearchPath("cpp"), + .headerSearchPath("ios/generated/ReactCodegen"), +<% } -%> + .define("DEBUG", .when(configuration: .debug)), + .define("NDEBUG", .when(configuration: .release)), + ] + ), + ], + cxxLanguageStandard: .cxx20 +) From 74973d1f6da218afb039c3d00873d45c7ee22674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Roz=CC=87niata?= <56474758+krozniata@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:06:32 +0200 Subject: [PATCH 2/5] feat: ci --- .github/workflows/build-templates.yml | 106 ++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index c72595791..60427f4bb 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -285,3 +285,109 @@ jobs: rm -rf lib/* yarn example build:web + + build-spm: + env: + XCODE_VERSION: 26 + # Swift Package Manager support was introduced in React Native 0.87 + REACT_NATIVE_VERSION: 0.87.1 + + 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 \ + --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 \ + --react-native-version ${{ env.REACT_NATIVE_VERSION }} \ + --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 + + - name: Build example (iOS, Release) + working-directory: ${{ env.work_dir }}/example/ios + 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 From 64a0b36ddd3f2ff03fbdc643e8e6c36acf92bb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Roz=CC=87niata?= <56474758+krozniata@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:01:06 +0200 Subject: [PATCH 3/5] chore: small adjustments --- docs/pages/create.md | 14 ++++++++++++++ packages/create-react-native-library/src/inform.ts | 5 ++--- .../example/react-native.config.js | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/pages/create.md b/docs/pages/create.md index c5dae2469..1a31b6891 100644 --- a/docs/pages/create.md +++ b/docs/pages/create.md @@ -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. + +### 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. + This will ask you a few questions about your project and generate a new project in a folder named `awesome-library`. ![Demo](../assets/create-react-native-library.svg) diff --git a/packages/create-react-native-library/src/inform.ts b/packages/create-react-native-library/src/inform.ts index 8d253c6f7..bcdbe0473 100644 --- a/packages/create-react-native-library/src/inform.ts +++ b/packages/create-react-native-library/src/inform.ts @@ -19,15 +19,14 @@ export function printNonLocalLibNextSteps(config: TemplateConfiguration) { )}${kleur.gray(':')} ${kleur.gray('$')} yarn${ - config.experimentalSpm && - (config.example === 'vanilla' || config.example === 'test-app') + config.experimentalSpm && config.example === 'vanilla' ? ` ${kleur.magenta( `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` + ${kleur.gray('$')} cd example && npx react-native spm --deintegrate` : '' } ${Object.entries(platforms) diff --git a/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js b/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js index e68d4d1ac..433fa4db0 100644 --- a/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js +++ b/packages/create-react-native-library/templates/example-native-common/example/react-native.config.js @@ -12,7 +12,7 @@ module.exports = { }, ios: { sourceDir: 'ios', - automaticPodsInstallation: <%- experimentalSpm ? 'false' : 'true' %>, + automaticPodsInstallation: true, }, }), <% } else if (example === 'vanilla') { -%> From 045c48c2470fde764435b9748bc22eb1d0f287c3 Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Thu, 22 Oct 2026 07:17:13 +0200 Subject: [PATCH 4/5] chore: test spm with the default react native version --- .github/workflows/build-templates.yml | 3 --- packages/create-react-native-library/src/template.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index de942f4d5..404bd8e36 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -284,8 +284,6 @@ jobs: build-spm: env: XCODE_VERSION: 26 - # Swift Package Manager support was introduced in React Native 0.87 - REACT_NATIVE_VERSION: 0.87.1 strategy: fail-fast: false @@ -336,7 +334,6 @@ jobs: --languages ${{ matrix.type.language }} \ --example vanilla \ --tools eslint \ - --react-native-version ${{ env.REACT_NATIVE_VERSION }} \ --experimental-spm - name: Install dependencies of library diff --git a/packages/create-react-native-library/src/template.ts b/packages/create-react-native-library/src/template.ts index 17cacf728..a7c0e6e82 100644 --- a/packages/create-react-native-library/src/template.ts +++ b/packages/create-react-native-library/src/template.ts @@ -193,7 +193,6 @@ export function generateTemplateConfiguration({ .toLowerCase()}`; return { - // Nitro requires Swift/C++ interoperability that's not supported by SPM experimentalSpm: answers.experimentalSpm === true && languages !== 'js' && From f59b05e647212520dc67757d658de70a43e72b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Roz=CC=87niata?= <56474758+krozniata@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:55:04 +0100 Subject: [PATCH 5/5] chore: address review comments --- .github/workflows/build-templates.yml | 167 ++++++------------ docs/pages/create.md | 36 ++-- .../create-react-native-library/src/index.ts | 16 +- .../create-react-native-library/src/inform.ts | 20 ++- .../create-react-native-library/src/prompt.ts | 14 +- .../src/template.ts | 38 +++- .../templates/common-local/$package.json | 5 + .../common/$.github/workflows/ci.yml | 15 +- .../templates/common/$package.json | 5 + .../templates/common/CONTRIBUTING.md | 12 ++ .../templates/spm/Package.swift | 18 +- .../templates/tools/turborepo/turbo.json | 1 + 12 files changed, 187 insertions(+), 160 deletions(-) diff --git a/.github/workflows/build-templates.yml b/.github/workflows/build-templates.yml index 404bd8e36..de21ec823 100644 --- a/.github/workflows/build-templates.yml +++ b/.github/workflows/build-templates.yml @@ -54,9 +54,24 @@ jobs: type: name: library language: js + - os: macos-latest + type: + name: turbo-module + language: kotlin-objc + spm: true + - os: macos-latest + type: + name: turbo-module + language: cpp + spm: true + - os: macos-latest + type: + name: fabric-view + language: kotlin-objc + spm: true concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }}-${{ matrix.type.name }}-${{ matrix.type.language }} + group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.os }}-${{ matrix.type.name }}-${{ matrix.type.language }}${{ matrix.type.spm && '-spm' || '' }} cancel-in-progress: true runs-on: ${{ matrix.os }} @@ -75,7 +90,7 @@ jobs: - name: Get working directory shell: bash run: | - echo "work_dir=${{ matrix.os }}-${{ matrix.type.name }}-${{ matrix.type.language }}" >> $GITHUB_ENV + echo "work_dir=${{ matrix.os }}-${{ matrix.type.name }}-${{ matrix.type.language }}${{ matrix.type.spm && '-spm' || '' }}" >> $GITHUB_ENV - name: Create library shell: bash @@ -83,7 +98,6 @@ jobs: rm -rf ${{ env.work_dir }} # Workaround for tests failing intermittently args=( - --slug @bob/react-native-test --description test --author-name test --author-email test@test @@ -99,6 +113,17 @@ jobs: --tools release-it ) + if [[ "${{ matrix.type.spm }}" == "true" ]]; then + args+=( + --slug react-native-test + --experimental-spm + ) + else + args+=( + --slug @bob/react-native-test + ) + fi + if [[ ${{ github.event_name }} == 'schedule' ]]; then args+=( --react-native-version latest @@ -173,15 +198,27 @@ jobs: working-directory: ${{ env.work_dir }} run: yarn nitrogen + - name: Use appropriate Xcode version (SwiftPM) + if: env.ios_build == 1 && matrix.type.spm + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Set up SwiftPM + if: env.ios_build == 1 && matrix.type.spm + working-directory: ${{ env.work_dir }} + run: | + yarn example react-native spm --deintegrate --yes + - name: Cache turborepo if: env.android_build == 1 || env.ios_build == 1 uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ${{ env.work_dir }}/.turbo - key: ${{ runner.os }}-library-turborepo-${{ matrix.type.name }}-${{ matrix.type.language }}-${{ hashFiles(format('{0}/yarn.lock', env.work_dir)) }} + key: ${{ runner.os }}-library-turborepo-${{ matrix.type.name }}-${{ matrix.type.language }}${{ matrix.type.spm && '-spm' || '' }}-${{ hashFiles(format('{0}/yarn.lock', env.work_dir)) }} restore-keys: | - ${{ runner.os }}-library-turborepo-${{ matrix.type.name }}-${{ matrix.type.language }}- + ${{ runner.os }}-library-turborepo-${{ matrix.type.name }}-${{ matrix.type.language }}${{ matrix.type.spm && '-spm' || '' }}- - name: Check turborepo cache if: env.android_build == 1 || env.ios_build == 1 @@ -200,21 +237,25 @@ jobs: fi - name: Lint library + if: ${{ !matrix.type.spm }} working-directory: ${{ env.work_dir }} run: | yarn lint - name: Typecheck library + if: ${{ !matrix.type.spm }} working-directory: ${{ env.work_dir }} run: | yarn typecheck - name: Test library + if: ${{ !matrix.type.spm }} working-directory: ${{ env.work_dir }} run: | yarn test - name: Build library + if: ${{ !matrix.type.spm }} working-directory: ${{ env.work_dir }} run: | yarn prepare @@ -251,13 +292,13 @@ jobs: yarn turbo run build:android --cache-dir=".turbo" - name: Use appropriate Xcode version - if: env.ios_build == 1 && env.turbo_cache_hit_ios != 1 + if: env.ios_build == 1 && env.turbo_cache_hit_ios != 1 && !matrix.type.spm uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: xcode-version: ${{ env.XCODE_VERSION }} - name: Install cocoapods - if: env.ios_build == 1 && env.turbo_cache_hit_ios != 1 + if: env.ios_build == 1 && env.turbo_cache_hit_ios != 1 && !matrix.type.spm working-directory: ${{ env.work_dir }} run: | cd example @@ -271,7 +312,16 @@ jobs: run: | yarn turbo run build:ios --cache-dir=".turbo" + - name: Build example (iOS, Release) + if: env.ios_build == 1 && env.turbo_cache_hit_ios != 1 && matrix.type.spm + working-directory: ${{ env.work_dir }}/example/ios + run: | + # 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 + - name: Build example (Web) + if: ${{ !matrix.type.spm }} working-directory: ${{ env.work_dir }} shell: bash run: | @@ -280,106 +330,3 @@ 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 \ - --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 - - - name: Build example (iOS, Release) - working-directory: ${{ env.work_dir }}/example/ios - 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 diff --git a/docs/pages/create.md b/docs/pages/create.md index 9197f3b00..fcbe0f464 100644 --- a/docs/pages/create.md +++ b/docs/pages/create.md @@ -25,20 +25,6 @@ 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. - -### 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. - This will ask you a few questions about your project and generate a new project in a folder named `awesome-library`. ![Demo](../assets/create-react-native-library.svg) @@ -98,6 +84,28 @@ By default, the generated library is automatically linked to the project using ` This creates a symlink to the library under `node_modules` which makes autolinking work. But if you're using a monorepo or have non-standard setup, you'll need to manually set up the package to be linked to your app based on your project setup. +## Swift Package Manager (experimental) + +To also generate a `Package.swift` for React Native's experimental [Swift Package Manager (SwiftPM) 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 SwiftPM instead of CocoaPods. Run the following once in the root of the generated project to set it up: + +```sh +yarn example react-native spm --deintegrate +``` + +Then commit the changes to the example app's Xcode project, so that CI and fresh clones use SwiftPM. After a fresh clone, run `yarn example react-native spm` to regenerate the SwiftPM build files, since they aren't committed to the repository. + +Limitations: + +- Requires React Native 0.88 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 SwiftPM yet. + ## Writing native code Once the project is created, you can follow the official React Native docs to learn the API for writing native modules and components: diff --git a/packages/create-react-native-library/src/index.ts b/packages/create-react-native-library/src/index.ts index d3bb671bd..56354b184 100644 --- a/packages/create-react-native-library/src/index.ts +++ b/packages/create-react-native-library/src/index.ts @@ -11,7 +11,11 @@ import { alignDependencyVersionsWithExampleApp } from './exampleApp/dependencies import generateExampleApp from './exampleApp/generateExampleApp.ts'; import { printLocalLibNextSteps, printNonLocalLibNextSteps } from './inform.ts'; import { prompt } from './prompt.ts'; -import { applyTemplates, generateTemplateConfiguration } from './template.ts'; +import { + applyTemplates, + generateTemplateConfiguration, + getExperimentalSpmUnsupportedReason, +} from './template.ts'; import { assertNpxExists } from './utils/assert.ts'; import { configureTools } from './utils/configureTools.ts'; import { createMetadata } from './utils/createMetadata.ts'; @@ -67,6 +71,16 @@ async function create() { answers, }); + if (answers.experimentalSpm === true) { + const reason = getExperimentalSpmUnsupportedReason(answers); + + if (reason != null) { + console.warn( + `${kleur.yellow('⚠')} Ignoring --experimental-spm ${reason}` + ); + } + } + const folder = path.resolve(process.cwd(), answers.directory); await fs.mkdirp(folder); diff --git a/packages/create-react-native-library/src/inform.ts b/packages/create-react-native-library/src/inform.ts index be2d74073..f4d4cff85 100644 --- a/packages/create-react-native-library/src/inform.ts +++ b/packages/create-react-native-library/src/inform.ts @@ -19,14 +19,16 @@ export function printNonLocalLibNextSteps(config: TemplateConfiguration) { )}${kleur.gray(':')} ${kleur.gray('$')} yarn${ - config.experimentalSpm && config.example === 'vanilla' + config.experimentalSpm ? ` ${kleur.magenta( - `Set up ${kleur.bold('Swift Package Manager')} for the example app (React Native 0.87+)` + `Set up ${kleur.bold('Swift Package Manager (SwiftPM)')} for the example app (React Native 0.88+)` )}${kleur.gray(':')} - ${kleur.gray('$')} cd example && npx react-native spm --deintegrate` + ${kleur.gray('$')} yarn example react-native spm --deintegrate + + Then commit the changes, so CI and fresh clones use SwiftPM.` : '' } ${Object.entries(platforms) @@ -90,9 +92,15 @@ export function printLocalLibNextSteps({ `cd ${path.relative(process.cwd(), folder)} && npx nitrogen` )} to generate the nitro code \n` : '') + - `- Run ${kleur.blue( - 'pod install --project-directory=ios' - )} to install dependencies with CocoaPods\n` + + (config.experimentalSpm + ? `- Run ${kleur.blue( + 'npx react-native spm' + )} to install dependencies with Swift Package Manager (SwiftPM), or ${kleur.blue( + 'pod install --project-directory=ios' + )} if the app uses CocoaPods\n` + : `- Run ${kleur.blue( + 'pod install --project-directory=ios' + )} to install dependencies with CocoaPods\n`) + `- Run ${kleur.blue('npx react-native run-android')} or ${kleur.blue( 'npx react-native run-ios' )} to build and run the app\n` + diff --git a/packages/create-react-native-library/src/prompt.ts b/packages/create-react-native-library/src/prompt.ts index 7a859bd7d..e097e7f6a 100644 --- a/packages/create-react-native-library/src/prompt.ts +++ b/packages/create-react-native-library/src/prompt.ts @@ -143,13 +143,6 @@ 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'), - }, local: { type: 'confirm', description: 'Whether to create a local library', @@ -439,4 +432,11 @@ export const prompt = create(['[name]'], { 'Must be a valid version', skip: true, }, + experimentalSpm: { + type: 'confirm', + description: + 'Whether to generate an experimental Swift Package Manager (SwiftPM) manifest', + message: 'Do you want to generate a SwiftPM manifest?', + skip: true, + }, }); diff --git a/packages/create-react-native-library/src/template.ts b/packages/create-react-native-library/src/template.ts index a7c0e6e82..de9510c8a 100644 --- a/packages/create-react-native-library/src/template.ts +++ b/packages/create-react-native-library/src/template.ts @@ -195,9 +195,7 @@ export function generateTemplateConfiguration({ return { experimentalSpm: answers.experimentalSpm === true && - languages !== 'js' && - getModuleConfig(type) !== 'nitro-modules' && - getViewConfig(type) !== 'nitro-view', + getExperimentalSpmUnsupportedReason(answers) == null, versions, project: { slug, @@ -234,6 +232,36 @@ export function generateTemplateConfiguration({ }; } +/** + * Returns why SwiftPM can't be set up for the given answers, or `undefined` if it can + */ +export function getExperimentalSpmUnsupportedReason( + answers: Answers +): string | undefined { + if (answers.languages === 'js') { + return 'for library without native code'; + } + + if ( + getModuleConfig(answers.type) === 'nitro-modules' || + getViewConfig(answers.type) === 'nitro-view' + ) { + return "for Nitro library, since SwiftPM can't compile Swift and C++ sources in the same target"; + } + + if (answers.example === 'expo' || answers.example === 'test-app') { + return `for ${answers.example} example app, since it doesn't support SwiftPM yet`; + } + + const minor = answers.reactNativeVersion?.match(/^0\.(\d+)/)?.[1]; + + if (minor != null && Number(minor) < 88) { + return 'for React Native older than 0.88, since SwiftPM requires React Native 0.88 or newer'; + } + + return undefined; +} + function getModuleConfig(projectType: ProjectType): ModuleConfig { switch (projectType) { case 'nitro-module': @@ -270,10 +298,6 @@ export async function applyTemplates( if (config.experimentalSpm) { 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." - ); } if (local) { diff --git a/packages/create-react-native-library/templates/common-local/$package.json b/packages/create-react-native-library/templates/common-local/$package.json index 894db6b5f..f4603c82e 100644 --- a/packages/create-react-native-library/templates/common-local/$package.json +++ b/packages/create-react-native-library/templates/common-local/$package.json @@ -21,6 +21,11 @@ <% } -%> } }, +<% } -%> +<% if (experimentalSpm) { -%> + "swiftpmConfig": { + "name": "<%- project.name -%>" + }, <% } -%> "author": "<%- author.name -%> <<%- author.email -%>> (<%- author.url -%>)", "license": "UNLICENSED", diff --git a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml index a7958ad1c..a1f52b473 100644 --- a/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml +++ b/packages/create-react-native-library/templates/common/$.github/workflows/ci.yml @@ -152,6 +152,17 @@ jobs: - name: Generate nitrogen code run: yarn nitrogen <% } -%> +<% if (experimentalSpm) { -%> + + - name: Use appropriate Xcode version + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 + with: + xcode-version: ${{ env.XCODE_VERSION }} + + - name: Set up SwiftPM + run: | + yarn example react-native spm --yes +<% } -%> - name: Cache turborepo for iOS uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -169,18 +180,20 @@ jobs: echo "turbo_cache_hit=1" >> $GITHUB_ENV fi +<% if (!experimentalSpm) { -%> - name: Use appropriate Xcode version if: env.turbo_cache_hit != 1 uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: xcode-version: ${{ env.XCODE_VERSION }} +<% } -%> <% if (example === 'expo') { -%> - name: Prebuild expo app for iOS if: env.turbo_cache_hit != 1 run: | yarn example expo prebuild --platform ios -<% } else { -%> +<% } else if (!experimentalSpm) { -%> - name: Install cocoapods if: env.turbo_cache_hit != 1 run: | diff --git a/packages/create-react-native-library/templates/common/$package.json b/packages/create-react-native-library/templates/common/$package.json index 6cc683c10..d897b951c 100644 --- a/packages/create-react-native-library/templates/common/$package.json +++ b/packages/create-react-native-library/templates/common/$package.json @@ -144,6 +144,11 @@ } <% } -%> } +<% if (experimentalSpm) { -%> + }, + "swiftpmConfig": { + "name": "<%- project.name -%>" +<% } -%> <% } -%> } } diff --git a/packages/create-react-native-library/templates/common/CONTRIBUTING.md b/packages/create-react-native-library/templates/common/CONTRIBUTING.md index 0c2d50746..0cac9f866 100644 --- a/packages/create-react-native-library/templates/common/CONTRIBUTING.md +++ b/packages/create-react-native-library/templates/common/CONTRIBUTING.md @@ -37,13 +37,25 @@ To invoke **Nitrogen**, use the following command: yarn nitrogen ``` +<% } -%> +<% if (experimentalSpm) { -%> +The example app uses [Swift Package Manager (SwiftPM)](https://reactnative.dev/blog/2026/08/11/react-native-0.87#experimental-swift-package-manager-support-for-ios) instead of CocoaPods for iOS. The SwiftPM build files are not committed to the repository, so you need to run the following after cloning the project or changing native dependencies: + +```sh +yarn example react-native spm +``` + <% } -%> The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. <% if (project.native) { -%> +<% if (experimentalSpm) { -%> +If you want to use Android Studio or Xcode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/<%- project.name -%>Example.xcodeproj` in Xcode and find the source files at `Package Dependencies > <%- project.name -%>`. +<% } else { -%> If you want to use Android Studio or Xcode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/<%- project.name -%>Example.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > <%- project.slug -%>`. +<% } -%> To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `<%- project.identifier -%>` under `Android`. diff --git a/packages/create-react-native-library/templates/spm/Package.swift b/packages/create-react-native-library/templates/spm/Package.swift index 2b131b5b7..82902ee09 100644 --- a/packages/create-react-native-library/templates/spm/Package.swift +++ b/packages/create-react-native-library/templates/spm/Package.swift @@ -1,20 +1,11 @@ // 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(''); --%> import PackageDescription let package = Package( - name: "<%- spmName -%>", - platforms: [.iOS(.v15)], + name: "<%- project.name -%>", + platforms: [.iOS("15.1")], products: [ - .library(name: "<%- spmName -%>", targets: ["<%- spmName -%>"]), + .library(name: "<%- project.name -%>", targets: ["<%- project.name -%>"]), ], dependencies: [ .package(name: "ReactNative", path: "../../../../xcframeworks"), @@ -22,7 +13,7 @@ let package = Package( ], targets: [ .target( - name: "<%- spmName -%>", + name: "<%- project.name -%>", dependencies: [ .product(name: "ReactHeaders", package: "ReactNative"), .product(name: "ReactNativeHeaders", package: "ReactNative"), @@ -42,7 +33,6 @@ let package = Package( cxxSettings: [ <% if (project.cpp) { -%> .headerSearchPath("cpp"), - .headerSearchPath("ios/generated/ReactCodegen"), <% } -%> .define("DEBUG", .when(configuration: .debug)), .define("NDEBUG", .when(configuration: .release)), diff --git a/packages/create-react-native-library/templates/tools/turborepo/turbo.json b/packages/create-react-native-library/templates/tools/turborepo/turbo.json index 504c3ee17..04d732d9f 100644 --- a/packages/create-react-native-library/templates/tools/turborepo/turbo.json +++ b/packages/create-react-native-library/templates/tools/turborepo/turbo.json @@ -30,6 +30,7 @@ "inputs": [ "package.json", "*.podspec", + "Package.swift", "nitro.json", "ios", "cpp",