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/3] 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/3] 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/3] 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') { -%>