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
64 changes: 57 additions & 7 deletions .github/workflows/build-templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -75,15 +90,14 @@ 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
run: |
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: |
Expand Down
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/
Comment thread
krozniata marked this conversation as resolved.
Resolved
22 changes: 22 additions & 0 deletions docs/pages/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,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:
Expand Down
16 changes: 15 additions & 1 deletion packages/create-react-native-library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
26 changes: 22 additions & 4 deletions packages/create-react-native-library/src/inform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,19 @@ export function printNonLocalLibNextSteps(config: TemplateConfiguration) {
`${kleur.bold('Get started')} with the project`
)}${kleur.gray(':')}

${kleur.gray('$')} yarn
${kleur.gray('$')} yarn${
config.experimentalSpm
? `

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

${kleur.gray('$')} yarn example react-native spm --deintegrate

Then commit the changes, so CI and fresh clones use SwiftPM.`
: ''
}
${Object.entries(platforms)
.map(
([script, { name, color }]) => `
Expand Down Expand Up @@ -80,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` +
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 @@ -432,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,
},
});
39 changes: 39 additions & 0 deletions packages/create-react-native-library/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -191,6 +193,9 @@ export function generateTemplateConfiguration({
.toLowerCase()}`;

return {
experimentalSpm:
answers.experimentalSpm === true &&
getExperimentalSpmUnsupportedReason(answers) == null,
versions,
project: {
slug,
Expand Down Expand Up @@ -227,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':
Expand Down Expand Up @@ -261,6 +296,10 @@ export async function applyTemplates(
) {
const { local } = answers;

if (config.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 also runs for --local, so local libraries get a Package.swift, but nothing else is set up for SwiftPM:

  • It has the same name mismatch, and common-local/$package.json needs swiftpmConfig.name too.
  • The next steps still say to run pod install. Apps using SwiftPM need npx react-native spm instead.

Local libraries need the same setup and instructions as published ones.

await applyTemplate(config, SPM_FILES, folder);
}

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',
Comment thread
krozniata marked this conversation as resolved.
Resolved
];

const libraryMetadata = Object.fromEntries(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<% } -%>
}
},
<% } -%>
<% if (experimentalSpm) { -%>
"swiftpmConfig": {
"name": "<%- project.name -%>"
},
<% } -%>
"author": "<%- author.name -%> <<%- author.email -%>> (<%- author.url -%>)",
"license": "UNLICENSED",
Expand Down
Loading
Loading