react-native
Version:
A framework for building native apps using React
1,415 lines • 56.5 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
/*::
import type {
AutolinkedDep,
AutolinkingIosPlatform,
PodspecModel,
ScaffoldResult,
SpmScaffoldSpec,
} from './spm-types';
*/
/**
* scaffold-package-swift.js — generates a `Package.swift` into
* `node_modules/<dep>/` for community RN libraries that don't ship SPM
* support. The autolinker's existing `isSelfManagedPackage` check
* (generate-spm-autolinking.js, AUTOGEN_MARKER) treats these scaffolded
* files as self-managed and references them directly from the aggregator —
* no autolinker changes required.
*
* Three layers:
* - translatePodspecToSpmTarget(model, dep): pure translation
* - emitScaffoldedPackageSwift(spec, ctx): pure Swift emission
* - scaffoldPackageSwiftForDep(dep, ctx): I/O + skip-rule + write
* - scaffoldAll(opts): orchestrator over autolinking.json
*/
const {
SpmNameCollisionError,
defaultReadConfig,
defaultReadPodspec,
defaultResolveDep,
expandSpmDependencies,
resolveSwiftName,
} = require('./expand-spm-dependencies');
const {expandSpmSourceGlobs} = require('./generate-spm-autolinking');
const {MIN_IOS_VERSION_SUPPORTED} = require('./ios-deployment-target');
const {findPodspecs, readPodspecCached} = require('./read-podspec');
const {
REACT_CODEGEN_PACKAGE_NAME,
REACT_CODEGEN_PRODUCTS,
REACT_NATIVE_PACKAGE_NAME,
REACT_NATIVE_PRODUCTS,
SCAFFOLDER_MARKER,
makeLogger,
remotePackageConfig,
} = require('./spm-utils');
const {readSwiftpmConfig, writeSwiftpmName} = require('./swiftpm-config');
const fs = require('node:fs');
const path = require('node:path');
const {log, warn} = makeLogger('scaffold-package-swift');
// SCAFFOLDER_MARKER lives in spm-utils.js (shared, no import cycle). It must NOT
// contain the autolinker's AUTOGEN_MARKER ('// AUTO-GENERATED by
// scripts/generate-spm-autolinking.js') — that marker is what the autolinker
// uses to tell self-managed deps from its own synth output. Our marker is
// recognized by the scaffolder itself (skip-rule), but invisible to the
// autolinker (treated as self-managed).
// Bump when the emitter's output format changes in a way that requires
// re-scaffolding existing files (rather than just being a content drift the
// cache-slot label already handles). v2: swift-tools-version moved to
// line 1 — pre-v2 scaffolds emit it on a later line, which recent Xcode
// rejects with "Swift tools version 3.1.0 ... no longer supported". v4: the
// single `let rnHeaders = appRoot + "/.../ReactHeadersAll"` split into the two
// `rnCoreHeaders` / `appHeaders` lets (merged tree replaced by two trees), so
// pre-v4 scaffolds reference a tree that is no longer materialized. v5:
// header resolution moved to product dependencies (ReactNativeHeaders +
// ReactAppHeaders binary/headers targets) — the rnCoreHeaders/appHeaders
// trees no longer exist, so pre-v5 scaffolds carry dead lets and would break
// if anything still referenced them. v7: the runtime appRoot walker and the
// siblingPath helper are gone — package paths are plain relative strings
// computed at scaffold time (the walker anchored on
// build/xcframeworks/Package.swift, which remote mode no longer writes).
//
// Skip-rule contract: when an existing file's version is < this constant,
// the scaffolder regenerates regardless of --force (the bump implies the
// existing file is broken under current tooling). A file with the marker
// but no version line is treated as v1.
// v8: relative app paths (codegen / xcframeworks) are now computed from the
// autolinker's libs/<SwiftName> symlink location instead of the real dep.root,
// fixing a doubled-path resolution failure on fresh SwiftPM resolves.
// v9: header search paths derived from each subspec's header_mappings_dir
// (dirname) so namespaced includes (`<reanimated/...>`) resolve. v10:
// publicHeadersPath derived from the header_mappings_dir namespace root so a
// package exposes `<namespace/...>` to dependents; pod-style sibling deps
// (reanimated's `s.dependency "RNWorklets"`) wired to their npm package. v11:
// sibling .package path uses the libs/<SwiftName> symlink name, not the npm
// name (fixes "package ... doesn't exist" on resolve). v12: preprocessor
// defines from pod_target_xcconfig emitted as `.define(...)`. v13: ObjC(++)
// targets get an ambient-import prefix header (Foundation/UIKit) `-include`d,
// replacing CocoaPods' generated prefix.pch. v18: React-core deps also get
// the ReactNativeDependenciesHeaders product (ReactNativeHeaders went
// pure-RN; folly/glog/boost/... now come from the deps sidecar);
// publicHeadersPath falls back to "." for root-level-source podspecs
// (`s.source_files = "*.{h,m,mm}"`) instead of SPM's nonexistent `include/`.
// v19: scaffolded C++ targets carry DEBUG/NDEBUG config defines so their Fabric
// ABI matches the prebuilt React.framework (Release strips DebugStringConvertible
// under NDEBUG). Bumped so existing scaffolds regenerate with the defines.
// v20: the platform floor is the app's iOS deployment target in string form
// (the `.v15` enum cannot express a dependency minimum like 16.4).
const SCAFFOLDER_VERSION = 20;
const SCAFFOLDER_VERSION_LINE_RE = /^\/\/ AUTO-SCAFFOLDED-VERSION: (\d+)$/m;
const AUTOGEN_MARKER =
'// AUTO-GENERATED by scripts/generate-spm-autolinking.js';
// CocoaPods auto-generates a `<Target>-prefix.pch` that imports Foundation +
// UIKit into every ObjC translation unit, so pod sources can use NSThread /
// dispatch / UIKit symbols without an explicit import. SPM has no prefix-header
// mechanism, so we emit this file at the dep root and `-include` it on the
// target (cSettings + cxxSettings) to reproduce that ambient import. The
// `__OBJC__` guard makes it inert for plain C/C++ sources, and UIKit is
// `__has_include`-guarded for platforms that lack it.
// The config-gated C++ defines the prebuilt React.framework is built with (see
// packages/react-native/Package.swift). Fabric C++ in autolinked deps must
// compile with the SAME NDEBUG state or its DebugStringConvertible / ShadowNode
// ABI diverges from the Release React.framework and the link fails.
const REACT_CXX_CONFIG_DEFINES = [
'.define("DEBUG", .when(configuration: .debug))',
'.define("NDEBUG", .when(configuration: .release))',
];
const SCAFFOLD_PREFIX_HEADER = 'react-native-spm-prefix.h';
const SCAFFOLD_PREFIX_HEADER_CONTENTS = `// AUTO-SCAFFOLDED by react-native spm scaffold — mirrors CocoaPods' default
// prefix header so ObjC sources that rely on an implicit Foundation/UIKit
// import compile under SPM (which has no prefix-header mechanism). Safe to
// delete + regenerate via \`npx react-native spm scaffold\`.
#ifdef __OBJC__
#import <Foundation/Foundation.h>
#if __has_include(<UIKit/UIKit.h>)
#import <UIKit/UIKit.h>
#endif
#endif
`;
// Names of deps the scaffolder always refuses to touch — `react-native`
// itself is handled by the xcframework subpackage, never as an autolinked
// target.
const NEVER_SCAFFOLD /*: ReadonlySet<string> */ = new Set(['react-native']);
// React-core / React-Fabric / etc. dependency names collapse to a single
// invariant React header-product set. The app target links the prebuilt
// runtime frameworks outside SwiftPM. This
// list matches what `install_modules_dependencies(s)` materializes plus
// common podspec hand-rolled additions.
const REACT_CORE_DEP_PREFIXES = [
'React-',
'React_',
'ReactCommon',
'RCT-Folly',
'RCT',
'glog',
'boost',
'fmt',
'DoubleConversion',
'Yoga',
'hermes-engine',
];
function isReactCoreDep(name /*: string */) /*: boolean */ {
return REACT_CORE_DEP_PREFIXES.some(p => name.startsWith(p));
}
// True when the dep ships a `codegenConfig` in package.json — RN's standard
// marker that the library participates in the New Architecture / codegen. Such
// a lib's Fabric sources include the app-generated component headers
// (`<react/renderer/components/<name>/ShadowNodes.h>` etc.), which are vended by
// the per-app React-GeneratedCode package — so it implicitly depends on React
// core even when its podspec only wires that via `install_modules_dependencies`
// (which we strip). Safe/quiet: a missing or unparseable package.json → false.
function depHasCodegenConfig(depRoot /*: string */) /*: boolean */ {
try {
const pkg = JSON.parse(
fs.readFileSync(path.join(depRoot, 'package.json'), 'utf8'),
);
return pkg != null && pkg.codegenConfig != null;
} catch {
return false;
}
}
// Every subdirectory (relative to depRoot) under depRoot/base, recursively —
// used to expand a CocoaPods `path/**` recursive header search path into the
// concrete dirs SPM needs (SPM has no recursive search-path syntax). Skips
// VCS / build / dependency noise.
function collectSubdirs(
depRoot /*: string */,
base /*: string */,
) /*: Array<string> */ {
const SKIP /*: Set<string> */ = new Set([
'node_modules',
'Pods',
'build',
'.git',
]);
const out /*: Array<string> */ = [];
const walk = (absDir /*: string */, relDir /*: string */) => {
let entries /*: Array<{name: string, isDirectory(): boolean}> */ = [];
try {
// $FlowFixMe[incompatible-type] Dirent typing
entries = fs.readdirSync(absDir, {withFileTypes: true});
} catch {
return;
}
for (const e of entries) {
// $FlowFixMe[incompatible-type] Dirent.name is string|Buffer in stubs
const name /*: string */ = e.name;
if (!e.isDirectory() || name.startsWith('.') || SKIP.has(name)) continue;
const rel = relDir === '.' ? name : `${relDir}/${name}`;
out.push(rel);
walk(path.join(absDir, name), rel);
}
};
walk(path.join(depRoot, base), base === '.' ? '.' : base);
return out;
}
// ---------------------------------------------------------------------------
// Translation
// ---------------------------------------------------------------------------
/**
* Pure: turns a flattened PodspecModel into an SpmScaffoldSpec the emitter
* can consume. Substitutes Xcode build-setting tokens (`$(PODS_TARGET_SRCROOT)`)
* against the dep root, buckets dependencies, drops unknown tokens with a
* warning.
*/
function translatePodspecToSpmTarget(
model /*: PodspecModel */,
dep /*: AutolinkedDep */,
// Maps a podspec name (e.g. "RNWorklets") to the npm package name of an
// autolinked sibling (e.g. "react-native-worklets"). Lets us wire a
// `s.dependency "RNWorklets"` — a pod-style name the `react-native-*`
// heuristic can't recognize — to the right sibling package. Empty by default.
podToNpm /*: Map<string, string> */ = new Map(),
// npm name → resolved Swift name for every autolinked dep, so a sibling
// reference honors that sibling's `spm.name` instead of re-deriving it.
swiftNameByNpm /*: Map<string, string> */ = new Map(),
) /*: SpmScaffoldSpec */ {
const warnings = [...model.warnings];
// Whatever the autolinker resolved (see resolveSwiftName). It registers the
// dep under that name in its aggregator and in any sibling spm.dependencies
// refs, so the scaffolded product/library name must match it exactly —
// otherwise SPM fails to resolve `.product(name: "X", package: "X")`.
const swiftName = dep.swiftName;
if (swiftName == null) {
throw new Error(
`react-native spm scaffold: no resolved Swift name for '${dep.name}'. expandSpmDependencies must resolve every dep's name before a manifest is written.`,
);
}
// Header search paths — substitute Xcode build-setting tokens against the
// dep root. Anything we can't substitute is dropped + warned (avoids
// emitting `$(SOMETHING)` literally into the Swift file).
const headerSearchPaths /*: Array<string> */ = [];
const addSearchPath = (p /*: string */) => {
if (p.length > 0 && !headerSearchPaths.includes(p)) {
headerSearchPaths.push(p);
}
};
for (const raw of model.headerSearchPaths) {
let substituted = raw
.replace(/\$\(PODS_TARGET_SRCROOT\)/g, '.')
.replace(/\$\{PODS_TARGET_SRCROOT\}/g, '.');
if (/\$[({]/.test(substituted)) {
warnings.push(
`Dropped HEADER_SEARCH_PATHS entry "${raw}" — contains unresolved Xcode token. ` +
`Add it manually if needed.`,
);
continue;
}
// CocoaPods `path/**` (or `/*`) = recursive search. SPM has no recursive
// search-path syntax, so add the base dir + every subdirectory under it.
const recursive = /\/\*\*?$/.test(substituted);
substituted = substituted
.replace(/\/\*\*?$/, '') // strip the glob marker
.replace(/\/{2,}/g, '/'); // collapse `cpp//` → `cpp/`
// Strip leading "./" (emitter prefixes with the target path) + trailing "/".
const base = substituted
.replace(/^\.\//, '')
.replace(/^\//, '')
.replace(/\/$/, '');
addSearchPath(base === '' ? '.' : base);
if (recursive) {
for (const sub of collectSubdirs(dep.root, base === '' ? '.' : base)) {
addSearchPath(sub);
}
}
}
// header_mappings_dir → search path. CocoaPods exposes a subspec's headers
// under `<basename(mappings_dir)>/...` by copying them into Pods/Headers
// preserving structure relative to the mappings dir. SPM has no such copy
// step, so namespaced includes like `<reanimated/apple/sensor/X.h>` (header
// physically at `apple/reanimated/apple/sensor/X.h`, mappings dir
// `apple/reanimated`) only resolve if the mappings dir's PARENT (`apple`) is
// on the search path. Add dirname() of every subspec's mappings dir.
for (const mappingsDir of model.headerMappingsDirs) {
const parent = path.posix.dirname(mappingsDir.replace(/^\.\//, ''));
// dirname of a single-segment dir is "." (root) — already implicitly
// searched; skip it and anything that doesn't exist on disk.
if (
parent.length > 0 &&
parent !== '.' &&
!headerSearchPaths.includes(parent) &&
fs.existsSync(path.join(dep.root, parent))
) {
headerSearchPaths.push(parent);
}
}
// Bucket dependencies. React-Core / React-jsi / RCT-Folly / glog etc. ALL
// collapse to the invariant React header products
// reference because they're bundled in the prebuilt React.xcframework.
// Sibling RN libs (autolinked or self-managed deps) flow through the
// existing spm-deps mechanism. Unknown names are dropped with a warning.
let coreReactNative = false;
const siblingNames /*: Array<string> */ = [];
const selfPodspecName = model.name;
for (const depName of model.dependencies) {
// Cross-subspec refs like "react-native-safe-area-context/common" are a
// CocoaPods construct for one subspec depending on another from the
// SAME podspec. After flattenSubspecs has merged everything into one
// target the subspec ref is meaningless — drop silently.
if (depName.includes('/') && depName.startsWith(`${selfPodspecName}/`)) {
continue;
}
const podSibling = podToNpm.get(depName.split('/')[0]);
if (isReactCoreDep(depName)) {
coreReactNative = true;
} else if (depName.startsWith('react-native-')) {
// Generic cross-subspec ref guard for any "package/subspec" form.
const baseName = depName.split('/')[0];
if (!siblingNames.includes(baseName)) {
siblingNames.push(baseName);
}
} else if (podSibling != null && podSibling !== dep.name) {
// A pod-style dependency name (e.g. reanimated's `s.dependency
// "RNWorklets"`) that resolves to an autolinked sibling's npm package
// (react-native-worklets). Wire it as a sibling .package/.product so
// the dep's cross-package includes (`<worklets/...>`) resolve.
if (!siblingNames.includes(podSibling)) {
siblingNames.push(podSibling);
}
} else {
// Could be a non-RN dep ("MMKV", "AFNetworking"). The scaffolder
// doesn't know how to wire those — surface a warning so user can
// edit the generated Package.swift.
warnings.push(
`Unknown dependency "${depName}" — not wired into the scaffolded Package.swift. Edit manually if needed.`,
);
}
}
// New-Architecture libraries declare their React-core dependency via the
// `install_modules_dependencies(s)` podspec helper (which auto-adds
// React-Core / React-RCTFabric / React-Codegen), NOT an explicit
// `s.dependency "React-Core"`. We strip that helper when evaluating the
// podspec, so the React-core dependency never surfaces in model.dependencies
// and `coreReactNative` would stay false. The authoritative, RN-standard
// marker for "this lib participates in the New Architecture / codegen" is a
// `codegenConfig` block in package.json — when present, the app's codegen has
// generated `react/renderer/components/<name>/{ShadowNodes,Props,…}.h` that
// the lib's Fabric sources include via angle brackets. Those generated
// headers live in the per-app React-GeneratedCode package, so the lib must
// depend on it (and, transitively, on React core). Treat codegenConfig as
// an implicit React-core dependency.
// `codegenConfig` only marks Fabric/New-Arch libraries. A plain ObjC module
// (e.g. rn-tester's TestLibrary*) that wires React core solely through
// `install_modules_dependencies(s)` has no codegenConfig — but the helper's
// presence is itself an authoritative "depends on React core" signal, so
// treat it the same way.
if (
!coreReactNative &&
(depHasCodegenConfig(dep.root) ||
model.usesInstallModulesDependencies === true)
) {
coreReactNative = true;
}
// SPM's `sources:` field does NOT accept CocoaPods-style globs — it wants
// a list of explicit file paths (relative to the target's `path:`).
// Expand the podspec globs against the actual filesystem so the emitted
// Package.swift parses cleanly.
let expandedSources /*: Array<string> */ = [];
try {
expandedSources = expandSpmSourceGlobs(dep.root, model.sourceFiles);
} catch (e) {
warnings.push(
`Source glob expansion failed (${e.message}); emitting raw globs — SPM will likely reject them.`,
);
expandedSources = model.sourceFiles;
}
// Strip files matching exclude_files (podspec's negation list).
if (model.excludeFiles.length > 0) {
try {
// $FlowFixMe[untyped-import] micromatch has no types
const micromatch = require('micromatch');
const isExcluded = micromatch.matcher(model.excludeFiles);
expandedSources = expandedSources.filter(f => !isExcluded(f));
} catch (e) {
warnings.push(
`exclude_files filtering failed (${e.message}); keeping all sources.`,
);
}
}
// Never self-ingest scaffold artifacts: a re-scaffold runs with the
// previous run's output on disk, and root-level podspec globs sweep it
// into sources — `*.{h,...,swift}` matches Package.swift itself, whose
// `.swift` extension then trips the mixed-language gate into DELETING the
// manifest it just wrote (alternating scaffold/self-destruct runs); the
// prefix header and the include/ shim mirror would similarly feed back.
expandedSources = expandedSources.filter(
f =>
f !== SCAFFOLD_PREFIX_HEADER &&
f !== 'Package.swift' &&
!f.startsWith('include/'),
);
// Header-map emulation: CocoaPods builds a header map (USE_HEADERMAP=YES by
// default) so a source can `#import "Foo.h"` by bare name regardless of which
// subdirectory Foo.h lives in. SPM has no header map, so add every directory
// that contains a header to the search path. Covers libs (e.g. svg) that
// spread flat-named headers across many subdirs.
const headerFiles = expandedSources.filter(f => /\.(h|hh|hpp)$/i.test(f));
try {
for (const f of expandSpmSourceGlobs(dep.root, model.publicHeaderFiles)) {
if (/\.(h|hh|hpp)$/i.test(f)) headerFiles.push(f);
}
} catch {
// public_header_files globbing is best-effort — source_files usually
// already covers the headers.
}
for (const f of headerFiles) {
const d = path.posix.dirname(f);
addSearchPath(d === '' ? '.' : d);
}
// ObjC(++) sources may rely on CocoaPods' implicit prefix-header import of
// Foundation/UIKit. When present, emit + `-include` a prefix header (below).
// The prefix is `-include`d by bare name, so the dep root (".") must be on
// the header search path for clang to find it.
const needsObjCPrefix = expandedSources.some(f => /\.mm?$/.test(f));
if (needsObjCPrefix && !headerSearchPaths.includes('.')) {
headerSearchPaths.push('.');
}
// SPM REQUIRES publicHeadersPath to be a real directory inside the target
// when the target compiles C-family sources (default is "include" which
// typically doesn't exist in a podspec-shaped dep). Pick the first
// existing prefix dir from public_header_files / source_files. The
// chosen dir doesn't need to be the include surface — path-style
// includes (`<react/renderer/.../X.h>`) resolve via headerSearchPaths
// instead. publicHeadersPath just has to point at a real dir containing
// some .h files so SPM accepts the target definition.
let publicHeadersPath /*: ?string */ = null;
// Prefer the namespace root (parent of a header_mappings_dir). SPM propagates
// a target's publicHeadersPath to DEPENDENT packages as a search path, so
// setting it to e.g. `Common/cpp` (parent of `Common/cpp/worklets`) is what
// lets a sibling package resolve `<worklets/...>`. Prefer a cross-platform
// (Common) dir — that holds the C++ API siblings consume — over a
// platform-specific (apple/) one.
const mappingsParents = model.headerMappingsDirs
.map(d => path.posix.dirname(d.replace(/^\.\//, '')))
.filter(
p => p.length > 0 && p !== '.' && fs.existsSync(path.join(dep.root, p)),
);
if (mappingsParents.length > 0) {
publicHeadersPath =
mappingsParents.find(p => /(?:^|\/)common(?:\/|$)/i.test(p)) ??
mappingsParents[0];
}
if (publicHeadersPath == null) {
for (const glob of [...model.publicHeaderFiles, ...model.sourceFiles]) {
const prefix = glob.split('/')[0];
if (
prefix.length > 0 &&
!prefix.includes('*') &&
fs.existsSync(path.join(dep.root, prefix))
) {
publicHeadersPath = prefix;
break;
}
}
}
let namespacedShimHeaders /*: Array<string> */ = [];
if (publicHeadersPath == null) {
// Root-level podspecs (`s.source_files = "*.{h,m,mm}"`) keep their
// headers in the package root. CocoaPods exposes them to dependents as
// `#import <PodName/Header.h>` via header maps; SPM has no equivalent, so
// the scaffold writer mirrors them into a generated include/<SwiftName>/
// shim dir and publicHeadersPath points there (also fixing SPM's
// rejection over the nonexistent default `include/`).
const rootHeaders = expandedSources.filter(
f => f.endsWith('.h') && !f.includes('/'),
);
if (rootHeaders.length > 0) {
namespacedShimHeaders = rootHeaders;
publicHeadersPath = 'include';
}
}
if (publicHeadersPath == null) {
warnings.push(
`Could not infer publicHeadersPath from source globs. SPM may reject the target — add a publicHeadersPath manually to the scaffolded Package.swift.`,
);
}
const siblingSwiftNames /*: {[npmName: string]: string} */ = {};
for (const npmName of siblingNames) {
const resolved = swiftNameByNpm.get(npmName);
if (resolved != null) {
siblingSwiftNames[npmName] = resolved;
}
}
return {
swiftName,
sources: expandedSources,
headerSearchPaths,
preprocessorDefines: model.preprocessorDefines,
needsObjCPrefix,
coreReactNative,
siblingNames,
siblingSwiftNames,
extraFrameworks: model.frameworks,
weakFrameworks: model.weakFrameworks,
compilerFlags: model.compilerFlags,
publicHeadersPath,
namespacedShimHeaders,
resources: model.resources,
warnings,
};
}
// ---------------------------------------------------------------------------
// Swift emission
// ---------------------------------------------------------------------------
/*::
type EmitContext = {
// Identifier embedded as a comment so SPM's manifest hash bumps when the
// active xcframework slot changes (otherwise cached evaluations would
// keep pointing at the prior slot's headers).
cacheSlotLabel: ?string,
// Remote SPM package mode (url/version/identity) — see remotePackageConfig.
remote?: ?{url: string, version: string, identity: string},
// Relative path (posix, from the dep's package dir) to the app's codegen
// package (<appRoot>/build/generated/ios). Computed at scaffold time —
// safe because the file is re-scaffolded per app/cache slot, and any
// node_modules relayout implies a reinstall that drops the file anyway.
codegenPackageDir?: ?string,
// Relative path to the app's local xcframeworks package
// (<appRoot>/build/xcframeworks). Only referenced when remote == null.
localXcfwPackageDir?: ?string,
iosDeploymentTarget?: ?string,
};
*/
/**
* Renders the SpmScaffoldSpec to a complete Package.swift string suitable
* for writing into `node_modules/<dep>/`. Fully declarative: all package
* references are plain relative paths computed at scaffold time (no runtime
* discovery — the scaffolder knows both the dep dir and the app root, and
* re-scaffolds whenever either could have moved).
*/
function emitScaffoldedPackageSwift(
spec /*: SpmScaffoldSpec */,
ctx /*:: ?: EmitContext */ = {cacheSlotLabel: null, remote: null},
) /*: string */ {
const slotComment =
ctx.cacheSlotLabel != null ? `\n// Cache slot: ${ctx.cacheSlotLabel}` : '';
const iosDeploymentTarget /*: string */ =
ctx.iosDeploymentTarget ?? MIN_IOS_VERSION_SUPPORTED;
// React headers need NO search paths — they come from the React /
// ReactNativeHeaders binaryTargets and the ReactAppHeaders product (see
// the dependencies block below). Per-dep HEADER_SEARCH_PATHS from the
// podspec are emitted as `.headerSearchPath("<rel>")` so they resolve
// relative to `path:` ("." below). Custom podspec compiler flags keep
// using unsafeFlags (rare; surfaced verbatim).
const customFlagsCxx = spec.compilerFlags.map(f => `"${f}"`);
const headerSearchPathDirectives = spec.headerSearchPaths
.map(p => `.headerSearchPath("${p}")`)
.join(', ');
// Preprocessor defines → `.define("NAME", to: "VALUE", .when(...))`. The
// value is escaped as a Swift string literal because it may itself contain
// quotes (e.g. a string-literal macro `-DFOO="[A:false]"`).
const swiftStr = (s /*: string */) =>
`"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
const defineDirectives = (spec.preprocessorDefines ?? [])
.map(d => {
const toPart = d.value != null ? `, to: ${swiftStr(d.value)}` : '';
const condPart =
d.config === 'debug'
? ', .when(configuration: .debug)'
: d.config === 'release'
? ', .when(configuration: .release)'
: '';
return `.define(${swiftStr(d.name)}${toPart}${condPart})`;
})
.join(', ');
const settingsEntries = (extra /*: Array<string> */) => {
const parts = [
defineDirectives,
headerSearchPathDirectives,
...extra,
].filter(e => e.length > 0);
return `[${parts.join(', ')}]`;
};
// Force-include the ambient-import prefix header on ObjC(++) sources (the
// `__OBJC__` guard makes it a no-op for plain C/C++). `-include` resolves the
// bare name via the dep-root header search path (".", ensured in translate).
const prefixFlag = spec.needsObjCPrefix
? `.unsafeFlags(["-include", "${SCAFFOLD_PREFIX_HEADER}"])`
: '';
const cSettings = settingsEntries([prefixFlag].filter(e => e.length > 0));
const cxxSettings = settingsEntries(
[
prefixFlag,
...(customFlagsCxx.length > 0
? [`.unsafeFlags([${customFlagsCxx.join(', ')}])`]
: []),
// Match the prebuilt React.framework's config-gated C++ ABI. NDEBUG in
// Release strips DebugStringConvertible's vtable (and shifts the
// ShadowNode layout that inherits it), so a Fabric C++ target compiled
// WITHOUT NDEBUG fails to link against a Release React.framework. Mirrors
// packages/react-native/Package.swift's own C++ targets.
...REACT_CXX_CONFIG_DEFINES,
].filter(e => e.length > 0),
);
// Linker frameworks: defaults + podspec-declared extras + weak frameworks.
// Dedup on render so the user doesn't see duplicate UIKit lines.
const defaults = ['UIKit', 'Foundation', 'CoreGraphics'];
const linkedFrameworks = Array.from(
new Set([...defaults, ...spec.extraFrameworks]),
);
const linkerEntries = [
...linkedFrameworks.map(f => `.linkedFramework("${f}")`),
...spec.weakFrameworks.map(
f => `.linkedFramework("${f}", .when(platforms: [.iOS]))`,
),
];
// Dependencies block. Always declares ReactNative if any React-core dep
// was in the podspec; sibling RN deps come from autolinking's existing
// .package(path: ...) graph, under the names the autolinker resolved.
const packageDeps /*: Array<string> */ = [];
const targetDeps /*: Array<string> */ = [];
if (spec.coreReactNative) {
// Remote mode: React Native comes from the remote package identity (no
// app-layout knowledge). The per-app codegen package is generated INTO
// the app by definition, so it stays a path reference — relative,
// computed at scaffold time.
const remote = ctx.remote;
const rnLabel =
remote != null ? remote.identity : REACT_NATIVE_PACKAGE_NAME;
const codegenDir = ctx.codegenPackageDir;
if (codegenDir == null) {
throw new Error(
'emitScaffoldedPackageSwift: codegenPackageDir is required when the dep depends on React core.',
);
}
if (remote != null) {
packageDeps.push(
`.package(url: "${remote.url}", exact: "${remote.version}")`,
);
} else {
const xcfwDir = ctx.localXcfwPackageDir;
if (xcfwDir == null) {
throw new Error(
'emitScaffoldedPackageSwift: localXcfwPackageDir is required when no remote package is configured.',
);
}
packageDeps.push(
`.package(name: "${REACT_NATIVE_PACKAGE_NAME}", path: "${xcfwDir}")`,
);
}
packageDeps.push(
`.package(name: "${REACT_CODEGEN_PACKAGE_NAME}", path: "${codegenDir}")`,
);
for (const product of REACT_NATIVE_PRODUCTS) {
targetDeps.push(`.product(name: "${product}", package: "${rnLabel}")`);
}
for (const product of REACT_CODEGEN_PRODUCTS) {
targetDeps.push(
`.product(name: "${product}", package: "${REACT_CODEGEN_PACKAGE_NAME}")`,
);
}
}
for (const siblingName of spec.siblingNames) {
const swiftSibling = spec.siblingSwiftNames?.[siblingName];
if (swiftSibling == null) {
throw new Error(
`react-native spm scaffold: no resolved Swift name for the sibling '${siblingName}'. expandSpmDependencies must resolve every dep's name before a manifest is written.`,
);
}
// The autolinker references each self-managed (scaffolded) dep through a
// `libs/<SwiftName>` symlink, and SPM resolves a manifest's relative
// package paths against that symlink location — so a sibling lives at
// `../<SwiftName>` (NOT `../<npm-name>`, which would be `libs/<npm-name>`
// and not exist).
packageDeps.push(
`.package(name: "${swiftSibling}", path: "../${swiftSibling}")`,
);
targetDeps.push(
`.product(name: "${swiftSibling}", package: "${swiftSibling}")`,
);
}
// sources: emit only when the podspec declared them. If empty (regex-fallback
// podspec), no `sources:`/`exclude:` is emitted and SPM auto-scans the target
// dir — which for `path: "."` is the entire dep root. That is a known
// limitation of the regex-fallback path (the pod-ipc parse yields explicit
// sources); a future improvement could emit an `exclude:` list for the dep's
// example/test/build dirs.
const sourcesLine =
spec.sources.length > 0
? `\n sources: [\n${spec.sources.map(s => ` "${s}",`).join('\n')}\n ],`
: '';
const publicHeadersLine =
spec.publicHeadersPath != null
? `\n publicHeadersPath: "${spec.publicHeadersPath}",`
: '';
const resourcesLine =
spec.resources.length > 0
? `\n resources: [${spec.resources.map(r => `.copy("${r}")`).join(', ')}],`
: '';
const packageDepsBlock =
packageDeps.length > 0
? ` dependencies: [\n ${packageDeps.join(',\n ')},\n ],\n`
: '';
const targetDepsLine =
targetDeps.length > 0
? `\n dependencies: [${targetDeps.join(', ')}],`
: '';
return `// swift-tools-version: 6.0
${SCAFFOLDER_MARKER}
// AUTO-SCAFFOLDED-VERSION: ${SCAFFOLDER_VERSION}${slotComment}
// Edit the contents below if needed and re-run \`npx patch-package <dep-name>\`
// to persist across \`npm install\`. To regenerate from the podspec, remove
// this file (or just this marker) and re-run \`npx react-native spm scaffold\`.
//
// Package references are plain relative paths, computed when this file was
// scaffolded. They stay correct because the file is re-scaffolded per app
// and cache slot, and any node_modules relayout reinstalls this package
// (dropping the file) anyway.
import PackageDescription
let package = Package(
name: "${spec.swiftName}",
platforms: [.iOS("${iosDeploymentTarget}")],
products: [
.library(name: "${spec.swiftName}", targets: ["${spec.swiftName}"]),
],
${packageDepsBlock} targets: [
.target(
name: "${spec.swiftName}",${targetDepsLine}
path: ".",${sourcesLine}${publicHeadersLine}${resourcesLine}
cSettings: ${cSettings},
cxxSettings: ${cxxSettings},
linkerSettings: [${linkerEntries.join(', ')}]
),
],
cxxLanguageStandard: .cxx20
)
`;
}
const PODSPEC_KEY_ORIGINS = {
header_dir: "its podspec's 'header_dir'",
module_name: "its podspec's 'module_name'",
name: 'its pod name',
};
// Null when the caller resolved the name without recording where it came from,
// since the alternative is inventing an origin.
function swiftNameOrigin(dep /*: AutolinkedDep */) /*: ?string */ {
switch (dep.swiftNameSource) {
case 'config':
return "its declared 'swiftpmConfig.name'";
case 'podspec':
return PODSPEC_KEY_ORIGINS[dep.swiftNamePodspecKey ?? 'name'];
case 'npm':
return 'its npm package name';
default:
return null;
}
}
/**
* Says which SwiftPM name the library got and where that name came from. The
* name is the prefix every `#import <Name/Header.h>` against the library has to
* spell, and a podspec-derived one is written into the library's package.json,
* so it is not a decision to make silently.
*/
function reportSwiftName(
dep /*: AutolinkedDep */,
swiftName /*: string */,
model /*: PodspecModel */,
) /*: void */ {
const origin = swiftNameOrigin(dep);
if (origin == null) {
return;
}
log(`${dep.name}: SwiftPM name '${swiftName}' (from ${origin}).`);
// Two declared namespaces, one name: CocoaPods compiles the module as
// `module_name` while consumers import through `header_dir`, so a library
// that declares both loses one of them here.
const {headerDir, moduleName} = model;
if (
dep.swiftNamePodspecKey === 'header_dir' &&
headerDir != null &&
moduleName != null &&
headerDir !== moduleName
) {
warn(
`${dep.name}'s podspec declares both a 'header_dir' ('${headerDir}') and a 'module_name' ('${moduleName}'): ` +
`'${swiftName}' is the SwiftPM name, and '${moduleName}' is not used. ` +
`Set 'swiftpmConfig.name' in its package.json to override that choice.`,
);
}
}
// ---------------------------------------------------------------------------
// Per-dep orchestrator
// ---------------------------------------------------------------------------
/*::
type ScaffoldContext = {
appRoot: string,
projectRoot: string,
reactNativeRoot: string,
// Forces overwrite of files carrying the scaffolder marker. Files
// WITHOUT the marker (user-edited, upstream-shipped) are never touched.
force: boolean,
// When true, no file is written — caller gets the would-be content and
// can preview.
dryRun: boolean,
// Slot label (e.g. "0.87.0-nightly-20260513-6e262624f/debug") embedded as
// a comment so SPM's manifest hash bumps on slot changes.
cacheSlotLabel: ?string,
// podspec-name → npm-name index over all autolinked deps, so pod-style
// `s.dependency` names (e.g. "RNWorklets") wire to the right sibling.
podToNpm?: Map<string, string>,
// npm-name → resolved Swift name over all autolinked deps, so sibling
// references honor each sibling's declared name.
swiftNameByNpm?: Map<string, string>,
remote: ?{url: string, version: string, identity: string},
iosDeploymentTarget?: ?string,
};
*/
/**
* Decides whether to scaffold one dep, runs the translation + emission,
* writes the file (unless dryRun). Pure path/skip logic — all I/O via fs.
*/
function scaffoldPackageSwiftForDep(
dep /*: AutolinkedDep */,
ctx /*: ScaffoldContext */,
) /*: ScaffoldResult */ {
const depName = dep.name;
if (NEVER_SCAFFOLD.has(depName)) {
return {
depName,
status: 'skipped-is-react-native',
reason: 'react-native itself is handled by the xcframework package.',
};
}
// ios platform missing → no native code to wrap.
if (dep.platforms.ios == null) {
return {
depName,
status: 'skipped-no-ios',
reason: 'autolinking.json has no ios platform entry.',
};
}
// Dep can opt out in its own package.json.
if (
readSwiftpmConfig(dep.root, defaultReadConfig(dep.root))?.scaffold === false
) {
return {
depName,
status: 'skipped-opt-out',
reason: "the library sets 'swiftpmConfig.scaffold' to false.",
};
}
const pkgSwiftPath = path.join(dep.root, 'Package.swift');
// A self-managed Package.swift may live either at the dep root (legacy
// convention) or inside ios/ (preferred for community RN libs that want
// their npm-package root free of SPM artifacts). If a nested manifest
// exists and is user-authored, skip — writing a stray root manifest would
// cause the autolinker to prefer the wrong file.
const nestedPkgSwiftPath = path.join(dep.root, 'ios', 'Package.swift');
if (!fs.existsSync(pkgSwiftPath) && fs.existsSync(nestedPkgSwiftPath)) {
const nested = fs.readFileSync(nestedPkgSwiftPath, 'utf8');
if (
!nested.includes(AUTOGEN_MARKER) &&
!nested.includes(SCAFFOLDER_MARKER)
) {
return {
depName,
status: 'skipped-self-managed',
reason:
'Existing ios/Package.swift was not produced by this scaffolder. ' +
'Leaving it alone.',
};
}
}
// Skip rules around existing files.
if (fs.existsSync(pkgSwiftPath)) {
const existing = fs.readFileSync(pkgSwiftPath, 'utf8');
if (existing.includes(AUTOGEN_MARKER)) {
return {
depName,
status: 'skipped-autogen',
reason:
'Existing Package.swift carries the autolinker AUTOGEN marker. ' +
'spm update will regenerate the synth wrapper.',
};
}
if (existing.includes(SCAFFOLDER_MARKER)) {
// Version bump → unconditional regen. A bumped SCAFFOLDER_VERSION
// implies the emitter's output format changed in a way that requires
// regenerating existing files; --force is not needed here.
const vMatch = existing.match(SCAFFOLDER_VERSION_LINE_RE);
const existingVersion = vMatch != null ? parseInt(vMatch[1], 10) : 1;
const versionStale = existingVersion < SCAFFOLDER_VERSION;
if (!ctx.force && !versionStale) {
// Slot label change is the other auto-regen trigger — bumps SPM's
// manifest hash so the cache-slot path gets re-resolved.
const m = existing.match(/^\/\/ Cache slot: (.+)$/m);
const existingSlot = m != null ? m[1] : null;
if (ctx.cacheSlotLabel == null || existingSlot === ctx.cacheSlotLabel) {
return {
depName,
status: 'skipped-scaffolder-marker',
reason:
'Already scaffolded for this cache slot. Pass --force to regenerate.',
};
}
}
if (versionStale) {
log(
`Re-scaffolding ${depName} (format v${existingVersion} → v${SCAFFOLDER_VERSION})`,
);
}
// Fall through: regenerate (force, slot changed, or version bumped).
} else {
// No marker at all → user-managed or upstream-shipped. Never touch.
return {
depName,
status: 'skipped-self-managed',
reason:
'Existing Package.swift was not produced by this scaffolder. ' +
'Leaving it alone.',
};
}
}
// autolinking.json may have provided podspecPath; otherwise search dep.root.
// $FlowFixMe[prop-missing] dynamic shape from autolinking.json
const podspecPath /*: ?string */ =
dep.platforms.ios.podspecPath ?? findPodspecs(dep.root)[0];
if (podspecPath == null || !fs.existsSync(podspecPath)) {
return {
depName,
status: 'skipped-no-podspec',
reason: 'No .podspec found in dep root.',
};
}
let model;
try {
model = readPodspecCached(podspecPath);
} catch (e) {
return {
depName,
status: 'error',
reason: `Podspec read failed: ${e.message}`,
};
}
const spec = translatePodspecToSpmTarget(
model,
dep,
ctx.podToNpm ?? new Map(),
ctx.swiftNameByNpm ?? new Map(),
);
// Mixed-language fail-closed: SPM can't compile Swift + C-family in one
// target. Don't emit a manifest that would fail with a cryptic "mixed
// language source files" resolve error — skip with a clear reason (and remove
// any stale scaffolded manifest so the autolinker reports it distinctly and
// points the user at opting it out / a binary distribution).
const hasSwift = spec.sources.some(f => /\.swift$/i.test(f));
const hasClang = spec.sources.some(f => /\.(mm?|c|cc|cpp|cxx)$/i.test(f));
if (hasSwift && hasClang) {
if (!ctx.dryRun && fs.existsSync(pkgSwiftPath)) {
const existing = fs.readFileSync(pkgSwiftPath, 'utf8');
if (existing.includes(SCAFFOLDER_MARKER)) {
fs.rmSync(pkgSwiftPath, {force: true});
}
}
return {
depName,
status: 'skipped-mixed-language',
reason:
'has mixed Swift + Objective-C/C++ sources, which SPM cannot compile ' +
'in one target (and the bidirectional ObjC↔Swift interop typical of ' +
'such libs cannot be split without a circular dependency). Opt it out ' +
'via react-native.config.js (platforms.ios = null) or use a prebuilt ' +
'xcframework.',
};
}
// Relative paths into the app, embedded in the scaffolded Package.swift.
//
// The manifest is written to <dep.root>/Package.swift, but the autolinker
// references the dep through a symlink at
// <appRoot>/build/generated/autolinking/libs/<SwiftName>. On a fresh resolve
// SwiftPM interprets a manifest's relative `.package(path:)` entries against
// that SYMLINK location (it does not canonicalize the symlink first), so the
// paths must be relative to the symlink dir — NOT the real dep.root.
// Computing from dep.root produced a doubled path
// (…/autolinking/ios/build/generated/ios) → opaque "package manifest cannot
// be accessed" resolution failure. posix separators, as SPM expects.
const symlinkDir = path.join(
ctx.appRoot,
'build',
'generated',
'autolinking',
'libs',
spec.swiftName,
);
const relFromManifest = (...segments /*: Array<string> */) =>
path
.relative(symlinkDir, path.join(ctx.appRoot, ...segments))
.split(path.sep)
.join('/');
const content = emitScaffoldedPackageSwift(spec, {
cacheSlotLabel: ctx.cacheSlotLabel,
remote: ctx.remote,
iosDeploymentTarget: ctx.iosDeploymentTarget,
codegenPackageDir: relFromManifest('build', 'generated', 'ios'),
localXcfwPackageDir: relFromManifest('build', 'xcframeworks'),
});
// Distinguish "first-time scaffold" (no file at all) from "regenerate"
// (file with the scaffolder marker, e.g. slot changed). Used by the
// CLI orchestrator to decide whether to prompt for confirmation:
// first-time scaffolds touch a node_modules dir the user may not expect,
// regens are silent because we're maintaining a file we already own.
const previouslyExisted = fs.existsSync(pkgSwiftPath);
if (!ctx.dryRun) {
fs.writeFileSync(pkgSwiftPath, content, 'utf8');
// Emit the ambient-import prefix header next to the manifest when the
// target has ObjC(++) sources (the manifest `-include`s it by bare name).
if (spec.needsObjCPrefix) {
fs.writeFileSync(
path.join(dep.root, SCAFFOLD_PREFIX_HEADER),
SCAFFOLD_PREFIX_HEADER_CONTENTS,
'utf8',
);
}
// Mirror root-level headers into the generated include/<SwiftName>/ shim
// dir (publicHeadersPath) so dependents can use the CocoaPods header-map
// spelling `#import <SwiftName/Header.h>`.
if (spec.namespacedShimHeaders.length > 0) {
const shimDir = path.join(dep.root, 'include', spec.swiftName);
fs.mkdirSync(shimDir, {recursive: true});
for (const header of spec.namespacedShimHeaders) {
fs.writeFileSync(
path.join(shimDir, header),
`// AUTO-SCAFFOLDED by react-native spm scaffold — the namespaced\n` +
`// spelling (<${spec.swiftName}/${header}>) of the package-root header,\n` +
`// matching how CocoaPods header maps expose it.\n` +
`#import "../../${header}"\n`,
'utf8',
);
}
}
}
reportSwiftName(dep, spec.swiftName, model);
// Migration: record a name we derived from the podspec, so the library can
// drop the podspec. A declared name is already where it belongs, and a name
// guessed from the npm name would freeze a guess into someone's config.
const swiftpmName =
dep.swiftNameSource === 'podspec'
? writeSwiftpmName(dep.root, spec.swiftName, {dryRun: ctx.dryRun})
: undefined;
return {
depName,
status: 'written',
packageSwiftPath: pkgSwiftPath,
warnings: spec.warnings,
previouslyExisted,
swiftpmName,
};
}
/**
* The dependency set the autolinker considers: the iOS entries of
* autolinking.json plus their transitive SwiftPM dependencies. Null when the
* file declares none; throws when it cannot be read or parsed. `onSkipped`
* reports the entries dropped for having no iOS platform.
*/
function collectAutolinkedDeps(
opts /*: {
autolinkingJsonPath: string,
remote: ?{url: string, version: string, identity: string},
onSkipped?: (name: string) => void,
} */,
) /*: ?Array<AutolinkedDep> */ {
const {autolinkingJsonPath, remote, onSkipped} = opts;
/*:: type AutolinkingJson = {dependencies?: ?{[string]: {root?: string, platforms?: {ios?: ?{...}, ...}, ...}}, ...}; */
// $FlowFixMe[incompatible-type] JSON.parse returns any
const data /*: AutolinkingJson */ = JSON.parse(
fs.readFileSync(autolinkingJsonPath, 'utf8'),
);
const deps = data.dependencies;
if (deps == null) {
return null;
}
const directDeps /*: Array<AutolinkedDep> */ = [];
for (const name of Object.keys(deps)) {
const raw = deps[name];
if (raw == null) continue;
const root = raw.root;
const ios = raw.platforms?.ios;
if (typeof root !== 'string' || ios == null) {
onSkipped?.(name);
continue;
}
// $FlowFixMe[incompatible-type] `ios` shape is runtime-validated above
const iosPlatform /*: AutolinkingIosPlatform */ = ios;
directDeps.push({name, root, platforms: {ios: iosPlatform}});
}
try {
return expandSpmDependencies(directDeps, {
readConfig: defaultReadConfig,
resolveDep: defaultResolveDep,
readPodspec: defaultReadPodspec,
extraReservedNames: remote != null ? [remote.identity] : undefined,
});
} catch (e) {
if (e instanceof SpmNameCollisionError) {
throw e;
}
// A transitive-resolution failure shouldn't abort the whole pass; fall back
// to the direct deps so at least those are covered. They are named the way
// the autolinker names them — a manifest written under any other name
// outlives this error on disk and then fails to resolve.
log(`Transitive dependency expansion failed: ${e.message}`);
return directDeps.map(dep => {
const resolved = resolveSwiftName(
dep.name,
readSwiftpmConfig(dep.root, defaultReadConfig(dep.root)),
defaultReadPodspec(dep.root, dep.platforms.ios.podspecPath),
);
return {
...dep,
swiftName: resolved.name,
swiftNameSource: resolved.source,
swiftNamePodspecKey: resolved.podspecKey,
};
});
}
}
// The `.iOS(...)` element of the emitted platforms array — the `.v15` enum form
// pre-v20 scaffolds carry included, and without the array's closing bracket so
// a user-extended array (`[.iOS(…), .macOS(…)]`) is still matched.
const PLATFORM_FLOOR_RE = /platforms: \[\.iOS\((\.v\d+|"[^"]*")\)/;
/**
* Bring the platform floor of already-scaffolded manifests up to the app's
* deployment target — `spm add`/`update` must not leave a dep pinned below the
* app (SwiftPM would refuse to link it), and re-scaffolding is the user's call.
* Only the platform line of a file carrying our own marker is rewritten; no
* file is created. `from` is the previous token, verbatim (`15.1` or `.v15`).
*/
function refreshScaffoldedPlatformFloors(
opts /*: {appRoot: string, autolinkingJsonPath?: string, iosDeploymentTarget: string} */,
) /*: Array<{depName: string, path: string, from: string, to: string}> */ {
const {appRoot, iosDeploymentTarget} = opts;
const autolinkingJsonPath =
opts.autolinkingJsonPath ??
path.join(appRoot, 'build', 'generated', 'autolinking', 'autolinking.json');
const refreshed = [];
// Best-effort: an unreadable autolinking.json, a name collision or a
// malformed remote config is the autolinker's error to report a moment
// later, not this pass's.
let deps /*: ?Array<AutolinkedDep> */ = null;
try {
deps = collectAutolinkedDeps({
autolinkingJsonPath,
remote: remotePackageConfig(appRoot),
});
} catch {
return refreshed;
}
if (deps == null) {
return refreshed;
}
for (const {name: depName, root} of deps) {
const manifestPath = path.join(root, 'Package.swift');
let content;
try {
content = fs.readFileSync(manifestPath, 'utf8');
} catch {
continue;
}
if (
!content.includes(SCAFFOLDER_MARKER) ||
content.includes(AUTOGEN_MARKER)
) {
continue;
}
const match = content.match(PLATFORM_FLOOR_RE);
if (match == null) {
continue;
}
const from = match[1].replace(/"/g, '');
if (from === iosDeploymentTarget) {
continue;
}
fs.writeFileSync(
manifestPath,
content.replace(match[0], `platforms: [.iOS("${iosDeploymentTarget}")`),
'utf8',
);
refreshed.push({
depName,
path: manifestPath,
from,
to: iosDeploymentTarget,
});
}
return refreshed;
}
// ---------------------------------------------------------------------------
// Multi-dep orchestrator
// ---------------------------------------------------------------------------
/*::
type ScaffoldAllOptions = {
appRoot: string,
projectRoot: string,
reactNativeRoot: string,
force?: boolean,
dryRun?: boolean,
cacheSlotLabel?: ?string,
autolinkingJsonPath?: string,
iosDeploymentTarget?: ?string,
// npm dep names to skip entirely — used when the user declined the
// confirmation prompt for first-time scaffolds. Skipped deps still
// appear in the returned results array with status='skipped-opt-out'.
skipDeps?: ReadonlyArray<string>,
};
*/
/**
* Walks autolinking.json, scaffolds each iOS-native dep, returns one
* ScaffoldResult per dep. Caller (setup-apple-spm.js) prints a structured
* summary.
*/
function scaffoldAll(
opts /*: ScaffoldAllOptions */,
) /*: Array<ScaffoldResult> */ {
const {appRoot, projectRoot, reactNativeRoot} = opts;
const autolinkingJsonPath =
opts.autolinkingJsonPath ??
path.join(appRoot, 'build', 'generated', 'autolinking', 'autolinking.json');
if (!fs.existsSync(autolinkingJsonPath)) {
log(
`autolinking.json not found at ${path.relative(appRoot, autolinkingJsonPath)}; nothing to scaffold.`,
);
return [];
}
// The scaffolder covers EXACTLY the set the autolinker considers, transitive
// `spm.dependencies` included. Without them, a transitive native dep that
// ships no Package.swift would be flagged by the autolinker but never
// scaffolded here — leaving `react-native spm scaffold` unable to clear the
// autolinker's missing-manifest error.
const results /*: Array<ScaffoldResult> */ = [];
// Outside collectAutolinkedDeps' own try: a malformed remote config
// (RemoteVersionError) is a misconfiguration to surface, not an expansion
// failure to degrade past.
const remote = remotePackageConfig(appRoot);
const allDeps = collectAutolinkedDeps({
autolinkingJsonPath,
remote,
onSkipped: name => {
results.push({
depName: name,
status: 'skipped-no-ios',
reason: 'no iOS platform in autolinking.json',
});
},
});
if (allDeps == null) {
return results;
}
// Index every autolinked dep's podspec name → its npm name, so a dep that
// depends on a sibling by its pod name (reanimated's `s.dependency
// "RNWorklets"`) can be wired to the sibling's package (react-native-worklets).
// The podspec file basename is the pod name for RN-ecosystem libs (and is
// cheap — no `pod ipc` pre-pass).
const podToNpm /*: Map<string, string> */ = new Map();
for (const dep of allDeps) {
const podspecPath = dep.platforms?.ios?.podspecPath;
if (typeof podspecPath === 'string' && podspecPath.length > 0) {
podToNpm.set(path.basename(podspecPath, '.podspec'), dep.name);
} else {
// Transitive entries synthesized by expandSpmDependencies (declared via
// `spm.dependencies` in react-native.config.js) carry no podspecPath —
// discover the podspec at the dep root so podspec-name sibling deps
// (e.g. `s.dependency "TestLibraryCommon"`) still wire to their npm
// package.
for (const found of findPodspecs(dep.root)) {
podToNpm.set(path.basename(found, '.podspec'), dep.name);
}
}
}
const swiftNameByNpm /*: Map<string, string> */ = new Map();
for (const dep of allDeps) {
if (dep.swiftName != null) {
swiftNameByNpm.set(dep.name, dep.swiftName);
}
}
const ctx /*: ScaffoldContext */ = {
appRoot,
projectRoot,
reactNativeRoot,
force: opts.force === true,
dryRun: opts.dryRun === true,
cacheSlotLabel: opts.cacheSlotLabel ?? null,
iosDeploymentTarget: opts.iosDeploymentTarget ?? null,
podToNpm,
swiftNameByNpm,
remote,
};
const skipSet /*: Set<string> */ = new Set(opts.skipDeps ?? []);
for (const dep of allDeps) {
if (skipSet.has(dep.name)) {
results.push({
depName: dep.name,
status: 'skipped-opt-out',
reason: 'User declined scaffolding for this dep.',
});
continue;
}
try {
results.push(scaffoldPackageSwiftForDep(dep, ctx));
} catch (e) {
results.push({depName: dep.name, status: 'error', reason: e.message});
}
}
return results;
}
module.exports = {
refreshScaffoldedPlatformFloors,
scaffoldAll,
scaffoldPackageSwiftForDep,
translatePodspecToSpmTarget,
emitScaffoldedPackageSwift,
SCAFFOLDER_MARKER,
SCAFFOLDER_VERSION,
};