diff --git a/packages/react-native/scripts/spm/__docs__/README.md b/packages/react-native/scripts/spm/__docs__/README.md index b04da44fc81f..6b1221d609d1 100644 --- a/packages/react-native/scripts/spm/__docs__/README.md +++ b/packages/react-native/scripts/spm/__docs__/README.md @@ -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: diff --git a/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md b/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md index 878d738f2804..b622e05cf401 100644 --- a/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md +++ b/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md @@ -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: '', relPath: ''} // local - | {name: '', url: '', version: ''}, // remote (SPM-resolved) + | {name: '', url: '', version: ''}, // remote (planned, unsupported) products: [ {name: 'ReactHeaders', package: 'ReactNative'}, {name: 'ReactNativeHeaders', package: 'ReactNative'}, @@ -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. diff --git a/packages/react-native/scripts/spm/__docs__/spm-header-paths-contract.md b/packages/react-native/scripts/spm/__docs__/spm-header-paths-contract.md index 4055fed9f182..5bb89e4da559 100644 --- a/packages/react-native/scripts/spm/__docs__/spm-header-paths-contract.md +++ b/packages/react-native/scripts/spm/__docs__/spm-header-paths-contract.md @@ -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/`, `libs/`, plugin packages) | | Local-module wrapper | `build/generated/autolinking/packages//` | `.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) | `.xcodeproj` | local `XCLocalSwiftPackageReference` (or `XCRemoteSwiftPackageReference` in remote mode) | -| Scaffolded community lib | `node_modules//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) | `.xcodeproj` | local `XCLocalSwiftPackageReference` | +| Scaffolded community lib | `node_modules//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 +> 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 @@ -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: "", exact: "")` + - `.product(name: "ReactHeaders", package: "")`, - `.product(name: "ReactNativeHeaders", package: "")`, and - `.product(name: "ReactNativeDependenciesHeaders", package: "")`, - where `` 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 + path. SwiftPM reaches the library's manifest through React Native's + per-library link folder, `build/generated/autolinking/libs/`, + 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 `/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/` 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. diff --git a/packages/react-native/scripts/spm/__docs__/spm-scripts.md b/packages/react-native/scripts/spm/__docs__/spm-scripts.md index 4a8460d1a3a7..a09739d8c42c 100644 --- a/packages/react-native/scripts/spm/__docs__/spm-scripts.md +++ b/packages/react-native/scripts/spm/__docs__/spm-scripts.md @@ -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`. | @@ -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 @@ -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.