react-native 0.87.0-rc.1 → 0.87.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/Libraries/Core/InitializeCore.js +8 -1
  2. package/Libraries/Core/ReactNativeVersion.js +1 -1
  3. package/Libraries/ReactNative/AppRegistry.flow.js +1 -0
  4. package/Libraries/ReactNative/AppRegistryImpl.js +2 -3
  5. package/React/Base/RCTVersion.m +1 -1
  6. package/React/I18n/RCTLocalizedString.mm +38 -2
  7. package/React-Core-prebuilt.podspec +45 -17
  8. package/React-Core.podspec +9 -2
  9. package/ReactAndroid/external-artifacts/build.gradle.kts +49 -0
  10. package/ReactAndroid/gradle.properties +1 -1
  11. package/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SynchronousMountItem.kt +8 -2
  12. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  13. package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
  14. package/package.json +10 -8
  15. package/react-native.config.js +81 -0
  16. package/scripts/cocoapods/fabric.rb +1 -1
  17. package/scripts/cocoapods/rncore.rb +35 -78
  18. package/scripts/cocoapods/rncore_facades.rb +232 -0
  19. package/scripts/cocoapods/rndependencies.rb +64 -3
  20. package/scripts/cocoapods/rndeps_facades.rb +193 -0
  21. package/scripts/cocoapods/spm.rb +78 -11
  22. package/scripts/codegen/templates/Package.swift.spm-template +97 -0
  23. package/scripts/react-native-xcode.sh +20 -0
  24. package/scripts/react_native_pods.rb +65 -16
  25. package/scripts/replace-rncore-version.js +53 -6
  26. package/scripts/setup-apple-spm.js +1165 -0
  27. package/scripts/spm/__doc__/rfc-spm-xcframework.md +707 -0
  28. package/scripts/spm/__doc__/spm-autolinking-plugins.md +244 -0
  29. package/scripts/spm/__doc__/spm-header-paths-contract.md +97 -0
  30. package/scripts/spm/__doc__/spm-plugins-assessment.md +128 -0
  31. package/scripts/spm/__doc__/spm-scripts.md +451 -0
  32. package/scripts/spm/autolinking-plugins.js +331 -0
  33. package/scripts/spm/download-spm-artifacts.js +1409 -0
  34. package/scripts/spm/expand-spm-dependencies.js +216 -0
  35. package/scripts/spm/flavored-frameworks.js +1008 -0
  36. package/scripts/spm/generate-spm-autolinking-config.js +161 -0
  37. package/scripts/spm/generate-spm-autolinking.js +1888 -0
  38. package/scripts/spm/generate-spm-package.js +302 -0
  39. package/scripts/spm/generate-spm-xcodeproj.js +2224 -0
  40. package/scripts/spm/read-podspec.js +695 -0
  41. package/scripts/spm/scaffold-package-swift.js +1206 -0
  42. package/scripts/spm/spm-pbxproj.js +654 -0
  43. package/scripts/spm/spm-types.js +517 -0
  44. package/scripts/spm/spm-utils.js +645 -0
  45. package/scripts/spm/sync-spm-autolinking.js +160 -0
  46. package/sdks/.hermesv1version +1 -0
  47. package/sdks/hermes-engine/utils/replace_hermes_version.js +18 -4
  48. package/sdks/hermes-engine/version.properties +1 -1
  49. package/third-party-podspecs/ReactNativeDependencies.podspec +2 -2
  50. package/types_generated/Libraries/ReactNative/AppRegistry.flow.d.ts +2 -2
  51. package/Libraries/Utilities/SceneTracker.js +0 -42
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const crypto = require('crypto');
14
+
15
+ /**
16
+ * Generate a deterministic 24-hex-character UUID from a seed string.
17
+ * SHA-256 truncated to 24 chars (standard Xcode pbxproj UUID length). Not a
18
+ * security use — the hash only provides stable, collision-unlikely IDs — but
19
+ * sha256 keeps static analysis (CodeQL weak-crypto) quiet.
20
+ */
21
+ function generateUUID(seed /*: string */) /*: string */ {
22
+ return crypto
23
+ .createHash('sha256')
24
+ .update(seed)
25
+ .digest('hex')
26
+ .substring(0, 24)
27
+ .toUpperCase();
28
+ }
29
+
30
+ /**
31
+ * Escapes a string for OpenStep plist format if needed.
32
+ */
33
+ function quoteIfNeeded(s /*: string */) /*: string */ {
34
+ if (/^[a-zA-Z0-9._/]+$/.test(s)) {
35
+ return s;
36
+ }
37
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
38
+ }
39
+
40
+ /**
41
+ * Serialize a single pbxproj object entry to its OpenStep text form,
42
+ * including the leading `\t\t<uuid>` and trailing `};` but NO trailing
43
+ * newline. Short entries (≤3 scalar fields) collapse to one line, matching
44
+ * Xcode's own formatting. Used by the in-place injector to splice single
45
+ * entries into an existing project.
46
+ */
47
+ function serializeEntry(
48
+ entry /*: {readonly uuid: string, readonly comment?: ?string, readonly fields: {readonly [string]: string}, ...} */,
49
+ ) /*: string */ {
50
+ const comment =
51
+ entry.comment != null && entry.comment !== ''
52
+ ? ` /* ${entry.comment} */`
53
+ : '';
54
+ let out = `\t\t${entry.uuid}${comment} = {`;
55
+ const fieldKeys = Object.keys(entry.fields);
56
+ if (
57
+ fieldKeys.length <= 3 &&
58
+ !fieldKeys.some(k => entry.fields[k].includes('\n'))
59
+ ) {
60
+ // Single-line format for short entries
61
+ out += fieldKeys.map(k => `${k} = ${entry.fields[k]};`).join(' ');
62
+ out += '};';
63
+ } else {
64
+ out += '\n';
65
+ for (const key of fieldKeys) {
66
+ out += `\t\t\t${key} = ${entry.fields[key]};\n`;
67
+ }
68
+ out += '\t\t};';
69
+ }
70
+ return out;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Surgical in-place pbxproj editing.
75
+ //
76
+ // To ADD SPM packages to a user's EXISTING project.pbxproj we splice new
77
+ // objects and array members into the existing text by string anchors, leaving
78
+ // every untouched byte identical (so the git diff is just the added lines).
79
+ // These helpers operate on the raw OpenStep text — there is no AST. Quote-aware
80
+ // delimiter matching lets them skip over field values (e.g. a shellScript
81
+ // containing braces/parens) without miscounting.
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Derive a deterministic UUID for an injected object, namespaced by the host
86
+ * project's root-object UUID so it is (a) stable across re-runs (idempotency)
87
+ * and (b) astronomically unlikely to collide with the user's existing
88
+ * randomly-assigned 24-hex IDs. `salt` lets the caller re-derive on the
89
+ * ~1-in-2^96 collision.
90
+ */
91
+ function namespacedUUID(
92
+ rootUUID /*: string */,
93
+ section /*: string */,
94
+ id /*: string */,
95
+ salt /*: string */ = '',
96
+ ) /*: string */ {
97
+ return generateUUID(`${rootUUID}:spm${salt}:${section}:${id}`);
98
+ }
99
+
100
+ function escapeRegExp(s /*: string */) /*: string */ {
101
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
102
+ }
103
+
104
+ /**
105
+ * Given an index pointing at an opening `"`, return the index of the matching
106
+ * closing `"` (honoring backslash escapes).
107
+ */
108
+ function scanString(text /*: string */, openIdx /*: number */) /*: number */ {
109
+ for (let i = openIdx + 1; i < text.length; i++) {
110
+ const c = text[i];
111
+ if (c === '\\') {
112
+ i++;
113
+ continue;
114
+ }
115
+ if (c === '"') {
116
+ return i;
117
+ }
118
+ }
119
+ throw new Error('pbxproj: unterminated string literal');
120
+ }
121
+
122
+ /**
123
+ * Given an index pointing at an opening `{` or `(`, return the index of the
124
+ * matching close delimiter. Nesting counts both brace and paren forms; quoted
125
+ * strings are skipped. Well-formed OpenStep never mismatches the two forms.
126
+ */
127
+ function scanToClose(text /*: string */, openIdx /*: number */) /*: number */ {
128
+ let depth = 0;
129
+ for (let i = openIdx; i < text.length; i++) {
130
+ const c = text[i];
131
+ if (c === '"') {
132
+ i = scanString(text, i);
133
+ continue;
134
+ }
135
+ if (c === '{' || c === '(') {
136
+ depth++;
137
+ } else if (c === '}' || c === ')') {
138
+ depth--;
139
+ if (depth === 0) {
140
+ return i;
141
+ }
142
+ }
143
+ }
144
+ throw new Error('pbxproj: unbalanced delimiters');
145
+ }
146
+
147
+ /*::
148
+ type ObjectRange = {uuid: string, bodyOpen: number, bodyClose: number};
149
+ // Any object whose body range is known — field accessors only need the body
150
+ // bounds, so they accept the richer shapes callers carry (e.g. app targets
151
+ // with a name, or buildSettings dicts) inexactly.
152
+ type BodyRange = {bodyOpen: number, bodyClose: number, ...};
153
+ type FieldRange = {matchStart: number, valueStart: number, value: string, tokenEnd: number};
154
+ */
155
+
156
+ /**
157
+ * Locate the object with the given 24-hex UUID. Returns the index of the body
158
+ * `{` and its matching `}`. Matches both single-line and multi-line entries.
159
+ */
160
+ function findObjectByUuid(
161
+ text /*: string */,
162
+ uuid /*: string */,
163
+ ) /*: ObjectRange | null */ {
164
+ const m = new RegExp(`\\n\\t*${uuid}\\b[^\\n]*?= \\{`).exec(text);
165
+ if (m == null) {
166
+ return null;
167
+ }
168
+ const bodyOpen = text.indexOf('{', m.index);
169
+ const bodyClose = scanToClose(text, bodyOpen);
170
+ return {uuid, bodyOpen, bodyClose};
171
+ }
172
+
173
+ /**
174
+ * Find a field within a multi-line object body (`\n\t+key = value;`). Returns
175
+ * the value token range (value excludes the trailing `;`; `tokenEnd` points AT
176
+ * the `;`). Containers (`( … )` / `{ … }`) and quoted strings are matched as a
177
+ * whole. Returns null when the key is absent.
178
+ */
179
+ function findField(
180
+ text /*: string */,
181
+ obj /*: BodyRange */,
182
+ key /*: string */,
183
+ ) /*: FieldRange | null */ {
184
+ const body = text.slice(obj.bodyOpen, obj.bodyClose);
185
+ const m = new RegExp(`\\n\\t+${escapeRegExp(key)} = `).exec(body);
186
+ if (m == null) {
187
+ return null;
188
+ }
189
+ const matchStart = obj.bodyOpen + m.index;
190
+ const valueStart = matchStart + m[0].length;
191
+ const fc = text[valueStart];
192
+ let tokenEnd;
193
+ if (fc === '(' || fc === '{') {
194
+ tokenEnd = scanToClose(text, valueStart) + 1;
195
+ } else if (fc === '"') {
196
+ tokenEnd = scanString(text, valueStart) + 1;
197
+ } else {
198
+ tokenEnd = text.indexOf(';', valueStart);
199
+ }
200
+ return {
201
+ matchStart,
202
+ valueStart,
203
+ value: text.slice(valueStart, tokenEnd),
204
+ tokenEnd,
205
+ };
206
+ }
207
+
208
+ /** Locate the `/* Begin X section *​/ … /* End X section *​/` byte range. */
209
+ function findSection(
210
+ text /*: string */,
211
+ name /*: string */,
212
+ ) /*: {begin: number, contentStart: number, end: number} | null */ {
213
+ const beginTag = `/* Begin ${name} section */`;
214
+ const endTag = `/* End ${name} section */`;
215
+ const begin = text.indexOf(beginTag);
216
+ const end = text.indexOf(endTag);
217
+ if (begin < 0 || end < 0) {
218
+ return null;
219
+ }
220
+ return {begin, contentStart: begin + beginTag.length, end};
221
+ }
222
+
223
+ /** The PBXProject root object (via the trailing `rootObject = <uuid>;`). */
224
+ function findProjectObject(text /*: string */) /*: ObjectRange | null */ {
225
+ const m = /\n\trootObject = ([0-9A-Fa-f]{24})/.exec(text);
226
+ if (m == null) {
227
+ return null;
228
+ }
229
+ return findObjectByUuid(text, m[1]);
230
+ }
231
+
232
+ /**
233
+ * Every PBXNativeTarget whose productType is an application. Returns uuid +
234
+ * name + body range for each. Used to pick the app target to inject into
235
+ * (and to refuse on ambiguity).
236
+ */
237
+ function findApplicationTargets(
238
+ text /*: string */,
239
+ ) /*: Array<{uuid: string, name: string, bodyOpen: number, bodyClose: number}> */ {
240
+ const section = findSection(text, 'PBXNativeTarget');
241
+ if (section == null) {
242
+ return [];
243
+ }
244
+ const out = [];
245
+ const re = /\n\t\t([0-9A-Fa-f]{24})(?: \/\* (.*?) \*\/)? = \{/g;
246
+ re.lastIndex = section.contentStart;
247
+ for (;;) {
248
+ const m = re.exec(text);
249
+ if (m == null || m.index >= section.end) {
250
+ break;
251
+ }
252
+ const uuid = m[1];
253
+ const comment = m[2];
254
+ const bodyOpen = text.indexOf('{', m.index);
255
+ const bodyClose = scanToClose(text, bodyOpen);
256
+ const obj = {uuid, bodyOpen, bodyClose};
257
+ const productType = findField(text, obj, 'productType');
258
+ if (
259
+ productType != null &&
260
+ /com\.apple\.product-type\.application/.test(productType.value)
261
+ ) {
262
+ const nameField = findField(text, obj, 'name');
263
+ const name =
264
+ nameField != null
265
+ ? nameField.value.replace(/^"|"$/g, '')
266
+ : (comment ?? uuid);
267
+ out.push({uuid, name, bodyOpen, bodyClose});
268
+ }
269
+ re.lastIndex = bodyClose;
270
+ }
271
+ return out;
272
+ }
273
+
274
+ /** UUIDs already referenced inside a `( … )` array field value. */
275
+ function uuidsInArray(value /*: string */) /*: Set<string> */ {
276
+ const found = new Set /*:: <string> */();
277
+ const re = /\b([0-9A-Fa-f]{24})\b/g;
278
+ for (;;) {
279
+ const m = re.exec(value);
280
+ if (m == null) {
281
+ break;
282
+ }
283
+ found.add(m[1]);
284
+ }
285
+ return found;
286
+ }
287
+
288
+ /**
289
+ * The leading-tab indent of fields inside an object body (e.g. `\t\t\t` for a
290
+ * top-level object, `\t\t\t\t` for a nested dict like buildSettings). Used so
291
+ * inserted fields/members match the surrounding depth at any nesting level.
292
+ */
293
+ function detectFieldIndent(
294
+ text /*: string */,
295
+ obj /*: BodyRange */,
296
+ ) /*: string */ {
297
+ const m = /\n(\t+)\S/.exec(text.slice(obj.bodyOpen, obj.bodyClose));
298
+ return m != null ? m[1] : '\t\t\t';
299
+ }
300
+
301
+ /**
302
+ * Insert one or more already-serialized object entries (text produced by
303
+ * serializeEntry, no surrounding newlines) into the named section — created
304
+ * just before the close of the `objects` dict if the section is absent.
305
+ */
306
+ function insertObjectsIntoSection(
307
+ text /*: string */,
308
+ sectionName /*: string */,
309
+ entriesText /*: string */,
310
+ ) /*: string */ {
311
+ const section = findSection(text, sectionName);
312
+ if (section != null) {
313
+ return (
314
+ text.slice(0, section.end) + entriesText + '\n' + text.slice(section.end)
315
+ );
316
+ }
317
+ // No such section yet — create it just before the `objects` dict closes.
318
+ const anchor = '\n\t};\n\trootObject = ';
319
+ const at = text.indexOf(anchor);
320
+ if (at < 0) {
321
+ throw new Error('pbxproj: could not find end of objects dict');
322
+ }
323
+ const block =
324
+ `/* Begin ${sectionName} section */\n${entriesText}\n` +
325
+ `/* End ${sectionName} section */\n\n`;
326
+ return text.slice(0, at + 1) + block + text.slice(at + 1);
327
+ }
328
+
329
+ /**
330
+ * Append members to a `( … )` array field, deduping by UUID. Creates the field
331
+ * (with a `$(inherited)`-free literal list) after the object's opening `{` when
332
+ * absent. `members` are `{uuid, comment}`. Indentation is derived from the
333
+ * object so it works for top-level fields and nested dicts alike.
334
+ */
335
+ function addArrayMembers(
336
+ text /*: string */,
337
+ obj /*: BodyRange */,
338
+ key /*: string */,
339
+ members /*: ReadonlyArray<{readonly uuid: string, readonly comment?: ?string, ...}> */,
340
+ options /*: {prepend?: boolean} */ = {},
341
+ ) /*: string */ {
342
+ const fieldIndent = detectFieldIndent(text, obj);
343
+ const memberIndent = fieldIndent + '\t';
344
+ const line = (
345
+ m /*: {readonly uuid: string, readonly comment?: ?string, ...} */,
346
+ ) =>
347
+ `${memberIndent}${m.uuid}${m.comment != null && m.comment !== '' ? ` /* ${m.comment} */` : ''},\n`;
348
+
349
+ const field = findField(text, obj, key);
350
+ if (field != null) {
351
+ const existing = uuidsInArray(field.value);
352
+ const fresh = members.filter(m => !existing.has(m.uuid));
353
+ if (fresh.length === 0) {
354
+ return text;
355
+ }
356
+ // Prepend: insert right after the array's opening `(\n` so the new members
357
+ // run first (used for the sync phase, which must precede Sources).
358
+ const insertAt =
359
+ options.prepend === true
360
+ ? text.indexOf('\n', field.valueStart) + 1
361
+ : text.lastIndexOf('\n', field.tokenEnd - 1) + 1;
362
+ return (
363
+ text.slice(0, insertAt) + fresh.map(line).join('') + text.slice(insertAt)
364
+ );
365
+ }
366
+ const block = `\n${fieldIndent}${key} = (\n${members.map(line).join('')}${fieldIndent});`;
367
+ return text.slice(0, obj.bodyOpen + 1) + block + text.slice(obj.bodyOpen + 1);
368
+ }
369
+
370
+ /**
371
+ * Append raw string values to a `( … )` array build-setting (e.g.
372
+ * OTHER_LDFLAGS), deduping by exact token. Creates the setting seeded with
373
+ * `"$(inherited)"` when absent. Values must already be plist-quoted by caller.
374
+ */
375
+ function addArrayStringValues(
376
+ text /*: string */,
377
+ obj /*: BodyRange */,
378
+ key /*: string */,
379
+ values /*: Array<string> */,
380
+ ) /*: string */ {
381
+ const fieldIndent = detectFieldIndent(text, obj);
382
+ const memberIndent = fieldIndent + '\t';
383
+ const arrayBlock = (members /*: Array<string> */) =>
384
+ `(\n${members.map(v => `${memberIndent}${v},\n`).join('')}${fieldIndent})`;
385
+
386
+ const field = findField(text, obj, key);
387
+ if (field != null) {
388
+ // Dedup by EXACT existing member, not substring — a substring check would
389
+ // treat `"-ObjC"` as already present when only `"-ObjCFoo"` is there (and
390
+ // vice-versa). Parse the current members (array `( … )` or bare scalar).
391
+ const existingMembers = new Set(
392
+ field.value
393
+ .replace(/^\s*\(/, '')
394
+ .replace(/\)\s*$/, '')
395
+ .split(',')
396
+ .map(s => s.trim())
397
+ .filter(s => s.length > 0),
398
+ );
399
+ const fresh = values.filter(v => !existingMembers.has(v));
400
+ if (fresh.length === 0) {
401
+ return text;
402
+ }
403
+ if (field.value.trimStart().startsWith('(')) {
404
+ // Existing array — splice fresh members before the closing `)`.
405
+ const lineStart = text.lastIndexOf('\n', field.tokenEnd - 1) + 1;
406
+ const lines = fresh.map(v => `${memberIndent}${v},\n`).join('');
407
+ return text.slice(0, lineStart) + lines + text.slice(lineStart);
408
+ }
409
+ // Existing scalar — promote to an array preserving the prior value.
410
+ const replacement = arrayBlock([
411
+ '"$(inherited)"',
412
+ field.value.trim(),
413
+ ...fresh,
414
+ ]);
415
+ return (
416
+ text.slice(0, field.valueStart) + replacement + text.slice(field.tokenEnd)
417
+ );
418
+ }
419
+ const block = `\n${fieldIndent}${key} = ${arrayBlock(['"$(inherited)"', ...values])};`;
420
+ return text.slice(0, obj.bodyOpen + 1) + block + text.slice(obj.bodyOpen + 1);
421
+ }
422
+
423
+ /**
424
+ * Add a scalar field after the object's `{` only when ABSENT (never clobbers a
425
+ * value the user already set). Returns text unchanged if the key exists.
426
+ */
427
+ function ensureScalarField(
428
+ text /*: string */,
429
+ obj /*: BodyRange */,
430
+ key /*: string */,
431
+ value /*: string */,
432
+ ) /*: string */ {
433
+ if (findField(text, obj, key) != null) {
434
+ return text;
435
+ }
436
+ const fieldIndent = detectFieldIndent(text, obj);
437
+ const block = `\n${fieldIndent}${key} = ${value};`;
438
+ return text.slice(0, obj.bodyOpen + 1) + block + text.slice(obj.bodyOpen + 1);
439
+ }
440
+
441
+ /**
442
+ * Set a scalar field's value, UNLIKE ensureScalarField this overwrites an
443
+ * existing value in place rather than leaving it alone — used by fields the
444
+ * injector itself owns (e.g. the generated `shellScript`) that must be kept
445
+ * in sync on re-injection. When the field is present, only the value token
446
+ * (`findField`'s `valueStart..tokenEnd` range — the trailing `;` is NOT part
447
+ * of that range and is preserved untouched) is replaced in place, so field
448
+ * order never shifts and passing the same `value` again yields
449
+ * byte-identical output. Falls back to `ensureScalarField`'s append-after-`{`
450
+ * behavior when the field is absent.
451
+ */
452
+ function setScalarField(
453
+ text /*: string */,
454
+ obj /*: BodyRange */,
455
+ key /*: string */,
456
+ value /*: string */,
457
+ ) /*: string */ {
458
+ const field = findField(text, obj, key);
459
+ if (field != null) {
460
+ return text.slice(0, field.valueStart) + value + text.slice(field.tokenEnd);
461
+ }
462
+ return ensureScalarField(text, obj, key, value);
463
+ }
464
+ // ---------------------------------------------------------------------------
465
+ // Surgical removal — the inverse of the additive helpers above. `deinit` uses
466
+ // these to undo exactly what injection added, leaving every other byte (incl.
467
+ // user edits made after injection) untouched. All are pure string transforms.
468
+ // ---------------------------------------------------------------------------
469
+
470
+ /**
471
+ * Remove the object whose UUID is `uuid` (its whole `\t\t<uuid> … = { … };`
472
+ * entry, single- or multi-line). No-op when the object is absent.
473
+ */
474
+ function removeObjectByUuid(
475
+ text /*: string */,
476
+ uuid /*: string */,
477
+ ) /*: string */ {
478
+ const obj = findObjectByUuid(text, uuid);
479
+ if (obj == null) {
480
+ return text;
481
+ }
482
+ // Start at the newline preceding the entry's line; end just past its `;`.
483
+ // Leaving the trailing newline in place preserves it as the next entry's
484
+ // separator (byte-identical to never having inserted the line).
485
+ const start = text.lastIndexOf('\n', obj.bodyOpen);
486
+ let end = obj.bodyClose + 1; // past `}`
487
+ if (text[end] === ';') {
488
+ end++;
489
+ }
490
+ return text.slice(0, start) + text.slice(end);
491
+ }
492
+
493
+ /**
494
+ * Remove array-member lines (`\n\t+<uuid> /* … *​/,`) referencing any of
495
+ * `uuids` from every `( … )` list in the file (packageReferences,
496
+ * packageProductDependencies, a Frameworks phase's `files`, buildPhases, …).
497
+ * Only matches member lines (trailing comma), never the object-definition line
498
+ * (which ends in `= {`), so it composes safely with removeObjectByUuid.
499
+ */
500
+ function removeArrayMembersByUuid(
501
+ text /*: string */,
502
+ uuids /*: ReadonlyArray<string> */,
503
+ ) /*: string */ {
504
+ let out = text;
505
+ for (const uuid of uuids) {
506
+ out = out.replace(
507
+ new RegExp(`\\n[\\t ]*${escapeRegExp(uuid)}\\b[^\\n]*,`, 'g'),
508
+ '',
509
+ );
510
+ }
511
+ return out;
512
+ }
513
+
514
+ /** Remove a whole `\n\t+key = value;` field from `obj`. No-op when absent. */
515
+ function removeField(
516
+ text /*: string */,
517
+ obj /*: BodyRange */,
518
+ key /*: string */,
519
+ ) /*: string */ {
520
+ const f = findField(text, obj, key);
521
+ if (f == null) {
522
+ return text;
523
+ }
524
+ // f.matchStart points at the leading `\n`; f.tokenEnd points AT the `;`.
525
+ return text.slice(0, f.matchStart) + text.slice(f.tokenEnd + 1);
526
+ }
527
+
528
+ /**
529
+ * Remove specific raw string members from an existing `( … )` array field
530
+ * (inverse of addArrayStringValues' append branch). Leaves the field and any
531
+ * other members in place. No-op when the field or a value is absent.
532
+ */
533
+ function removeArrayStringValues(
534
+ text /*: string */,
535
+ obj /*: BodyRange */,
536
+ key /*: string */,
537
+ values /*: ReadonlyArray<string> */,
538
+ ) /*: string */ {
539
+ const f = findField(text, obj, key);
540
+ if (f == null) {
541
+ return text;
542
+ }
543
+ let region = text.slice(f.valueStart, f.tokenEnd);
544
+ for (const val of values) {
545
+ region = region.replace(new RegExp(`\\n[\\t ]*${escapeRegExp(val)},`), '');
546
+ }
547
+ return text.slice(0, f.valueStart) + region + text.slice(f.tokenEnd);
548
+ }
549
+
550
+ /**
551
+ * Remove the empty `Pods` PBXGroup that `pod deintegrate` can leave behind in
552
+ * the navigator (build integration is already gone — xcconfigs/[CP] phases/
553
+ * linking — but the group lingers). Removes the group object AND its membership
554
+ * in any parent group. Only acts when the group is EMPTY (`children = ()`), so a
555
+ * still-integrated project (non-empty Pods group) is never touched. No-op when
556
+ * absent. PBXGroup bodies contain no nested braces, so `[^{}]` body matching is
557
+ * safe.
558
+ */
559
+ function removeEmptyPodsGroup(text /*: string */) /*: string */ {
560
+ const m = /\n[\t ]*([0-9A-Fa-f]{24}) \/\* Pods \*\/ = \{[^{}]*?\};/.exec(
561
+ text,
562
+ );
563
+ if (m == null) {
564
+ return text;
565
+ }
566
+ const block = m[0];
567
+ if (
568
+ !/isa = PBXGroup;/.test(block) ||
569
+ !/children = \(\s*\);/.test(block) ||
570
+ !/\b(?:path|name) = Pods;/.test(block)
571
+ ) {
572
+ return text;
573
+ }
574
+ const uuid = m[1];
575
+ // Drop the parent group's child reference first, then the group object.
576
+ return removeObjectByUuid(removeArrayMembersByUuid(text, [uuid]), uuid);
577
+ }
578
+
579
+ /**
580
+ * Remove the dangling `JavaScriptCore.framework` PBXFileReference the
581
+ * community template has carried since RN 0.60 (an SDK framework reference
582
+ * left over from the pre-Hermes JSC-via-CocoaPods era). It's navigator-only —
583
+ * never wired into any PBXBuildFile/build phase — so it's meaningless (and
584
+ * confusing) now that React Native uses Hermes. Only removes references that
585
+ * are truly unlinked: if a PBXBuildFile still references the UUID (e.g. an
586
+ * app that deliberately links JSC, such as via react-native-javascriptcore),
587
+ * that reference is left completely untouched. Removes the file-reference
588
+ * object AND its membership in any parent group. No-op when absent. Like
589
+ * `removeEmptyPodsGroup`, `deinit` does not restore this removal — git is the
590
+ * safety net.
591
+ */
592
+ function removeDanglingJavaScriptCoreRef(text /*: string */) /*: string */ {
593
+ const re =
594
+ /\n[\t ]*([0-9A-Fa-f]{24}) \/\* JavaScriptCore\.framework \*\/ = \{[^{}]*?\};/g;
595
+ const uuidsToRemove = [];
596
+ for (const m of text.matchAll(re)) {
597
+ const block = m[0];
598
+ if (
599
+ !/isa = PBXFileReference;/.test(block) ||
600
+ !/path = System\/Library\/Frameworks\/JavaScriptCore\.framework;/.test(
601
+ block,
602
+ ) ||
603
+ !/sourceTree = SDKROOT;/.test(block)
604
+ ) {
605
+ continue;
606
+ }
607
+ const uuid = m[1];
608
+ const linked = new RegExp(`\\bfileRef = ${escapeRegExp(uuid)}\\b`).test(
609
+ text,
610
+ );
611
+ if (!linked) {
612
+ uuidsToRemove.push(uuid);
613
+ }
614
+ }
615
+ if (uuidsToRemove.length === 0) {
616
+ return text;
617
+ }
618
+ // Drop the parent group's child reference(s) first, then the file objects.
619
+ let out = removeArrayMembersByUuid(text, uuidsToRemove);
620
+ for (const uuid of uuidsToRemove) {
621
+ out = removeObjectByUuid(out, uuid);
622
+ }
623
+ return out;
624
+ }
625
+
626
+ module.exports = {
627
+ generateUUID,
628
+ namespacedUUID,
629
+ serializeEntry,
630
+ quoteIfNeeded,
631
+ // Surgical-edit toolkit (in-place injection):
632
+ scanString,
633
+ scanToClose,
634
+ findObjectByUuid,
635
+ findField,
636
+ findSection,
637
+ findProjectObject,
638
+ findApplicationTargets,
639
+ uuidsInArray,
640
+ detectFieldIndent,
641
+ insertObjectsIntoSection,
642
+ addArrayMembers,
643
+ addArrayStringValues,
644
+ ensureScalarField,
645
+ setScalarField,
646
+ escapeRegExp,
647
+ // Surgical removal (deinit):
648
+ removeObjectByUuid,
649
+ removeArrayMembersByUuid,
650
+ removeField,
651
+ removeArrayStringValues,
652
+ removeEmptyPodsGroup,
653
+ removeDanglingJavaScriptCoreRef,
654
+ };