Skip to content
Open
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
156 changes: 148 additions & 8 deletions packages/react-native/scripts/spm/__docs__/spm-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,29 +329,72 @@ build phase calls — a fine trade for not having to remember a command.
> and fetches the artifacts itself during normal package resolution. Until then,
> the one-time setup run is required on clean machines.

## Local Native Modules
## App-local native modules

Modules not discovered via autolinking can be declared in
`react-native.config.js`:
Native code that lives in the app itself, rather than in an npm package, is
declared in `react-native.config.js`:

```js
module.exports = {
spm: {
modules: [
{
name: 'MyNativeModule',
path: 'ios/MyNativeModule', // relative to app root
path: 'ios/MyNativeModule', // relative to the app root
exclude: ['*.podspec'], // optional
publicHeadersPath: '.', // optional
sources: ['**/*.{h,m,mm}'], // optional
},
],
},
};
```

Each entry becomes a target in `build/generated/autolinking/Package.swift`.
Sources outside `build/generated/autolinking/` are automatically mirrored with
file-level symlinks.
### Why it is a separate mechanism

Autolinking only visits installed npm packages, so a directory with no
`package.json` is never discovered — it has to be named somewhere, and this is
where. Each entry becomes its own SwiftPM target with React Native's headers
already wired up: `<React/…>` resolves inside the module, and the module's own
headers resolve as `#import <MyNativeModule/MyHeader.h>` from the app and from
other modules.

By design, the entry needs no podspec and no `Package.swift` — app-local native
code is not required to carry either. (A hand-written `Package.swift` in the
directory is used as-is; React Native generates nothing.)

| Field | Meaning |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | The target's name and its header prefix. A valid Swift identifier, not [reserved](#names-react-native-reserves), and not already taken by another module or an autolinked library. |
| `path` | The module's directory, relative to the app root. |
| `exclude` | Paths inside `path` to keep out of the target, like a podspec's `exclude_files`. Name a directory with a trailing `/`. |
| `sources` | Glob allowlist replacing automatic discovery, like a podspec's `source_files`. Without it, every `.h/.m/.mm/.c/.cpp/.swift` file under `path` is compiled, minus `exclude` and the directories the autolinker always skips (`android/`, `test/`, `tests/`, `__tests__/`, `__mocks__/`, `jest/`, `node_modules/`). |

Names must be unique because wrapper directories are keyed by name: two entries
resolving to one name — or an entry matching an autolinked library — would share
a directory, and the losing module would silently not be built. So a clash fails
the build with a rename instruction instead.

### This is an app surface

`spm.modules` is read from the app's own `react-native.config.js` only — entries
in a dependency's config are ignored, so a library cannot declare its native
code this way. A library ships its own `Package.swift`, or has one scaffolded
from its podspec — see
[community packages without a `Package.swift`](#community-packages-without-a-packageswift).

### Limits

- **One target.** `spm add` attaches the `Autolinked` aggregate to a single app
target (`--productName` picks it when the project has several), and modules
reach only that target. Known gap: rn-tester's test targets
(`RNTesterUnitTests`, `RNTesterIntegrationTests`) get their own pods under
CocoaPods, and `spm.modules` has no equivalent for them yet.
- **One language per module.** SwiftPM cannot mix Swift and Objective-C/C++ in
one target; a module that does fails the build. Split it per language, or ship
a hand-written `Package.swift` with a target for each.
- **No third-party Swift packages.** Add those in Xcode (File → Add Package
Dependencies…); React Native does not need to know. Pods you keep are still
[installed by CocoaPods](#keeping-non-rn-pods).

## Dependencies between libraries

Expand All @@ -376,6 +419,103 @@ library's target can import it.
This is a **library-author** surface, like the podspec dependency it replaces —
apps don't normally set it.

## Library authors: your library's Swift name

Every autolinked library becomes one SwiftPM target, whose name is also its
header prefix (`#import <SwiftName/MyHeader.h>`). The name is derived from the
npm package name unless the library overrides it.

### How the name is derived

The scope is dropped, the rest is split on runs of non-alphanumeric characters,
each part gets its first character upper-cased, and the parts are joined.
Existing casing is kept (`RNWorklets` stays `RNWorklets`).

| npm package | Swift name |
| ----------------------- | --------------------- |
| `react-native-worklets` | `ReactNativeWorklets` |
| `@react-native/foo` | `Foo` |
| `@scope/common` | `Common` |
| `@scope/react-native` | `ReactNative` |

The last row is the surprise: dropping the scope makes `@scope/react-native`
derive `ReactNative`, a [reserved name](#names-react-native-reserves). A scoped
package then gets its scope back — the target becomes `ScopeReactNative`, and
the build logs the rewrite. The same borrow settles a
[collision between two libraries](#when-two-libraries-derive-the-same-name), so
neither case needs an override.

### Overriding it with `spm.name`

Set `spm.name` in the library's **own** `react-native.config.js`:

```js
// react-native-worklets/react-native.config.js
module.exports = {
dependency: {platforms: {ios: {}}},
spm: {name: 'worklets'},
};
```

The value must start with a letter or underscore and may contain only letters,
digits, underscores and hyphens. Set it when:

- **The derived name is reserved** and your package is unscoped — see below.
- **Your headers use a different prefix** (a podspec `s.header_dir` unlike the
derived name): `react-native-worklets` derives `ReactNativeWorklets` but ships
headers as `<worklets/…>`, so it sets `spm.name: 'worklets'` to keep
consumers' `#import <worklets/...>` lines resolving.

### Names React Native reserves

React Native registers these names for its own package and products, so no
library's target may end up with one of them (matched case-insensitively). They
are also refused to an app's own `spm.modules` entries.

| Name | What it is |
| -------------------------------- | --------------------------- |
| `ReactNative` | The React Native package |
| `React-GeneratedCode` | The per-app codegen package |
| `Autolinked` | The autolinking aggregator |
| `ReactHeaders` | Product |
| `ReactNativeHeaders` | Product |
| `ReactNativeDependenciesHeaders` | Product |
| `ReactAppHeaders` | Product |
| `ReactCodegen` | Product |
| `ReactAppDependencyProvider` | Product |

A scoped package that derives a reserved name is auto-corrected by
[borrowing its scope](#how-the-name-is-derived). When the scope cannot help, the
build fails, naming the package and `spm.name` as the fix:

- **An unscoped package** — no scope to borrow.
- **An `spm.name` you set yourself** — your explicit choice is never rewritten.
- **A scope-prefixed name that is itself reserved.**

### When two libraries derive the same name

Dropping the scope also makes `@a/foo` and `@b/foo` both derive `Foo`. Every
scoped library in the group gets its scope prepended, and each rewrite is logged
— neither author has to do anything:

| npm packages | Swift names |
| -------------------------------------------- | --------------- |
| `@a/foo` + `@b/foo` | `AFoo` + `BFoo` |
| `@a/foo` + `foo` | `AFoo` + `Foo` |
| `@a/foo` (with `spm.name: 'Foo'`) + `@b/foo` | `Foo` + `BFoo` |

Two kinds of member never move: an unscoped package (no scope to borrow) and an
explicit `spm.name` (your choice wins). If a borrowed name is itself taken
(`AFoo` while a package `a-foo` is installed too) or
[reserved](#names-react-native-reserves), the build fails and one library must
set `spm.name`.

### Ship the config file

`react-native.config.js` must be in your package's npm `files` allowlist. If it
isn't published, consumers get the derived name instead of your override, and
the mismatch only shows up as a build error in their app.

## Self-managed community packages

A community library that ships its own `Package.swift` is referenced directly by
Expand Down
Loading