From c3fee6619740faa0cf6150d29a87e32708473c21 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Thu, 29 Oct 2026 18:08:20 +0100 Subject: [PATCH 1/5] Mark SwiftPM remote-package mode as planned in the docs The SwiftPM docs described remote-package mode (React Native fetched by URL through RN_SPM_REMOTE_URL) as a current feature. It is planned and not supported. Mark it as planned, and describe the hand-authored library contract in the supported local form. --- .../scripts/spm/__docs__/README.md | 2 +- .../spm/__docs__/spm-autolinking-plugins.md | 10 ++-- .../spm/__docs__/spm-header-paths-contract.md | 54 +++++++++++-------- .../scripts/spm/__docs__/spm-scripts.md | 12 +++-- 4 files changed, 46 insertions(+), 32 deletions(-) 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..ee6165b25104 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,8 @@ react: { } ``` -- The `packageRef` keys signal local or remote mode: `path` xor `url`+`version`. +- `packageRef` has the local `path` shape. The `url`+`version` shape appears + only in the unsupported, planned remote mode. - `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..a733958d938b 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,34 @@ 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 one thing, and **no +discovery code**. A second 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/`. 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. +2. Optionally, ship its own generated code. This suits a library that wants to + be self-contained. 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. +These relative paths are the intended design. The folders they reach, +`build/xcframeworks` and `build/generated/ios` under the app's `ios/` folder, +are stable, so a library may rely on them. + +The normal pattern is rule 1 alone: the library uses the app's generated code +from `React-GeneratedCode`. A library that takes rule 2 carries its own +generated code instead. `@chrfalch/react-native-calculator` (a hand-authored +Fabric/TurboModule library) is an example of that option. 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. From b32bad9b83b09a63bbd6b4a5890e1ffdec1d1f84 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Fri, 30 Oct 2026 20:09:45 +0100 Subject: [PATCH 2/5] Address review: describe library link paths as current layout, not a contract - Say the build folders are generated and gitignored, and resolve only through the libs/ link folder. - Name the link folder after the library's Swift name. - Tell plugin authors to handle the url+version packageRef shape too. --- .../spm/__docs__/spm-autolinking-plugins.md | 5 +++-- .../spm/__docs__/spm-header-paths-contract.md | 15 ++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) 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 ee6165b25104..b982a147314c 100644 --- a/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md +++ b/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md @@ -322,8 +322,9 @@ react: { } ``` -- `packageRef` has the local `path` shape. The `url`+`version` shape appears - only in the unsupported, planned remote mode. +- Today `packageRef` always has the local `path` shape. Handle the + `url`+`version` shape too, because remote mode is planned and `context.react` + exists so that plugins keep working when it arrives. - `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 a733958d938b..71c93e0756aa 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 @@ -115,8 +115,9 @@ discovery code**. A second 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/`. The paths - are relative to that folder: + 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")`, @@ -133,9 +134,13 @@ discovery code**. A second thing is optional: own tree. The app-side codegen then skips the library's spec, so there are no duplicate symbols. -These relative paths are the intended design. The folders they reach, -`build/xcframeworks` and `build/generated/ios` under the app's `ios/` folder, -are stable, so a library may rely on them. +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). Libraries depend on this +layout today, so React Native will not change it without notice. The normal pattern is rule 1 alone: the library uses the app's generated code from `React-GeneratedCode`. A library that takes rule 2 carries its own From c0e4b96bcfcef952ed2f5e861451d02476499e71 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Tue, 3 Nov 2026 12:47:02 +0100 Subject: [PATCH 3/5] Require RCT_NEW_ARCH_ENABLED in hand-authored library manifests SwiftPM mode is New Architecture only, and nothing else sets the define for a SwiftPM target. Libraries keep their #ifdef gates for CocoaPods builds. --- .../spm/__docs__/spm-header-paths-contract.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 71c93e0756aa..afd00db13ef7 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 @@ -110,8 +110,8 @@ 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 one thing, and **no -discovery code**. A second thing is optional: +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 @@ -125,7 +125,13 @@ discovery code**. A second thing is optional: `.product(name: "ReactNativeDependenciesHeaders", package: "ReactNative")`, and `.product(name: "ReactAppHeaders", package: "React-GeneratedCode")`. A scaffolded manifest depends on the same products. -2. Optionally, ship its own generated code. This suits a library that wants to +2. Define `RCT_NEW_ARCH_ENABLED=1` in both `cSettings` and `cxxSettings` of each + target: `.define("RCT_NEW_ARCH_ENABLED", to: "1")`. SwiftPM mode supports + only the New Architecture, and nothing else sets this define for a SwiftPM + target. 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. +3. Optionally, ship its own generated code. This suits a library that wants to be self-contained. Set `codegenConfig.includesGeneratedCode: true` and generate with `generate-codegen-artifacts.js --path . --targetPlatform ios --source library`. @@ -142,7 +148,7 @@ through its `libs/` link folder, so the library cannot build on its own (for example, `swift build` in its own repository). Libraries depend on this layout today, so React Native will not change it without notice. -The normal pattern is rule 1 alone: the library uses the app's generated code -from `React-GeneratedCode`. A library that takes rule 2 carries its own +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. `@chrfalch/react-native-calculator` (a hand-authored Fabric/TurboModule library) is an example of that option. From 7cd0824c6eeb87a4fb41f7af4584d51b627139e0 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Tue, 3 Nov 2026 13:46:34 +0100 Subject: [PATCH 4/5] Address review: complete the hand-authored library defines and packageRef shapes - Rule 2 adds the DEBUG/NDEBUG configuration defines and the Swift form of RCT_NEW_ARCH_ENABLED. - The define note is scoped to hand-authored manifests. - The link-folder paths assume the default layout; --output and --xcframeworks-path move them. - Libraries that depend on ReactAppHeaders must declare iOS 15 or higher. - Rule 3 no longer calls the library self-contained. - The plugins doc says packageRef has two shapes and context.react can be null. --- .../spm/__docs__/spm-autolinking-plugins.md | 8 ++-- .../spm/__docs__/spm-header-paths-contract.md | 40 ++++++++++++++----- 2 files changed, 34 insertions(+), 14 deletions(-) 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 b982a147314c..b622e05cf401 100644 --- a/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md +++ b/packages/react-native/scripts/spm/__docs__/spm-autolinking-plugins.md @@ -322,9 +322,11 @@ react: { } ``` -- Today `packageRef` always has the local `path` shape. Handle the - `url`+`version` shape too, because remote mode is planned and `context.react` - exists so that plugins keep working when it arrives. +- `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 afd00db13ef7..c49dd185e516 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 @@ -124,16 +124,31 @@ discovery code**. A third thing is optional: `.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. -2. Define `RCT_NEW_ARCH_ENABLED=1` in both `cSettings` and `cxxSettings` of each - target: `.define("RCT_NEW_ARCH_ENABLED", to: "1")`. SwiftPM mode supports - only the New Architecture, and nothing else sets this define for a SwiftPM - target. Without it, the preprocessor drops all code behind + 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. -3. Optionally, ship its own generated code. This suits a library that wants to - be self-contained. Set `codegenConfig.includesGeneratedCode: true` and - generate with + 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 @@ -145,8 +160,11 @@ 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). Libraries depend on this -layout today, so React Native will not change it without notice. +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 From 9ad602dfd8b489ba267993405505cc8fcb5cdd57 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Tue, 3 Nov 2026 13:49:18 +0100 Subject: [PATCH 5/5] Remove the out-of-repo calculator example from the SwiftPM contract doc --- .../scripts/spm/__docs__/spm-header-paths-contract.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 c49dd185e516..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 @@ -168,5 +168,4 @@ 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. `@chrfalch/react-native-calculator` (a hand-authored -Fabric/TurboModule library) is an example of that option. +generated code instead.