feat(iOS) swiftpm support in iOS#57332
Conversation
64fcaab to
68bbbba
Compare
68bbbba to
a0a1630
Compare
85a1d6f to
40320bc
Compare
40320bc to
9623012
Compare
8177e87 to
c47b761
Compare
The minimal machinery to build the packaged header structures: - headers-spec.js: the executable layout contract (rules R1-R8) — which namespaces are hoisted into the React framework, which carry module maps, and how collisions are rejected. - headers-inventory.js: scans the source tree and classifies every shipped header (language surface + modularizability bucket) — the input to the spec. computeInventory() feeds the build in-memory; the CLI writes a JSON manifest. - headers-compose.js: emits the layout — writes the <React/...> headers + umbrella + module map into each React.framework slice (detected by the framework's presence), and assembles the headers-only ReactNativeHeaders.xcframework (every other namespace + deps + Hermes). Called by xcframework.js during compose. This is the alternative header source that lets consumers resolve React Native headers without a clang VFS overlay. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emit the headers-spec layout unconditionally and delete the VFS overlay across
JS, CI publish, and Ruby. Consumers resolve headers the way the SwiftPM branch
does: <React/...> from the vendored React.framework, every other namespace from
ReactNativeHeaders. No root Headers/ on the xcframework, no VFS.
JS:
- xcframework.js: always emit the React.framework spec layout and build
ReactNativeHeaders.xcframework (was gated behind RN_ZERO_I_LAYOUT=1). Remove
the legacy header path entirely — the podspec->root-Headers enumeration,
createModuleMapFile, and copyHeaderFilesToSlices — so the published
React.xcframework is a standard framework (Info.plist + per-slice
React.framework/{Headers,Modules}), no root Headers/ or Modules/. Ship
ReactNativeHeaders.xcframework inside the reactnative-core tarball (sibling of
React.xcframework) so the prebuilt pod can vend both; keep the standalone
ReactNativeHeaders.xcframework.tar.gz for the SPM path. Drop the
React-VFS-template.yaml emit and the ./vfs import.
- vfs.js: deleted (its only consumer was xcframework.js).
- types.js: drop the now-unused VFSEntry/VFSOverlay/HeaderMapping types.
- replace-rncore-version.js: drop the React-VFS.yaml preservation rationale.
Ruby/CocoaPods:
- React-Core-prebuilt.podspec: vend React.xcframework (its per-slice
React.framework + module map serves <React/...> and @import React via
FRAMEWORK_SEARCH_PATHS); flatten ReactNativeHeaders' headers into a top-level
Headers/ in prepare_command and expose them via the pod header search path.
Drop the VFS-era root header_mappings_dir/module_map.
- rncore.rb: remove the -ivfsoverlay injection and process_vfs_overlay;
add_rncore_dependency and configure_aggregate_xcconfig now add a
HEADER_SEARCH_PATHS to React-Core-prebuilt/Headers for podspec, aggregate, and
third-party targets.
- react_native_pods.rb: drop the process_vfs_overlay post-install call.
Docs: replace the "VFS Overlay System" section with the headers-spec layout;
drop the obsolete "Known Issues" (pre-headers-spec) section.
Verified end-to-end: prebuild compose produces a VFS-free, root-Headers-free
React.xcframework; rn-tester pod install + xcodebuild (prebuilt path) BUILD
SUCCEEDED with zero -ivfsoverlay.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
React.framework is a clang module; when an SPM consumer precompiles it, a
modular React/ header that #imports <react/...> hit
-Wnon-modular-include-in-framework-module because the lowercase react/
namespace (served from ReactNativeHeaders, per R1's Linux/Windows-safe layout)
was deliberately kept out of any module.
Give react/ a module where it already lives instead of relocating it (relocation
would require case-folding react.framework -> React.framework, which only works
on case-insensitive filesystems):
- headers-spec.js: drop the react/ namespace-module exemption so its
objc-modular-candidates get a module; emit that module as
ReactNativeHeaders_react (a module literally named 'react' would alias the
React framework module on a case-insensitive filesystem). Module names are
internal; <react/...> still resolves by header path and is now modular.
- headers-inventory.js: classify C++ default member initializers in aggregates
(e.g. struct { NSString *family = nil; } in RCTFontProperties.h) as ObjC++ so
these are not misclassified objc-modular-candidate and pulled into a plain
ObjC module they cannot compile in.
The unguarded ObjC/C react/ headers (e.g. JSRuntimeFactoryCAPI.h) now resolve
modularly; the C++ react/renderer/* includes are #ifdef __cplusplus-guarded and
skipped during the ObjC module emit, so they need no module.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In prebuilt mode the React core pods' code + headers live entirely in React.xcframework / React-Core-prebuilt. Re-installing their SOURCE podspecs made them ship duplicate headers that shadow the prebuilt artifact and break the React framework's clang explicit-module precompile (-Wnon-modular-include-in-framework-module) under Xcode 26. Install those core pods as dependency-only FACADES instead: generated podspecs with no sources/headers, installed via :path (so nothing is fetched), each depending on React-Core-prebuilt. Version, subspecs, default_subspec and resources (e.g. the privacy manifest) are DERIVED from the real podspec so the facade stays graph- and resource-equivalent to the source pod. With the shadowing gone the React module precompiles cleanly with SWIFT_ENABLE_EXPLICIT_MODULES on, so the Xcode-26 workaround (#53457) is removed. The prebuilt header search path + ReactNativeHeaders module-map activation are consolidated into a single post-install injection site (configure_aggregate_xcconfig); add_rncore_dependency now only declares the React-Core-prebuilt dependency. rn-tester's NativeComponentExample uses the canonical <React/...> include for RCTFabricComponentsPlugins.h (resolved from the framework) so it builds against the facaded React-RCTFabric in prebuilt mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`yarn format-check` (prettier) was failing CI on PR #57285. Run prettier on the ios-prebuild headers scripts (headers-compose.js, headers-inventory.js), replace-rncore-version.js, and __docs__/README.md so format-check passes. No logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tarball The prebuilt React core now ships two xcframeworks — React.xcframework and the headers-only ReactNativeHeaders.xcframework. React-Core-prebuilt's prepare_command flattens the latter's Headers (including module.modulemap) into the pod. The compose step only tar'd React.xcframework, so consumers got no React-Core-prebuilt/Headers/module.modulemap and failed the build with "module map file ... not found". Tar both xcframeworks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le (R9) The public-umbrella model (which replaced the VFS overlay) excludes `+Private` and objc-blocked headers from React.framework's module, so privileged framework consumers (e.g. Expo) that `#import <React/RCTBridge+Private.h>`, `<React/RCTMountingManager.h>`, etc. fail to compile under explicit modules even though the headers still ship in React.framework/Headers. Add R9: a curated allowlist appended to the React module map — `RCTBridge+Private.h` as a real `header` (objc-modular-candidate, reaches no C++) and the six Fabric headers as `textual header` (objc-blocked; a real member would re-trip -Wnon-modular-include, and their <react/...> C++ includes resolve at the consumer's use site). Backwards-compatible: existing `#import <React/...>` (and Swift `import React`) sites are unchanged. Fails closed if an allowlisted header is removed/renamed or drifts bucket. Note: RCTUIKit.h / RCTRootContentView.h are absent from source entirely and need restoration, not exposure — out of scope here.
The flattened ReactNativeHeaders layout ships the individual React_RCTAppDelegate/*.h headers but no per-namespace umbrella. Consumers like Expo probe `<React_RCTAppDelegate/React_RCTAppDelegate-umbrella.h>` via __has_include (RCTAppDelegateUmbrella.h); with the umbrella gone the probe fails and RCTReactNativeFactory / RCTRootViewFactory are never declared, breaking the Expo pod's clang module. Add R10: emit a per-namespace umbrella (content DERIVED from namespaceModules so it can't drift — e.g. RCTArchConfiguratorProtocol.h, gone from this branch, is correctly omitted) and add it to that namespace's module so the import stays modular under explicit modules. Targeted via UMBRELLA_NAMESPACES (currently just React_RCTAppDelegate, the only umbrella Expo imports); fails closed if a listed namespace loses its modular headers.
…ic facade Community Fabric modules quote-import "RCTFabricComponentsPlugins.h" (~47x: slider, maps, pager-view, keyboard-controller, ...). In source, React-RCTFabric vended it at header_dir "React", so it landed in dependents' CocoaPods header maps and the bare quoted name resolved. In prebuilt mode React-RCTFabric is a dependency-only facade that ships no headers — the only copy is baked angle-only into React.framework (it's objc-blocked, excluded from the framework module map), so quoted imports fail to resolve. Re-vend JUST that one header from the facade (FACADE_REEXPOSED_HEADERS), copied as a self-contained snapshot at header_dir "React", restoring dependents' header maps exactly as the source pod did. Header-only (no compiled sources, no duplicate symbols — the impl stays in React.framework). Re-exposing a single header does not put <react/...>/<yoga/...> on -I, so it does not reintroduce the non-modular-include shadowing the modular layout eliminates. Fails closed if the glob matches nothing.
…the headers compose buildReactNativeHeadersXcframework copied each declared deps namespace (folly/glog/boost/fmt/double-conversion/fast_float) from the staged ReactNativeDependencies headers, but only console.warn'd on a missing one and kept going — silently shipping a ReactNativeHeaders.xcframework without third-party header resolution (consumers then fail on <folly/...> etc.). The summary log also printed the INTENDED namespace list, masking the gap. Throw instead: a missing declared deps namespace means deps weren't staged (third-party/ReactNativeDependencies.xcframework/Headers — from a full prebuild or the cache slot), so refuse to emit an incomplete artifact.
…est imports Two CI fixes for the prebuild-ios-core workflow: - prebuild-ios-core.yml: the compose-xcframework job downloaded the build slices and React headers but never staged third-party/ReactNativeDependencies.xcframework. buildReactNativeHeadersXcframework folds the third-party deps namespaces (folly/glog/boost/...) into ReactNativeHeaders.xcframework, so it fail-closed with "deps namespace 'folly' missing ... refusing to ship an incomplete ReactNativeHeaders.xcframework". Add the Download + Extract ReactNativeDependencies steps (mirroring build-slices) so the deps headers are present before composing. - headers-spec-test.js: reorder requires so the `../headers-spec` import sorts before `fs`, fixing the @react-native/monorepo/sort-imports warning that failed `eslint --max-warnings 0` in test_js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eaders to Maven
Publish the two headers-only xcframeworks as standalone artifacts on
the react-native-artifacts coordinate so SwiftPM consumers can wire
them as separate binaryTargets (they also keep shipping inside the
ReactCore / ReactNativeDependencies tarballs for CocoaPods):
- external-artifacts/build.gradle.kts: four new PublishArtifacts with
classifiers reactnative-headers-{debug,release} and
reactnative-dependencies-headers-{debug,release}.
- prebuild-ios-core.yml: upload the standalone
ReactNativeHeaders<flavor>.xcframework.tar.gz the compose step
already produces (previously only bundled inside the ReactCore
tarball); include it in the workflow cache.
- build-npm-package action: download ReactNativeHeaders* artifacts
(ReactNativeDependenciesHeaders* already rides the existing
ReactNativeDependencies* pattern).
- verifyArtifactsAreOnMaven.js: HEAD-check every classifier tarball,
not just the POM - a release with a missing classifier artifact now
fails verification instead of passing silently.
Validated locally end-to-end with publishReleasePublicationToMavenLocal:
all 12 files + POM land on com.facebook.react:react-native-artifacts
with the expected classifier names.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verifier now HEAD-checks the POM plus all eight classifier tarballs instead of GETting only the POM; the mocks/assertions follow, and a new case covers a missing classifier behind an existing POM (the exact gap the classifier checks were added to catch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scripts/spm toolchain that generates a SwiftPM Package.swift + xcodeproj for React Native and community libraries, on top of the headers-spec header layout (React.framework + ReactNativeHeaders, no VFS). Consumes headers-compose.ensureHeadersLayout to compose the header artifacts; ships the compose runtime + scripts/spm via the npm files allowlist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPM-mode consumption for the example apps: rn-tester and helloworld each get a Package.swift + *-SPM xcodeproj that consume React via SwiftPM product deps (zero header search paths). Adds react-native-test-library (apple + common) and its rn-tester example, plus the react-native.config.js spmModules wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…scripts generate-spm-package previously composed the headers-spec layout on the consumer side when the prebuilt artifacts lacked ReactNativeHeaders.xcframework. That pulled the ios-prebuild build scripts (headers-compose and its deps) into the published npm package, which in turn required the `plist` dependency at consumer runtime. The prebuilt core artifact ships React.xcframework AND ReactNativeHeaders.xcframework together, so a consumer never needs to compose the layout. Remove the consumer-side compose: fail with a clear error if ReactNativeHeaders is absent, and stop listing scripts/ios-prebuild/* in the package files allowlist — those are full-repo prebuild scripts, not consumer runtime code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m-scratch generator
Replace the ambiguous `init`/`--from-scratch` (with its silent
in-place→from-scratch fall-through that could rename a user's project to
.legacy) with four clear verbs:
add inject SPM into the existing .xcodeproj, in place (idempotent;
fail-loud on a CocoaPods-integrated pbxproj). --deintegrate runs
`pod deintegrate` + strips RN from the Podfile + removes the
leftover empty Pods group.
update re-run the pipeline + refresh the injection.
deinit surgical inverse of add (reverse exactly what .spm-injected.json
records → byte-identical revert; no prompt).
scaffold unchanged.
sync/codegen/download stay but are hidden from primary help.
Zero-arg `npx react-native spm` auto-resolves: injected→update; a fresh
CocoaPods project (safe-gate: stock Podfile + git-clean pbxproj/Podfile)→
add --deintegrate; else strict add.
- Delete the from-scratch xcodeproj generator + legacy-migration/Podfile-patch
machinery and the now-dead whole-file-generation helpers in spm-pbxproj.js.
- Add surgical pbxproj removal primitives (removeObjectByUuid,
removeArrayMembersByUuid, removeField, removeArrayStringValues,
removeEmptyPodsGroup); injection now records its exact edits in
.spm-injected.json so deinit can reverse them.
- Merge flags: --skip/force-download → --download <auto|skip|force>;
--local-xcframework + --artifacts-dir → --artifacts <path>. Remove
--skip-xcodeproj/--force-xcodeproj/--bundle-identifier/--entry-file and the
--platform-name CLI shim.
- generate-spm-package now throws on artifact-slot failures (incl. missing
ReactNativeHeaders) instead of a silent exitCode+return.
- Update CLI registration, __doc__/spm-scripts.md, and the spm test suite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The newer toolchain on the rebased base flags deprecated Flow utility types and a few lint nits in the SPM scripts + test library: - Flow: `$ReadOnly`/`$ReadOnlyArray`/`$ReadOnlySet` -> `Readonly*`, `$Shape` -> `Partial`, `mixed` -> `unknown`, `+` variance sigil -> `readonly`; narrow the nullable remote-package config before reading `.url`/`.version` (the `plan.isRemote` branch didn't refine it). - ESLint: sort requires in expand-spm-dependencies.js and the two react-native-test-library entrypoints; drop unnecessary `\"` escapes in generate-spm-autolinking-test.js. - Prettier formatting on spm-pbxproj.js. No behavior change; 349 scripts/spm tests still green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…orld
The `*-SPM.xcodeproj` projects (+ their app-local Package.swift) added to
the example apps were untested demo scaffolding — no CI job builds them.
In helloworld they also broke `test_ios_helloworld`: two `.xcodeproj` in
`ios/` made CocoaPods unable to auto-select the project ("Could not
automatically select an Xcode project") during `pod install`.
Remove Helloworld-SPM.xcodeproj and RNTester-SPM.xcodeproj plus their
Package.swift, and revert the SPM-artifact lines from the .gitignore
files. The rn-tester TestLibrary example and SPM tooling are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReactNativeHeaders is pure-RN after the deps-headers split: the third-party namespaces (folly/glog/boost/fmt/double-conversion/ fast_float/SocketRocket) ship in the ReactNativeDependenciesHeaders sidecar the deps prebuild emits (the binary deps xcframework is framework-type — invisible to SwiftPM binaryTargets). - download-spm-artifacts: headers-only companions are staged out of their parent tarball (ReactNativeHeaders from the ReactCore tarball, the sidecar from the deps tarball) on fresh extracts, with fast-path backfill — generalizing the hermes-headers pattern. This retires the --headers-tarball priming workaround for ReactNativeHeaders; the override remains for testing, joined by --deps-headers-tarball / RN_DEPS_HEADERS_TARBALL_PATH. Both companions register in artifacts.json fail-closed (pre-companion tarballs die with actionable advice). REQUIRED_ARTIFACTS now lists all five artifacts, fixing the inconsistency where generate-spm-package hard-required ReactNativeHeaders but cache validation never checked it. - generate-spm-package needs no code change: it renders one product + binaryTarget per artifacts.json entry. - Product consumers add the ReactNativeDependenciesHeaders product wherever deps namespaces must resolve: reactProductDeps() (autolinking), the codegen Package.swift template, and scaffolded community-library manifests (SCAFFOLDER_VERSION 17 -> 18 so existing scaffolds regenerate). Verified: fresh-app E2E (verdaccio -> cli init -> spm add -> build), rn-tester and helloworld in-place migrations, 350 spm jest tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrating rn-tester (RNTesterPods.xcodeproj, in-place) and helloworld to
SwiftPM surfaced five latent bugs:
- scaffold: podspecs with root-level sources (s.source_files =
"*.{h,m,mm}") got no publicHeadersPath, so SPM rejected the target
over the nonexistent default include/. The scaffolder now mirrors
root headers into a generated include/<SwiftName>/ shim dir
(publicHeadersPath: "include"), giving dependents the CocoaPods
header-map spelling `#import <SwiftName/Header.h>`.
- scaffold: root podspec globs matched the scaffolder's own output on
re-runs — Package.swift's `.swift` extension tripped the
mixed-language gate into DELETING the manifest it had just written
(runs alternated between scaffolding and self-destructing). Scaffold
artifacts (Package.swift, the prefix header, include/ shims) are now
excluded from source expansion.
- autolinking: the one-time legacy-layout cleanup unconditionally
removed `<dep>/include/`, nuking the scaffolder's shim dirs on every
sync. It now only removes include/ together with a marker-bearing
legacy in-place Package.swift.
- scaffold: transitive spm.dependencies entries carry no podspecPath,
so podspec-name sibling deps (s.dependency "TestLibraryCommon")
could not be wired ("Unknown dependency"). The sibling map now
discovers podspecs at those deps' roots.
- add --deintegrate: a ${PODS_ROOT}-anchored REACT_NATIVE_PATH (the
CocoaPods template default) dangles once CocoaPods is deintegrated,
breaking the "Bundle React Native code and images" phase. The
injection now replaces it with the SPM-computed path, recording the
original in the marker so deinit restores it exactly.
Also declares TestLibraryApple's real dependency on TestLibraryCommon
in its podspec (CocoaPods resolved the undeclared edge leniently
through the shared Public headers dir; SPM needs it for sibling
package wiring).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- download-spm-artifacts.js: the tar/cp invocations interpolated URL/env-derived paths into shell strings (js/shell-command-injection, incl. one new critical on the companion backfill path). All four now use execFileSync argv form — no shell involved. - spm-pbxproj.js: generateUUID used md5 (js/weak-cryptographic-algorithm). Not a security use — the hash only derives deterministic pbxproj UUIDs — but sha256 (truncated to the same 24 hex chars) is just as deterministic and keeps the scanner green. Affects only not-yet- released injections; the marker records actual UUIDs so deinit of md5-era injections is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c47b761 to
f05129b
Compare
Overhaul the SwiftPM tooling docs to match current behavior: - Mark the feature Preview (header + intro) — commands/layout/distribution may change; CocoaPods stays the default. - Fresh clones & CI: document that Xcode resolves the package graph before any build phase, so a clean checkout needs one `npx react-native spm` run (or a `postinstall` hook) before xcodebuild — the auto-sync phase can't bootstrap it. Correct the Quick Start lines that implied otherwise, and add a "Resolve Package Graph" row + the matching Troubleshooting entry. - Drop stale VFS overlay references (VFS was removed; headers now come from the React / ReactNativeHeaders / ReactNativeDependenciesHeaders products) — rewrite Header Resolution, and fix the sync-spm-autolinking.js comment. - Directory Layout: list all five artifacts. - Migration: spell out exactly what `--deintegrate` removes vs preserves and how to keep non-RN pods (re-`pod install`, open the .xcworkspace, never re-add `use_react_native!`). - New Brownfield section (project must live inside the JS tree; use `--xcodeproj`/`--productName`; native-root-with-RN-subfolder unsupported). - New "Community packages without a Package.swift" section — scaffold + patch-package, and recommend filing an issue/PR to ship the manifest upstream. - Use "SwiftPM" in prose throughout (identifiers, the `spm` command, and the literal "Sync SPM Autolinking" phase name kept verbatim); move the Pipeline/Directory/Header/Auto-Sync deep-dives into a Reference section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re dep
rn-tester's test libraries (TestLibraryApple / TestLibraryCommon) failed to
build under SwiftPM with `RCTDeprecation/RCTDeprecation.h file not found`: the
scaffolded Package.swift carried no React products because the scaffolder's
`coreReactNative` detection missed them.
Both are plain ObjC modules that wire React core ONLY through the
`install_modules_dependencies(s)` podspec helper and ship no `codegenConfig`.
The scaffolder's two detection paths both fall through for that shape:
1. Explicit `s.dependency "React-Core"` — but the helper is stripped before
`pod ipc spec` (and its `min_supported_versions` call makes pod-ipc fail
outright, falling back to the regex parser, which can't expand the helper
either), so React-Core never surfaces in the parsed dependencies.
2. package.json `codegenConfig` — only marks Fabric / New-Arch libraries,
not plain ObjC modules.
Record whether the podspec source calls `install_modules_dependencies` (visible
even though the helper is stripped for pod-ipc) and treat its presence as an
authoritative React-core signal alongside codegenConfig — injecting that
family is the helper's entire job.
- read-podspec.js: set `usesInstallModulesDependencies` from the podspec source
- scaffold-package-swift.js: OR it into the coreReactNative fallback
- spm-types.js: add the field to PodspecModel
Verified: TestLibraryApple/Common now scaffold with the full React product set
(ReactNative, ReactNativeHeaders, ReactNativeDependenciesHeaders,
ReactAppHeaders); rn-tester BUILD SUCCEEDED. New focused unit test covers the
plain-ObjC-module-with-install_modules_dependencies shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iew) Add an extension seam so a framework with its own module system (Expo is the first consumer) can contribute to the SwiftPM autolinking graph that `npx react-native spm` generates — the analog of the Podfile / `use_expo_modules!` / `react_native_post_install` hooks CocoaPods gave Expo. A one-shot post-process of the generated Package.swift is clobbered on the next sync (the Xcode auto-sync build phase re-runs autolinking on every dependency change), so plugins are invoked from generate-spm-autolinking's `main()` — the single function both `add`/`update` and the build-time `sync` call — and run on every regeneration. Discovery is transitive and zero-config: any autolinked dep opts in from its own react-native.config.js (`spm.autolinkingPlugin`), mirroring how CocoaPods pulls in `use_expo_modules!`. An app can exclude a plugin via `spm.denyPlugins` (escape hatch, not a required allowlist). Fail-closed and named: a plugin that can't load, doesn't export a function, throws, or returns a malformed contribution aborts the run identifying the framework — a silent drop (green build, missing native code) is worse than a loud stop. A plugin returns DATA (packageDependencies / productDependencies / generatedSources); RN owns the merge, so regeneration stays deterministic and idempotent, and package/product deps are deduped by name. The plugin context exposes `react` — RN's own React package ref + product set (local xor remote, incl. ReactAppHeaders from the per-app React-GeneratedCode package), derived from one source of truth so a plugin depends on exactly RN's React surface without re-deriving paths/identity that move as RN repackages. Also threads `--flavor` (debug/release) through setup + sync into the context so plugins can pick the matching prebuilt slice. Contract is Preview / unstable while Expo validates it; to be ratified via RFC. `generatedSources` is captured but its codegen registration + provider ordering is intentionally left for the first real plugin to drive to a stable shape. - autolinking-plugins.js: discoverPlugins + invokePlugins (new) - generate-spm-autolinking.js: discovery/invoke in main(), reactDescriptor, plugin-host-dep target exemption, merge into the aggregator manifest - spm-types.js: PluginContext / ReactDescriptor / PluginResult / …; flavor arg - setup-apple-spm.js, sync-spm-autolinking.js: thread --flavor - __doc__/spm-autolinking-plugins.md (new) + spm-scripts.md pointer - tests: autolinking-plugins-test.js (new) + reactDescriptor / contribution cases in generate-spm-autolinking-test.js Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive generate-spm-autolinking's main() over a minimal app fixture whose only autolinked iOS dep is a plugin host (expo: native sources, no Package.swift) to pin down the exemption added with the plugin system: a dep that declares an `spm.autolinkingPlugin` owns its native contribution and must NOT also be source-built through the community-lib path. A regression pair, so the guard is meaningful rather than incidental: - With the plugin declared: main() does not throw, the plugin's ExpoModulesCore package + product contribution is merged into the aggregator, and expo is not source-built (no `packages/<Name>` wrapper ref, no `__rnAutolinkedLibs` guard). - Negative control: the SAME fixture with the plugin declaration removed throws MissingManifestError — proving the exemption is load-bearing. This exercises the full main() path (dep walk, plugin discovery/invoke, manifest write) rather than a pure helper, which is why it was missing; the surrounding pieces (discoverPlugins / invokePlugins / the manifest merge) were already covered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| cd "$SRCROOT" | ||
| if command -v npx >/dev/null 2>&1; then | ||
| npx react-native spm sync | ||
| RC=$? |
There was a problem hiding this comment.
Major: This generated build phase uses set -euo pipefail, then runs npx react-native spm sync followed by RC=$?. Under set -e, a non-zero exit terminates the script immediately, so this RC=$? line never runs for a failure — the intended 'transient sync failure (exit 1) → print warning, exit 0, don't break the build' branch is unreachable and the build fails on any transient sync error. Capture with npx react-native spm sync || RC=$? (or wrap in an if). Not covered by tests since the generated shell is never executed.
| // Build into a temp sibling, then swap: the farm now lives INSIDE | ||
| // build/generated/ios (one of the trees being folded), so building in place | ||
| // would make foldDir walk the half-built farm itself. | ||
| const tmpDir = outDir + '.tmp'; |
There was a problem hiding this comment.
Major: tmpDir = outDir + '.tmp' sits inside build/generated/ios, which foldDir then descends into, so the folded tree gets a spurious ReactAppHeaders.tmp/ namespace duplicating every codegen header on every build. Harm is bounded, but it's a clear logic bug — place tmpDir outside the folded root, or have foldDir/buildPerAppHeaderTree skip the outDir/tmpDir basename. (The existing test only asserts the nested ReactAppHeaders/ is absent, not ReactAppHeaders.tmp/.)
| } | ||
| } | ||
|
|
||
| fs.writeFileSync( |
There was a problem hiding this comment.
Major: The --artifacts <local.xcframework> path writes artifacts.json without ReactNativeHeaders/ReactNativeDependenciesHeaders, but validateArtifactsCache (download-spm-artifacts.js) requires them, so the local slot reports incomplete and triggers a full Maven download — defeating the documented 'use it directly, no download' contract. And even if skipped, generate-spm-package.js throws when ReactNativeHeaders is absent. The advanced local-xcframework flow appears broken under the sidecar-headers model; worth an end-to-end check.
| // raw stdout then throws and we'd silently fall back to the (much weaker) | ||
| // regex parser. Extract just the JSON object (first `{` … last `}`). | ||
| const stdout = result.stdout; | ||
| const start = stdout.indexOf('{'); |
There was a problem hiding this comment.
Major: Slicing between the first { and last } assumes pre-JSON diagnostics contain no braces. A diagnostic like Building {module} shifts start, JSON.parse throws, runPodIpcSpec returns null, and the code silently falls back to the weaker, subspec-blind regex parser with no warning that the authoritative parse was discarded. Consider parsing from the last balanced object, requiring JSON to start at a line boundary, or at least logging when the primary parse is abandoned.
|
|
||
| // sources: emit only when podspec declared them. If empty, SPM auto-scans | ||
| // the target dir — which for `path: "."` means the entire dep root. We | ||
| // exclude common non-source files via the `exclude:` list to keep that |
There was a problem hiding this comment.
Minor: The comment says non-source files are excluded via an exclude: list, but emitScaffoldedPackageSwift emits no exclude:. When spec.sources is empty (regex-fallback podspec), the target uses path: "." with no excludes and SPM scans the whole dep root. Either emit the promised exclude list or fix the comment.
|
|
||
| const field = findField(text, obj, key); | ||
| if (field != null) { | ||
| const fresh = values.filter(v => !field.value.includes(v)); |
There was a problem hiding this comment.
Minor: Dedup by includes can false-positive if an injected token is a substring of an existing one (e.g. -ObjC vs a hypothetical -ObjCFoo). Harmless for current values, but token/exact-member matching would be safer.
| if (swiftName == null) { | ||
| continue; | ||
| } | ||
| const existing = seen.get(swiftName); |
There was a problem hiding this comment.
Minor: The collision check uses exact string equality on swiftName, but resolveSwiftName allows lowercase (worklets) while toSwiftName produces TitleCase (Worklets); the two pass the check yet collide as directories on the default case-insensitive macOS filesystem. Consider case-insensitive collision detection.
| depDir, | ||
| `.spm-scaffold-${process.pid}-${path.basename(podspecPath)}`, | ||
| ); | ||
| fs.writeFileSync(tmpFile, patched, 'utf8'); |
There was a problem hiding this comment.
Minor: The patched temp copy ends in .podspec next to the original. If a run crashes before cleanup, the leftover is matched by the endsWith('.podspec') scans in scaffold-package-swift.js and generate-spm-autolinking.js (readdir order is unspecified) and could mis-drive scaffolding. Consider a non-.podspec staging location or filtering the .spm-scaffold- prefix in those scanners.
|
|
||
| // Default sources allowlist when no explicit glob is provided — analog of | ||
| // CocoaPods' `s.source_files` auto-discovery. | ||
| function collectSpmSources(sourcePath /*: string */) /*: Array<string> */ { |
There was a problem hiding this comment.
Minor: The mixed-language guard (hasMixedLanguageSources) is only applied on the community-dep missing-manifest path, not for user-declared spm.modules or synth wrappers. A mixed-language local module produces a synth manifest that fails with SPM's cryptic 'mixed language sources' error instead of the friendly diagnostic used elsewhere.
| if (!res.ok) { | ||
| throw new Error(`npm lookup failed for ${npmPackage}: ${res.status}`); | ||
| } | ||
| const ver = (await res.json()).version; |
There was a problem hiding this comment.
Minor: resolveNightlyVersion/resolveLatestV1Version read .version off the registry response with no shape check; a response missing version yields undefined that flows into Maven URLs, producing confusing 404s rather than a clear error.
| * a value the user already had (key insight: ensureScalarField/ | ||
| * addArrayStringValues are no-ops / dedupe when a value is already present). | ||
| */ | ||
| function mergeReactBuildSettings( |
There was a problem hiding this comment.
Worth calling out: the pbxproj editing is purely additive with a precise .spm-injected.json reversal record, and mergeReactBuildSettings distinguishes created-vs-appended-vs-replaced settings so deinit lands byte-identical. Nicely done.
Builds on, and should land after (bottom-up):
s.dependency "RCT-Folly"resolves to local facades instead of trunk source pods.The whole stack is rebased onto current main (2026-07-06).
This PR consumes the final five-artifact set: React, ReactNativeHeaders (pure-RN), ReactNativeDependencies, ReactNativeDependenciesHeaders, hermes-engine.
Summary
Adds npx react-native spm and the package-generation tooling that turns an app into a SwiftPM-integrated RN app using the base stack's prebuilt XCFrameworks. CocoaPods stays supported — additive, opt-in, no Ruby toolchain. Integration is injected into the existing .xcodeproj in place (nothing generated/renamed/replaced), recorded in .spm-injected.json so it reverses exactly.
What this PR adds
Documentation
scripts/spm/__doc__/spm-scripts.mdscripts/spm/__doc__/spm-header-paths-contract.mdChangelog:
[IOS] [ADDED] -
npx react-native spmcommand + SwiftPM package-generation tooling (opt-in; CocoaPods stays supported)Test Plan
350 scripts/spm unit tests (incl. byte-identical add→deinit round-trip); manual E2E against the five-artifact set: fresh app via
cli init→spm add --deintegrate→build (artifact resources verified in the app), rn-tester in-place migration (RNTesterPods.xcodeproj, test libraries + spmModules), helloworld in-place migration — all BUILD SUCCEEDED. Verifiednpx react-native spm <cmd>from the command line in each journey.Scope & limitations
iOS, prebuilt-only (no build-from-source yet); full spm.xcframework/spm.source library metadata not yet (app-local spm.modules + scaffolding are); Expo not yet.
Follow-ups
Remote-mode ReactNative Package.swift must vend the ReactNativeDependenciesHeaders product (spm-distribution repo); library self-containment for repo-portable manifests; build-from-source.