packages/react-native/scripts/spm/__doc__/spm-plugins-assessment.md
An evaluation of whether Swift Package Manager plugins can replace the Xcode build
phase scripts currently injected by generate-spm-xcodeproj.js.
| # | Phase | Timing | SPM Plugin Feasible? |
|---|---|---|---|
| 1 | Sync SPM Autolinking | Pre-build | Partially |
| 2 | Prepare VFS Overlay | Pre-build | Partially |
| 3 | Sources (compile) | Build | N/A (standard) |
| 4 | Frameworks (link) | Build | N/A (standard) |
| 5 | Resources (copy) | Build | N/A (standard) |
| 6 | Build JS Bundle | Post-build | See below |
Removed: The "Copy Hermes Framework" phase was removed — it was a no-op. The underlying
copy-hermes-xcode.shscript has been empty since Dec 2022. Hermes is already properly linked as an xcframework SPM dependency.
SPM offers two plugin types:
BuildToolPlugin) — run pre-build, can generate source
files/resources via prebuildCommands or per-file buildCommands.CommandPlugin) — run on-demand via
swift package <command>.SPM plugins run sandboxed by default — no network access, limited filesystem access. The current scripts need to:
node (not on the sandbox-allowed path)autolinked/, build/)node_modules/Command plugins can request --allow-writing-to-package-directory, but build tool
plugins can only write to a designated plugin work directory, not the source tree.
SPM plugins do not receive Xcode build settings such as CONFIGURATION,
PLATFORM_NAME, BUILT_PRODUCTS_DIR, or DERIVED_FILE_DIR. The JS bundling script
relies heavily on these to decide debug-vs-release behavior and output paths.
The JS bundle has no dependency on native compilation. It only needs JS source files,
Metro, and knowledge of debug vs release. The current post-build placement is
historical — the script writes directly into BUILT_PRODUCTS_DIR.
A potential restructuring:
build/jsbundle/)Challenges:
CONFIGURATION == Debug, which is unavailable to SPM plugins.swift package bundle-js --configuration release) would
lose the automatic behavior — developers would need to run it explicitly.A Swift command plugin could shell out to node to run codegen:
@main struct CodegenPlugin: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = ["node", "scripts/codegen/generate-codegen-artifacts.js"]
try process.run()
process.waitUntilExit()
}
}
Invoked as swift package codegen. This is essentially wrapping a shell script in
Swift with no real benefit over the current approach.
A prebuildCommand runs before every build, similar to Phase 1. But:
autolinked/)nodeThis is a significant architectural rework for marginal benefit.
Do not invest in SPM plugins for this use case. Reasons:
Pre-build phases already work well as Xcode build phase scripts. Moving them
to SPM plugins adds Swift boilerplate around Process() calls to node, while
losing access to Xcode build settings.
The ROI is poor — a hybrid (some SPM plugins + some Xcode build phases) is harder to reason about than the current uniform approach of all Xcode build phases.
SPM plugins shine for pure Swift source generation (SwiftGen, SwiftProtobuf)
where the plugin generates .swift files that feed into compilation. React
Native's build steps are fundamentally different — they orchestrate a JS toolchain
and copy runtime artifacts.
The JS bundle phase could move pre-build but would lose automatic debug/release detection without Xcode build settings. Worth revisiting if SPM gains access to build configuration in a future Swift version.
prepare script in package.json so it runs at
yarn install time instead of every build, reducing build-time overhead.