react-native
Version:
A framework for building native apps using React
844 lines (797 loc) • 32.4 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
*/
;
/*:: import type {PodspecModel, PreprocessorDefine} from './spm-types'; */
/**
* read-podspec.js — produces a flattened, SPM-friendly view of an iOS
* podspec for the scaffolder.
*
* Strategy: prefer `pod ipc spec <path>` (CocoaPods evaluates the Ruby DSL,
* including `install_modules_dependencies(s)` and `$config[:...]`
* interpolation, so the JSON output reflects the real spec). Fall back to a
* best-effort regex parser when CocoaPods isn't available — handles simple
* RN libs but skips subspec blocks and Ruby helpers with a warning.
*
* The output `PodspecModel` collapses the spec's default subspecs into a
* single logical target view, since that's how RN consumers compile these
* libraries in practice.
*/
const {spawnSync} = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
/*::
type RawSpec = {[string]: unknown};
*/
// ---------------------------------------------------------------------------
// Pod-IPC primary path
// ---------------------------------------------------------------------------
/**
* Runs `pod ipc spec <path>` and parses the JSON it prints. Returns null
* on any failure: CocoaPods missing, command threw, output not JSON, etc.
* Callers should treat null as a signal to fall back to the regex parser.
*/
/**
* Strips RN-specific Ruby helpers from a podspec source so `pod ipc spec`
* can evaluate it without RN's Podfile-side helpers loaded. The stripped
* helpers (`install_modules_dependencies(s)`) typically inject the React-Core
* / React-Fabric family — all of which the scaffolder collapses into the
* single `ReactNative` product anyway, so dropping them is safe as long as
* the podspec also has an explicit `s.dependency "React-Core"` (almost all
* RN libs do). Writes the patched content to a temp file and returns its
* path; caller deletes when done.
*/
function patchPodspecForPodIpc(podspecPath /*: string */) /*: string */ {
const content = fs.readFileSync(podspecPath, 'utf8');
// Comment out RN-Podfile-only helpers. We do NOT remove the lines so
// line numbers in any pod ipc errors still match the original file.
const patched = content
.replace(
/^(\s*)install_modules_dependencies\(([^)]*)\)/gm,
'$1# install_modules_dependencies($2) # stripped for pod ipc',
)
.replace(
/^(\s*)use_react_native!\(([^)]*)\)/gm,
'$1# use_react_native!($2) # stripped for pod ipc',
);
// Write the patched copy NEXT TO the original podspec, not in os.tmpdir.
// Podspecs commonly do `File.read(File.join(__dir__, 'package.json'))` or
// similar, expecting the package.json sibling. Keeping the patched file
// in the same directory makes those reads continue to work.
const depDir = path.dirname(podspecPath);
// pod ipc only accepts files ending in `.podspec` or `.podspec.json`.
// Use a dotfile prefix so the patched copy is invisible to normal listing
// but the suffix CocoaPods requires is preserved.
const tmpFile = path.join(
depDir,
`.spm-scaffold-${process.pid}-${path.basename(podspecPath)}`,
);
fs.writeFileSync(tmpFile, patched, 'utf8');
return tmpFile;
}
function runPodIpcSpec(podspecPath /*: string */) /*: RawSpec | null */ {
// RN podspecs often call `install_modules_dependencies(s)`, a helper
// defined by RN's Podfile-side scripts. `pod ipc spec` doesn't load
// those, so the helper is undefined and the whole spec fails to parse.
// Pre-process to strip those calls before invoking pod ipc.
let patchedPath /*: ?string */ = null;
try {
patchedPath = patchPodspecForPodIpc(podspecPath);
} catch {
return null;
}
let result;
try {
result = spawnSync('pod', ['ipc', 'spec', patchedPath], {
encoding: 'utf8',
timeout: 30000,
maxBuffer: 8 * 1024 * 1024,
// Many RN podspecs gate subspec definitions on `RCT_NEW_ARCH_ENABLED`
// (e.g. safe-area-context wraps its Fabric `common` + `fabric`
// subspecs in `if fabric_enabled`). RN 0.76+ defaults to the new
// architecture, so unless the caller has explicitly opted out, set
// the env var here so `pod ipc spec` evaluates the full podspec.
env: {
...process.env,
RCT_NEW_ARCH_ENABLED: process.env.RCT_NEW_ARCH_ENABLED ?? '1',
},
});
} catch {
cleanupPatchedPodspec(patchedPath);
return null;
}
cleanupPatchedPodspec(patchedPath);
if (result == null || result.error != null) {
return null;
}
if (typeof result.status !== 'number' || result.status !== 0) {
return null;
}
if (typeof result.stdout !== 'string' || result.stdout.length === 0) {
return null;
}
// Some podspecs print diagnostics to stdout during evaluation (e.g. skia:
// `-- SK_GRAPHITE: OFF ...`) before `pod ipc` emits the JSON, and a
// diagnostic can itself contain braces (`Building {module}`). `pod ipc`
// pretty-prints the spec as a JSON object whose outermost braces sit at
// column 0, so ANCHOR on a line-boundary `{`…`}` rather than the first/last
// brace anywhere — otherwise a brace inside a diagnostic shifts the slice and
// JSON.parse throws. When the authoritative parse is abandoned we log (not
// silently drop to the much weaker, subspec-blind regex parser).
const stdout = result.stdout;
const startMatch = stdout.match(/^\{/m);
const start = startMatch != null ? startMatch.index : -1;
const lastLineBrace = stdout.lastIndexOf('\n}');
const end =
lastLineBrace >= 0 ? lastLineBrace + 2 : stdout.lastIndexOf('}') + 1;
if (start < 0 || end <= start) {
console.warn(
'[read-podspec] pod ipc spec produced no line-anchored JSON object; ' +
'falling back to the regex parser (subspecs/helpers may be missed).',
);
return null;
}
try {
return JSON.parse(stdout.slice(start, end));
} catch (e) {
console.warn(
`[read-podspec] pod ipc spec JSON parse failed (${e.message}); ` +
'falling back to the regex parser (subspecs/helpers may be missed).',
);
return null;
}
}
function cleanupPatchedPodspec(patchedPath /*: ?string */) /*: void */ {
if (patchedPath == null) return;
try {
fs.unlinkSync(patchedPath);
} catch {
// best-effort cleanup; the file is named with `.spm-scaffold-...tmp`
// so a leftover is identifiable.
}
}
// ---------------------------------------------------------------------------
// Regex fallback
// ---------------------------------------------------------------------------
// Comment-only lines are dropped so a commented-out `# s.header_dir` cannot win
// over the live one. A trailing `#` and a `#` inside a string survive: only the
// first non-whitespace character counts.
function readPodspecSource(podspecPath /*: string */) /*: string */ {
return fs
.readFileSync(podspecPath, 'utf8')
.split('\n')
.filter(line => !/^\s*#/.test(line))
.join('\n');
}
// A subspec that re-binds a receiver the identity regexes trust: inside
// `s.subspec "common" do |s|`, `s` is the CHILD, and no regex over flat text
// can see that. Any other block variable (`do |ss|`) is unambiguous.
const SHADOWING_SUBSPEC_RE =
/\.subspec\s+["'][^"']*["']\s+do\s*\|\s*(?:s|spec)\s*\|/;
function hasShadowingSubspec(source /*: string */) /*: boolean */ {
return SHADOWING_SUBSPEC_RE.test(source);
}
/**
* Best-effort Ruby podspec parser. Extracts the literal-string and
* literal-array fields most RN libs use. Skips subspec blocks, Ruby helper
* calls (install_modules_dependencies, ENV[], $config[:...]), and
* interpolation — appends a warning so the caller can surface it to the
* user. Always returns a RawSpec; pure-JS, no Ruby dep required.
*/
function regexPodspec(podspecPath /*: string */) /*: RawSpec */ {
const content = readPodspecSource(podspecPath);
const warnings /*: Array<string> */ = [];
// Matches: s.<field> = "value" or s.<field> = 'value' — at ANY scope,
// so a subspec's value counts. Right for the fields flattenSubspecs merges
// (source globs, header mappings); wrong for the fields that identify the
// spec, which use getSpecStringField.
function getStringField(name /*: string */) /*: string | null */ {
const re = new RegExp(`(?:s|spec)\\.${name}\\s*=\\s*["']([^"']+)["']`);
const m = content.match(re);
return m ? m[1] : null;
}
// The same, but only where the receiver starts the statement — so a subspec's
// block variable (`ss.header_dir`, the shape react-native-screens and
// react-native-svg ship) cannot answer for the spec itself.
function getSpecStringField(name /*: string */) /*: string | null */ {
const re = new RegExp(
`(?:^|[;|])\\s*(?:s|spec)\\.${name}\\s*=\\s*["']([^"']+)["']`,
'm',
);
const m = content.match(re);
return m ? m[1] : null;
}
// The fields that identify the spec, and only when the receiver spelling
// still says which scope declared them: a subspec re-binding `s` puts its own
// `header_dir` exactly where the library's would be.
const shadowedIdentity = hasShadowingSubspec(content);
function getIdentityField(name /*: string */) /*: string | null */ {
return shadowedIdentity ? null : getSpecStringField(name);
}
// Matches:
// s.<field> = ["a", "b"] (array)
// s.<field> = "single" (single value treated as 1-element array)
function getArrayField(name /*: string */) /*: Array<string> */ {
const reArr = new RegExp(`(?:s|spec)\\.${name}\\s*=\\s*\\[([^\\]]*)\\]`);
const mArr = content.match(reArr);
if (mArr != null) {
const inner = mArr[1];
const out /*: Array<string> */ = [];
const itemRe = /["']([^"']+)["']/g;
while (true) {
const m = itemRe.exec(inner);
if (m == null) break;
out.push(m[1]);
}
return out;
}
const single = getStringField(name);
return single != null ? [single] : [];
}
// Matches: s.dependency "Name" (with optional version constraint)
function getDependencies() /*: Array<string> */ {
const out /*: Array<string> */ = [];
const re = /(?:s|spec)\.dependency\s+["']([^"']+)["']/g;
while (true) {
const m = re.exec(content);
if (m == null) break;
out.push(m[1]);
}
return out;
}
// Matches:
// s.framework "X" → ["X"]
// s.framework = "X" → ["X"]
// s.frameworks = ["X","Y"] → ["X","Y"]
function getFrameworks(weak /*: boolean */) /*: Array<string> */ {
const prefix = weak ? 'weak_framework' : 'framework';
const out /*: Array<string> */ = [];
// Plural form: s.frameworks = [...] or s.frameworks = "Foo"
out.push(...getArrayField(`${prefix}s`));
// Singular form: s.framework = "Foo" — single value treated as a 1-elem list
out.push(...getArrayField(prefix));
// Method-call form: s.framework "Foo", "Bar" — no `=`. `getArrayField`
// only matches assignment, so handle this case separately.
const callRe = new RegExp(
`(?:s|spec)\\.${prefix}s?\\s+((?:["'][^"']+["']\\s*,?\\s*)+)`,
'g',
);
while (true) {
const m = callRe.exec(content);
if (m == null) break;
const itemRe = /["']([^"']+)["']/g;
while (true) {
const im = itemRe.exec(m[1]);
if (im == null) break;
out.push(im[1]);
}
}
return Array.from(new Set(out));
}
// Matches: 'HEADER_SEARCH_PATHS' => '...' or => ["...","..."]
// Returns the raw token strings (still containing $(PODS_TARGET_SRCROOT)
// etc.); substitution happens at translation time.
function getHeaderSearchPaths() /*: Array<string> */ {
const out /*: Array<string> */ = [];
// Array form
const arrRe = /["']HEADER_SEARCH_PATHS["']\s*=>\s*\[([\s\S]*?)\]/g;
while (true) {
const m = arrRe.exec(content);
if (m == null) break;
const itemRe = /["']((?:[^"'\\]|\\.)+)["']/g;
while (true) {
const im = itemRe.exec(m[1]);
if (im == null) break;
// Each entry can be a single path or a space-separated list of paths.
for (const token of im[1].split(/\s+/)) {
if (token.length > 0) {
out.push(stripWrappingQuotes(token));
}
}
}
}
// String form
const strRe =
/["']HEADER_SEARCH_PATHS["']\s*=>\s*["']((?:[^"'\\]|\\.)+)["']/g;
while (true) {
const m = strRe.exec(content);
if (m == null) break;
for (const token of m[1].split(/\s+/)) {
if (token.length > 0) {
out.push(stripWrappingQuotes(token));
}
}
}
return Array.from(new Set(out));
}
// Surface known unparseable constructs so the caller can warn the user.
if (shadowedIdentity) {
warnings.push(
"A subspec rebinds the spec variable (`do |s|`), so its `name`, `module_name` and `header_dir` cannot be told from the library's own — those are left unread. Install CocoaPods (`gem install cocoapods`) to enable full `pod ipc spec` parsing.",
);
}
if (/(?:s|spec)\.subspec\s+["']/.test(content)) {
warnings.push(
'Subspecs detected — regex parser only extracts top-level fields. Install CocoaPods (`gem install cocoapods`) to enable full `pod ipc spec` parsing.',
);
}
if (/install_modules_dependencies/.test(content)) {
warnings.push(
'`install_modules_dependencies(s)` detected — dependency wiring may be incomplete without CocoaPods.',
);
}
if (/\bENV\[/.test(content) || /\$config\[/.test(content)) {
warnings.push(
'Env-var or Ruby-config interpolation detected — values may not translate cleanly without CocoaPods.',
);
}
return {
name: getIdentityField('name'),
module_name: getIdentityField('module_name'),
version: getSpecStringField('version'),
source_files: getArrayField('source_files'),
public_header_files: getArrayField('public_header_files'),
private_header_files: getArrayField('private_header_files'),
exclude_files: getArrayField('exclude_files'),
header_mappings_dir: getStringField('header_mappings_dir'),
header_dir: getIdentityField('header_dir'),
frameworks: getFrameworks(false),
weak_frameworks: getFrameworks(true),
libraries: getArrayField('libraries'),
dependencies: getDependencies(),
compiler_flags: tokenizeFlags(getStringField('compiler_flags')),
pod_target_xcconfig: {HEADER_SEARCH_PATHS: getHeaderSearchPaths()},
resources: getArrayField('resources'),
requires_arc: /(?:s|spec)\.requires_arc\s*=\s*true/.test(content),
__regex_partial__: true,
__warnings__: warnings,
};
}
// Strips outer quote-like characters from a captured token. Handles bare
// quotes (`"foo"`, `'foo'`) AND Ruby-escaped quotes (`\"foo\"`) which show
// up verbatim when the regex captures something like
// `"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/x\""`.
function stripWrappingQuotes(s /*: string */) /*: string */ {
return s.replace(/^\\?["']/, '').replace(/\\?["']$/, '');
}
function tokenizeFlags(value /*: string | null */) /*: Array<string> */ {
if (value == null) return [];
return value.split(/\s+/).filter(Boolean);
}
/**
* Split a flag string on whitespace while keeping quoted spans intact, so a
* define like `-DWORKLETS_FEATURE_FLAGS="[A:false][B:true]"` stays one token
* (the quotes are part of the macro value). A naive whitespace split would
* shred any define whose value contains spaces.
*/
function shellTokenize(value /*: string */) /*: Array<string> */ {
const tokens /*: Array<string> */ = [];
let cur = '';
let quote /*: string | null */ = null;
let has = false;
for (let i = 0; i < value.length; i++) {
const c = value[i];
if (quote != null) {
cur += c;
if (c === quote) quote = null;
} else if (c === '"' || c === "'") {
cur += c;
quote = c;
has = true;
} else if (/\s/.test(c)) {
if (has) tokens.push(cur);
cur = '';
has = false;
} else {
cur += c;
has = true;
}
}
if (has) tokens.push(cur);
return tokens;
}
// ---------------------------------------------------------------------------
// Subspec flattening
// ---------------------------------------------------------------------------
/**
* Merges a podspec's default subspecs (or all subspecs when no defaults are
* declared) into a single logical view. RN libraries use subspecs for
* platform/feature gating (`apple` vs `common`); consumers compile the
* union anyway, so the flat view matches the actual build.
*
* Merge rules:
* - Array fields → concat + dedup
* - String fields → top-level wins; if absent, take the first subspec's value
* - pod_target_xcconfig HEADER_SEARCH_PATHS → array-union across all selected subspecs
* - dependencies → array-union of name strings (version constraints dropped)
*/
function flattenSubspecs(rawSpec /*: RawSpec */) /*: PodspecModel */ {
const warnings /*: Array<string> */ = [];
// $FlowFixMe[prop-missing] dynamic shape
const partial /*: boolean */ = rawSpec.__regex_partial__ === true;
if (Array.isArray(rawSpec.__warnings__)) {
// $FlowFixMe[incompatible-type] runtime-validated dynamic shape
const rawWarnings /*: ReadonlyArray<string> */ = rawSpec.__warnings__;
for (const w of rawWarnings) {
warnings.push(w);
}
}
// Determine which subspecs to merge. pod ipc returns:
// - `subspecs`: array of nested specs (or undefined)
// - `default_subspecs`: array of names (or undefined → use all subspecs)
const subspecs = Array.isArray(rawSpec.subspecs) ? rawSpec.subspecs : [];
let selected = subspecs;
if (Array.isArray(rawSpec.default_subspecs)) {
const wanted = new Set(rawSpec.default_subspecs);
selected = subspecs.filter(s => {
// $FlowFixMe[prop-missing] dynamic shape
const name = s != null && typeof s.name === 'string' ? s.name : null;
return name != null && wanted.has(name);
});
}
const layers = [rawSpec, ...selected];
function mergeArrayField(key /*: string */) /*: Array<string> */ {
const out /*: Array<string> */ = [];
for (const layer of layers) {
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`; runtime-validated below
const value = layer != null ? layer[key] : null;
if (Array.isArray(value)) {
for (const v of value) {
if (typeof v === 'string') out.push(v);
}
} else if (typeof value === 'string') {
out.push(value);
}
}
return Array.from(new Set(out));
}
// Only the spec's own value: a subspec's `header_dir` names that subspec's
// headers, and several subspecs can disagree.
function specStringField(key /*: string */) /*: string | null */ {
// $FlowFixMe[incompatible-use] dynamic shape
const value = rawSpec[key];
return typeof value === 'string' && value.length > 0 ? value : null;
}
function mergeStringField(key /*: string */) /*: string | null */ {
for (const layer of layers) {
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`; runtime-validated below
const value = layer != null ? layer[key] : null;
if (typeof value === 'string' && value.length > 0) {
return value;
}
}
return null;
}
function mergeHeaderSearchPaths() /*: Array<string> */ {
const out /*: Array<string> */ = [];
// HSP can live in any of the xcconfig blocks, and a single value often
// PACKS multiple space-separated paths, each individually quoted, plus a
// CocoaPods `/**` recursive-glob suffix (e.g. skia:
// `"$(SRCROOT)/cpp/"/** "$(SRCROOT)/cpp/skia" ...`). Shell-tokenize to keep
// each path intact, then strip ALL quotes (not just wrapping) per token.
const XCCONFIG_KEYS = [
'pod_target_xcconfig',
'xcconfig',
'user_target_xcconfig',
];
for (const layer of layers) {
if (layer == null || typeof layer !== 'object') continue;
for (const xcKey of XCCONFIG_KEYS) {
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`
const xc = layer[xcKey];
if (xc == null || typeof xc !== 'object') continue;
const hsp = xc.HEADER_SEARCH_PATHS;
const values =
typeof hsp === 'string'
? [hsp]
: Array.isArray(hsp)
? hsp.filter(v => typeof v === 'string')
: [];
for (const v of values) {
for (const tok of shellTokenize(v)) {
const cleaned = tok.replace(/['"]/g, '');
if (cleaned.length > 0) out.push(cleaned);
}
}
}
}
return Array.from(new Set(out));
}
// Lift preprocessor defines from pod_target_xcconfig across all layers:
// `-D` tokens in OTHER_CFLAGS, and NAME[=VALUE] entries in
// GCC_PREPROCESSOR_DEFINITIONS (incl. per-config `[config=*Debug*]` keys).
// Non-define compiler flags in OTHER_CFLAGS are intentionally dropped — only
// `-D`s are safe to forward; arbitrary flags may be machine- or
// example-app-specific. $(inherited), unresolved $(...) tokens, and invalid
// C identifiers are skipped.
function mergePreprocessorDefines() /*: Array<PreprocessorDefine> */ {
const out /*: Array<PreprocessorDefine> */ = [];
const seen /*: Set<string> */ = new Set();
const validName = /^[A-Za-z_]\w*$/;
const add = (
name /*: string */,
value /*: ?string */,
config /*: ?('debug' | 'release') */,
) => {
if (!validName.test(name)) return;
if (/\$[({]/.test(name) || (value != null && /\$[({]/.test(value))) {
return; // unresolved Xcode/Ruby token — don't emit a broken define
}
const key = `${name}|${config ?? ''}`;
if (seen.has(key)) return;
seen.add(key);
out.push({name, value, config});
};
// Defines can live in the target xcconfig (pod_target_xcconfig) OR the
// aggregate/user xcconfig (`s.xcconfig` / user_target_xcconfig). worklets
// puts its version define in pod_target_xcconfig; reanimated puts its in
// `s.xcconfig` — scan all three.
const XCCONFIG_KEYS = [
'pod_target_xcconfig',
'xcconfig',
'user_target_xcconfig',
];
for (const layer of layers) {
if (layer == null || typeof layer !== 'object') continue;
for (const xcKey of XCCONFIG_KEYS) {
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`
const xc = layer[xcKey];
if (xc == null || typeof xc !== 'object') continue;
for (const rawKey of Object.keys(xc)) {
const cflags = /^OTHER_CFLAGS(?:\[config=\*(\w+)\*\])?$/i.exec(
rawKey,
);
const ppDefs =
/^GCC_PREPROCESSOR_DEFINITIONS(?:\[config=\*(\w+)\*\])?$/i.exec(
rawKey,
);
if (cflags == null && ppDefs == null) continue;
const cfgRaw = (
(cflags?.[1] ?? ppDefs?.[1] ?? '') + ''
).toLowerCase();
const config =
cfgRaw === 'debug'
? 'debug'
: cfgRaw === 'release'
? 'release'
: null;
// $FlowFixMe[incompatible-use] xc value access is intentional
const val = xc[rawKey];
const strs =
typeof val === 'string'
? [val]
: Array.isArray(val)
? val.filter(v => typeof v === 'string')
: [];
for (const s of strs) {
for (const tok of shellTokenize(s)) {
if (tok === '$(inherited)') continue;
// OTHER_CFLAGS: only `-D` tokens are defines; others are flags.
// GCC_PREPROCESSOR_DEFINITIONS: every token is `NAME[=VALUE]`.
let body /*: ?string */ = null;
if (cflags != null) {
if (tok.startsWith('-D')) body = tok.slice(2);
} else {
body = tok;
}
if (body == null || body.length === 0) continue;
const eq = body.indexOf('=');
const name = eq >= 0 ? body.slice(0, eq) : body;
const value = eq >= 0 ? body.slice(eq + 1) : null;
add(name, value, config);
}
}
}
}
}
return out;
}
function mergeDependencies() /*: Array<string> */ {
const out /*: Array<string> */ = [];
for (const layer of layers) {
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`; runtime-validated below
const deps = layer != null ? layer.dependencies : null;
if (deps == null) continue;
// pod ipc returns deps as: {name: [versionConstraint, ...], ...}
// regex returns deps as: [name, name, ...]
if (Array.isArray(deps)) {
for (const d of deps) {
if (typeof d === 'string') out.push(d);
}
} else if (typeof deps === 'object') {
for (const name of Object.keys(deps)) {
out.push(name);
}
}
}
return Array.from(new Set(out));
}
function mergeCompilerFlags() /*: Array<string> */ {
const out /*: Array<string> */ = [];
for (const layer of layers) {
// $FlowFixMe[incompatible-use] layer narrowed from `mixed`; runtime-validated below
const value = layer != null ? layer.compiler_flags : null;
if (typeof value === 'string') {
for (const tok of value.split(/\s+/)) {
if (tok.length > 0) out.push(tok);
}
} else if (Array.isArray(value)) {
for (const v of value) {
if (typeof v === 'string') {
for (const tok of v.split(/\s+/)) {
if (tok.length > 0) out.push(tok);
}
}
}
}
}
return out;
}
// $FlowFixMe[prop-missing] dynamic shape
const reqArc /*: unknown */ = rawSpec.requires_arc;
const requiresArc =
reqArc === false || (Array.isArray(reqArc) && reqArc.length === 0)
? false
: true; // RN ecosystem default
return {
name: specStringField('name') ?? '',
version: specStringField('version') ?? '',
sourceFiles: mergeArrayField('source_files'),
publicHeaderFiles: mergeArrayField('public_header_files'),
privateHeaderFiles: mergeArrayField('private_header_files'),
excludeFiles: mergeArrayField('exclude_files'),
headerMappingsDir: mergeStringField('header_mappings_dir'),
// ALL subspecs' header_mappings_dir values (not just the merged one). Each
// implies a header search path of its parent dir so namespaced includes
// (`<reanimated/apple/sensor/X.h>` for a header physically at
// `apple/reanimated/apple/sensor/X.h` with mappings dir `apple/reanimated`)
// resolve from the physical tree — CocoaPods does this via the
// header_mappings_dir copy step, which SPM has no equivalent for.
headerMappingsDirs: mergeArrayField('header_mappings_dir'),
headerDir: specStringField('header_dir'),
moduleName: specStringField('module_name'),
frameworks: mergeArrayField('frameworks'),
weakFrameworks: mergeArrayField('weak_frameworks'),
libraries: mergeArrayField('libraries'),
dependencies: mergeDependencies(),
preprocessorDefines: mergePreprocessorDefines(),
compilerFlags: mergeCompilerFlags(),
headerSearchPaths: mergeHeaderSearchPaths(),
resources: mergeArrayField('resources'),
requiresArc,
warnings,
partial,
// Default: overridden in readPodspec, which can see the podspec source.
usesInstallModulesDependencies: false,
};
}
// ---------------------------------------------------------------------------
// Top-level entry
// ---------------------------------------------------------------------------
/**
* Reads and flattens a podspec at `podspecPath`. Tries `pod ipc spec` first
* (full Ruby DSL evaluation); falls back to a regex parser when CocoaPods
* isn't available.
*
* Throws when the file doesn't exist. Otherwise always returns a
* PodspecModel — warnings on the model surface partial parses to the
* caller, who can decide whether to proceed or abort scaffolding for that
* dep.
*/
function readPodspec(podspecPath /*: string */) /*: PodspecModel */ {
if (!fs.existsSync(podspecPath)) {
throw new Error(`readPodspec: file does not exist: ${podspecPath}`);
}
const podIpc = runPodIpcSpec(podspecPath);
const raw = podIpc != null ? podIpc : regexPodspec(podspecPath);
const model = flattenSubspecs(raw);
// Record whether the podspec calls `install_modules_dependencies(s)`. We
// strip that helper before `pod ipc` and the regex parser can't expand it,
// so the React-Core family it injects never lands in `model.dependencies`.
// The scaffolder uses this as an implicit React-core marker (plain ObjC
// modules using the helper have no `codegenConfig` to detect instead).
try {
const src = fs.readFileSync(podspecPath, 'utf8');
model.usesInstallModulesDependencies =
/\binstall_modules_dependencies\b/.test(src);
} catch {
// best-effort; leave the flattenSubspecs default (false)
}
return model;
}
const podspecModels /*: Map<string, PodspecModel> */ = new Map();
/**
* readPodspec memoized on the resolved path. `pod ipc spec` costs a process
* spawn, and one autolinking run reads the same podspec from several sites
* (name resolution, header search paths, scaffolding). The model is shared, so
* callers must treat it as read-only.
*/
function readPodspecCached(podspecPath /*: string */) /*: PodspecModel */ {
const key = path.resolve(podspecPath);
const cached = podspecModels.get(key);
if (cached != null) {
return cached;
}
const model = readPodspec(podspecPath);
podspecModels.set(key, model);
return model;
}
/**
* The three fields that name a library, read WITHOUT `pod ipc spec`'s process
* spawn — name resolution runs for every dep, including self-managed ones that
* need nothing else from the podspec.
*
* Null when the answer cannot be trusted without Ruby: an interpolated value,
* or a spec-level `header_dir`/`module_name` the regex could not read. Both
* outrank the pod name, so a literal name alone is not enough — resolving
* without them would pick the wrong prefix. A value only a subspec declares is
* not the library's, so it neither answers nor defers — unless that subspec
* re-binds `s` (or `spec`), which makes every field's scope unreadable.
*/
function readPodspecNames(
podspecPath /*: string */,
) /*: ?{name: ?string, moduleName: ?string, headerDir: ?string} */ {
const raw = regexPodspec(podspecPath);
const field = (key /*: string */) /*: ?string */ => {
// $FlowFixMe[prop-missing] RawSpec is dynamically shaped
const value = raw[key];
return typeof value === 'string' &&
value.length > 0 &&
!value.includes('#{')
? value
: null;
};
const source = readPodspecSource(podspecPath);
if (hasShadowingSubspec(source)) {
return null;
}
const declaredUnread = (
key /*: string */,
value /*: ?string */,
) /*: boolean */ =>
value == null &&
new RegExp(`(?:^|[;|])\\s*(?:s|spec)\\.${key}\\s*=`, 'm').test(source);
const name = field('name');
const moduleName = field('module_name');
const headerDir = field('header_dir');
if (
declaredUnread('header_dir', headerDir) ||
declaredUnread('module_name', moduleName)
) {
return null;
}
return name == null && moduleName == null && headerDir == null
? null
: {name, moduleName, headerDir};
}
/**
* Every podspec in `dir`, sorted so two of them name a target the same way on
* every machine (readdir order is unspecified). Skips a crashed run's leftover
* `.spm-scaffold-<pid>-<name>.podspec` copy.
*/
function findPodspecs(dir /*: string */) /*: Array<string> */ {
try {
return fs
.readdirSync(dir)
.filter(e => e.endsWith('.podspec') && !e.startsWith('.spm-scaffold-'))
.sort()
.map(e => path.join(dir, e));
} catch {
return [];
}
}
module.exports = {
findPodspecs,
readPodspec,
readPodspecCached,
readPodspecNames,
runPodIpcSpec,
regexPodspec,
flattenSubspecs,
};