Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/build-templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@ lib/
typescript/
doc_build/
*.tsbuildinfo

# Swift Package Manager
.build/
14 changes: 14 additions & 0 deletions docs/pages/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion packages/create-react-native-library/src/inform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
? `

${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 --deintegrate`
: ''
}
${Object.entries(platforms)
.map(
([script, { name, color }]) => `
Expand Down
7 changes: 7 additions & 0 deletions packages/create-react-native-library/src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
16 changes: 16 additions & 0 deletions packages/create-react-native-library/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function createMetadata(answers: Partial<Answers>) {
'example',
'reactNativeVersion',
'local',
'experimentalSpm',
];

const libraryMetadata = Object.fromEntries(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ DerivedData
*.xcuserstate
project.xcworkspace
**/.xcode.env.local
<% if (experimentalSpm) { -%>

# Swift Package Manager
#
.build/
.swiftpm/
<% } -%>

# Android/IJ
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"nitro.json",
<% } -%>
"*.podspec",
<% if (experimentalSpm) { -%>
"Package.swift",
<% } -%>
"react-native.config.js",
"!ios/build",
"!android/build",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
<% } else if (example === 'vanilla') { -%>
project: {
ios: {
automaticPodsInstallation: true,
automaticPodsInstallation: <%- experimentalSpm ? 'false' : 'true' %>,
},
},
<% } -%>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#import <react/renderer/components/<%- project.name -%>ViewSpec/Props.h>
#import <react/renderer/components/<%- project.name -%>ViewSpec/RCTComponentViewHelpers.h>

#import "RCTFabricComponentsPlugins.h"
#import <React/RCTFabricComponentsPlugins.h>

using namespace facebook::react;

Expand Down
53 changes: 53 additions & 0 deletions packages/create-react-native-library/templates/spm/Package.swift
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('');
-%>
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
)
Loading