@depup/react-native 0.87.0-depup.0 → 0.87.1-depup.0
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/Animated/AnimatedEvent.js +1 -1
- package/Libraries/Animated/AnimatedImplementation.js +6 -2
- package/Libraries/Animated/nodes/AnimatedNode.js +1 -1
- package/Libraries/Animated/nodes/AnimatedValue.js +2 -2
- package/Libraries/Core/ReactNativeVersion.js +1 -1
- package/README.md +3 -3
- package/React/Base/RCTVersion.m +1 -1
- package/ReactAndroid/gradle.properties +1 -1
- package/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.kt +7 -3
- package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
- package/ReactCommon/cxxreact/ReactNativeVersion.h +2 -2
- package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm +1 -1
- package/changes.json +2 -2
- package/package.json +12 -12
- package/scripts/cocoapods/rndependencies.rb +31 -25
- package/scripts/replace-rncore-version.js +18 -2
- package/scripts/setup-apple-spm.js +28 -12
- package/scripts/spm/autolinking-plugins.js +80 -0
- package/scripts/spm/download-spm-artifacts.js +64 -6
- package/scripts/spm/expand-spm-dependencies.js +249 -30
- package/scripts/spm/generate-spm-autolinking.js +157 -40
- package/scripts/spm/generate-spm-package.js +30 -18
- package/scripts/spm/generate-spm-xcodeproj.js +595 -71
- package/scripts/spm/scaffold-package-swift.js +65 -24
- package/scripts/spm/spm-pbxproj.js +158 -28
- package/scripts/spm/spm-types.js +29 -3
- package/scripts/spm/spm-utils.js +117 -2
- package/sdks/.hermesv1version +1 -1
- package/sdks/hermes-engine/version.properties +1 -1
- package/types_generated/Libraries/Animated/AnimatedImplementation.d.ts +2 -2
- package/types_generated/Libraries/Animated/nodes/AnimatedNode.d.ts +4 -1
- package/types_generated/Libraries/Animated/nodes/AnimatedValue.d.ts +3 -3
- package/scripts/spm/__doc__/rfc-spm-xcframework.md +0 -707
- package/scripts/spm/__doc__/spm-autolinking-plugins.md +0 -244
- package/scripts/spm/__doc__/spm-header-paths-contract.md +0 -97
- package/scripts/spm/__doc__/spm-plugins-assessment.md +0 -128
- package/scripts/spm/__doc__/spm-scripts.md +0 -486
|
@@ -26,6 +26,7 @@ const {parseConfigCommandJson} = require('./generate-spm-autolinking-config');
|
|
|
26
26
|
const {
|
|
27
27
|
addArrayMembers,
|
|
28
28
|
addArrayStringValues,
|
|
29
|
+
commentSafe,
|
|
29
30
|
ensureScalarField,
|
|
30
31
|
findApplicationTargets,
|
|
31
32
|
findField,
|
|
@@ -42,13 +43,25 @@ const {
|
|
|
42
43
|
removeObjectByUuid,
|
|
43
44
|
serializeEntry,
|
|
44
45
|
setScalarField,
|
|
46
|
+
uuidComment,
|
|
45
47
|
} = require('./spm-pbxproj');
|
|
46
|
-
const {
|
|
48
|
+
const {
|
|
49
|
+
AUTOLINKED_PACKAGE_NAME,
|
|
50
|
+
REACT_CODEGEN_APP_PRODUCTS,
|
|
51
|
+
REACT_CODEGEN_PACKAGE_NAME,
|
|
52
|
+
REACT_NATIVE_PACKAGE_NAME,
|
|
53
|
+
REACT_NATIVE_PRODUCTS,
|
|
54
|
+
isValidScriptPhaseId,
|
|
55
|
+
isValidScriptPhaseName,
|
|
56
|
+
makeLogger,
|
|
57
|
+
remotePackageConfig,
|
|
58
|
+
} = require('./spm-utils');
|
|
47
59
|
const fs = require('fs');
|
|
48
60
|
const path = require('path');
|
|
49
61
|
|
|
50
62
|
/*:: import type {
|
|
51
63
|
FlavoredFrameworkManifestEntry,
|
|
64
|
+
PluginScriptPhase,
|
|
52
65
|
XcframeworkSlice,
|
|
53
66
|
} from './spm-types'; */
|
|
54
67
|
|
|
@@ -73,6 +86,17 @@ const SPM_GENERATED_SOURCES_MANIFEST = path.join(
|
|
|
73
86
|
'.spm-plugin-generated-sources.json',
|
|
74
87
|
);
|
|
75
88
|
|
|
89
|
+
// Manifest of plugin-contributed build-time shell phases for the app target —
|
|
90
|
+
// SwiftPM has no `script_phase`, so a framework that must generate content into
|
|
91
|
+
// the app bundle (expo-constants' `EXConstants.bundle/app.config`) declares one
|
|
92
|
+
// through the plugin contract.
|
|
93
|
+
const SPM_SCRIPT_PHASES_MANIFEST = path.join(
|
|
94
|
+
'build',
|
|
95
|
+
'generated',
|
|
96
|
+
'autolinking',
|
|
97
|
+
'.spm-plugin-script-phases.json',
|
|
98
|
+
);
|
|
99
|
+
|
|
76
100
|
// The single navigator group all injected generated sources are parented under
|
|
77
101
|
// (created on first use). Its namespacedUUID id + display name.
|
|
78
102
|
const SPM_GENERATED_SOURCES_GROUP_ID = 'SPMGeneratedSources';
|
|
@@ -92,36 +116,21 @@ const GENERATED_SOURCE_FILE_TYPES /*: {[string]: string} */ = {
|
|
|
92
116
|
// resolve the product dependencies — SPM doesn't expose transitive products.
|
|
93
117
|
const SPM_PRODUCT_PACKAGES /*: Array<{product: string, packagePath: string, packageName: string}> */ =
|
|
94
118
|
[
|
|
95
|
-
{
|
|
96
|
-
product
|
|
97
|
-
packagePath: 'build/xcframeworks',
|
|
98
|
-
packageName: 'ReactNative',
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
product: 'ReactNativeHeaders',
|
|
102
|
-
packagePath: 'build/xcframeworks',
|
|
103
|
-
packageName: 'ReactNative',
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
product: 'ReactNativeDependenciesHeaders',
|
|
119
|
+
...REACT_NATIVE_PRODUCTS.map(product => ({
|
|
120
|
+
product,
|
|
107
121
|
packagePath: 'build/xcframeworks',
|
|
108
|
-
packageName:
|
|
109
|
-
},
|
|
122
|
+
packageName: REACT_NATIVE_PACKAGE_NAME,
|
|
123
|
+
})),
|
|
110
124
|
{
|
|
111
|
-
product:
|
|
125
|
+
product: AUTOLINKED_PACKAGE_NAME,
|
|
112
126
|
packagePath: 'build/generated/autolinking',
|
|
113
|
-
packageName:
|
|
127
|
+
packageName: AUTOLINKED_PACKAGE_NAME,
|
|
114
128
|
},
|
|
115
|
-
{
|
|
116
|
-
product
|
|
117
|
-
packagePath: 'build/generated/ios',
|
|
118
|
-
packageName: 'React-GeneratedCode',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
product: 'ReactAppDependencyProvider',
|
|
129
|
+
...REACT_CODEGEN_APP_PRODUCTS.map(product => ({
|
|
130
|
+
product,
|
|
122
131
|
packagePath: 'build/generated/ios',
|
|
123
|
-
packageName:
|
|
124
|
-
},
|
|
132
|
+
packageName: REACT_CODEGEN_PACKAGE_NAME,
|
|
133
|
+
})),
|
|
125
134
|
];
|
|
126
135
|
|
|
127
136
|
/*::
|
|
@@ -137,7 +146,15 @@ type BuildSettingChange = {
|
|
|
137
146
|
// value), e.g. a ${PODS_ROOT}-anchored REACT_NATIVE_PATH that dangles once
|
|
138
147
|
// CocoaPods is deintegrated. Deinit restores the original.
|
|
139
148
|
replacedScalars?: {[string]: string},
|
|
149
|
+
// Array settings that existed as a SCALAR and were promoted to a `( … )`
|
|
150
|
+
// array (key → the pre-injection raw value text, quotes included). Deinit
|
|
151
|
+
// restores that value verbatim; removing the injected members would leave
|
|
152
|
+
// the promoted array and its `"$(inherited)"` seed behind.
|
|
153
|
+
promotedArrayScalars?: {[string]: string},
|
|
140
154
|
};
|
|
155
|
+
// An array field injection CREATED (rather than appended to a pre-existing
|
|
156
|
+
// one), so deinit removes the whole field and lands byte-identical.
|
|
157
|
+
type CreatedArrayField = {container: 'project' | 'target', key: string};
|
|
141
158
|
// A plugin-contributed source, normalized for pbxproj emission. `path` is
|
|
142
159
|
// SRCROOT-relative when under the app root, else absolute; `sourceTree` is the
|
|
143
160
|
// matching pbxproj token ('SOURCE_ROOT' or '"<absolute>"').
|
|
@@ -265,30 +282,42 @@ function spmGraphToEntries(
|
|
|
265
282
|
return {localRefs, remoteRef, productDeps, buildFiles};
|
|
266
283
|
}
|
|
267
284
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Build a PBXShellScriptBuildPhase entry. `inputPaths`/`outputPaths` accept
|
|
287
|
+
* either a plain path array or an already-serialized pbxproj list.
|
|
288
|
+
* `alwaysOutOfDate` emits Xcode's own `alwaysOutOfDate = 1;` (unquoted, right
|
|
289
|
+
* after `isa`, so a project Xcode rewrites stays diff-free) and is omitted
|
|
290
|
+
* entirely when false. `comment` overrides the cosmetic `/* … */` label, which
|
|
291
|
+
* otherwise derives from `name`.
|
|
292
|
+
*/
|
|
273
293
|
function shellScriptPhase(
|
|
274
294
|
phaseUUID /*: string */,
|
|
275
295
|
name /*: string */,
|
|
276
296
|
script /*: string */,
|
|
277
|
-
options /*: {inputPaths?: string, outputPaths?: string} */ = {},
|
|
297
|
+
options /*: {inputPaths?: ?(string | ReadonlyArray<string>), outputPaths?: ?(string | ReadonlyArray<string>), alwaysOutOfDate?: ?boolean, comment?: string} */ = {},
|
|
278
298
|
) /*: {uuid: string, comment: string, fields: {[string]: string}} */ {
|
|
279
299
|
const empty = '(\n\t\t\t)';
|
|
300
|
+
const pathList = (
|
|
301
|
+
value /*: ?(string | ReadonlyArray<string>) */,
|
|
302
|
+
) /*: string */ => {
|
|
303
|
+
if (value == null) {
|
|
304
|
+
return empty;
|
|
305
|
+
}
|
|
306
|
+
return typeof value === 'string' ? value : pbxPathList(value);
|
|
307
|
+
};
|
|
280
308
|
return {
|
|
281
309
|
uuid: phaseUUID,
|
|
282
|
-
comment: name,
|
|
310
|
+
comment: options.comment ?? name,
|
|
283
311
|
fields: {
|
|
284
312
|
isa: 'PBXShellScriptBuildPhase',
|
|
313
|
+
...(options.alwaysOutOfDate === true ? {alwaysOutOfDate: '1'} : {}),
|
|
285
314
|
buildActionMask: '2147483647',
|
|
286
315
|
files: empty,
|
|
287
316
|
inputFileListPaths: empty,
|
|
288
|
-
inputPaths: options.inputPaths
|
|
317
|
+
inputPaths: pathList(options.inputPaths),
|
|
289
318
|
name: quoteIfNeeded(name),
|
|
290
319
|
outputFileListPaths: empty,
|
|
291
|
-
outputPaths: options.outputPaths
|
|
320
|
+
outputPaths: pathList(options.outputPaths),
|
|
292
321
|
runOnlyForDeploymentPostprocessing: '0',
|
|
293
322
|
shellPath: '/bin/sh',
|
|
294
323
|
shellScript: quoteIfNeeded(script),
|
|
@@ -468,11 +497,126 @@ ${copies}
|
|
|
468
497
|
`;
|
|
469
498
|
}
|
|
470
499
|
|
|
500
|
+
/**
|
|
501
|
+
* The app target's `buildPhases` members, in the order the file lists them.
|
|
502
|
+
* Line-leading UUIDs only: a member's trailing comment carries a plugin-supplied
|
|
503
|
+
* phase name, and one that happens to look like a UUID would otherwise read as an
|
|
504
|
+
* extra member — leaving the actual order permanently at odds with the declared
|
|
505
|
+
* one, and offering a non-member as a re-seating anchor.
|
|
506
|
+
*/
|
|
507
|
+
function buildPhaseOrder(
|
|
508
|
+
text /*: string */,
|
|
509
|
+
target /*: {bodyOpen: number, bodyClose: number, ...} */,
|
|
510
|
+
) /*: Array<string> */ {
|
|
511
|
+
const field = findField(text, target, 'buildPhases');
|
|
512
|
+
if (field == null) {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
return [...field.value.matchAll(/^[\t ]*([0-9A-Fa-f]{24})\b/gm)].map(
|
|
516
|
+
m => m[1],
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Rewrite the trailing comment Xcode keeps beside a UUID — on the object's own
|
|
522
|
+
* definition line and on every array-member line referencing it. Xcode
|
|
523
|
+
* normalizes those comments on its next write, so leaving a stale one behind
|
|
524
|
+
* (after a plugin renames a phase) plants a spurious diff in the user's repo.
|
|
525
|
+
* No-op when the comment already reads `comment`.
|
|
526
|
+
*/
|
|
527
|
+
function setUuidComment(
|
|
528
|
+
text /*: string */,
|
|
529
|
+
uuid /*: string */,
|
|
530
|
+
comment /*: string */,
|
|
531
|
+
) /*: string */ {
|
|
532
|
+
return text.replace(
|
|
533
|
+
new RegExp(`(\\n[\\t ]*${uuid})(?: /\\* [^\\n]*? \\*/)?( = \\{|,)`, 'g'),
|
|
534
|
+
(_match, head, tail) => `${head}${uuidComment(comment)}${tail}`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/*:: type SeatedPhase = {uuid: string, comment: string, position: 'beforeCompile' | 'end'}; */
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Seat the plugin phases in the order the manifest declares: `beforeCompile`
|
|
542
|
+
* ones directly after the Sync SPM Autolinking phase (which stays first — it
|
|
543
|
+
* regenerates the content everything else reads) and always before Sources,
|
|
544
|
+
* `end` ones at the true end of `buildPhases`, after the app's own JS-bundle
|
|
545
|
+
* phase. The other members keep their relative order.
|
|
546
|
+
*
|
|
547
|
+
* `beforeCompile` is anchored on Sources, not merely on the sync phase: RN never
|
|
548
|
+
* re-seats its own sync phase, so a user who drags it below Sources would
|
|
549
|
+
* otherwise have every `beforeCompile` phase seated after compilation — the one
|
|
550
|
+
* thing the position promises not to do. Without a Sources phase the sync phase
|
|
551
|
+
* is the anchor.
|
|
552
|
+
*
|
|
553
|
+
* Rewrites the membership lines ONLY when the actual order differs from that,
|
|
554
|
+
* which is what keeps an unchanged sync byte-identical — `addBuildPhaseAfter`
|
|
555
|
+
* and `addArrayMembers` both short-circuit on a UUID that is already a member,
|
|
556
|
+
* so re-placing has to be driven from here. A phase a user dragged elsewhere in
|
|
557
|
+
* Xcode is therefore moved back: the declared position wins.
|
|
558
|
+
*/
|
|
559
|
+
function seatScriptPhases(
|
|
560
|
+
input /*: string */,
|
|
561
|
+
targetUuid /*: string */,
|
|
562
|
+
syncPhaseUuid /*: string */,
|
|
563
|
+
phases /*: ReadonlyArray<SeatedPhase> */,
|
|
564
|
+
sourcesPhaseUuid /*: ?string */,
|
|
565
|
+
) /*: string */ {
|
|
566
|
+
const pluginUuids = new Set(phases.map(p => p.uuid));
|
|
567
|
+
const actual = buildPhaseOrder(
|
|
568
|
+
input,
|
|
569
|
+
findApplicationTargetByUuid(input, targetUuid),
|
|
570
|
+
);
|
|
571
|
+
const others = actual.filter(uuid => !pluginUuids.has(uuid));
|
|
572
|
+
const beforeCompile = phases.filter(p => p.position === 'beforeCompile');
|
|
573
|
+
const atEnd = phases.filter(p => p.position !== 'beforeCompile');
|
|
574
|
+
const sourcesAt =
|
|
575
|
+
sourcesPhaseUuid != null ? others.indexOf(sourcesPhaseUuid) : -1;
|
|
576
|
+
const afterSyncAt = others.indexOf(syncPhaseUuid) + 1;
|
|
577
|
+
// 0 — no sync phase member, or Sources ahead of it — makes the beforeCompile
|
|
578
|
+
// phases lead the array, matching addArrayMembers' prepend fallback below.
|
|
579
|
+
const insertAt =
|
|
580
|
+
sourcesAt >= 0 ? Math.min(afterSyncAt, sourcesAt) : afterSyncAt;
|
|
581
|
+
const desired = [
|
|
582
|
+
...others.slice(0, insertAt),
|
|
583
|
+
...beforeCompile.map(p => p.uuid),
|
|
584
|
+
...others.slice(insertAt),
|
|
585
|
+
...atEnd.map(p => p.uuid),
|
|
586
|
+
];
|
|
587
|
+
if (
|
|
588
|
+
desired.length === actual.length &&
|
|
589
|
+
desired.every((uuid, i) => uuid === actual[i])
|
|
590
|
+
) {
|
|
591
|
+
return input;
|
|
592
|
+
}
|
|
593
|
+
// File-wide removal is safe for a shell-phase UUID: it appears on its own
|
|
594
|
+
// definition line (which ends `= {`, never matched) and on `buildPhases`
|
|
595
|
+
// member lines. The uncovered case is a user who copied the same phase object
|
|
596
|
+
// into a SECOND target's buildPhases — it is stripped there too.
|
|
597
|
+
let text = removeArrayMembersByUuid(input, [...pluginUuids]);
|
|
598
|
+
const target = () => findApplicationTargetByUuid(text, targetUuid);
|
|
599
|
+
let anchor = insertAt > 0 ? others[insertAt - 1] : null;
|
|
600
|
+
for (const member of beforeCompile) {
|
|
601
|
+
text =
|
|
602
|
+
anchor == null
|
|
603
|
+
? addArrayMembers(text, target(), 'buildPhases', [member], {
|
|
604
|
+
prepend: true,
|
|
605
|
+
})
|
|
606
|
+
: addBuildPhaseAfter(text, target(), anchor, member);
|
|
607
|
+
anchor = member.uuid;
|
|
608
|
+
}
|
|
609
|
+
for (const member of atEnd) {
|
|
610
|
+
text = addArrayMembers(text, target(), 'buildPhases', [member]);
|
|
611
|
+
}
|
|
612
|
+
return text;
|
|
613
|
+
}
|
|
614
|
+
|
|
471
615
|
function addBuildPhaseAfter(
|
|
472
616
|
text /*: string */,
|
|
473
617
|
target /*: {bodyOpen: number, bodyClose: number, ...} */,
|
|
474
618
|
afterUuid /*: string */,
|
|
475
|
-
member /*: {uuid: string, comment: string} */,
|
|
619
|
+
member /*: {uuid: string, comment: string, ...} */,
|
|
476
620
|
) /*: string */ {
|
|
477
621
|
const field = findField(text, target, 'buildPhases');
|
|
478
622
|
if (field == null || field.value.includes(member.uuid)) {
|
|
@@ -487,7 +631,7 @@ function addBuildPhaseAfter(
|
|
|
487
631
|
const absoluteStart = field.valueStart + after.index;
|
|
488
632
|
const lineEnd = text.indexOf('\n', absoluteStart + after[0].length);
|
|
489
633
|
const indent = after[2];
|
|
490
|
-
const line = `\n${indent}${member.uuid}
|
|
634
|
+
const line = `\n${indent}${member.uuid}${uuidComment(member.comment)},`;
|
|
491
635
|
return text.slice(0, lineEnd) + line + text.slice(lineEnd);
|
|
492
636
|
}
|
|
493
637
|
|
|
@@ -724,6 +868,18 @@ function escapeXmlAttribute(s /*: string */) /*: string */ {
|
|
|
724
868
|
.replace(/'/g, ''');
|
|
725
869
|
}
|
|
726
870
|
|
|
871
|
+
// The inverse of escapeXmlAttribute. `&` is expanded LAST so an entity that
|
|
872
|
+
// was itself escaped (`<` → `&lt;`) round-trips back to its own text
|
|
873
|
+
// rather than to `<`.
|
|
874
|
+
function unescapeXmlAttribute(s /*: string */) /*: string */ {
|
|
875
|
+
return s
|
|
876
|
+
.replace(/</g, '<')
|
|
877
|
+
.replace(/>/g, '>')
|
|
878
|
+
.replace(/"/g, '"')
|
|
879
|
+
.replace(/'/g, "'")
|
|
880
|
+
.replace(/&/g, '&');
|
|
881
|
+
}
|
|
882
|
+
|
|
727
883
|
function generateXcscheme(
|
|
728
884
|
appName /*: string */,
|
|
729
885
|
targetUUID /*: string */,
|
|
@@ -932,6 +1088,25 @@ const INJECTED_ARRAY_SETTINGS = [
|
|
|
932
1088
|
},
|
|
933
1089
|
];
|
|
934
1090
|
|
|
1091
|
+
// Array build settings injected only into debug-flavored configurations.
|
|
1092
|
+
//
|
|
1093
|
+
// Swift's `#if DEBUG` is gated by SWIFT_ACTIVE_COMPILATION_CONDITIONS, NOT by
|
|
1094
|
+
// GCC_PREPROCESSOR_DEFINITIONS (which only reaches C/ObjC/C++). The app
|
|
1095
|
+
// template does not commit the setting: CocoaPods injects it at `pod install`
|
|
1096
|
+
// time (react_native_post_install → set_build_setting
|
|
1097
|
+
// SWIFT_ACTIVE_COMPILATION_CONDITIONS = ["$(inherited)", "DEBUG"] on Debug).
|
|
1098
|
+
// An SPM app never runs CocoaPods, so without this `#if DEBUG` is false even
|
|
1099
|
+
// in a Debug build — AppDelegate.swift's `bundleURL()` skips the Metro URL,
|
|
1100
|
+
// falls back to a main.jsbundle that a Debug build never produced, and the app
|
|
1101
|
+
// dies at launch with "No script url provided … unsanitizedScriptURLString =
|
|
1102
|
+
// (null)" while Metro is running right there.
|
|
1103
|
+
//
|
|
1104
|
+
// Paired with RN_SPM_FLAVOR via flavorForBuildConfiguration, so a config that
|
|
1105
|
+
// links the debug xcframeworks also compiles its Swift with DEBUG.
|
|
1106
|
+
const DEBUG_ARRAY_SETTINGS = [
|
|
1107
|
+
{key: 'SWIFT_ACTIVE_COMPILATION_CONDITIONS', values: ['DEBUG']},
|
|
1108
|
+
];
|
|
1109
|
+
|
|
935
1110
|
/** The XCBuildConfiguration UUIDs of a target (via its buildConfigurationList). */
|
|
936
1111
|
function targetBuildConfigUuids(
|
|
937
1112
|
text /*: string */,
|
|
@@ -1078,7 +1253,8 @@ function injectSpmIntoPbxproj(
|
|
|
1078
1253
|
hermesCliPath /*: ?string */ = null,
|
|
1079
1254
|
generatedSources /*: ReadonlyArray<GeneratedSource> */ = [],
|
|
1080
1255
|
flavoredFrameworks /*: ReadonlyArray<FlavoredFrameworkManifestEntry> */ = [],
|
|
1081
|
-
|
|
1256
|
+
scriptPhases /*: ReadonlyArray<PluginScriptPhase> */ = [],
|
|
1257
|
+
) /*: {text: string, injectedUuids: Array<string>, createdArrayFields: Array<CreatedArrayField>, buildSettingChanges: Array<BuildSettingChange>, generatedSourceUuids: {[string]: Array<string>}, scriptPhaseUuids: {[string]: string}} */ {
|
|
1082
1258
|
let text = input;
|
|
1083
1259
|
const mkUuid = (section /*: string */, id /*: string */) =>
|
|
1084
1260
|
namespacedUUID(plan.rootUuid, section, id);
|
|
@@ -1111,10 +1287,7 @@ function injectSpmIntoPbxproj(
|
|
|
1111
1287
|
insertObjects('XCSwiftPackageProductDependency', entries.productDeps);
|
|
1112
1288
|
insertObjects('PBXBuildFile', entries.buildFiles);
|
|
1113
1289
|
|
|
1114
|
-
|
|
1115
|
-
// can remove the whole field and land byte-identical to the original.
|
|
1116
|
-
const createdArrayFields /*: Array<{container: 'project' | 'target', key: string}> */ =
|
|
1117
|
-
[];
|
|
1290
|
+
const createdArrayFields /*: Array<CreatedArrayField> */ = [];
|
|
1118
1291
|
|
|
1119
1292
|
// 2. packageReferences on the PBXProject.
|
|
1120
1293
|
const pkgRefMembers = [
|
|
@@ -1305,9 +1478,14 @@ function injectSpmIntoPbxproj(
|
|
|
1305
1478
|
const fileRefUuid = mkUuid('PBXFileReference', `gensrc:${src.path}`);
|
|
1306
1479
|
const buildFileUuid = mkUuid('PBXBuildFile', `gensrc:${src.path}`);
|
|
1307
1480
|
generatedSourceUuids[src.path] = [fileRefUuid, buildFileUuid];
|
|
1481
|
+
// The name/path VALUES stay verbatim (escaped) — only the cosmetic
|
|
1482
|
+
// comments are normalized, falling back to the normalized path (the
|
|
1483
|
+
// ledger key) and then to no comment at all, which is well-formed.
|
|
1484
|
+
const label = commentSafe(src.name) || commentSafe(src.path);
|
|
1485
|
+
const inSources = label === '' ? '' : `${label} in Sources`;
|
|
1308
1486
|
fileRefs.push({
|
|
1309
1487
|
uuid: fileRefUuid,
|
|
1310
|
-
comment:
|
|
1488
|
+
comment: label,
|
|
1311
1489
|
fields: {
|
|
1312
1490
|
isa: 'PBXFileReference',
|
|
1313
1491
|
lastKnownFileType: src.fileType,
|
|
@@ -1318,17 +1496,14 @@ function injectSpmIntoPbxproj(
|
|
|
1318
1496
|
});
|
|
1319
1497
|
buildFiles.push({
|
|
1320
1498
|
uuid: buildFileUuid,
|
|
1321
|
-
comment:
|
|
1499
|
+
comment: inSources,
|
|
1322
1500
|
fields: {
|
|
1323
1501
|
isa: 'PBXBuildFile',
|
|
1324
|
-
fileRef: `${fileRefUuid}
|
|
1502
|
+
fileRef: `${fileRefUuid}${uuidComment(label)}`,
|
|
1325
1503
|
},
|
|
1326
1504
|
});
|
|
1327
|
-
sourcesMembers.push({
|
|
1328
|
-
|
|
1329
|
-
comment: `${src.name} in Sources`,
|
|
1330
|
-
});
|
|
1331
|
-
groupChildren.push({uuid: fileRefUuid, comment: src.name});
|
|
1505
|
+
sourcesMembers.push({uuid: buildFileUuid, comment: inSources});
|
|
1506
|
+
groupChildren.push({uuid: fileRefUuid, comment: label});
|
|
1332
1507
|
}
|
|
1333
1508
|
insertObjects('PBXFileReference', fileRefs);
|
|
1334
1509
|
insertObjects('PBXBuildFile', buildFiles);
|
|
@@ -1388,12 +1563,87 @@ function injectSpmIntoPbxproj(
|
|
|
1388
1563
|
}
|
|
1389
1564
|
}
|
|
1390
1565
|
|
|
1566
|
+
// 9. Plugin-declared build phases (SwiftPM has no `script_phase`). Each
|
|
1567
|
+
// phase's UUID is keyed on its plugin id, so re-runs refresh in place and
|
|
1568
|
+
// `deinit` reverses it.
|
|
1569
|
+
const scriptPhaseUuids /*: {[string]: string} */ = {};
|
|
1570
|
+
if (scriptPhases.length > 0) {
|
|
1571
|
+
const seated /*: Array<SeatedPhase> */ = [];
|
|
1572
|
+
for (const scriptPhase of scriptPhases) {
|
|
1573
|
+
const uuid = mkUuid(
|
|
1574
|
+
'PBXShellScriptBuildPhase',
|
|
1575
|
+
`plugin:${scriptPhase.id}`,
|
|
1576
|
+
);
|
|
1577
|
+
scriptPhaseUuids[scriptPhase.id] = uuid;
|
|
1578
|
+
// The full name goes into the `name` field (escaped) — that is what Xcode
|
|
1579
|
+
// displays. The cosmetic comments get it normalized, falling back to the
|
|
1580
|
+
// normalized id and then to no comment at all, which is well-formed. The
|
|
1581
|
+
// fallback is sanitized rather than trusted to the id charset, so widening
|
|
1582
|
+
// that charset can never reach a comment.
|
|
1583
|
+
const comment =
|
|
1584
|
+
commentSafe(scriptPhase.name) || commentSafe(scriptPhase.id);
|
|
1585
|
+
const entry = shellScriptPhase(
|
|
1586
|
+
uuid,
|
|
1587
|
+
scriptPhase.name,
|
|
1588
|
+
scriptPhase.script,
|
|
1589
|
+
{
|
|
1590
|
+
inputPaths: scriptPhase.inputPaths,
|
|
1591
|
+
outputPaths: scriptPhase.outputPaths,
|
|
1592
|
+
alwaysOutOfDate: scriptPhase.alwaysOutOfDate,
|
|
1593
|
+
comment,
|
|
1594
|
+
},
|
|
1595
|
+
);
|
|
1596
|
+
if (!text.includes(uuid)) {
|
|
1597
|
+
text = insertObjectsIntoSection(
|
|
1598
|
+
text,
|
|
1599
|
+
'PBXShellScriptBuildPhase',
|
|
1600
|
+
serializeEntry(entry),
|
|
1601
|
+
);
|
|
1602
|
+
} else {
|
|
1603
|
+
// Rewrite the fields we own unconditionally — byte-identical when the
|
|
1604
|
+
// plugin's declaration hasn't changed, so no content hashing is needed.
|
|
1605
|
+
// Each write shifts offsets, hence the re-lookup per field.
|
|
1606
|
+
for (const key of [
|
|
1607
|
+
'name',
|
|
1608
|
+
'shellScript',
|
|
1609
|
+
'inputPaths',
|
|
1610
|
+
'outputPaths',
|
|
1611
|
+
]) {
|
|
1612
|
+
const current = findObjectByUuid(text, uuid);
|
|
1613
|
+
if (current != null) {
|
|
1614
|
+
text = setScalarField(text, current, key, entry.fields[key]);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
// Flipping alwaysOutOfDate back off means REMOVING the field —
|
|
1618
|
+
// setScalarField would only ever write a value.
|
|
1619
|
+
const current = findObjectByUuid(text, uuid);
|
|
1620
|
+
if (current != null) {
|
|
1621
|
+
text =
|
|
1622
|
+
scriptPhase.alwaysOutOfDate === true
|
|
1623
|
+
? setScalarField(text, current, 'alwaysOutOfDate', '1')
|
|
1624
|
+
: removeField(text, current, 'alwaysOutOfDate');
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
injectedUuids.push(uuid);
|
|
1628
|
+
text = setUuidComment(text, uuid, comment);
|
|
1629
|
+
seated.push({uuid, comment, position: scriptPhase.position});
|
|
1630
|
+
}
|
|
1631
|
+
text = seatScriptPhases(
|
|
1632
|
+
text,
|
|
1633
|
+
plan.targetUuid,
|
|
1634
|
+
syncPhaseUuid,
|
|
1635
|
+
seated,
|
|
1636
|
+
sourcesPhaseUuid,
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1391
1640
|
return {
|
|
1392
1641
|
text,
|
|
1393
1642
|
injectedUuids,
|
|
1394
1643
|
createdArrayFields,
|
|
1395
1644
|
buildSettingChanges,
|
|
1396
1645
|
generatedSourceUuids,
|
|
1646
|
+
scriptPhaseUuids,
|
|
1397
1647
|
};
|
|
1398
1648
|
}
|
|
1399
1649
|
|
|
@@ -1451,6 +1701,28 @@ function resolveHermesCliPathSetting(
|
|
|
1451
1701
|
}
|
|
1452
1702
|
}
|
|
1453
1703
|
|
|
1704
|
+
/** Strip the surrounding plist quotes from a build-setting token, if any. */
|
|
1705
|
+
function unquotePlist(s /*: string */) /*: string */ {
|
|
1706
|
+
return s.replace(/^"/, '').replace(/"$/, '');
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* The individual values a build setting already carries, unquoted — for both
|
|
1711
|
+
* shapes a pbxproj uses: the array form Xcode writes for a multi-value setting
|
|
1712
|
+
* (`("$(inherited)", DEBUG)`) and the scalar form the app template and
|
|
1713
|
+
* hand-edits use (`"$(inherited) DEBUG"`). Membership, not substring: the
|
|
1714
|
+
* latter would read `MY_DEBUG_FLAG` as `DEBUG` already being set and silently
|
|
1715
|
+
* skip the injection.
|
|
1716
|
+
*/
|
|
1717
|
+
function buildSettingValueTokens(value /*: string */) /*: Set<string> */ {
|
|
1718
|
+
return new Set(
|
|
1719
|
+
value
|
|
1720
|
+
.split(/[\s,()]+/)
|
|
1721
|
+
.filter(Boolean)
|
|
1722
|
+
.map(unquotePlist),
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1454
1726
|
function mergeReactBuildSettings(
|
|
1455
1727
|
input /*: string */,
|
|
1456
1728
|
configUuid /*: string */,
|
|
@@ -1493,9 +1765,13 @@ function mergeReactBuildSettings(
|
|
|
1493
1765
|
};
|
|
1494
1766
|
const createdArrayKeys /*: Array<string> */ = [];
|
|
1495
1767
|
const appendedArrayValues /*: {[string]: Array<string>} */ = {};
|
|
1768
|
+
const promotedArrayScalars /*: {[string]: string} */ = {};
|
|
1496
1769
|
const createdScalars /*: Array<string> */ = [];
|
|
1497
1770
|
const arraySettings = [
|
|
1498
1771
|
...INJECTED_ARRAY_SETTINGS,
|
|
1772
|
+
...(flavorForBuildConfiguration(configurationName) === 'debug'
|
|
1773
|
+
? DEBUG_ARRAY_SETTINGS
|
|
1774
|
+
: []),
|
|
1499
1775
|
...frameworkArrayBuildSettings(flavoredFrameworks),
|
|
1500
1776
|
];
|
|
1501
1777
|
for (const {key, values} of arraySettings) {
|
|
@@ -1504,15 +1780,41 @@ function mergeReactBuildSettings(
|
|
|
1504
1780
|
continue;
|
|
1505
1781
|
}
|
|
1506
1782
|
const existing = findField(text, d, key);
|
|
1783
|
+
// Non-null only for a scalar addArrayStringValues would promote to an array
|
|
1784
|
+
// (same array-vs-scalar test it uses). Kept RAW: findField's token for a
|
|
1785
|
+
// bare scalar ends at the `;`, so it carries any whitespace before it, and
|
|
1786
|
+
// deinit has to write those bytes back verbatim.
|
|
1787
|
+
const priorScalar =
|
|
1788
|
+
existing != null && !existing.value.trimStart().startsWith('(')
|
|
1789
|
+
? existing.value
|
|
1790
|
+
: null;
|
|
1507
1791
|
if (existing == null) {
|
|
1508
1792
|
createdArrayKeys.push(key);
|
|
1509
1793
|
} else {
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1794
|
+
const present = buildSettingValueTokens(existing.value);
|
|
1795
|
+
const fresh = values.filter(v => !present.has(unquotePlist(v)));
|
|
1796
|
+
if (fresh.length === 0) {
|
|
1797
|
+
// Nothing to add. Skip addArrayStringValues entirely: its dedupe is by
|
|
1798
|
+
// EXACT array member, so a value the user carries in the scalar form
|
|
1799
|
+
// (`SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"`) would
|
|
1800
|
+
// otherwise be promoted to an array and re-appended — an edit `deinit`
|
|
1801
|
+
// has no record of and so could never reverse.
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
if (priorScalar == null) {
|
|
1512
1805
|
appendedArrayValues[key] = fresh;
|
|
1513
1806
|
}
|
|
1514
1807
|
}
|
|
1808
|
+
const beforeAdd = text;
|
|
1515
1809
|
text = addArrayStringValues(text, d, key, values);
|
|
1810
|
+
// Record only a promotion that actually happened: addArrayStringValues
|
|
1811
|
+
// no-ops when `values` is empty or every value is already a member, and a
|
|
1812
|
+
// recorded-but-untouched field would have deinit clobber whatever the user
|
|
1813
|
+
// has there by then. Restoring the scalar subsumes removing the injected
|
|
1814
|
+
// members, so the two records stay mutually exclusive per key.
|
|
1815
|
+
if (priorScalar != null && text !== beforeAdd) {
|
|
1816
|
+
promotedArrayScalars[key] = priorScalar;
|
|
1817
|
+
}
|
|
1516
1818
|
}
|
|
1517
1819
|
const replacedScalars /*: {[string]: string} */ = {};
|
|
1518
1820
|
for (const {key, value} of scalars) {
|
|
@@ -1571,6 +1873,9 @@ function mergeReactBuildSettings(
|
|
|
1571
1873
|
appendedArrayValues,
|
|
1572
1874
|
createdScalars,
|
|
1573
1875
|
replacedScalars,
|
|
1876
|
+
...(Object.keys(promotedArrayScalars).length > 0
|
|
1877
|
+
? {promotedArrayScalars}
|
|
1878
|
+
: {}),
|
|
1574
1879
|
},
|
|
1575
1880
|
};
|
|
1576
1881
|
}
|
|
@@ -1838,6 +2143,86 @@ function readGeneratedSourcesManifest(
|
|
|
1838
2143
|
return out;
|
|
1839
2144
|
}
|
|
1840
2145
|
|
|
2146
|
+
const nonEmptyStrings = (value /*: unknown */) /*: ?Array<string> */ =>
|
|
2147
|
+
Array.isArray(value)
|
|
2148
|
+
? value.filter(p => typeof p === 'string' && p.length > 0)
|
|
2149
|
+
: null;
|
|
2150
|
+
|
|
2151
|
+
/**
|
|
2152
|
+
* Read the plugin script-phases manifest at
|
|
2153
|
+
* `<appRoot>/build/generated/autolinking/.spm-plugin-script-phases.json`.
|
|
2154
|
+
* Absent, unparseable, or malformed → `[]`. Lenient by design even though the
|
|
2155
|
+
* plugin contract validates these entries fatally at invoke time: the sidecar
|
|
2156
|
+
* legitimately does not exist yet on a first `spm add`, and a stale or
|
|
2157
|
+
* hand-edited file must not break injection. This reader is the ONLY gate on
|
|
2158
|
+
* such a file, so it applies the same id/name rules (from spm-utils) that
|
|
2159
|
+
* invokePlugins enforces fatally.
|
|
2160
|
+
*/
|
|
2161
|
+
function readScriptPhasesManifest(
|
|
2162
|
+
appRoot /*: string */,
|
|
2163
|
+
) /*: Array<PluginScriptPhase> */ {
|
|
2164
|
+
const manifestPath = path.join(appRoot, SPM_SCRIPT_PHASES_MANIFEST);
|
|
2165
|
+
let raw /*: string */ = '';
|
|
2166
|
+
try {
|
|
2167
|
+
raw = fs.readFileSync(manifestPath, 'utf8');
|
|
2168
|
+
} catch {
|
|
2169
|
+
return [];
|
|
2170
|
+
}
|
|
2171
|
+
let entries /*: unknown */ = null;
|
|
2172
|
+
try {
|
|
2173
|
+
entries = JSON.parse(raw);
|
|
2174
|
+
} catch {
|
|
2175
|
+
log(
|
|
2176
|
+
`warning: could not parse ${SPM_SCRIPT_PHASES_MANIFEST}; ` +
|
|
2177
|
+
'skipping script phases.',
|
|
2178
|
+
);
|
|
2179
|
+
return [];
|
|
2180
|
+
}
|
|
2181
|
+
if (!Array.isArray(entries)) {
|
|
2182
|
+
return [];
|
|
2183
|
+
}
|
|
2184
|
+
const out /*: Array<PluginScriptPhase> */ = [];
|
|
2185
|
+
for (const entry of entries) {
|
|
2186
|
+
if (
|
|
2187
|
+
entry == null ||
|
|
2188
|
+
typeof entry !== 'object' ||
|
|
2189
|
+
!isValidScriptPhaseId(entry.id) ||
|
|
2190
|
+
!isValidScriptPhaseName(entry.name) ||
|
|
2191
|
+
typeof entry.script !== 'string' ||
|
|
2192
|
+
entry.script.length === 0 ||
|
|
2193
|
+
// An unknown position is a malformed entry, not something to coerce: it
|
|
2194
|
+
// would silently run somewhere the plugin didn't ask for.
|
|
2195
|
+
(entry.position != null &&
|
|
2196
|
+
entry.position !== 'beforeCompile' &&
|
|
2197
|
+
entry.position !== 'end') ||
|
|
2198
|
+
// Dedupe by id — the id seeds the phase's UUID, so a duplicate would
|
|
2199
|
+
// insert two objects with identical UUIDs.
|
|
2200
|
+
out.some(phase => phase.id === entry.id)
|
|
2201
|
+
) {
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const phase /*: PluginScriptPhase */ = {
|
|
2205
|
+
id: entry.id,
|
|
2206
|
+
name: entry.name,
|
|
2207
|
+
script: entry.script,
|
|
2208
|
+
position: entry.position ?? 'end',
|
|
2209
|
+
};
|
|
2210
|
+
const inputPaths = nonEmptyStrings(entry.inputPaths);
|
|
2211
|
+
const outputPaths = nonEmptyStrings(entry.outputPaths);
|
|
2212
|
+
if (inputPaths != null) {
|
|
2213
|
+
phase.inputPaths = inputPaths;
|
|
2214
|
+
}
|
|
2215
|
+
if (outputPaths != null) {
|
|
2216
|
+
phase.outputPaths = outputPaths;
|
|
2217
|
+
}
|
|
2218
|
+
if (typeof entry.alwaysOutOfDate === 'boolean') {
|
|
2219
|
+
phase.alwaysOutOfDate = entry.alwaysOutOfDate;
|
|
2220
|
+
}
|
|
2221
|
+
out.push(phase);
|
|
2222
|
+
}
|
|
2223
|
+
return out;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
1841
2226
|
/**
|
|
1842
2227
|
* Read the `.spm-injected.json` marker of a previously-injected project, or
|
|
1843
2228
|
* null when absent/unreadable. Used to reconcile generated sources on `update`
|
|
@@ -1845,7 +2230,7 @@ function readGeneratedSourcesManifest(
|
|
|
1845
2230
|
*/
|
|
1846
2231
|
function readMarker(
|
|
1847
2232
|
xcodeprojPath /*: string */,
|
|
1848
|
-
) /*: ?{generatedSources?: {[string]: Array<string>}, artifactsVersionOverride?: ?string, configCommand?: ?Array<string>, buildSettingChanges?: Array<BuildSettingChange>, ...} */ {
|
|
2233
|
+
) /*: ?{generatedSources?: {[string]: Array<string>}, scriptPhases?: {[string]: string}, artifactsVersionOverride?: ?string, configCommand?: ?Array<string>, buildSettingChanges?: Array<BuildSettingChange>, createdArrayFields?: Array<CreatedArrayField>, scheme?: {file?: ?string, created?: ?boolean}, ...} */ {
|
|
1849
2234
|
const markerPath = path.join(xcodeprojPath, SPM_INJECTED_MARKER);
|
|
1850
2235
|
try {
|
|
1851
2236
|
// $FlowFixMe[incompatible-return] JSON.parse returns any
|
|
@@ -1886,13 +2271,11 @@ function findInjectedXcodeproj(appRoot /*: string */) /*: string | null */ {
|
|
|
1886
2271
|
* update --version` pinned into the injected xcodeproj's `.spm-injected.json`
|
|
1887
2272
|
* marker (see the field's doc comment in injectSpmIntoExistingXcodeproj
|
|
1888
2273
|
* below), or null when no project is injected yet, no override is pinned, or
|
|
1889
|
-
* the marker can't be read (never throws). Pure fs reads.
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
1893
|
-
*
|
|
1894
|
-
* different artifact slot than the explicit `--version` selected. Only the
|
|
1895
|
-
* tests cover it.
|
|
2274
|
+
* the marker can't be read (never throws). Pure fs reads — setup-apple-spm.js's
|
|
2275
|
+
* determineVersion prefers the pinned version over the one derived from
|
|
2276
|
+
* node_modules/react-native/package.json, so a later flagless `add`/`update`
|
|
2277
|
+
* (and `download`) stays on the SAME artifact slot the explicit `--version`
|
|
2278
|
+
* selected.
|
|
1896
2279
|
*/
|
|
1897
2280
|
function readArtifactsVersionOverride(appRoot /*: string */) /*: ?string */ {
|
|
1898
2281
|
const xcodeprojPath = findInjectedXcodeproj(appRoot);
|
|
@@ -1929,6 +2312,39 @@ function readPinnedConfigCommand(appRoot /*: string */) /*: ?Array<string> */ {
|
|
|
1929
2312
|
}
|
|
1930
2313
|
}
|
|
1931
2314
|
|
|
2315
|
+
/**
|
|
2316
|
+
* Union the array fields RN has created across syncs, deduped by container+key.
|
|
2317
|
+
*
|
|
2318
|
+
* THE CANONICAL STATEMENT of why anything RN created is recorded stickily (the
|
|
2319
|
+
* marker's `scheme.created` carries it forward for the same reason): a re-sync
|
|
2320
|
+
* re-injects from a baseline with only the BUILD SETTINGS reversed, so it finds
|
|
2321
|
+
* whatever the first run created already there and reports creating nothing. Once
|
|
2322
|
+
* RN has created something, that fact has to be carried forward, or the new
|
|
2323
|
+
* marker forgets it and `deinit` leaves an empty `packageReferences` /
|
|
2324
|
+
* `packageProductDependencies` behind and the generated scheme on disk.
|
|
2325
|
+
*
|
|
2326
|
+
* The record licenses removal; it does not order it. `deinit` still checks that
|
|
2327
|
+
* what it is about to remove is RN's (see its step 1 and isGeneratedScheme), so
|
|
2328
|
+
* carrying forward a field the user has since taken over — or deleted by hand —
|
|
2329
|
+
* is safe.
|
|
2330
|
+
*/
|
|
2331
|
+
function mergeCreatedArrayFields(
|
|
2332
|
+
previous /*: ReadonlyArray<CreatedArrayField> */,
|
|
2333
|
+
current /*: ReadonlyArray<CreatedArrayField> */,
|
|
2334
|
+
) /*: Array<CreatedArrayField> */ {
|
|
2335
|
+
const merged = [...previous];
|
|
2336
|
+
for (const field of current) {
|
|
2337
|
+
if (
|
|
2338
|
+
!merged.some(
|
|
2339
|
+
seen => seen.container === field.container && seen.key === field.key,
|
|
2340
|
+
)
|
|
2341
|
+
) {
|
|
2342
|
+
merged.push(field);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return merged;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
1932
2348
|
/**
|
|
1933
2349
|
* Add SPM packages to a user's EXISTING xcodeproj in place. Returns
|
|
1934
2350
|
* {status: 'injected', target} on success, or {status: 'refused', reason}
|
|
@@ -1954,6 +2370,7 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
1954
2370
|
const remote = remotePackageConfig(appRoot);
|
|
1955
2371
|
const hermesCliPath = resolveHermesCliPathSetting(reactNativeRoot);
|
|
1956
2372
|
const generatedSources = readGeneratedSourcesManifest(appRoot);
|
|
2373
|
+
const scriptPhases = readScriptPhasesManifest(appRoot);
|
|
1957
2374
|
const flavoredFrameworks = readFlavoredFrameworksManifest(appRoot).frameworks;
|
|
1958
2375
|
|
|
1959
2376
|
const prevMarker = readMarker(xcodeprojPath);
|
|
@@ -1983,6 +2400,15 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
1983
2400
|
namespacedUUID(plan.rootUuid, 'PBXGroup', SPM_GENERATED_SOURCES_GROUP_ID),
|
|
1984
2401
|
);
|
|
1985
2402
|
}
|
|
2403
|
+
// Same reconciliation for script phases, keyed on the plugin-supplied id.
|
|
2404
|
+
const prevScriptPhases /*: {[string]: string} */ =
|
|
2405
|
+
prevMarker?.scriptPhases ?? {};
|
|
2406
|
+
const currentPhaseIds = new Set(scriptPhases.map(p => p.id));
|
|
2407
|
+
for (const id of Object.keys(prevScriptPhases)) {
|
|
2408
|
+
if (!currentPhaseIds.has(id)) {
|
|
2409
|
+
staleUuids.push(prevScriptPhases[id]);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
1986
2412
|
// Re-apply generated settings from a clean recorded baseline. This removes
|
|
1987
2413
|
// linker entries for plugin frameworks that disappeared and keeps the new
|
|
1988
2414
|
// marker a complete inverse after an idempotent update.
|
|
@@ -2003,6 +2429,7 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2003
2429
|
createdArrayFields,
|
|
2004
2430
|
buildSettingChanges,
|
|
2005
2431
|
generatedSourceUuids,
|
|
2432
|
+
scriptPhaseUuids,
|
|
2006
2433
|
} = injectSpmIntoPbxproj(
|
|
2007
2434
|
base,
|
|
2008
2435
|
{
|
|
@@ -2017,6 +2444,7 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2017
2444
|
hermesCliPath,
|
|
2018
2445
|
generatedSources,
|
|
2019
2446
|
flavoredFrameworks,
|
|
2447
|
+
scriptPhases,
|
|
2020
2448
|
);
|
|
2021
2449
|
|
|
2022
2450
|
const changed = writeIfChanged(pbxprojPath, text);
|
|
@@ -2044,9 +2472,9 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2044
2472
|
// intentional pin, not something to silently re-derive from
|
|
2045
2473
|
// node_modules/react-native/package.json. There is no "clear" verb yet;
|
|
2046
2474
|
// `deinit` (removeSpmInjection) drops the whole marker, including this
|
|
2047
|
-
// field. Read back by readArtifactsVersionOverride (above) so
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2475
|
+
// field. Read back by readArtifactsVersionOverride (above) so a later
|
|
2476
|
+
// flagless `add`/`update`/`download` resolves to the SAME slot, even on a
|
|
2477
|
+
// version-mismatched setup.
|
|
2050
2478
|
const artifactsVersionOverride =
|
|
2051
2479
|
opts.artifactsVersionOverride ??
|
|
2052
2480
|
prevMarker?.artifactsVersionOverride ??
|
|
@@ -2072,16 +2500,26 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2072
2500
|
target: plan.target.name,
|
|
2073
2501
|
targetUuid: plan.target.uuid,
|
|
2074
2502
|
injectedUuids: Array.from(new Set(injectedUuids)).sort(),
|
|
2075
|
-
createdArrayFields
|
|
2503
|
+
createdArrayFields: mergeCreatedArrayFields(
|
|
2504
|
+
prevMarker?.createdArrayFields ?? [],
|
|
2505
|
+
createdArrayFields,
|
|
2506
|
+
),
|
|
2076
2507
|
buildSettingChanges,
|
|
2077
2508
|
// Normalized path → [fileRefUuid, buildFileUuid]. Read back on the next
|
|
2078
2509
|
// `update` to reconcile away entries that left the manifest.
|
|
2079
2510
|
generatedSources: generatedSourceUuids,
|
|
2511
|
+
// Plugin phase id → its PBXShellScriptBuildPhase UUID, reconciled the
|
|
2512
|
+
// same way.
|
|
2513
|
+
scriptPhases: scriptPhaseUuids,
|
|
2080
2514
|
artifactsVersionOverride,
|
|
2081
2515
|
configCommand,
|
|
2082
2516
|
scheme: {
|
|
2083
2517
|
file: schemeResult.file,
|
|
2084
|
-
|
|
2518
|
+
// Sticky — see mergeCreatedArrayFields for why a later sync cannot
|
|
2519
|
+
// observe this for itself.
|
|
2520
|
+
created:
|
|
2521
|
+
schemeResult.status === 'created' ||
|
|
2522
|
+
prevMarker?.scheme?.created === true,
|
|
2085
2523
|
},
|
|
2086
2524
|
},
|
|
2087
2525
|
null,
|
|
@@ -2093,6 +2531,52 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2093
2531
|
return {status: 'injected', target: plan.target.name};
|
|
2094
2532
|
}
|
|
2095
2533
|
|
|
2534
|
+
/** The sync pre-action's script, unescaped, or null when the scheme has none. */
|
|
2535
|
+
function schemePreActionScript(xml /*: string */) /*: ?string */ {
|
|
2536
|
+
const titleIdx = xml.indexOf('title = "Sync SPM Autolinking"');
|
|
2537
|
+
if (titleIdx < 0) {
|
|
2538
|
+
return null;
|
|
2539
|
+
}
|
|
2540
|
+
const marker = 'scriptText = "';
|
|
2541
|
+
const start = xml.indexOf(marker, titleIdx);
|
|
2542
|
+
if (start < 0) {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
const valueStart = start + marker.length;
|
|
2546
|
+
// escapeXmlAttribute maps a literal `"` to `"`, so the next `"` is always
|
|
2547
|
+
// the closing delimiter.
|
|
2548
|
+
const valueEnd = xml.indexOf('"', valueStart);
|
|
2549
|
+
return valueEnd < 0
|
|
2550
|
+
? null
|
|
2551
|
+
: unescapeXmlAttribute(xml.slice(valueStart, valueEnd));
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* Whether `xml` is still, byte for byte, the scheme RN generates for this target
|
|
2556
|
+
* — everything but the pre-action's script, which RN rewrites in place on every
|
|
2557
|
+
* sync and which varies with the app's react-native path, so it cannot be part of
|
|
2558
|
+
* an ownership test.
|
|
2559
|
+
*
|
|
2560
|
+
* `deinit` deletes a scheme the marker says RN created only while this holds. A
|
|
2561
|
+
* user may replace a generated scheme with one of their own under the same name
|
|
2562
|
+
* (same target, so `injectOrCreateScheme` finds and updates it, and the created
|
|
2563
|
+
* record stays), and destroying that is unrecoverable where leaving a scheme
|
|
2564
|
+
* behind is not — so the harmless way of being wrong wins: a generated scheme the
|
|
2565
|
+
* user has since edited leaks, minus its pre-action.
|
|
2566
|
+
*/
|
|
2567
|
+
function isGeneratedScheme(
|
|
2568
|
+
xml /*: string */,
|
|
2569
|
+
appName /*: string */,
|
|
2570
|
+
targetUuid /*: string */,
|
|
2571
|
+
projName /*: string */,
|
|
2572
|
+
) /*: boolean */ {
|
|
2573
|
+
const script = schemePreActionScript(xml);
|
|
2574
|
+
return (
|
|
2575
|
+
script != null &&
|
|
2576
|
+
xml === generateXcscheme(appName, targetUuid, projName, script)
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2096
2580
|
/**
|
|
2097
2581
|
* Remove the "Sync SPM Autolinking" pre-action that addPreActionToScheme added
|
|
2098
2582
|
* to a scheme, and drop the `<PreActions>` wrapper if it is left empty (the
|
|
@@ -2138,6 +2622,25 @@ function removeRecordedBuildSettings(
|
|
|
2138
2622
|
);
|
|
2139
2623
|
}
|
|
2140
2624
|
}
|
|
2625
|
+
const promotedArrayScalars /*: {[string]: string} */ =
|
|
2626
|
+
change.promotedArrayScalars ?? {};
|
|
2627
|
+
for (const key of Object.keys(promotedArrayScalars)) {
|
|
2628
|
+
const current = dict();
|
|
2629
|
+
const originalValue = promotedArrayScalars[key];
|
|
2630
|
+
// A field that is gone was deleted by the user after injection; restoring
|
|
2631
|
+
// it would resurrect it, at the top of the dict, matching neither state.
|
|
2632
|
+
if (
|
|
2633
|
+
current != null &&
|
|
2634
|
+
typeof originalValue === 'string' &&
|
|
2635
|
+
findField(text, current, key) != null
|
|
2636
|
+
) {
|
|
2637
|
+
// Rewriting the whole value is what makes the promotion reversible at
|
|
2638
|
+
// all — its members and its `"$(inherited)"` seed are indistinguishable
|
|
2639
|
+
// from the user's own once folded together. The tradeoff: members the
|
|
2640
|
+
// user hand-added to the promoted array afterwards are discarded.
|
|
2641
|
+
text = setScalarField(text, current, key, originalValue);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2141
2644
|
for (const key of change.createdArrayKeys ?? []) {
|
|
2142
2645
|
const current = dict();
|
|
2143
2646
|
if (current != null) {
|
|
@@ -2199,7 +2702,15 @@ function removeSpmInjection(
|
|
|
2199
2702
|
f.container === 'project'
|
|
2200
2703
|
? findProjectObject(text)
|
|
2201
2704
|
: findObjectByUuid(text, marker.targetUuid);
|
|
2202
|
-
if (obj
|
|
2705
|
+
if (obj == null) {
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
// The record says the field did not exist before RN created it, which is
|
|
2709
|
+
// necessary but not sufficient: anything still in it after our own members
|
|
2710
|
+
// are gone is the user's (their own SPM package, added to the same field),
|
|
2711
|
+
// and dropping the field would orphan it.
|
|
2712
|
+
const field = findField(text, obj, f.key);
|
|
2713
|
+
if (field != null && /^\(\s*\)$/.test(field.value)) {
|
|
2203
2714
|
text = removeField(text, obj, f.key);
|
|
2204
2715
|
}
|
|
2205
2716
|
}
|
|
@@ -2219,7 +2730,8 @@ function removeSpmInjection(
|
|
|
2219
2730
|
writeIfChanged(pbxprojPath, text);
|
|
2220
2731
|
log(`Removed SPM injection from ${path.relative(appRoot, pbxprojPath)}`);
|
|
2221
2732
|
|
|
2222
|
-
// 3. Scheme: delete it if injection created it
|
|
2733
|
+
// 3. Scheme: delete it if injection created it AND still owns its contents,
|
|
2734
|
+
// else strip the pre-action and leave the file (see isGeneratedScheme).
|
|
2223
2735
|
const scheme = marker.scheme;
|
|
2224
2736
|
if (scheme != null && scheme.file != null) {
|
|
2225
2737
|
const schemePath = path.join(
|
|
@@ -2228,11 +2740,21 @@ function removeSpmInjection(
|
|
|
2228
2740
|
'xcschemes',
|
|
2229
2741
|
scheme.file,
|
|
2230
2742
|
);
|
|
2231
|
-
if (
|
|
2232
|
-
fs.rmSync(schemePath, {force: true});
|
|
2233
|
-
} else if (fs.existsSync(schemePath)) {
|
|
2743
|
+
if (fs.existsSync(schemePath)) {
|
|
2234
2744
|
const xml = fs.readFileSync(schemePath, 'utf8');
|
|
2235
|
-
|
|
2745
|
+
const ours =
|
|
2746
|
+
scheme.created === true &&
|
|
2747
|
+
isGeneratedScheme(
|
|
2748
|
+
xml,
|
|
2749
|
+
marker.target,
|
|
2750
|
+
marker.targetUuid,
|
|
2751
|
+
path.basename(xcodeprojPath, '.xcodeproj'),
|
|
2752
|
+
);
|
|
2753
|
+
if (ours) {
|
|
2754
|
+
fs.rmSync(schemePath, {force: true});
|
|
2755
|
+
} else {
|
|
2756
|
+
writeIfChanged(schemePath, removePreActionFromScheme(xml));
|
|
2757
|
+
}
|
|
2236
2758
|
}
|
|
2237
2759
|
}
|
|
2238
2760
|
|
|
@@ -2251,6 +2773,7 @@ module.exports = {
|
|
|
2251
2773
|
ensureStubPackages,
|
|
2252
2774
|
buildSpmDependencyGraph,
|
|
2253
2775
|
spmGraphToEntries,
|
|
2776
|
+
buildPhaseOrder,
|
|
2254
2777
|
planInjection,
|
|
2255
2778
|
injectSpmIntoPbxproj,
|
|
2256
2779
|
injectSpmIntoExistingXcodeproj,
|
|
@@ -2262,5 +2785,6 @@ module.exports = {
|
|
|
2262
2785
|
findInjectedXcodeproj,
|
|
2263
2786
|
readArtifactsVersionOverride,
|
|
2264
2787
|
readPinnedConfigCommand,
|
|
2788
|
+
readScriptPhasesManifest,
|
|
2265
2789
|
SPM_INJECTED_MARKER,
|
|
2266
2790
|
};
|