react-native-tvos 0.87.0-0rc3 → 0.87.1-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/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +2 -0
- package/Libraries/Components/ScrollView/ScrollView.d.ts +26 -0
- package/Libraries/Components/ScrollView/ScrollView.js +32 -0
- package/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +2 -0
- package/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +11 -0
- package/Libraries/Components/ScrollView/processScrollAnimationEasing.js +40 -0
- package/Libraries/Components/TV/TVFocusGuideView.js +31 -0
- package/Libraries/Components/TV/useFocusEnterLeave.js +112 -0
- package/Libraries/Core/ReactNativeVersion.js +2 -2
- package/README.md +17 -0
- package/React/Base/RCTVersion.m +2 -2
- package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +30 -0
- 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 +2 -2
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt +36 -13
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt +13 -0
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt +25 -3
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +62 -0
- package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt +13 -0
- package/ReactCommon/cxxreact/ReactNativeVersion.h +3 -3
- package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp +26 -0
- package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h +3 -0
- package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTFontUtils.mm +1 -1
- package/package.json +11 -9
- package/scripts/cocoapods/rncore.rb +1 -0
- package/scripts/cocoapods/rndependencies.rb +31 -25
- package/scripts/replace-rncore-version.js +69 -129
- package/scripts/setup-apple-spm.js +139 -21
- package/scripts/spm/autolinking-plugins.js +82 -2
- package/scripts/spm/download-spm-artifacts.js +67 -9
- package/scripts/spm/expand-spm-dependencies.js +249 -30
- package/scripts/spm/generate-spm-autolinking-config.js +62 -2
- package/scripts/spm/generate-spm-autolinking.js +158 -41
- package/scripts/spm/generate-spm-package.js +30 -18
- package/scripts/spm/generate-spm-xcodeproj.js +641 -75
- package/scripts/spm/scaffold-package-swift.js +68 -27
- package/scripts/spm/spm-pbxproj.js +158 -28
- package/scripts/spm/spm-types.js +32 -3
- package/scripts/spm/spm-utils.js +117 -2
- package/sdks/.hermesv1version +1 -1
- package/sdks/hermes-engine/version.properties +1 -1
- package/types/public/ReactNativeTVTypes.d.ts +40 -1
- 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 -451
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
const {readFlavoredFrameworksManifest} = require('./flavored-frameworks');
|
|
25
|
+
const {parseConfigCommandJson} = require('./generate-spm-autolinking-config');
|
|
25
26
|
const {
|
|
26
27
|
addArrayMembers,
|
|
27
28
|
addArrayStringValues,
|
|
29
|
+
commentSafe,
|
|
28
30
|
ensureScalarField,
|
|
29
31
|
findApplicationTargets,
|
|
30
32
|
findField,
|
|
@@ -41,13 +43,25 @@ const {
|
|
|
41
43
|
removeObjectByUuid,
|
|
42
44
|
serializeEntry,
|
|
43
45
|
setScalarField,
|
|
46
|
+
uuidComment,
|
|
44
47
|
} = require('./spm-pbxproj');
|
|
45
|
-
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');
|
|
46
59
|
const fs = require('fs');
|
|
47
60
|
const path = require('path');
|
|
48
61
|
|
|
49
62
|
/*:: import type {
|
|
50
63
|
FlavoredFrameworkManifestEntry,
|
|
64
|
+
PluginScriptPhase,
|
|
51
65
|
XcframeworkSlice,
|
|
52
66
|
} from './spm-types'; */
|
|
53
67
|
|
|
@@ -72,6 +86,17 @@ const SPM_GENERATED_SOURCES_MANIFEST = path.join(
|
|
|
72
86
|
'.spm-plugin-generated-sources.json',
|
|
73
87
|
);
|
|
74
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
|
+
|
|
75
100
|
// The single navigator group all injected generated sources are parented under
|
|
76
101
|
// (created on first use). Its namespacedUUID id + display name.
|
|
77
102
|
const SPM_GENERATED_SOURCES_GROUP_ID = 'SPMGeneratedSources';
|
|
@@ -91,36 +116,21 @@ const GENERATED_SOURCE_FILE_TYPES /*: {[string]: string} */ = {
|
|
|
91
116
|
// resolve the product dependencies — SPM doesn't expose transitive products.
|
|
92
117
|
const SPM_PRODUCT_PACKAGES /*: Array<{product: string, packagePath: string, packageName: string}> */ =
|
|
93
118
|
[
|
|
94
|
-
{
|
|
95
|
-
product
|
|
96
|
-
packagePath: 'build/xcframeworks',
|
|
97
|
-
packageName: 'ReactNative',
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
product: 'ReactNativeHeaders',
|
|
101
|
-
packagePath: 'build/xcframeworks',
|
|
102
|
-
packageName: 'ReactNative',
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
product: 'ReactNativeDependenciesHeaders',
|
|
119
|
+
...REACT_NATIVE_PRODUCTS.map(product => ({
|
|
120
|
+
product,
|
|
106
121
|
packagePath: 'build/xcframeworks',
|
|
107
|
-
packageName:
|
|
108
|
-
},
|
|
122
|
+
packageName: REACT_NATIVE_PACKAGE_NAME,
|
|
123
|
+
})),
|
|
109
124
|
{
|
|
110
|
-
product:
|
|
125
|
+
product: AUTOLINKED_PACKAGE_NAME,
|
|
111
126
|
packagePath: 'build/generated/autolinking',
|
|
112
|
-
packageName:
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
product: 'ReactCodegen',
|
|
116
|
-
packagePath: 'build/generated/ios',
|
|
117
|
-
packageName: 'React-GeneratedCode',
|
|
127
|
+
packageName: AUTOLINKED_PACKAGE_NAME,
|
|
118
128
|
},
|
|
119
|
-
{
|
|
120
|
-
product
|
|
129
|
+
...REACT_CODEGEN_APP_PRODUCTS.map(product => ({
|
|
130
|
+
product,
|
|
121
131
|
packagePath: 'build/generated/ios',
|
|
122
|
-
packageName:
|
|
123
|
-
},
|
|
132
|
+
packageName: REACT_CODEGEN_PACKAGE_NAME,
|
|
133
|
+
})),
|
|
124
134
|
];
|
|
125
135
|
|
|
126
136
|
/*::
|
|
@@ -136,7 +146,15 @@ type BuildSettingChange = {
|
|
|
136
146
|
// value), e.g. a ${PODS_ROOT}-anchored REACT_NATIVE_PATH that dangles once
|
|
137
147
|
// CocoaPods is deintegrated. Deinit restores the original.
|
|
138
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},
|
|
139
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};
|
|
140
158
|
// A plugin-contributed source, normalized for pbxproj emission. `path` is
|
|
141
159
|
// SRCROOT-relative when under the app root, else absolute; `sourceTree` is the
|
|
142
160
|
// matching pbxproj token ('SOURCE_ROOT' or '"<absolute>"').
|
|
@@ -264,30 +282,42 @@ function spmGraphToEntries(
|
|
|
264
282
|
return {localRefs, remoteRef, productDeps, buildFiles};
|
|
265
283
|
}
|
|
266
284
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
*/
|
|
272
293
|
function shellScriptPhase(
|
|
273
294
|
phaseUUID /*: string */,
|
|
274
295
|
name /*: string */,
|
|
275
296
|
script /*: string */,
|
|
276
|
-
options /*: {inputPaths?: string, outputPaths?: string} */ = {},
|
|
297
|
+
options /*: {inputPaths?: ?(string | ReadonlyArray<string>), outputPaths?: ?(string | ReadonlyArray<string>), alwaysOutOfDate?: ?boolean, comment?: string} */ = {},
|
|
277
298
|
) /*: {uuid: string, comment: string, fields: {[string]: string}} */ {
|
|
278
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
|
+
};
|
|
279
308
|
return {
|
|
280
309
|
uuid: phaseUUID,
|
|
281
|
-
comment: name,
|
|
310
|
+
comment: options.comment ?? name,
|
|
282
311
|
fields: {
|
|
283
312
|
isa: 'PBXShellScriptBuildPhase',
|
|
313
|
+
...(options.alwaysOutOfDate === true ? {alwaysOutOfDate: '1'} : {}),
|
|
284
314
|
buildActionMask: '2147483647',
|
|
285
315
|
files: empty,
|
|
286
316
|
inputFileListPaths: empty,
|
|
287
|
-
inputPaths: options.inputPaths
|
|
317
|
+
inputPaths: pathList(options.inputPaths),
|
|
288
318
|
name: quoteIfNeeded(name),
|
|
289
319
|
outputFileListPaths: empty,
|
|
290
|
-
outputPaths: options.outputPaths
|
|
320
|
+
outputPaths: pathList(options.outputPaths),
|
|
291
321
|
runOnlyForDeploymentPostprocessing: '0',
|
|
292
322
|
shellPath: '/bin/sh',
|
|
293
323
|
shellScript: quoteIfNeeded(script),
|
|
@@ -467,11 +497,126 @@ ${copies}
|
|
|
467
497
|
`;
|
|
468
498
|
}
|
|
469
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
|
+
|
|
470
615
|
function addBuildPhaseAfter(
|
|
471
616
|
text /*: string */,
|
|
472
617
|
target /*: {bodyOpen: number, bodyClose: number, ...} */,
|
|
473
618
|
afterUuid /*: string */,
|
|
474
|
-
member /*: {uuid: string, comment: string} */,
|
|
619
|
+
member /*: {uuid: string, comment: string, ...} */,
|
|
475
620
|
) /*: string */ {
|
|
476
621
|
const field = findField(text, target, 'buildPhases');
|
|
477
622
|
if (field == null || field.value.includes(member.uuid)) {
|
|
@@ -486,7 +631,7 @@ function addBuildPhaseAfter(
|
|
|
486
631
|
const absoluteStart = field.valueStart + after.index;
|
|
487
632
|
const lineEnd = text.indexOf('\n', absoluteStart + after[0].length);
|
|
488
633
|
const indent = after[2];
|
|
489
|
-
const line = `\n${indent}${member.uuid}
|
|
634
|
+
const line = `\n${indent}${member.uuid}${uuidComment(member.comment)},`;
|
|
490
635
|
return text.slice(0, lineEnd) + line + text.slice(lineEnd);
|
|
491
636
|
}
|
|
492
637
|
|
|
@@ -723,6 +868,18 @@ function escapeXmlAttribute(s /*: string */) /*: string */ {
|
|
|
723
868
|
.replace(/'/g, ''');
|
|
724
869
|
}
|
|
725
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
|
+
|
|
726
883
|
function generateXcscheme(
|
|
727
884
|
appName /*: string */,
|
|
728
885
|
targetUUID /*: string */,
|
|
@@ -931,6 +1088,25 @@ const INJECTED_ARRAY_SETTINGS = [
|
|
|
931
1088
|
},
|
|
932
1089
|
];
|
|
933
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
|
+
|
|
934
1110
|
/** The XCBuildConfiguration UUIDs of a target (via its buildConfigurationList). */
|
|
935
1111
|
function targetBuildConfigUuids(
|
|
936
1112
|
text /*: string */,
|
|
@@ -1077,7 +1253,8 @@ function injectSpmIntoPbxproj(
|
|
|
1077
1253
|
hermesCliPath /*: ?string */ = null,
|
|
1078
1254
|
generatedSources /*: ReadonlyArray<GeneratedSource> */ = [],
|
|
1079
1255
|
flavoredFrameworks /*: ReadonlyArray<FlavoredFrameworkManifestEntry> */ = [],
|
|
1080
|
-
|
|
1256
|
+
scriptPhases /*: ReadonlyArray<PluginScriptPhase> */ = [],
|
|
1257
|
+
) /*: {text: string, injectedUuids: Array<string>, createdArrayFields: Array<CreatedArrayField>, buildSettingChanges: Array<BuildSettingChange>, generatedSourceUuids: {[string]: Array<string>}, scriptPhaseUuids: {[string]: string}} */ {
|
|
1081
1258
|
let text = input;
|
|
1082
1259
|
const mkUuid = (section /*: string */, id /*: string */) =>
|
|
1083
1260
|
namespacedUUID(plan.rootUuid, section, id);
|
|
@@ -1110,10 +1287,7 @@ function injectSpmIntoPbxproj(
|
|
|
1110
1287
|
insertObjects('XCSwiftPackageProductDependency', entries.productDeps);
|
|
1111
1288
|
insertObjects('PBXBuildFile', entries.buildFiles);
|
|
1112
1289
|
|
|
1113
|
-
|
|
1114
|
-
// can remove the whole field and land byte-identical to the original.
|
|
1115
|
-
const createdArrayFields /*: Array<{container: 'project' | 'target', key: string}> */ =
|
|
1116
|
-
[];
|
|
1290
|
+
const createdArrayFields /*: Array<CreatedArrayField> */ = [];
|
|
1117
1291
|
|
|
1118
1292
|
// 2. packageReferences on the PBXProject.
|
|
1119
1293
|
const pkgRefMembers = [
|
|
@@ -1304,9 +1478,14 @@ function injectSpmIntoPbxproj(
|
|
|
1304
1478
|
const fileRefUuid = mkUuid('PBXFileReference', `gensrc:${src.path}`);
|
|
1305
1479
|
const buildFileUuid = mkUuid('PBXBuildFile', `gensrc:${src.path}`);
|
|
1306
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`;
|
|
1307
1486
|
fileRefs.push({
|
|
1308
1487
|
uuid: fileRefUuid,
|
|
1309
|
-
comment:
|
|
1488
|
+
comment: label,
|
|
1310
1489
|
fields: {
|
|
1311
1490
|
isa: 'PBXFileReference',
|
|
1312
1491
|
lastKnownFileType: src.fileType,
|
|
@@ -1317,17 +1496,14 @@ function injectSpmIntoPbxproj(
|
|
|
1317
1496
|
});
|
|
1318
1497
|
buildFiles.push({
|
|
1319
1498
|
uuid: buildFileUuid,
|
|
1320
|
-
comment:
|
|
1499
|
+
comment: inSources,
|
|
1321
1500
|
fields: {
|
|
1322
1501
|
isa: 'PBXBuildFile',
|
|
1323
|
-
fileRef: `${fileRefUuid}
|
|
1502
|
+
fileRef: `${fileRefUuid}${uuidComment(label)}`,
|
|
1324
1503
|
},
|
|
1325
1504
|
});
|
|
1326
|
-
sourcesMembers.push({
|
|
1327
|
-
|
|
1328
|
-
comment: `${src.name} in Sources`,
|
|
1329
|
-
});
|
|
1330
|
-
groupChildren.push({uuid: fileRefUuid, comment: src.name});
|
|
1505
|
+
sourcesMembers.push({uuid: buildFileUuid, comment: inSources});
|
|
1506
|
+
groupChildren.push({uuid: fileRefUuid, comment: label});
|
|
1331
1507
|
}
|
|
1332
1508
|
insertObjects('PBXFileReference', fileRefs);
|
|
1333
1509
|
insertObjects('PBXBuildFile', buildFiles);
|
|
@@ -1387,12 +1563,87 @@ function injectSpmIntoPbxproj(
|
|
|
1387
1563
|
}
|
|
1388
1564
|
}
|
|
1389
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
|
+
|
|
1390
1640
|
return {
|
|
1391
1641
|
text,
|
|
1392
1642
|
injectedUuids,
|
|
1393
1643
|
createdArrayFields,
|
|
1394
1644
|
buildSettingChanges,
|
|
1395
1645
|
generatedSourceUuids,
|
|
1646
|
+
scriptPhaseUuids,
|
|
1396
1647
|
};
|
|
1397
1648
|
}
|
|
1398
1649
|
|
|
@@ -1450,6 +1701,28 @@ function resolveHermesCliPathSetting(
|
|
|
1450
1701
|
}
|
|
1451
1702
|
}
|
|
1452
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
|
+
|
|
1453
1726
|
function mergeReactBuildSettings(
|
|
1454
1727
|
input /*: string */,
|
|
1455
1728
|
configUuid /*: string */,
|
|
@@ -1492,9 +1765,13 @@ function mergeReactBuildSettings(
|
|
|
1492
1765
|
};
|
|
1493
1766
|
const createdArrayKeys /*: Array<string> */ = [];
|
|
1494
1767
|
const appendedArrayValues /*: {[string]: Array<string>} */ = {};
|
|
1768
|
+
const promotedArrayScalars /*: {[string]: string} */ = {};
|
|
1495
1769
|
const createdScalars /*: Array<string> */ = [];
|
|
1496
1770
|
const arraySettings = [
|
|
1497
1771
|
...INJECTED_ARRAY_SETTINGS,
|
|
1772
|
+
...(flavorForBuildConfiguration(configurationName) === 'debug'
|
|
1773
|
+
? DEBUG_ARRAY_SETTINGS
|
|
1774
|
+
: []),
|
|
1498
1775
|
...frameworkArrayBuildSettings(flavoredFrameworks),
|
|
1499
1776
|
];
|
|
1500
1777
|
for (const {key, values} of arraySettings) {
|
|
@@ -1503,15 +1780,41 @@ function mergeReactBuildSettings(
|
|
|
1503
1780
|
continue;
|
|
1504
1781
|
}
|
|
1505
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;
|
|
1506
1791
|
if (existing == null) {
|
|
1507
1792
|
createdArrayKeys.push(key);
|
|
1508
1793
|
} else {
|
|
1509
|
-
const
|
|
1510
|
-
|
|
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) {
|
|
1511
1805
|
appendedArrayValues[key] = fresh;
|
|
1512
1806
|
}
|
|
1513
1807
|
}
|
|
1808
|
+
const beforeAdd = text;
|
|
1514
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
|
+
}
|
|
1515
1818
|
}
|
|
1516
1819
|
const replacedScalars /*: {[string]: string} */ = {};
|
|
1517
1820
|
for (const {key, value} of scalars) {
|
|
@@ -1570,6 +1873,9 @@ function mergeReactBuildSettings(
|
|
|
1570
1873
|
appendedArrayValues,
|
|
1571
1874
|
createdScalars,
|
|
1572
1875
|
replacedScalars,
|
|
1876
|
+
...(Object.keys(promotedArrayScalars).length > 0
|
|
1877
|
+
? {promotedArrayScalars}
|
|
1878
|
+
: {}),
|
|
1573
1879
|
},
|
|
1574
1880
|
};
|
|
1575
1881
|
}
|
|
@@ -1799,13 +2105,13 @@ function readGeneratedSourcesManifest(
|
|
|
1799
2105
|
appRoot /*: string */,
|
|
1800
2106
|
) /*: Array<GeneratedSource> */ {
|
|
1801
2107
|
const manifestPath = path.join(appRoot, SPM_GENERATED_SOURCES_MANIFEST);
|
|
1802
|
-
let raw
|
|
2108
|
+
let raw /*: string */ = '';
|
|
1803
2109
|
try {
|
|
1804
2110
|
raw = fs.readFileSync(manifestPath, 'utf8');
|
|
1805
2111
|
} catch {
|
|
1806
2112
|
return [];
|
|
1807
2113
|
}
|
|
1808
|
-
let entries
|
|
2114
|
+
let entries /*: unknown */ = null;
|
|
1809
2115
|
try {
|
|
1810
2116
|
entries = JSON.parse(raw);
|
|
1811
2117
|
} catch {
|
|
@@ -1837,6 +2143,86 @@ function readGeneratedSourcesManifest(
|
|
|
1837
2143
|
return out;
|
|
1838
2144
|
}
|
|
1839
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
|
+
|
|
1840
2226
|
/**
|
|
1841
2227
|
* Read the `.spm-injected.json` marker of a previously-injected project, or
|
|
1842
2228
|
* null when absent/unreadable. Used to reconcile generated sources on `update`
|
|
@@ -1844,7 +2230,7 @@ function readGeneratedSourcesManifest(
|
|
|
1844
2230
|
*/
|
|
1845
2231
|
function readMarker(
|
|
1846
2232
|
xcodeprojPath /*: string */,
|
|
1847
|
-
) /*: ?{generatedSources?: {[string]: Array<string>}, artifactsVersionOverride?: ?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}, ...} */ {
|
|
1848
2234
|
const markerPath = path.join(xcodeprojPath, SPM_INJECTED_MARKER);
|
|
1849
2235
|
try {
|
|
1850
2236
|
// $FlowFixMe[incompatible-return] JSON.parse returns any
|
|
@@ -1857,10 +2243,11 @@ function readMarker(
|
|
|
1857
2243
|
// Returns the `*.xcodeproj` under `appRoot` carrying a `.spm-injected.json`
|
|
1858
2244
|
// marker (the user-owned project SPM packages were injected into in place),
|
|
1859
2245
|
// or null when none has been injected yet. Pure fs reads — safe for the
|
|
1860
|
-
//
|
|
1861
|
-
//
|
|
2246
|
+
// marker readers below, and for callers that only locate the project (setup-
|
|
2247
|
+
// apple-spm.js's action defaulting and `deinit`), to call without exercising
|
|
2248
|
+
// any pbxproj-editing machinery.
|
|
1862
2249
|
function findInjectedXcodeproj(appRoot /*: string */) /*: string | null */ {
|
|
1863
|
-
let entries
|
|
2250
|
+
let entries /*: Array<{name: string, isDirectory(): boolean}> */ = [];
|
|
1864
2251
|
try {
|
|
1865
2252
|
// $FlowFixMe[incompatible-type] Dirent typing
|
|
1866
2253
|
entries = fs.readdirSync(appRoot, {withFileTypes: true});
|
|
@@ -1884,11 +2271,11 @@ function findInjectedXcodeproj(appRoot /*: string */) /*: string | null */ {
|
|
|
1884
2271
|
* update --version` pinned into the injected xcodeproj's `.spm-injected.json`
|
|
1885
2272
|
* marker (see the field's doc comment in injectSpmIntoExistingXcodeproj
|
|
1886
2273
|
* below), or null when no project is injected yet, no override is pinned, or
|
|
1887
|
-
* the marker can't be read (never throws). Pure fs reads —
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
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.
|
|
1892
2279
|
*/
|
|
1893
2280
|
function readArtifactsVersionOverride(appRoot /*: string */) /*: ?string */ {
|
|
1894
2281
|
const xcodeprojPath = findInjectedXcodeproj(appRoot);
|
|
@@ -1899,13 +2286,72 @@ function readArtifactsVersionOverride(appRoot /*: string */) /*: ?string */ {
|
|
|
1899
2286
|
return typeof override === 'string' && override.length > 0 ? override : null;
|
|
1900
2287
|
}
|
|
1901
2288
|
|
|
2289
|
+
/**
|
|
2290
|
+
* Read the autolinking config command a previous `spm add`/`update` pinned into
|
|
2291
|
+
* the injected xcodeproj's `.spm-injected.json` marker, or null when nothing
|
|
2292
|
+
* usable is pinned. Pure fs reads, like readArtifactsVersionOverride above, but
|
|
2293
|
+
* this one IS wired: setup-apple-spm.js's resolveExplicitConfigCommand reads it
|
|
2294
|
+
* on add/update/scaffold and on the build-time `sync`. Re-validated through the
|
|
2295
|
+
* same parseConfigCommandJson the flag goes through, and never throws, so a
|
|
2296
|
+
* hand-edited marker degrades to the env-var/default command instead of
|
|
2297
|
+
* injecting a bogus argv into the build.
|
|
2298
|
+
*/
|
|
2299
|
+
function readPinnedConfigCommand(appRoot /*: string */) /*: ?Array<string> */ {
|
|
2300
|
+
const xcodeprojPath = findInjectedXcodeproj(appRoot);
|
|
2301
|
+
if (xcodeprojPath == null) {
|
|
2302
|
+
return null;
|
|
2303
|
+
}
|
|
2304
|
+
const pinned = readMarker(xcodeprojPath)?.configCommand;
|
|
2305
|
+
if (pinned == null) {
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
try {
|
|
2309
|
+
return parseConfigCommandJson(JSON.stringify(pinned), SPM_INJECTED_MARKER);
|
|
2310
|
+
} catch {
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
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
|
+
|
|
1902
2348
|
/**
|
|
1903
2349
|
* Add SPM packages to a user's EXISTING xcodeproj in place. Returns
|
|
1904
2350
|
* {status: 'injected', target} on success, or {status: 'refused', reason}
|
|
1905
2351
|
* when the project can't be safely edited (caller surfaces it; fail-loud).
|
|
1906
2352
|
*/
|
|
1907
2353
|
function injectSpmIntoExistingXcodeproj(
|
|
1908
|
-
opts /*: {appRoot: string, reactNativeRoot: string, xcodeprojPath: string, appName?: ?string, artifactsVersionOverride?: ?string} */,
|
|
2354
|
+
opts /*: {appRoot: string, reactNativeRoot: string, xcodeprojPath: string, appName?: ?string, artifactsVersionOverride?: ?string, configCommand?: ?Array<string>} */,
|
|
1909
2355
|
) /*: {status: 'injected', target: string} | {status: 'refused', reason: string} */ {
|
|
1910
2356
|
const {appRoot, reactNativeRoot, xcodeprojPath} = opts;
|
|
1911
2357
|
const pbxprojPath = path.join(xcodeprojPath, 'project.pbxproj');
|
|
@@ -1924,6 +2370,7 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
1924
2370
|
const remote = remotePackageConfig(appRoot);
|
|
1925
2371
|
const hermesCliPath = resolveHermesCliPathSetting(reactNativeRoot);
|
|
1926
2372
|
const generatedSources = readGeneratedSourcesManifest(appRoot);
|
|
2373
|
+
const scriptPhases = readScriptPhasesManifest(appRoot);
|
|
1927
2374
|
const flavoredFrameworks = readFlavoredFrameworksManifest(appRoot).frameworks;
|
|
1928
2375
|
|
|
1929
2376
|
const prevMarker = readMarker(xcodeprojPath);
|
|
@@ -1953,6 +2400,15 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
1953
2400
|
namespacedUUID(plan.rootUuid, 'PBXGroup', SPM_GENERATED_SOURCES_GROUP_ID),
|
|
1954
2401
|
);
|
|
1955
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
|
+
}
|
|
1956
2412
|
// Re-apply generated settings from a clean recorded baseline. This removes
|
|
1957
2413
|
// linker entries for plugin frameworks that disappeared and keeps the new
|
|
1958
2414
|
// marker a complete inverse after an idempotent update.
|
|
@@ -1973,6 +2429,7 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
1973
2429
|
createdArrayFields,
|
|
1974
2430
|
buildSettingChanges,
|
|
1975
2431
|
generatedSourceUuids,
|
|
2432
|
+
scriptPhaseUuids,
|
|
1976
2433
|
} = injectSpmIntoPbxproj(
|
|
1977
2434
|
base,
|
|
1978
2435
|
{
|
|
@@ -1987,6 +2444,7 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
1987
2444
|
hermesCliPath,
|
|
1988
2445
|
generatedSources,
|
|
1989
2446
|
flavoredFrameworks,
|
|
2447
|
+
scriptPhases,
|
|
1990
2448
|
);
|
|
1991
2449
|
|
|
1992
2450
|
const changed = writeIfChanged(pbxprojPath, text);
|
|
@@ -2014,14 +2472,24 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2014
2472
|
// intentional pin, not something to silently re-derive from
|
|
2015
2473
|
// node_modules/react-native/package.json. There is no "clear" verb yet;
|
|
2016
2474
|
// `deinit` (removeSpmInjection) drops the whole marker, including this
|
|
2017
|
-
// field. Read back by readArtifactsVersionOverride (above) so
|
|
2018
|
-
//
|
|
2019
|
-
//
|
|
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.
|
|
2020
2478
|
const artifactsVersionOverride =
|
|
2021
2479
|
opts.artifactsVersionOverride ??
|
|
2022
2480
|
prevMarker?.artifactsVersionOverride ??
|
|
2023
2481
|
null;
|
|
2024
2482
|
|
|
2483
|
+
// Same set-or-preserve contract as the version pin above, for the autolinking
|
|
2484
|
+
// config command `add`/`update` resolved (`--config-command` or
|
|
2485
|
+
// RCT_SPM_AUTOLINKING_CONFIG_COMMAND) — the build-time sync sees neither the
|
|
2486
|
+
// flag nor the developer's shell environment, so without the pin it
|
|
2487
|
+
// re-derives autolinking.json with the default @react-native-community/cli
|
|
2488
|
+
// command and breaks apps that replace it. Read back by
|
|
2489
|
+
// readPinnedConfigCommand (above). No "clear" verb yet either; `deinit` drops
|
|
2490
|
+
// the whole marker, this field with it.
|
|
2491
|
+
const configCommand = opts.configCommand ?? prevMarker?.configCommand ?? null;
|
|
2492
|
+
|
|
2025
2493
|
// Marker: idempotency signal + the exact, reversible record of every edit so
|
|
2026
2494
|
// `deinit` (removeSpmInjection) can undo precisely what was added.
|
|
2027
2495
|
writeIfChanged(
|
|
@@ -2032,15 +2500,26 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2032
2500
|
target: plan.target.name,
|
|
2033
2501
|
targetUuid: plan.target.uuid,
|
|
2034
2502
|
injectedUuids: Array.from(new Set(injectedUuids)).sort(),
|
|
2035
|
-
createdArrayFields
|
|
2503
|
+
createdArrayFields: mergeCreatedArrayFields(
|
|
2504
|
+
prevMarker?.createdArrayFields ?? [],
|
|
2505
|
+
createdArrayFields,
|
|
2506
|
+
),
|
|
2036
2507
|
buildSettingChanges,
|
|
2037
2508
|
// Normalized path → [fileRefUuid, buildFileUuid]. Read back on the next
|
|
2038
2509
|
// `update` to reconcile away entries that left the manifest.
|
|
2039
2510
|
generatedSources: generatedSourceUuids,
|
|
2511
|
+
// Plugin phase id → its PBXShellScriptBuildPhase UUID, reconciled the
|
|
2512
|
+
// same way.
|
|
2513
|
+
scriptPhases: scriptPhaseUuids,
|
|
2040
2514
|
artifactsVersionOverride,
|
|
2515
|
+
configCommand,
|
|
2041
2516
|
scheme: {
|
|
2042
2517
|
file: schemeResult.file,
|
|
2043
|
-
|
|
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,
|
|
2044
2523
|
},
|
|
2045
2524
|
},
|
|
2046
2525
|
null,
|
|
@@ -2052,6 +2531,52 @@ function injectSpmIntoExistingXcodeproj(
|
|
|
2052
2531
|
return {status: 'injected', target: plan.target.name};
|
|
2053
2532
|
}
|
|
2054
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
|
+
|
|
2055
2580
|
/**
|
|
2056
2581
|
* Remove the "Sync SPM Autolinking" pre-action that addPreActionToScheme added
|
|
2057
2582
|
* to a scheme, and drop the `<PreActions>` wrapper if it is left empty (the
|
|
@@ -2097,6 +2622,25 @@ function removeRecordedBuildSettings(
|
|
|
2097
2622
|
);
|
|
2098
2623
|
}
|
|
2099
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
|
+
}
|
|
2100
2644
|
for (const key of change.createdArrayKeys ?? []) {
|
|
2101
2645
|
const current = dict();
|
|
2102
2646
|
if (current != null) {
|
|
@@ -2158,7 +2702,15 @@ function removeSpmInjection(
|
|
|
2158
2702
|
f.container === 'project'
|
|
2159
2703
|
? findProjectObject(text)
|
|
2160
2704
|
: findObjectByUuid(text, marker.targetUuid);
|
|
2161
|
-
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)) {
|
|
2162
2714
|
text = removeField(text, obj, f.key);
|
|
2163
2715
|
}
|
|
2164
2716
|
}
|
|
@@ -2178,7 +2730,8 @@ function removeSpmInjection(
|
|
|
2178
2730
|
writeIfChanged(pbxprojPath, text);
|
|
2179
2731
|
log(`Removed SPM injection from ${path.relative(appRoot, pbxprojPath)}`);
|
|
2180
2732
|
|
|
2181
|
-
// 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).
|
|
2182
2735
|
const scheme = marker.scheme;
|
|
2183
2736
|
if (scheme != null && scheme.file != null) {
|
|
2184
2737
|
const schemePath = path.join(
|
|
@@ -2187,11 +2740,21 @@ function removeSpmInjection(
|
|
|
2187
2740
|
'xcschemes',
|
|
2188
2741
|
scheme.file,
|
|
2189
2742
|
);
|
|
2190
|
-
if (
|
|
2191
|
-
fs.rmSync(schemePath, {force: true});
|
|
2192
|
-
} else if (fs.existsSync(schemePath)) {
|
|
2743
|
+
if (fs.existsSync(schemePath)) {
|
|
2193
2744
|
const xml = fs.readFileSync(schemePath, 'utf8');
|
|
2194
|
-
|
|
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
|
+
}
|
|
2195
2758
|
}
|
|
2196
2759
|
}
|
|
2197
2760
|
|
|
@@ -2210,6 +2773,7 @@ module.exports = {
|
|
|
2210
2773
|
ensureStubPackages,
|
|
2211
2774
|
buildSpmDependencyGraph,
|
|
2212
2775
|
spmGraphToEntries,
|
|
2776
|
+
buildPhaseOrder,
|
|
2213
2777
|
planInjection,
|
|
2214
2778
|
injectSpmIntoPbxproj,
|
|
2215
2779
|
injectSpmIntoExistingXcodeproj,
|
|
@@ -2220,5 +2784,7 @@ module.exports = {
|
|
|
2220
2784
|
removePreActionFromScheme,
|
|
2221
2785
|
findInjectedXcodeproj,
|
|
2222
2786
|
readArtifactsVersionOverride,
|
|
2787
|
+
readPinnedConfigCommand,
|
|
2788
|
+
readScriptPhasesManifest,
|
|
2223
2789
|
SPM_INJECTED_MARKER,
|
|
2224
2790
|
};
|