Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/react-native/scripts/spm/__docs__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Three documents cover the design, each owning one area:
| Document | Covers |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [spm-scripts.md](./spm-scripts.md) | The tool itself: CLI surface, the six-step pipeline, [every file it creates or modifies](./spm-scripts.md#files-the-tool-touches), the two auto-sync hooks, and how Debug/Release flavor selection works. |
| [spm-header-paths-contract.md](./spm-header-paths-contract.md) | How headers and package references resolve. The contract is **zero-`-I`**: no generated manifest carries a search path into React Native's own headers. Also covers remote mode. |
| [spm-header-paths-contract.md](./spm-header-paths-contract.md) | How headers and package references resolve. The contract is **zero-`-I`**: no generated manifest carries a search path into React Native's own headers. Also covers the planned remote mode. |
| [spm-autolinking-plugins.md](./spm-autolinking-plugins.md) | The extension seam for frameworks with their own module system (Expo is the first consumer): discovery, the full context/return contract including `flavoredFrameworks`, `watchPaths` and `scriptPhases`, and failure behavior. |

Two ideas explain most of the architecture:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,14 @@ only, simulator only, …), the script must check `$CONFIGURATION` /
#### `context.react` — depending on React

A plugin that emits its own `Package.swift` must declare React as a dependency.
React Native's package path, identity, and product names differ between local
and remote mode, and they **move as RN repackages**. So take them from
`context.react` rather than re-deriving them:
React Native's package path, identity, and product names **move as RN
repackages**. So take them from `context.react` rather than re-deriving them:

```js
react: {
packageRef:
{name: 'ReactNative', path: '<absolute>', relPath: '<relative-to-outputDir>'} // local
| {name: '<identity>', url: '<url>', version: '<version>'}, // remote (SPM-resolved)
| {name: '<identity>', url: '<url>', version: '<version>'}, // remote (planned, unsupported)
products: [
{name: 'ReactHeaders', package: 'ReactNative'},
{name: 'ReactNativeHeaders', package: 'ReactNative'},
Expand All @@ -323,7 +322,11 @@ react: {
}
```

- The `packageRef` keys signal local or remote mode: `path` xor `url`+`version`.
- `packageRef` has the local `path` shape by default. It has the `url`+`version`
shape when remote settings are present: `RN_SPM_REMOTE_URL`, or a `url`
persisted in `spm-remote.json`. Remote mode is planned, not supported, but
plugins must handle both shapes.
- `context.react` can be `null` (see the table above). Check it before use.
- `packageRef.path` is **absolute**, so it is correct in any subdirectory of
`outputDir` the plugin writes its manifest into. The generated manifests are
gitignored and regenerated every sync, so this has no portability cost.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@ walk-up, no JSON, no `import Foundation`.
| ------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Autolinked aggregator | `build/generated/autolinking/Package.swift` | Neither: it has no inline targets, and references only its dependencies' packages (`packages/<Name>`, `libs/<Name>`, plugin packages) |
| Local-module wrapper | `build/generated/autolinking/packages/<Name>/` | `.package(path: "../../../../xcframeworks")` + `"../../../ios"` (only for `swiftpmConfig.modules` entries) |
| Codegen template | `build/generated/ios/Package.swift` | `.package(path: "../../xcframeworks")` (or the remote url) |
| App target (pbxproj) | `<App>.xcodeproj` | local `XCLocalSwiftPackageReference` (or `XCRemoteSwiftPackageReference` in remote mode) |
| Scaffolded community lib | `node_modules/<dep>/Package.swift` | scaffold-time relative paths to the app's xcframeworks + codegen packages (or `.package(url:exact:)` in remote mode) |
| Codegen template | `build/generated/ios/Package.swift` | `.package(path: "../../xcframeworks")` |
| App target (pbxproj) | `<App>.xcodeproj` | local `XCLocalSwiftPackageReference` |
| Scaffolded community lib | `node_modules/<dep>/Package.swift` | scaffold-time relative paths to the app's xcframeworks + codegen packages |

## Remote-package mode
## Remote-package mode (planned)

> **Planned, not supported.** The `RN_SPM_REMOTE_URL` code path exists, but it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but since RN_SPM_REMOTE_URL is now "do not use", should the code print a warning when it's set? Today it's still honored without any message.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, a warning makes sense. I will do it in a separate follow-up PR, so this one stays docs-only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say not to use remote mode, but remotePackageConfig in spm-utils.js still turns it on silently whenever RN_SPM_REMOTE_URL or a leftover spm-remote.json url is present. The failure path also tells users to set RN_SPM_REMOTE_VERSION, which leads them further into the unsupported path. Could you add a runtime warning where remote mode is read, so the code backs up the docs? Fine as a follow-up PR if you prefer.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. As promised in the last round, the runtime warning comes in a separate PR. It will also stop pointing users at RN_SPM_REMOTE_VERSION.

> is unsupported. Do not use it. Libraries and apps do not need a remote form.
> The text below records the design intent.

A **URL alone** turns remote mode on: `RN_SPM_REMOTE_URL`, or the persisted
`url`. The whole app graph then uses a single remote React Native package
Expand Down Expand Up @@ -106,26 +110,62 @@ tooling never silently pins an unpublished tag.
## Hand-authored community library contract

The tooling leaves untouched a library that ships its own `Package.swift` (no
scaffolder or autolinker marker). That library needs only two things, and **no
discovery code**:

1. Depend on the React Native SPM package and its products — in remote mode
`.package(url: "<repo>", exact: "<version>")` +
`.product(name: "ReactHeaders", package: "<identity>")`,
`.product(name: "ReactNativeHeaders", package: "<identity>")`, and
`.product(name: "ReactNativeDependenciesHeaders", package: "<identity>")`,
where `<identity>` is the URL tail, lowercased, without `.git`. A scaffolded
manifest depends on the same React Native products. Libraries should declare
a version RANGE in production; the consuming app pins EXACT.
2. Ship its own generated code: set `codegenConfig.includesGeneratedCode: true`
and generate with
scaffolder or autolinker marker). That library needs two things, and **no
discovery code**. A third thing is optional:

1. Depend on the React Native package and the app's codegen package by relative

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that React-GeneratedCode declares platforms: [.iOS(.v15), ...] (codegen/templates/Package.swift.spm-template:31). A library that declares a lower floor (e.g. .iOS(.v13), common for libs supporting older RN) will fail resolution when it adds the ReactAppHeaders product. Could the contract say libraries need .iOS(.v15) or higher?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added: libraries that depend on ReactAppHeaders must declare .iOS(.v15) or higher, because React-GeneratedCode declares .iOS(.v15). Fixed in 7cd0824.

path. SwiftPM reaches the library's manifest through React Native's
per-library link folder, `build/generated/autolinking/libs/<SwiftName>`,
named after the library's Swift name, not its npm name. The paths are
relative to that folder:
`.package(name: "ReactNative", path: "../../../../xcframeworks")` and
`.package(name: "React-GeneratedCode", path: "../../../ios")`. The target
then depends on `.product(name: "ReactHeaders", package: "ReactNative")`,
`.product(name: "ReactNativeHeaders", package: "ReactNative")`,
`.product(name: "ReactNativeDependenciesHeaders", package: "ReactNative")`,
and `.product(name: "ReactAppHeaders", package: "React-GeneratedCode")`. A
scaffolded manifest depends on the same products. `React-GeneratedCode`
declares `.iOS(.v15)`, so the library must declare `.iOS(.v15)` or higher.
2. Set the React Native defines on each target. SwiftPM mode supports only the
New Architecture, and nothing sets `RCT_NEW_ARCH_ENABLED` for a hand-authored
manifest. Without it, the preprocessor drops all code behind
`#ifdef RCT_NEW_ARCH_ENABLED`. Keep those `#ifdef` gates in the sources: the
same sources still build under CocoaPods. C++ and Objective-C++ sources also
need the config-gated `DEBUG` and `NDEBUG` defines that React Native's own
targets get. Without `NDEBUG` in Release, the `DebugStringConvertible` vtable
and the `ShadowNode` layout differ from the prebuilt React framework, and the
link fails. A library with Swift sources sets the define in `swiftSettings`
too, as CocoaPods does through `OTHER_SWIFT_FLAGS`:

```swift
cSettings: [.define("RCT_NEW_ARCH_ENABLED", to: "1")],
cxxSettings: [
.define("RCT_NEW_ARCH_ENABLED", to: "1"),
.define("DEBUG", .when(configuration: .debug)),
.define("NDEBUG", .when(configuration: .release)),
],
swiftSettings: [.define("RCT_NEW_ARCH_ENABLED")],
```

3. Optionally, ship its own generated code. Set
`codegenConfig.includesGeneratedCode: true` and generate with
`generate-codegen-artifacts.js --path . --targetPlatform ios --source library`.
The output lands at `<outputDir>/build/generated/ios/ReactCodegen/`. The
manifest reaches it with one safe `.headerSearchPath(...)` into the library's
own tree. The app-side codegen then skips the library's spec, so there are no
duplicate symbols.

The library is then self-contained. It carries no app-layout knowledge and needs
no per-app codegen headers from the consuming app.
`@chrfalch/react-native-calculator` (a hand-authored Fabric/TurboModule library)
proves this.
This is how a hand-authored library links today. The folders these relative
paths reach, `build/xcframeworks` and `build/generated/ios` under the app's
`ios/` folder, are generated output: `spm` recreates them on each machine, and
they are gitignored. The paths resolve only when SwiftPM reaches the library
through its `libs/<SwiftName>` link folder, so the library cannot build on its
own (for example, `swift build` in its own repository). The relative paths
`../../../ios` and `../../../../xcframeworks` assume the default layout. Two
options of `generate-spm-autolinking.js` change it: `--output` moves the link
folder, and `--xcframeworks-path` moves the React Native package. With either
option set, the hardcoded paths do not resolve.

The normal pattern is rules 1 and 2 alone: the library uses the app's generated
code from `React-GeneratedCode`. A library that takes rule 3 carries its own
generated code instead.
12 changes: 7 additions & 5 deletions packages/react-native/scripts/spm/__docs__/spm-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ value, so **members you added to it by hand are lost**.
| `RN_DEPS_HEADERS_TARBALL_PATH` | Local `ReactNativeDependenciesHeaders` tarball, used instead of the copy inside the dependencies tarball. |
| `RCT_REACT_NATIVE_MAVEN_MIRROR_ENABLED` | `false` or `0` turns off the React Native Maven mirror. The mirror is on by default and is tried before Maven Central. |
| `ENTERPRISE_REPOSITORY` | Maven repository URL. When set, it is the only repository consulted for releases; the snapshot fallback still uses Maven Central snapshots. |
| `RN_SPM_REMOTE_URL`, `RN_SPM_REMOTE_VERSION` | Turn on remote-package mode — see [Remote-package mode](./spm-header-paths-contract.md#remote-package-mode). |
| `RN_SPM_REMOTE_URL`, `RN_SPM_REMOTE_VERSION` | Reserved for the planned remote-package mode. Not supported. See [Remote-package mode (planned)](./spm-header-paths-contract.md#remote-package-mode-planned). |
| `RN_DEP_VERSION` | Version of the `ReactNativeDependencies` artifact, or `nightly`. Defaults to the React Native version. |
| `HERMES_VERSION` | Version of the Hermes artifact, `nightly`, or `latest-v1`. Defaults to the locally pinned `hermes-compiler` version, else `latest-v1`. |

Expand Down Expand Up @@ -865,8 +865,10 @@ script branches on the exit code:
[Community packages without a Package.swift](#community-packages-without-a-packageswift));
- the autolinking config command fails (see
[The autolinking config command is remembered](#the-autolinking-config-command-is-remembered));
- remote mode is on but no usable React Native version resolves (see
[Remote-package mode](./spm-header-paths-contract.md#remote-package-mode));
- `RN_SPM_REMOTE_URL`, or a persisted `url`, is set but no usable React Native
version resolves. This happens only in the unsupported, planned remote mode
(see
[Remote-package mode (planned)](./spm-header-paths-contract.md#remote-package-mode-planned));
- The precompiled frameworks that autolinking plugins provide differ from the
frameworks that the Xcode project links (see
[Troubleshooting](#troubleshooting)). Sync writes the stamp and records the
Expand All @@ -883,5 +885,5 @@ script branches on the exit code:

These errors have their own exit code because a transient sync hiccup should not
break a build that could still succeed, while a missing manifest, an out-of-date
project, a broken config command, or an unresolvable remote version should not
pass silently.
project, a broken config command, or an unresolvable remote version (planned
remote mode) should not pass silently.
Loading