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.
- package/Libraries/Core/InitializeCore.js +8 -1
- package/Libraries/Core/ReactNativeVersion.js +1 -1
- package/Libraries/ReactNative/AppRegistry.flow.js +1 -0
- package/Libraries/ReactNative/AppRegistryImpl.js +2 -3
- package/React/Base/RCTVersion.m +1 -1
- package/React/I18n/RCTLocalizedString.mm +38 -2
- package/React-Core-prebuilt.podspec +45 -17
- package/React-Core.podspec +9 -2
- package/ReactAndroid/external-artifacts/build.gradle.kts +49 -0
- package/ReactAndroid/gradle.properties +1 -1
- package/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/mountitems/SynchronousMountItem.kt +8 -2
- package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
- package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
- package/package.json +10 -8
- package/react-native.config.js +81 -0
- package/scripts/cocoapods/fabric.rb +1 -1
- package/scripts/cocoapods/rncore.rb +35 -78
- package/scripts/cocoapods/rncore_facades.rb +232 -0
- package/scripts/cocoapods/rndependencies.rb +64 -3
- package/scripts/cocoapods/rndeps_facades.rb +193 -0
- package/scripts/cocoapods/spm.rb +78 -11
- package/scripts/codegen/templates/Package.swift.spm-template +97 -0
- package/scripts/react-native-xcode.sh +20 -0
- package/scripts/react_native_pods.rb +65 -16
- package/scripts/replace-rncore-version.js +53 -6
- package/scripts/setup-apple-spm.js +1165 -0
- package/scripts/spm/__doc__/rfc-spm-xcframework.md +707 -0
- package/scripts/spm/__doc__/spm-autolinking-plugins.md +244 -0
- package/scripts/spm/__doc__/spm-header-paths-contract.md +97 -0
- package/scripts/spm/__doc__/spm-plugins-assessment.md +128 -0
- package/scripts/spm/__doc__/spm-scripts.md +451 -0
- package/scripts/spm/autolinking-plugins.js +331 -0
- package/scripts/spm/download-spm-artifacts.js +1409 -0
- package/scripts/spm/expand-spm-dependencies.js +216 -0
- package/scripts/spm/flavored-frameworks.js +1008 -0
- package/scripts/spm/generate-spm-autolinking-config.js +161 -0
- package/scripts/spm/generate-spm-autolinking.js +1888 -0
- package/scripts/spm/generate-spm-package.js +302 -0
- package/scripts/spm/generate-spm-xcodeproj.js +2224 -0
- package/scripts/spm/read-podspec.js +695 -0
- package/scripts/spm/scaffold-package-swift.js +1206 -0
- package/scripts/spm/spm-pbxproj.js +654 -0
- package/scripts/spm/spm-types.js +517 -0
- package/scripts/spm/spm-utils.js +645 -0
- package/scripts/spm/sync-spm-autolinking.js +160 -0
- package/sdks/.hermesv1version +1 -0
- package/sdks/hermes-engine/utils/replace_hermes_version.js +18 -4
- package/sdks/hermes-engine/version.properties +1 -1
- package/third-party-podspecs/ReactNativeDependencies.podspec +2 -2
- package/types_generated/Libraries/ReactNative/AppRegistry.flow.d.ts +2 -2
- 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
|
+
};
|