react-native
Version:
A framework for building native apps using React
787 lines (737 loc) • 28.7 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
*/
;
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
// The package and product names React Native's own generated manifests use. A
// dependency whose Swift name lands on one of them surfaces as a duplicate-name
// error from inside SPM's resolution, far from the react-native.config.js that
// caused it, so the autolinker rejects the collision up front instead.
//
// These cover the emitters in this directory and the collision guard. Adding a
// React Native SPM product also touches, depending on what the product is:
// - scripts/codegen/templates/Package.swift.spm-template — if the per-app
// codegen package must depend on it (a static Swift file, patched by
// string replacement, not generated from these constants)
// - flavored-frameworks.js INVARIANT_BINARY_TARGETS — if it is
// xcframework-backed
// - download-spm-artifacts.js REQUIRED_ARTIFACTS — if it ships as its own
// downloadable artifact (that list names ARTIFACTS, which overlap with
// product names without being the same set)
const REACT_NATIVE_PACKAGE_NAME /*: string */ = 'ReactNative';
const REACT_CODEGEN_PACKAGE_NAME /*: string */ = 'React-GeneratedCode';
// The autolinking aggregator package, whose single product shares its name.
const AUTOLINKED_PACKAGE_NAME /*: string */ = 'Autolinked';
// React Native's products by KIND, so no consumer has to infer the kind from a
// position in a flat list. The umbrella is a Clang target over the staged
// headers that re-exports the pure-RN namespaces; the rest are each backed by
// an xcframework of the same name.
const REACT_NATIVE_UMBRELLA_PRODUCT /*: string */ = 'ReactHeaders';
const REACT_NATIVE_HEADERS_PRODUCT /*: string */ = 'ReactNativeHeaders';
const REACT_NATIVE_DEPENDENCIES_HEADERS_PRODUCT /*: string */ =
'ReactNativeDependenciesHeaders';
const REACT_NATIVE_XCFRAMEWORK_PRODUCTS /*: ReadonlyArray<string> */ =
Object.freeze([
REACT_NATIVE_HEADERS_PRODUCT,
REACT_NATIVE_DEPENDENCIES_HEADERS_PRODUCT,
]);
// Derived, so a new product cannot be added without choosing a kind above.
const REACT_NATIVE_PRODUCTS /*: ReadonlyArray<string> */ = Object.freeze([
REACT_NATIVE_UMBRELLA_PRODUCT,
...REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
]);
// The codegen package product every autolinked target depends on for the app's
// generated headers.
const REACT_CODEGEN_PRODUCTS /*: ReadonlyArray<string> */ = Object.freeze([
'ReactAppHeaders',
]);
// The codegen package products the APP target links, declared by the static
// template rather than by any emitter here.
const REACT_CODEGEN_APP_PRODUCTS /*: ReadonlyArray<string> */ = Object.freeze([
'ReactCodegen',
'ReactAppDependencyProvider',
]);
const REACT_HEADERS_TARGET_DIR /*: string */ = 'ReactHeadersTarget';
// Target names only have to be unique within their own package, so
// REACT_HEADERS_TARGET_DIR is deliberately absent: a dependency named after it
// collides with nothing.
const RESERVED_SWIFT_NAMES /*: ReadonlyArray<string> */ = Object.freeze([
REACT_NATIVE_PACKAGE_NAME,
REACT_CODEGEN_PACKAGE_NAME,
AUTOLINKED_PACKAGE_NAME,
...REACT_NATIVE_PRODUCTS,
...REACT_CODEGEN_PRODUCTS,
...REACT_CODEGEN_APP_PRODUCTS,
]);
/**
* Creates a logger trio {log, warn, die} that prefixes messages with [name].
* log – green prefix, writes to stdout
* warn – yellow prefix, writes to stderr
* die – red prefix, writes to stderr, sets exitCode=1, throws
*/
function makeLogger(name /*: string */) /*: {
log: (msg: string) => void,
warn: (msg: string) => void,
die: (msg: string) => empty,
} */ {
// Prefix every newline-separated line of the message so multi-line output
// wraps cleanly when terminal log scrapers look for the `[name]` tag.
function format(color /*: string */, msg /*: string */) /*: string */ {
const prefix = `\x1b[${color}m[${name}]\x1b[0m`;
return msg
.split('\n')
.map(line => `${prefix} ${line}`)
.join('\n');
}
return {
log(msg /*: string */) /*: void */ {
console.log(format('32', msg));
},
warn(msg /*: string */) /*: void */ {
console.warn(format('33', msg));
},
die(msg /*: string */) /*: empty */ {
console.error(format('31', msg));
process.exitCode = 1;
throw new Error(msg);
},
};
}
/**
* Returns a short, human-readable representation of an absolute path:
* - Paths under $HOME are shown as ~/...
* - Paths under cwd are shown as relative (if ≤2 levels up)
* - Otherwise the absolute path is returned unchanged
*/
function displayPath(p /*: string */) /*: string */ {
const home = os.homedir();
if (p === home) return '~';
if (p.startsWith(home + path.sep)) {
return '~' + p.slice(home.length);
}
const rel = path.relative(process.cwd(), p);
if (rel && !rel.startsWith('../../..')) {
return rel;
}
return p;
}
/**
* Canonical React Native binary cache root. Mirrors CocoaPods'
* `ReactNativePodsUtils.shared_cache_dir()` (~/Library/Caches/ReactNative, added
* in #56847) so SPM and CocoaPods share one cache root — and so SPM stops using
* a `com.facebook.ReactNative` (bundle-id) dir that other tools may also touch.
* Honor `RCT_SKIP_CACHES=1` (same env var as CocoaPods) to bypass the shared
* tarball cache.
*/
function sharedCacheDir() /*: string */ {
return path.join(os.homedir(), 'Library', 'Caches', 'ReactNative');
}
/**
* Returns the default versioned cache directory for SPM's EXTRACTED xcframeworks,
* nested under the canonical cache root. Downloaded tarballs themselves go in the
* flat shared cache (sharedCacheDir()) so they are reused across SPM/CocoaPods.
*
* @param {string} versionKey Version string used as directory name.
* Pass the raw --version arg (e.g. 'nightly') so the
* cache slot is stable regardless of the resolved hash.
* @param {string} flavor 'debug' or 'release'
*/
function defaultCacheDir(
versionKey /*: string */,
flavor /*: string */,
) /*: string */ {
return path.join(sharedCacheDir(), 'spm-artifacts', versionKey, flavor);
}
/**
* Sanitize a package/app name to a valid Swift identifier.
* e.g. "@react-native/tester" -> "RNTester", "my-app" -> "MyApp"
*/
function toSwiftName(name /*: string */) /*: string */ {
const base = name.replace(/^@[^/]+\//, '');
return base
.split(/[^a-zA-Z0-9]+/)
.filter(Boolean)
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join('');
}
// The charset a declared SwiftPM name must satisfy — permissive on purpose,
// since it has to admit header-dir style (lowercase with hyphens) as well as
// Swift identifiers.
function isValidSwiftName(name /*: unknown */) /*: boolean */ {
return typeof name === 'string' && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(name);
}
/**
* SwiftPM's `c99name` — the identifier a target's module is compiled under,
* with every character C99 rejects replaced by `_`. `react-native-svg` and
* `react_native_svg` share one, so they cannot both be targets.
*/
function toC99Name(name /*: string */) /*: string */ {
const mangled = name.replace(/[^A-Za-z0-9_]/g, '_');
return /^[0-9]/.test(mangled) ? `_${mangled}` : mangled;
}
// Collision key for a Swift target name: two names that share one collide, as a
// module (punctuation) or as a headers directory (case).
function swiftNameKey(name /*: string */) /*: string */ {
return toC99Name(name).toLowerCase();
}
/**
* Derive a default app name from the raw package name and source path.
* Prefers the source directory name when it's meaningful (e.g. "RNTester"),
* falls back to the package name for generic dirs like "ios" or "src".
*/
function deriveAppName(
rawName /*: string */,
sourcePath /*: string */,
) /*: string */ {
const genericSourceDirs = new Set(['ios', 'app', 'sources', 'src']);
const cleanName = rawName.replace(/^@[^/]+\//, '');
return toSwiftName(
sourcePath !== toSwiftName(cleanName) &&
!genericSourceDirs.has(sourcePath.toLowerCase())
? sourcePath
: cleanName,
);
}
// $FlowFixMe[unclear-type] JSON data has dynamic shape
function readPackageJson(dir /*: string */) /*: Object | null */ {
const pkgPath = path.join(dir, 'package.json');
if (!fs.existsSync(pkgPath)) {
return null;
}
// $FlowFixMe[incompatible-return] JSON.parse returns any
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
}
/**
* Walk up from startDir until we find a directory containing package.json.
* Returns startDir itself if it contains package.json, or startDir as fallback
* if no package.json is found anywhere up the tree.
*/
function findProjectRoot(startDir /*: string */) /*: string */ {
const start = path.resolve(startDir);
let dir = start;
// Bounded by filesystem depth — path.dirname converges to '/' or 'C:\\'.
// The `dir = ...` updates would otherwise drop the start-fallback narrowing.
while (dir !== path.dirname(dir)) {
if (fs.existsSync(path.join(dir, 'package.json'))) {
return dir;
}
dir = path.dirname(dir);
}
// At filesystem root — last check before falling back.
if (fs.existsSync(path.join(dir, 'package.json'))) {
return dir;
}
return start;
}
/**
* Resolve the react-native package root from an app directory.
* Checks appRoot/projectRoot and their ancestors for node_modules/react-native,
* then falls back to __dirname-relative resolution (monorepo layout).
*
* Returns null if react-native cannot be found.
*/
function resolveReactNativeRoot(
appRoot /*: string */,
projectRoot /*: string */,
) /*: string | null */ {
const candidates /*: Array<string> */ = [];
const seen /*: Set<string> */ = new Set();
function addAncestorCandidates(startDir /*: string */) /*: void */ {
let dir = path.resolve(startDir);
while (true) {
const candidate = path.join(dir, 'node_modules', 'react-native');
if (!seen.has(candidate)) {
seen.add(candidate);
candidates.push(candidate);
}
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
}
addAncestorCandidates(appRoot);
addAncestorCandidates(projectRoot);
candidates.push(path.resolve(__dirname, '../..'));
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return path.resolve(candidate);
}
}
return null;
}
// The per-app farm lives INSIDE the codegen package (build/generated/ios) so
// it can be vended as a normal SPM headers target ("ReactAppHeaders") — the
// farm reaches consumers via SPM product dependencies, not -I.
const PER_APP_HEADERS_REL = 'build/generated/ios/ReactAppHeaders';
// Marker at the top of a scaffolder-generated Package.swift. Lives here (not in
// scaffold-package-swift.js) so the autolinker can recognize scaffolded files
// without a circular import (scaffold-package-swift requires the autolinker).
const SCAFFOLDER_MARKER =
'// AUTO-SCAFFOLDED by react-native spm scaffold — safe to edit & commit via patch-package.';
// Remote SPM package mode (prototype of the GitHub-distribution endgame):
// when active, app + libraries all depend on ONE remote package identity
// (`.package(url:exact:)`) instead of the local path-based artifacts package
// — SPM unifies the version across the graph and the local compose/symlink
// machinery is skipped. Activated via RN_SPM_REMOTE_URL +
// RN_SPM_REMOTE_VERSION (persisted per-app so Xcode-phase re-syncs without
// the env keep the mode).
const REMOTE_CONFIG_REL = 'build/generated/autolinking/spm-remote.json';
function remotePackageIdentity(url /*: string */) /*: string */ {
const tail = url.replace(/\/+$/, '').split('/').pop() ?? '';
return tail.replace(/\.git$/, '').toLowerCase();
}
/**
* Thrown by remotePackageConfig when remote SPM mode is active (a URL is set)
* but no usable RN version can be determined: react-native isn't installed, or
* the installed version is a non-publishable dev placeholder (e.g. the monorepo
* '1000.0.0', which has no remote tag) and no override was supplied. Carries a
* developer-facing message; the CLI turns it into a hard build error (exit 2).
*/
class RemoteVersionError extends Error {
constructor(message /*: string */) {
super(message);
this.name = 'RemoteVersionError';
}
}
/**
* Resolve the version of the installed react-native by walking up from appRoot
* looking for node_modules/react-native/package.json. Mirrors the autolinker's
* appRoot-first, up-to-5-ancestors walk-up. Returns null when not found or the
* package.json has no string version.
*/
function resolveInstalledRnVersion(appRoot /*: string */) /*: ?string */ {
let dir = path.resolve(appRoot);
for (let i = 0; i <= 5; i++) {
const pkgPath = path.join(
dir,
'node_modules',
'react-native',
'package.json',
);
if (fs.existsSync(pkgPath)) {
try {
const j = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (typeof j.version === 'string') {
return j.version;
}
} catch {}
return null;
}
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
return null;
}
/**
* True when a version string can be resolved to a published remote tag. False
* for the monorepo dev placeholder ('1000.0.0') and 0.0.0-* dev builds, neither
* of which is published — in remote mode these require an explicit override.
*/
function isPublishableVersion(v /*: ?string */) /*: boolean */ {
if (v == null || v === '') {
return false;
}
if (v === '1000.0.0') {
return false;
}
if (/^0\.0\.0(-|$)/.test(v)) {
return false;
}
return true;
}
/**
* Resolve the remote SPM package config for `appRoot`, or null for local mode.
*
* Remote mode is gated by a URL alone (env RN_SPM_REMOTE_URL or persisted
* `url`). The version is an OVERRIDE chain — env RN_SPM_REMOTE_VERSION →
* persisted `versionOverride` → legacy persisted `version` (back-compat) — and
* when no override is set it is DERIVED from the installed react-native. The
* derived value is never persisted, so an npm RN upgrade auto-re-pins the SPM
* graph on the next sync. A derived version that isn't publishable (or a
* missing RN install) throws RemoteVersionError so the dev placeholder doesn't
* silently pin an unpublished tag.
*/
function remotePackageConfig(
appRoot /*: string */,
) /*: ?{url: string, version: string, identity: string} */ {
const envUrl = process.env.RN_SPM_REMOTE_URL;
const envVersion = process.env.RN_SPM_REMOTE_VERSION;
const cfgPath = path.join(appRoot, REMOTE_CONFIG_REL);
// Read any persisted config first. Legacy {url, version} (where `version`
// was a hard pin) is read with `version` honored as an override.
let persisted /*: {url?: string, versionOverride?: string, version?: string} */ =
{};
if (fs.existsSync(cfgPath)) {
try {
const j = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
if (j != null && typeof j === 'object') {
persisted = j;
}
} catch {}
}
// URL alone activates remote mode (env wins over persisted).
const url = envUrl != null && envUrl !== '' ? envUrl : persisted.url;
if (url == null || url === '') {
return null; // local mode
}
// Version override chain. When none is set, derive from the installed RN —
// the SPM graph must compile against the same RN the JS/native code uses.
const override =
envVersion != null && envVersion !== ''
? envVersion
: (persisted.versionOverride ?? persisted.version);
const version = override ?? resolveInstalledRnVersion(appRoot);
if (version == null) {
throw new RemoteVersionError(
`Remote SPM mode is on (URL=${url}) but no React Native version could ` +
'be resolved (react-native was not found in node_modules). Set ' +
'RN_SPM_REMOTE_VERSION to a published tag, or install react-native.',
);
}
if (override == null && !isPublishableVersion(version)) {
throw new RemoteVersionError(
`Remote SPM mode is on (URL=${url}) but React Native resolves to ` +
`non-publishable version '${version}'. Set RN_SPM_REMOTE_VERSION to a ` +
'published tag, or install a released react-native.',
);
}
// Persist {url, versionOverride?} only when env-driven (matching the prior
// behavior of capturing env so Xcode-phase re-syncs keep the mode). A derived
// version is never frozen: omit versionOverride so the next run re-derives.
const envDriven =
(envUrl != null && envUrl !== '') ||
(envVersion != null && envVersion !== '');
if (envDriven) {
const toPersist /*: {url: string, versionOverride?: string} */ = {url};
if (override != null) {
toPersist.versionOverride = override;
}
fs.mkdirSync(path.dirname(cfgPath), {recursive: true});
fs.writeFileSync(cfgPath, JSON.stringify(toPersist, null, 2) + '\n');
}
return {
url,
version,
identity: remotePackageIdentity(url),
};
}
function perAppHeadersDir(appRoot /*: string */) /*: string */ {
return path.join(appRoot, PER_APP_HEADERS_REL);
}
/**
* Creates a first-wins symlink linker rooted at `outDir`. `linkInto` maps a
* virtual import path to a physical file; `foldDir` recursively links every
* .h/.hpp under a source root. `seen` records virtual->realpath so identical
* duplicates collapse to one inode and non-identical collisions are surfaced.
* Each header tree gets its OWN linker (own `seen` map) so first-wins does not
* span the shared/per-app boundary.
*/
function createHeaderLinker(
outDir /*: string */,
logger /*: {log: (msg: string) => void} */,
) /*: {
seen: Map<string, string>,
stats: {collisions: number},
linkInto: (virtualPath: string, physical: string) => void,
foldDir: (srcRoot: string) => void,
} */ {
const seen /*: Map<string, string> */ = new Map();
const stats = {collisions: 0};
function linkInto(
virtualPath /*: string */,
physical /*: string */,
) /*: void */ {
let real;
try {
real = fs.realpathSync(physical);
} catch {
return; // physical file missing — skip
}
const prev = seen.get(virtualPath);
if (prev != null) {
if (prev !== real) {
stats.collisions++;
logger.log(
`WARNING: merged headers: non-identical collision for ${virtualPath} (kept first)`,
);
}
return;
}
seen.set(virtualPath, real);
const dest = path.join(outDir, virtualPath);
fs.mkdirSync(path.dirname(dest), {recursive: true});
fs.symlinkSync(real, dest);
}
function foldDir(srcRoot /*: string */) /*: void */ {
let real;
try {
real = fs.realpathSync(srcRoot);
} catch {
return;
}
if (!fs.statSync(real).isDirectory()) {
return;
}
const dirs /*: Array<string> */ = [real];
while (dirs.length > 0) {
const dir = dirs.pop();
if (dir == null) {
break;
}
for (const ent of fs.readdirSync(dir, {withFileTypes: true})) {
const name = String(ent.name);
const child = path.join(dir, name);
// statSync (not the Dirent flags) so symlinks are followed — the
// autolinking header farm is itself a symlink farm, so its leaf
// headers are symlinks, not regular files.
let st;
try {
st = fs.statSync(child);
} catch {
continue; // broken symlink — skip
}
if (st.isDirectory()) {
dirs.push(child);
} else if (
st.isFile() &&
(name.endsWith('.h') || name.endsWith('.hpp'))
) {
linkInto(path.relative(real, child), child);
}
}
}
}
return {seen, stats, linkInto, foldDir};
}
/*::
type HeaderTreeResult = {path: ?string, virtualPaths: Set<string>};
*/
/**
* Materializes the PER-APP header tree at
* <appRoot>/build/xcframeworks/ReactAppHeaders: autolinking dep headers + codegen
* output. Per-app because it depends on which libraries the app links and the
* app's generated specs. Returns {path, virtualPaths}.
*/
function buildPerAppHeaderTree(
appRoot /*: string */,
logger /*: {log: (msg: string) => void} */ = {log() {}},
) /*: HeaderTreeResult */ {
const outDir = perAppHeadersDir(appRoot);
// Build into a temp dir OUTSIDE every folded root, then swap into place. The
// farm's final home (outDir) is INSIDE build/generated/ios — a tree foldDir
// walks — so `outDir + '.tmp'` would ALSO sit there and foldDir would fold
// the half-built farm back into itself (a spurious `ReactAppHeaders.tmp/`
// namespace duplicating every codegen header). build/.react-app-headers.tmp
// is under build/ but not under any folded subdir.
const tmpDir = path.join(appRoot, 'build', '.react-app-headers.tmp');
fs.rmSync(tmpDir, {recursive: true, force: true});
fs.rmSync(outDir, {recursive: true, force: true});
fs.mkdirSync(tmpDir, {recursive: true});
const linker = createHeaderLinker(tmpDir, logger);
linker.foldDir(
path.join(appRoot, 'build', 'generated', 'autolinking', 'headers'),
);
linker.foldDir(path.join(appRoot, 'build', 'generated', 'ios'));
linker.foldDir(
path.join(appRoot, 'build', 'generated', 'ios', 'ReactCodegen'),
);
// Stub source so the farm is a valid SPM target (vended headers-only —
// see the ReactAppHeaders target in the codegen Package.swift template).
fs.writeFileSync(
path.join(tmpDir, 'ReactAppHeadersStub.c'),
'// ReactAppHeaders vends the per-app generated headers; this stub\n' +
'// satisfies SPM, which requires at least one source file per target.\n' +
'static int ReactAppHeadersStub __attribute__((unused)) = 0;\n',
);
fs.mkdirSync(path.dirname(outDir), {recursive: true});
fs.renameSync(tmpDir, outDir);
logger.log(
`Built per-app header tree (${linker.seen.size} headers` +
(linker.stats.collisions > 0
? `, ${linker.stats.collisions} non-identical collisions`
: '') +
')',
);
return {path: outDir, virtualPaths: new Set(linker.seen.keys())};
}
/**
* Runs React Native codegen and installs the SPM Package.swift template
* into build/generated/ios/. Used by both setup-apple-spm.js and
* sync-spm-autolinking.js.
*/
/**
* Installs the SPM codegen template into build/generated/ios/Package.swift.
* No-op when the template or the generated/ios dir is missing — codegen
* may not have produced output yet, or the project may be SPM-only.
*
* The template is copied verbatim in local mode: it holds only fixed-relative
* `.package(path:)` references (it lives at a known depth inside the app), and
* headers come from the ReactHeaders/ReactNativeHeaders targets + the
* ReactAppHeaders product — no loader, no absolute paths. In remote mode the
* ReactNative path-dep is rewritten to the `.package(url:exact:)` identity.
*/
function installSpmCodegenTemplate(
appRoot /*: string */,
reactNativeRoot /*: string */,
logger /*: {log: (msg: string) => void} */ = {log() {}},
) /*: void */ {
const spmTemplate = path.join(
reactNativeRoot,
'scripts',
'codegen',
'templates',
'Package.swift.spm-template',
);
const codegenPkgSwift = path.join(
appRoot,
'build',
'generated',
'ios',
'Package.swift',
);
if (!fs.existsSync(path.dirname(codegenPkgSwift))) {
// Codegen hasn't produced build/generated/ios yet — normal, nothing to do.
return;
}
if (!fs.existsSync(spmTemplate)) {
// Abnormal: the react-native package is missing its SPM codegen template.
// Without it, codegen's default mis-rooted Package.swift stays in place
// and every subsequent Resolve Package Graph fails — say so loudly.
logger.log(
`warning: SPM codegen template missing at ${spmTemplate} — ` +
'build/generated/ios/Package.swift was NOT corrected and package ' +
'resolution will likely fail. Is the react-native package complete?',
);
return;
}
let content = fs.readFileSync(spmTemplate, 'utf8');
// Remote mode: the codegen package depends on the remote ReactNative
// package identity instead of the local path-based artifacts package.
const remote = remotePackageConfig(appRoot);
if (remote != null) {
content = content
.replace(
`.package(name: "${REACT_NATIVE_PACKAGE_NAME}", path: "../../xcframeworks"),`,
`.package(url: "${remote.url}", exact: "${remote.version}"),`,
)
.split(`package: "${REACT_NATIVE_PACKAGE_NAME}")`)
.join(`package: "${remote.identity}")`);
}
fs.writeFileSync(codegenPkgSwift, content, 'utf8');
logger.log(
'Installed SPM codegen template' + (remote != null ? ' (remote mode)' : ''),
);
}
function runCodegenAndInstallTemplate(
projectRoot /*: string */,
appRoot /*: string */,
reactNativeRoot /*: string */,
logger /*: {log: (msg: string) => void} */ = {log() {}},
opts /*: {installTemplate?: boolean} */ = {},
) /*: void */ {
const codegenScript = path.join(
reactNativeRoot,
'scripts',
'generate-codegen-artifacts.js',
);
if (!fs.existsSync(codegenScript)) {
return;
}
logger.log('Running codegen...');
const {execSync} = require('node:child_process');
const codegenArgs =
`node "${codegenScript}" -p "${projectRoot}" -t ios` +
(projectRoot !== appRoot ? ` -o "${appRoot}"` : '');
execSync(codegenArgs, {stdio: 'inherit', cwd: projectRoot});
// Callers that re-point the xcframework symlinks after codegen (e.g. the SPM
// sync, which runs generate-spm-package afterwards) install the template
// themselves once the symlinks are final; they pass installTemplate: false to
// avoid a wasted write that the later install would immediately supersede.
if (opts.installTemplate !== false) {
installSpmCodegenTemplate(appRoot, reactNativeRoot, logger);
}
}
// ---------------------------------------------------------------------------
// Autolinking-plugin script phases — the shared validation rules. Two gates
// apply them with different POLICIES (invokePlugins throws, the injector's
// sidecar reader skips the entry), so only the rules live here. See
// __doc__/spm-autolinking-plugins.md for the reasoning.
// ---------------------------------------------------------------------------
// `@` and `/` are admitted so a package can use its own scoped npm name
// (`@expo/log-box`). `:` is not: the id is hashed into the UUID seed as
// `plugin:<id>`, and keeping the separator out of the id keeps that seed
// unambiguous.
const SCRIPT_PHASE_ID_PATTERN = /^[@A-Za-z0-9_./-]+$/;
// `ledger.__proto__ = uuid` sets the prototype instead of an own property, so a
// phase with one of these ids would look recorded, vanish through
// JSON.stringify, and never be removable by `deinit`.
const RESERVED_SCRIPT_PHASE_IDS = new Set([
'__proto__',
'constructor',
'prototype',
]);
function isValidScriptPhaseId(value /*: unknown */) /*: boolean */ {
return (
typeof value === 'string' &&
SCRIPT_PHASE_ID_PATTERN.test(value) &&
!RESERVED_SCRIPT_PHASE_IDS.has(value)
);
}
/**
* The name reaches the project file twice: verbatim in the escaped `name` field
* Xcode displays, and normalized (spm-pbxproj's commentSafe) in the cosmetic
* `/* … */` comments. Both are safe for any single-line string, so the only
* thing left to refuse is a line break — which no Xcode phase display name can
* carry anyway.
*/
function isValidScriptPhaseName(value /*: unknown */) /*: boolean */ {
return typeof value === 'string' && value.length > 0 && !/[\r\n]/.test(value);
}
module.exports = {
REACT_NATIVE_PACKAGE_NAME,
REACT_CODEGEN_PACKAGE_NAME,
AUTOLINKED_PACKAGE_NAME,
REACT_NATIVE_UMBRELLA_PRODUCT,
REACT_NATIVE_HEADERS_PRODUCT,
REACT_NATIVE_XCFRAMEWORK_PRODUCTS,
REACT_NATIVE_PRODUCTS,
REACT_CODEGEN_PRODUCTS,
REACT_CODEGEN_APP_PRODUCTS,
REACT_HEADERS_TARGET_DIR,
RESERVED_SWIFT_NAMES,
makeLogger,
displayPath,
sharedCacheDir,
defaultCacheDir,
toSwiftName,
toC99Name,
swiftNameKey,
isValidSwiftName,
deriveAppName,
readPackageJson,
findProjectRoot,
resolveReactNativeRoot,
buildPerAppHeaderTree,
remotePackageConfig,
resolveInstalledRnVersion,
isPublishableVersion,
RemoteVersionError,
installSpmCodegenTemplate,
runCodegenAndInstallTemplate,
isValidScriptPhaseId,
isValidScriptPhaseName,
SCAFFOLDER_MARKER,
};