Skip to content

Commit 0d7aeb7

Browse files
chrfalchclaude
andcommitted
Document how a library's SPM Swift name is derived and overridden
The derivation rule, the `spm.name` override and the names React Native reserves were only discoverable from the autolinker's source or from the error a collision produces. Write them down where a library author looks: the scoped package case that surprises people (`@scope/react-native` derives `ReactNative`, and gets its scope back as `ScopeReactNative`), the two reasons to set `spm.name` and its charset, the reserved names in one table, what happens when two libraries derive the same name — who moves, who stays, and when the build fails anyway — and the npm `files` requirement that makes the override reach consumers at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dfdb704 commit 0d7aeb7

1 file changed

Lines changed: 148 additions & 7 deletions

File tree

‎packages/react-native/scripts/spm/__docs__/spm-scripts.md‎

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -329,28 +329,72 @@ build phase calls — a fine trade for not having to remember a command.
329329
> and fetches the artifacts itself during normal package resolution. Until then,
330330
> the one-time setup run is required on clean machines.
331331
332-
## Local Native Modules
332+
## App-local native modules
333333

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

337337
```js
338338
module.exports = {
339339
spm: {
340340
modules: [
341341
{
342342
name: 'MyNativeModule',
343-
path: 'ios/MyNativeModule', // relative to app root
343+
path: 'ios/MyNativeModule', // relative to the app root
344344
exclude: ['*.podspec'], // optional
345+
sources: ['**/*.{h,m,mm}'], // optional
345346
},
346347
],
347348
},
348349
};
349350
```
350351

351-
Each entry becomes a target in `build/generated/autolinking/Package.swift`.
352-
Sources outside `build/generated/autolinking/` are automatically mirrored with
353-
file-level symlinks.
352+
### Why it is a separate mechanism
353+
354+
Autolinking only visits installed npm packages, so a directory with no
355+
`package.json` is never discovered — it has to be named somewhere, and this is
356+
where. Each entry becomes its own SwiftPM target with React Native's headers
357+
already wired up: `<React/…>` resolves inside the module, and the module's own
358+
headers resolve as `#import <MyNativeModule/MyHeader.h>` from the app and from
359+
other modules.
360+
361+
By design, the entry needs no podspec and no `Package.swift` — app-local native
362+
code is not required to carry either. (A hand-written `Package.swift` in the
363+
directory is used as-is; React Native generates nothing.)
364+
365+
| Field | Meaning |
366+
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
367+
| `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. |
368+
| `path` | The module's directory, relative to the app root. |
369+
| `exclude` | Paths inside `path` to keep out of the target, like a podspec's `exclude_files`. Name a directory with a trailing `/`. |
370+
| `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/`). |
371+
372+
Names must be unique because wrapper directories are keyed by name: two entries
373+
resolving to one name — or an entry matching an autolinked library — would share
374+
a directory, and the losing module would silently not be built. So a clash fails
375+
the build with a rename instruction instead.
376+
377+
### This is an app surface
378+
379+
`spm.modules` is read from the app's own `react-native.config.js` only — entries
380+
in a dependency's config are ignored, so a library cannot declare its native
381+
code this way. A library ships its own `Package.swift`, or has one scaffolded
382+
from its podspec — see
383+
[community packages without a `Package.swift`](#community-packages-without-a-packageswift).
384+
385+
### Limits
386+
387+
- **One target.** `spm add` attaches the `Autolinked` aggregate to a single app
388+
target (`--productName` picks it when the project has several), and modules
389+
reach only that target. Known gap: rn-tester's test targets
390+
(`RNTesterUnitTests`, `RNTesterIntegrationTests`) get their own pods under
391+
CocoaPods, and `spm.modules` has no equivalent for them yet.
392+
- **One language per module.** SwiftPM cannot mix Swift and Objective-C/C++ in
393+
one target; a module that does fails the build. Split it per language, or ship
394+
a hand-written `Package.swift` with a target for each.
395+
- **No third-party Swift packages.** Add those in Xcode (File → Add Package
396+
Dependencies…); React Native does not need to know. Pods you keep are still
397+
[installed by CocoaPods](#keeping-non-rn-pods).
354398

355399
## Dependencies between libraries
356400

@@ -391,6 +435,103 @@ module.exports = {dependency: {platforms: {ios: {}}}, spm: {name: 'worklets'}};
391435
If the config fails to load, a warning names the file and the reason — the `spm`
392436
settings in it are ignored rather than silently applied.
393437

438+
## Library authors: your library's Swift name
439+
440+
Every autolinked library becomes one SwiftPM target, whose name is also its
441+
header prefix (`#import <SwiftName/MyHeader.h>`). The name is derived from the
442+
npm package name unless the library overrides it.
443+
444+
### How the name is derived
445+
446+
The scope is dropped, the rest is split on runs of non-alphanumeric characters,
447+
each part gets its first character upper-cased, and the parts are joined.
448+
Existing casing is kept (`RNWorklets` stays `RNWorklets`).
449+
450+
| npm package | Swift name |
451+
| ----------------------- | --------------------- |
452+
| `react-native-worklets` | `ReactNativeWorklets` |
453+
| `@react-native/foo` | `Foo` |
454+
| `@scope/common` | `Common` |
455+
| `@scope/react-native` | `ReactNative` |
456+
457+
The last row is the surprise: dropping the scope makes `@scope/react-native`
458+
derive `ReactNative`, a [reserved name](#names-react-native-reserves). A scoped
459+
package then gets its scope back — the target becomes `ScopeReactNative`, and
460+
the build logs the rewrite. The same borrow settles a
461+
[collision between two libraries](#when-two-libraries-derive-the-same-name), so
462+
neither case needs an override.
463+
464+
### Overriding it with `spm.name`
465+
466+
Set `spm.name` in the library's **own** `react-native.config.js`:
467+
468+
```js
469+
// react-native-worklets/react-native.config.js
470+
module.exports = {
471+
dependency: {platforms: {ios: {}}},
472+
spm: {name: 'worklets'},
473+
};
474+
```
475+
476+
The value must start with a letter or underscore and may contain only letters,
477+
digits, underscores and hyphens. Set it when:
478+
479+
- **The derived name is reserved** and your package is unscoped — see below.
480+
- **Your headers use a different prefix** (a podspec `s.header_dir` unlike the
481+
derived name): `react-native-worklets` derives `ReactNativeWorklets` but ships
482+
headers as `<worklets/…>`, so it sets `spm.name: 'worklets'` to keep
483+
consumers' `#import <worklets/...>` lines resolving.
484+
485+
### Names React Native reserves
486+
487+
React Native registers these names for its own package and products, so no
488+
library's target may end up with one of them (matched case-insensitively). They
489+
are also refused to an app's own `spm.modules` entries.
490+
491+
| Name | What it is |
492+
| -------------------------------- | --------------------------- |
493+
| `ReactNative` | The React Native package |
494+
| `React-GeneratedCode` | The per-app codegen package |
495+
| `Autolinked` | The autolinking aggregator |
496+
| `ReactHeaders` | Product |
497+
| `ReactNativeHeaders` | Product |
498+
| `ReactNativeDependenciesHeaders` | Product |
499+
| `ReactAppHeaders` | Product |
500+
| `ReactCodegen` | Product |
501+
| `ReactAppDependencyProvider` | Product |
502+
503+
A scoped package that derives a reserved name is auto-corrected by
504+
[borrowing its scope](#how-the-name-is-derived). When the scope cannot help, the
505+
build fails, naming the package and `spm.name` as the fix:
506+
507+
- **An unscoped package** — no scope to borrow.
508+
- **An `spm.name` you set yourself** — your explicit choice is never rewritten.
509+
- **A scope-prefixed name that is itself reserved.**
510+
511+
### When two libraries derive the same name
512+
513+
Dropping the scope also makes `@a/foo` and `@b/foo` both derive `Foo`. Every
514+
scoped library in the group gets its scope prepended, and each rewrite is logged
515+
— neither author has to do anything:
516+
517+
| npm packages | Swift names |
518+
| -------------------------------------------- | --------------- |
519+
| `@a/foo` + `@b/foo` | `AFoo` + `BFoo` |
520+
| `@a/foo` + `foo` | `AFoo` + `Foo` |
521+
| `@a/foo` (with `spm.name: 'Foo'`) + `@b/foo` | `Foo` + `BFoo` |
522+
523+
Two kinds of member never move: an unscoped package (no scope to borrow) and an
524+
explicit `spm.name` (your choice wins). If a borrowed name is itself taken
525+
(`AFoo` while a package `a-foo` is installed too) or
526+
[reserved](#names-react-native-reserves), the build fails and one library must
527+
set `spm.name`.
528+
529+
### Ship the config file
530+
531+
`react-native.config.js` must be in your package's npm `files` allowlist. If it
532+
isn't published, consumers get the derived name instead of your override, and
533+
the mismatch only shows up as a build error in their app.
534+
394535
## Self-managed community packages
395536

396537
A community library that ships its own `Package.swift` is referenced directly by

0 commit comments

Comments
 (0)