react-native
Version:
A framework for building native apps using React
800 lines (761 loc) • 28 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';
const crypto = require('node:crypto');
/**
* Generate a deterministic 24-hex-character UUID from a seed string.
* SHA-256 truncated to 24 chars (standard Xcode pbxproj UUID length). Not a
* security use — the hash only provides stable, collision-unlikely IDs — but
* sha256 keeps static analysis (CodeQL weak-crypto) quiet.
*/
function generateUUID(seed /*: string */) /*: string */ {
return crypto
.createHash('sha256')
.update(seed)
.digest('hex')
.substring(0, 24)
.toUpperCase();
}
/**
* Escapes a string for OpenStep plist format if needed. A literal CR or tab in
* a quoted value is legal but Xcode rewrites it to its escape on the next save,
* planting a spurious diff in the user's repo — so escape those too (a plugin
* `script` with CRLF line endings is how one gets in).
*/
function quoteIfNeeded(s /*: string */) /*: string */ {
if (/^[a-zA-Z0-9._/]+$/.test(s)) {
return s;
}
return `"${s
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')}"`;
}
/**
* Normalize an UNTRUSTED label (a plugin's script-phase name, say) for use
* inside a pbxproj `/* … */` comment. Such a comment is purely cosmetic — Xcode
* regenerates it from the object's own fields — while the text AROUND it is
* scanned by delimiter, so every character that could be read as structure (or
* split the line) becomes a space, and runs of whitespace collapse:
* `findObjectByUuid` takes the first `{` after a UUID as that object's body,
* `removeArrayMembersByUuid` identifies a member line by its trailing comma, and
* `scanToClose` treats a `"` as opening a string. Stable (same input ⇒ same
* output), so re-syncing stays byte-identical. Returns '' when nothing survives;
* callers supply the fallback label.
*
* Comments RN writes to Xcode's OWN convention (`XCLocalSwiftPackageReference
* "build/xcframeworks"`, `<file> in Sources`) are NOT run through this: they are
* fixed strings, and normalizing them would rewrite bytes Xcode itself produces —
* a spurious diff in the user's repo on their next save.
*/
function commentSafe(s /*: string */) /*: string */ {
return s
.replace(/[{}(),;="*/\s]/g, ' ')
.replace(/ +/g, ' ')
.trim();
}
/**
* Format the ` /* … */` suffix pbxproj writes after a UUID, omitted entirely for
* an empty label (an uncommented UUID is well-formed — Xcode writes those itself
* for unnamed groups). Labels that did not originate in this repo must already
* have been through `commentSafe`.
*/
function uuidComment(comment /*: ?string */) /*: string */ {
return comment != null && comment !== '' ? ` /* ${comment} */` : '';
}
/**
* Serialize a single pbxproj object entry to its OpenStep text form,
* including the leading `\t\t<uuid>` and trailing `};` but NO trailing
* newline. Short entries (≤3 scalar fields) collapse to one line, matching
* Xcode's own formatting. Used by the in-place injector to splice single
* entries into an existing project.
*/
function serializeEntry(
entry /*: {readonly uuid: string, readonly comment?: ?string, readonly fields: {readonly [string]: string}, ...} */,
) /*: string */ {
let out = `\t\t${entry.uuid}${uuidComment(entry.comment)} = {`;
const fieldKeys = Object.keys(entry.fields);
if (
fieldKeys.length <= 3 &&
!fieldKeys.some(k => entry.fields[k].includes('\n'))
) {
// Single-line format for short entries
out += fieldKeys.map(k => `${k} = ${entry.fields[k]};`).join(' ');
out += '};';
} else {
out += '\n';
for (const key of fieldKeys) {
out += `\t\t\t${key} = ${entry.fields[key]};\n`;
}
out += '\t\t};';
}
return out;
}
// ---------------------------------------------------------------------------
// Surgical in-place pbxproj editing.
//
// To ADD SPM packages to a user's EXISTING project.pbxproj we splice new
// objects and array members into the existing text by string anchors, leaving
// every untouched byte identical (so the git diff is just the added lines).
// These helpers operate on the raw OpenStep text — there is no AST. Quote-aware
// delimiter matching lets them skip over field values (e.g. a shellScript
// containing braces/parens) without miscounting.
// ---------------------------------------------------------------------------
/**
* Derive a deterministic UUID for an injected object, namespaced by the host
* project's root-object UUID so it is (a) stable across re-runs (idempotency)
* and (b) astronomically unlikely to collide with the user's existing
* randomly-assigned 24-hex IDs. `salt` lets the caller re-derive on the
* ~1-in-2^96 collision.
*/
function namespacedUUID(
rootUUID /*: string */,
section /*: string */,
id /*: string */,
salt /*: string */ = '',
) /*: string */ {
return generateUUID(`${rootUUID}:spm${salt}:${section}:${id}`);
}
function escapeRegExp(s /*: string */) /*: string */ {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Given an index pointing at an opening `"`, return the index of the matching
* closing `"` (honoring backslash escapes).
*/
function scanString(text /*: string */, openIdx /*: number */) /*: number */ {
for (let i = openIdx + 1; i < text.length; i++) {
const c = text[i];
if (c === '\\') {
i++;
continue;
}
if (c === '"') {
return i;
}
}
throw new Error('pbxproj: unterminated string literal');
}
/**
* Given an index pointing at an opening `{` or `(`, return the index of the
* matching close delimiter. Nesting counts both brace and paren forms; quoted
* strings are skipped. Well-formed OpenStep never mismatches the two forms.
*/
function scanToClose(text /*: string */, openIdx /*: number */) /*: number */ {
let depth = 0;
for (let i = openIdx; i < text.length; i++) {
const c = text[i];
if (c === '"') {
i = scanString(text, i);
continue;
}
if (c === '{' || c === '(') {
depth++;
} else if (c === '}' || c === ')') {
depth--;
if (depth === 0) {
return i;
}
}
}
throw new Error('pbxproj: unbalanced delimiters');
}
/*::
type ObjectRange = {uuid: string, bodyOpen: number, bodyClose: number};
// Any object whose body range is known — field accessors only need the body
// bounds, so they accept the richer shapes callers carry (e.g. app targets
// with a name, or buildSettings dicts) inexactly.
type BodyRange = {bodyOpen: number, bodyClose: number, ...};
type FieldRange = {matchStart: number, valueStart: number, value: string, tokenEnd: number};
*/
/**
* Locate the object with the given 24-hex UUID. Returns the index of the body
* `{` and its matching `}`. Matches both single-line and multi-line entries.
*/
function findObjectByUuid(
text /*: string */,
uuid /*: string */,
) /*: ObjectRange | null */ {
const m = new RegExp(`\\n\\t*${uuid}\\b[^\\n]*?= \\{`).exec(text);
if (m == null) {
return null;
}
const bodyOpen = text.indexOf('{', m.index);
const bodyClose = scanToClose(text, bodyOpen);
return {uuid, bodyOpen, bodyClose};
}
/**
* Find a field within a multi-line object body (`\n\t+key = value;`). Returns
* the value token range (value excludes the trailing `;`; `tokenEnd` points AT
* the `;`). Containers (`( … )` / `{ … }`) and quoted strings are matched as a
* whole. Returns null when the key is absent.
*/
function findField(
text /*: string */,
obj /*: BodyRange */,
key /*: string */,
) /*: FieldRange | null */ {
const body = text.slice(obj.bodyOpen, obj.bodyClose);
const m = new RegExp(`\\n\\t+${escapeRegExp(key)} = `).exec(body);
if (m == null) {
return null;
}
const matchStart = obj.bodyOpen + m.index;
const valueStart = matchStart + m[0].length;
const fc = text[valueStart];
let tokenEnd;
if (fc === '(' || fc === '{') {
tokenEnd = scanToClose(text, valueStart) + 1;
} else if (fc === '"') {
tokenEnd = scanString(text, valueStart) + 1;
} else {
tokenEnd = text.indexOf(';', valueStart);
}
return {
matchStart,
valueStart,
value: text.slice(valueStart, tokenEnd),
tokenEnd,
};
}
/** Locate the `/* Begin X section */ … /* End X section */` byte range. */
function findSection(
text /*: string */,
name /*: string */,
) /*: {begin: number, contentStart: number, end: number} | null */ {
const beginTag = `/* Begin ${name} section */`;
const endTag = `/* End ${name} section */`;
const begin = text.indexOf(beginTag);
const end = text.indexOf(endTag);
if (begin < 0 || end < 0) {
return null;
}
return {begin, contentStart: begin + beginTag.length, end};
}
/** The PBXProject root object (via the trailing `rootObject = <uuid>;`). */
function findProjectObject(text /*: string */) /*: ObjectRange | null */ {
const m = /\n\trootObject = ([0-9A-Fa-f]{24})/.exec(text);
if (m == null) {
return null;
}
return findObjectByUuid(text, m[1]);
}
/** Visit every top-level object of a section, in file order. */
function forEachObjectInSection(
text /*: string */,
name /*: string */,
fn /*: (obj: {uuid: string, comment: ?string, bodyOpen: number, bodyClose: number}) => void */,
) /*: void */ {
const section = findSection(text, name);
if (section == null) {
return;
}
const re = /\n\t\t([0-9A-Fa-f]{24})(?: \/\* (.*?) \*\/)? = \{/g;
re.lastIndex = section.contentStart;
for (;;) {
const m = re.exec(text);
if (m == null || m.index >= section.end) {
break;
}
const bodyOpen = text.indexOf('{', m.index);
const bodyClose = scanToClose(text, bodyOpen);
fn({uuid: m[1], comment: m[2], bodyOpen, bodyClose});
re.lastIndex = bodyClose;
}
}
/**
* Every PBXNativeTarget whose productType is an application. Returns uuid +
* name + body range for each. Used to pick the app target to inject into
* (and to refuse on ambiguity).
*/
function findApplicationTargets(
text /*: string */,
) /*: Array<{uuid: string, name: string, bodyOpen: number, bodyClose: number}> */ {
const out = [];
forEachObjectInSection(
text,
'PBXNativeTarget',
({uuid, comment, bodyOpen, bodyClose}) => {
const obj = {uuid, bodyOpen, bodyClose};
const productType = findField(text, obj, 'productType');
if (
productType == null ||
!/com\.apple\.product-type\.application/.test(productType.value)
) {
return;
}
const nameField = findField(text, obj, 'name');
const name =
nameField != null
? nameField.value.replace(/^"|"$/g, '')
: (comment ?? uuid);
out.push({uuid, name, bodyOpen, bodyClose});
},
);
return out;
}
/** UUIDs already referenced inside a `( … )` array field value. */
function uuidsInArray(value /*: string */) /*: Set<string> */ {
const found = new Set /*:: <string> */();
const re = /\b([0-9A-Fa-f]{24})\b/g;
for (;;) {
const m = re.exec(value);
if (m == null) {
break;
}
found.add(m[1]);
}
return found;
}
/**
* The leading-tab indent of fields inside an object body (e.g. `\t\t\t` for a
* top-level object, `\t\t\t\t` for a nested dict like buildSettings). Used so
* inserted fields/members match the surrounding depth at any nesting level.
*/
function detectFieldIndent(
text /*: string */,
obj /*: BodyRange */,
) /*: string */ {
const m = /\n(\t+)\S/.exec(text.slice(obj.bodyOpen, obj.bodyClose));
return m != null ? m[1] : '\t\t\t';
}
/**
* Insert one or more already-serialized object entries (text produced by
* serializeEntry, no surrounding newlines) into the named section — created
* just before the close of the `objects` dict if the section is absent.
*/
function insertObjectsIntoSection(
text /*: string */,
sectionName /*: string */,
entriesText /*: string */,
) /*: string */ {
const section = findSection(text, sectionName);
if (section != null) {
return (
text.slice(0, section.end) + entriesText + '\n' + text.slice(section.end)
);
}
// No such section yet — create it just before the `objects` dict closes.
const anchor = '\n\t};\n\trootObject = ';
const at = text.indexOf(anchor);
if (at < 0) {
throw new Error('pbxproj: could not find end of objects dict');
}
const block =
`/* Begin ${sectionName} section */\n${entriesText}\n` +
`/* End ${sectionName} section */\n\n`;
return text.slice(0, at + 1) + block + text.slice(at + 1);
}
/**
* Append members to a `( … )` array field, deduping by UUID. Creates the field
* (with a `$(inherited)`-free literal list) after the object's opening `{` when
* absent. `members` are `{uuid, comment}`. Indentation is derived from the
* object so it works for top-level fields and nested dicts alike.
*/
function addArrayMembers(
text /*: string */,
obj /*: BodyRange */,
key /*: string */,
members /*: ReadonlyArray<{readonly uuid: string, readonly comment?: ?string, ...}> */,
options /*: {prepend?: boolean} */ = {},
) /*: string */ {
const fieldIndent = detectFieldIndent(text, obj);
const memberIndent = fieldIndent + '\t';
const line = (
m /*: {readonly uuid: string, readonly comment?: ?string, ...} */,
) => `${memberIndent}${m.uuid}${uuidComment(m.comment)},\n`;
const field = findField(text, obj, key);
if (field != null) {
const existing = uuidsInArray(field.value);
const fresh = members.filter(m => !existing.has(m.uuid));
if (fresh.length === 0) {
return text;
}
// Prepend: insert right after the array's opening `(\n` so the new members
// run first (used for the sync phase, which must precede Sources).
const insertAt =
options.prepend === true
? text.indexOf('\n', field.valueStart) + 1
: text.lastIndexOf('\n', field.tokenEnd - 1) + 1;
return (
text.slice(0, insertAt) + fresh.map(line).join('') + text.slice(insertAt)
);
}
const block = `\n${fieldIndent}${key} = (\n${members.map(line).join('')}${fieldIndent});`;
return text.slice(0, obj.bodyOpen + 1) + block + text.slice(obj.bodyOpen + 1);
}
/**
* Indices of the `,` separators at an array's top level — a comma inside a
* quoted member (`"$(FOO),weird"`) separates nothing.
*/
function topLevelCommas(inner /*: string */) /*: Array<number> */ {
const out = [];
for (let i = 0; i < inner.length; i++) {
if (inner[i] === '"') {
i = scanString(inner, i);
} else if (inner[i] === ',') {
out.push(i);
}
}
return out;
}
/**
* The members of an array's inner text (what sits between its parens), trimmed
* and with empty slots — e.g. the one a trailing comma leaves — dropped. A bare
* scalar value parses as its own single member.
*/
function arrayMembers(inner /*: string */) /*: Array<string> */ {
const members = [];
let start = 0;
for (const comma of topLevelCommas(inner)) {
members.push(inner.slice(start, comma));
start = comma + 1;
}
members.push(inner.slice(start));
return members.map(m => m.trim()).filter(m => m !== '');
}
/**
* Append raw string values to a `( … )` array build-setting (e.g.
* OTHER_LDFLAGS), deduping by exact token. Creates the setting seeded with
* `"$(inherited)"` when absent. Values must already be plist-quoted by caller.
*/
function addArrayStringValues(
text /*: string */,
obj /*: BodyRange */,
key /*: string */,
values /*: Array<string> */,
) /*: string */ {
const fieldIndent = detectFieldIndent(text, obj);
const memberIndent = fieldIndent + '\t';
const arrayBlock = (members /*: Array<string> */) =>
`(\n${members.map(v => `${memberIndent}${v},\n`).join('')}${fieldIndent})`;
const field = findField(text, obj, key);
if (field != null) {
const isArray = field.value.trimStart().startsWith('(');
const openParen = isArray
? field.valueStart + field.value.indexOf('(')
: -1;
const closeParen = isArray ? scanToClose(text, openParen) : -1;
const inner = isArray ? text.slice(openParen + 1, closeParen) : field.value;
// Dedup by EXACT existing member, not substring — a substring check would
// treat `"-ObjC"` as already present when only `"-ObjCFoo"` is there (and
// vice-versa).
const existingMembers = new Set(arrayMembers(inner));
const fresh = values.filter(v => !existingMembers.has(v));
if (fresh.length === 0) {
return text;
}
if (isArray) {
if (inner.includes('\n')) {
// Multi-line array — one member per line, before the closing `)`.
const lineStart = text.lastIndexOf('\n', closeParen) + 1;
const lines = fresh.map(v => `${memberIndent}${v},\n`).join('');
return text.slice(0, lineStart) + lines + text.slice(lineStart);
}
// One-line array (hand-edited projects, XcodeGen, Tuist) — splice the
// members in ahead of the `)`, in the separator style already there.
// Reformatting it multi-line instead would have to record the old shape
// to stay reversible on deinit.
const commas = topLevelCommas(inner);
const gapMatch =
commas.length > 0 ? /^[\t ]*/.exec(inner.slice(commas[0] + 1)) : null;
const gap = gapMatch != null ? gapMatch[0] : ' ';
const core = inner.replace(/[\t ]+$/, '');
const joined = fresh.join(`,${gap}`);
const insertion =
core === ''
? joined
: core.endsWith(',')
? `${gap}${joined},`
: `,${gap}${joined}`;
const at = openParen + 1 + core.length;
return text.slice(0, at) + insertion + text.slice(at);
}
// Existing scalar — promote to an array preserving the prior value. Skip it
// when it IS the `"$(inherited)"` the array is seeded with (emitted twice),
// or when it is empty (a bare `,` is not a valid plist element). The seed
// test unquotes first: pbxproj accepts `$(inherited)` bare, and Xcode's own
// quoted form is the same value to the build system.
const prior = field.value.trim();
const carriesPrior =
prior !== '' && prior.replace(/^"(.*)"$/s, '$1') !== '$(inherited)';
const replacement = arrayBlock([
'"$(inherited)"',
...(carriesPrior ? [prior] : []),
...fresh,
]);
return (
text.slice(0, field.valueStart) + replacement + text.slice(field.tokenEnd)
);
}
const block = `\n${fieldIndent}${key} = ${arrayBlock(['"$(inherited)"', ...values])};`;
return text.slice(0, obj.bodyOpen + 1) + block + text.slice(obj.bodyOpen + 1);
}
/**
* Add a scalar field after the object's `{` only when ABSENT (never clobbers a
* value the user already set). Returns text unchanged if the key exists.
*/
function ensureScalarField(
text /*: string */,
obj /*: BodyRange */,
key /*: string */,
value /*: string */,
) /*: string */ {
if (findField(text, obj, key) != null) {
return text;
}
const fieldIndent = detectFieldIndent(text, obj);
const block = `\n${fieldIndent}${key} = ${value};`;
return text.slice(0, obj.bodyOpen + 1) + block + text.slice(obj.bodyOpen + 1);
}
/**
* Set a scalar field's value, UNLIKE ensureScalarField this overwrites an
* existing value in place rather than leaving it alone — used by fields the
* injector itself owns (e.g. the generated `shellScript`) that must be kept
* in sync on re-injection. When the field is present, only the value token
* (`findField`'s `valueStart..tokenEnd` range — the trailing `;` is NOT part
* of that range and is preserved untouched) is replaced in place, so field
* order never shifts and passing the same `value` again yields
* byte-identical output. Falls back to `ensureScalarField`'s append-after-`{`
* behavior when the field is absent.
*/
function setScalarField(
text /*: string */,
obj /*: BodyRange */,
key /*: string */,
value /*: string */,
) /*: string */ {
const field = findField(text, obj, key);
if (field != null) {
return text.slice(0, field.valueStart) + value + text.slice(field.tokenEnd);
}
return ensureScalarField(text, obj, key, value);
}
// ---------------------------------------------------------------------------
// Surgical removal — the inverse of the additive helpers above. `deinit` uses
// these to undo exactly what injection added, leaving every other byte (incl.
// user edits made after injection) untouched. The exception is a scalar that
// injection promoted to an array: reversing that rewrites the whole field (see
// removeRecordedBuildSettings), so members added to it afterwards are lost.
// That is not deinit-only — every re-sync reverts from the recorded baseline
// before re-injecting, so an `spm update` discards them just the same.
// All are pure string transforms.
// ---------------------------------------------------------------------------
/**
* Remove the object whose UUID is `uuid` (its whole `\t\t<uuid> … = { … };`
* entry, single- or multi-line). No-op when the object is absent.
*/
function removeObjectByUuid(
text /*: string */,
uuid /*: string */,
) /*: string */ {
const obj = findObjectByUuid(text, uuid);
if (obj == null) {
return text;
}
// Start at the newline preceding the entry's line; end just past its `;`.
// Leaving the trailing newline in place preserves it as the next entry's
// separator (byte-identical to never having inserted the line).
const start = text.lastIndexOf('\n', obj.bodyOpen);
let end = obj.bodyClose + 1; // past `}`
if (text[end] === ';') {
end++;
}
return text.slice(0, start) + text.slice(end);
}
/**
* Remove array-member lines (`\n\t+<uuid> /* … */,`) referencing any of
* `uuids` from every `( … )` list in the file (packageReferences,
* packageProductDependencies, a Frameworks phase's `files`, buildPhases, …).
* Only matches member lines (trailing comma), never the object-definition line
* (which ends in `= {`), so it composes safely with removeObjectByUuid — which
* holds because no comment can contain a comma (see commentSafe).
*/
function removeArrayMembersByUuid(
text /*: string */,
uuids /*: ReadonlyArray<string> */,
) /*: string */ {
let out = text;
for (const uuid of uuids) {
out = out.replace(
new RegExp(`\\n[\\t ]*${escapeRegExp(uuid)}\\b[^\\n]*,`, 'g'),
'',
);
}
return out;
}
/** Remove a whole `\n\t+key = value;` field from `obj`. No-op when absent. */
function removeField(
text /*: string */,
obj /*: BodyRange */,
key /*: string */,
) /*: string */ {
const f = findField(text, obj, key);
if (f == null) {
return text;
}
// f.matchStart points at the leading `\n`; f.tokenEnd points AT the `;`.
return text.slice(0, f.matchStart) + text.slice(f.tokenEnd + 1);
}
/**
* Drop one member from an array field's value text. The patterns mirror the
* forms addArrayStringValues inserts, tried in the order that makes the span
* removed exactly the span it added: a line of its own (multi-line array), then
* after a comma, before a comma, or alone ahead of the `)` (the one-line
* shapes). Each is anchored on a delimiter, so a value that is merely a prefix
* of a longer member is never mistaken for it.
*/
function removeArrayMember(
region /*: string */,
value /*: string */,
) /*: string */ {
const v = escapeRegExp(value);
for (const pattern of [
`\\n[\\t ]*${v},`,
`,[\\t ]*${v}(?=[\\t ]*[,)])`,
`[\\t ]*${v},[\\t ]*`,
`[\\t ]*${v}(?=[\\t ]*\\))`,
]) {
const shorter = region.replace(new RegExp(pattern), '');
if (shorter !== region) {
return shorter;
}
}
return region;
}
/**
* Remove specific raw string members from an existing `( … )` array field
* (inverse of addArrayStringValues' append branch). Leaves the field and any
* other members in place. No-op when the field or a value is absent.
*/
function removeArrayStringValues(
text /*: string */,
obj /*: BodyRange */,
key /*: string */,
values /*: ReadonlyArray<string> */,
) /*: string */ {
const f = findField(text, obj, key);
if (f == null) {
return text;
}
let region = text.slice(f.valueStart, f.tokenEnd);
for (const val of values) {
region = removeArrayMember(region, val);
}
return text.slice(0, f.valueStart) + region + text.slice(f.tokenEnd);
}
/**
* Remove the empty `Pods` PBXGroup that `pod deintegrate` can leave behind in
* the navigator (build integration is already gone — xcconfigs/[CP] phases/
* linking — but the group lingers). Removes the group object AND its membership
* in any parent group. Only acts when the group is EMPTY (`children = ()`), so a
* still-integrated project (non-empty Pods group) is never touched. No-op when
* absent. PBXGroup bodies contain no nested braces, so `[^{}]` body matching is
* safe.
*/
function removeEmptyPodsGroup(text /*: string */) /*: string */ {
const m = /\n[\t ]*([0-9A-Fa-f]{24}) \/\* Pods \*\/ = \{[^{}]*?\};/.exec(
text,
);
if (m == null) {
return text;
}
const block = m[0];
if (
!/isa = PBXGroup;/.test(block) ||
!/children = \(\s*\);/.test(block) ||
!/\b(?:path|name) = Pods;/.test(block)
) {
return text;
}
const uuid = m[1];
// Drop the parent group's child reference first, then the group object.
return removeObjectByUuid(removeArrayMembersByUuid(text, [uuid]), uuid);
}
/**
* Remove the dangling `JavaScriptCore.framework` PBXFileReference the
* community template has carried since RN 0.60 (an SDK framework reference
* left over from the pre-Hermes JSC-via-CocoaPods era). It's navigator-only —
* never wired into any PBXBuildFile/build phase — so it's meaningless (and
* confusing) now that React Native uses Hermes. Only removes references that
* are truly unlinked: if a PBXBuildFile still references the UUID (e.g. an
* app that deliberately links JSC, such as via react-native-javascriptcore),
* that reference is left completely untouched. Removes the file-reference
* object AND its membership in any parent group. No-op when absent. Like
* `removeEmptyPodsGroup`, `deinit` does not restore this removal — git is the
* safety net.
*/
function removeDanglingJavaScriptCoreRef(text /*: string */) /*: string */ {
const re =
/\n[\t ]*([0-9A-Fa-f]{24}) \/\* JavaScriptCore\.framework \*\/ = \{[^{}]*?\};/g;
const uuidsToRemove = [];
for (const m of text.matchAll(re)) {
const block = m[0];
if (
!/isa = PBXFileReference;/.test(block) ||
!/path = System\/Library\/Frameworks\/JavaScriptCore\.framework;/.test(
block,
) ||
!/sourceTree = SDKROOT;/.test(block)
) {
continue;
}
const uuid = m[1];
const linked = new RegExp(`\\bfileRef = ${escapeRegExp(uuid)}\\b`).test(
text,
);
if (!linked) {
uuidsToRemove.push(uuid);
}
}
if (uuidsToRemove.length === 0) {
return text;
}
// Drop the parent group's child reference(s) first, then the file objects.
let out = removeArrayMembersByUuid(text, uuidsToRemove);
for (const uuid of uuidsToRemove) {
out = removeObjectByUuid(out, uuid);
}
return out;
}
module.exports = {
generateUUID,
namespacedUUID,
serializeEntry,
quoteIfNeeded,
commentSafe,
uuidComment,
// Surgical-edit toolkit (in-place injection):
scanString,
scanToClose,
findObjectByUuid,
findField,
findSection,
findProjectObject,
findApplicationTargets,
forEachObjectInSection,
uuidsInArray,
detectFieldIndent,
insertObjectsIntoSection,
addArrayMembers,
addArrayStringValues,
ensureScalarField,
setScalarField,
escapeRegExp,
// Surgical removal (deinit):
removeObjectByUuid,
removeArrayMembersByUuid,
removeField,
removeArrayStringValues,
removeEmptyPodsGroup,
removeDanglingJavaScriptCoreRef,
};