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
|
@@ -18,6 +18,9 @@ const yargs = require('yargs');
|
|
|
18
18
|
|
|
19
19
|
const LAST_BUILD_FILENAME = 'React-Core-prebuilt/.last_build_configuration';
|
|
20
20
|
|
|
21
|
+
// Not a valid configuration, so finding it means the swap did not finish.
|
|
22
|
+
const REPLACEMENT_IN_PROGRESS = 'in-progress';
|
|
23
|
+
|
|
21
24
|
function validateBuildConfiguration(configuration /*: string */) {
|
|
22
25
|
if (!['Debug', 'Release'].includes(configuration)) {
|
|
23
26
|
throw new Error(`Invalid configuration ${configuration}`);
|
|
@@ -42,10 +45,12 @@ function shouldReplaceRnCoreConfiguration(configuration /*: string */) {
|
|
|
42
45
|
);
|
|
43
46
|
return false;
|
|
44
47
|
}
|
|
48
|
+
return true;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
//
|
|
48
|
-
|
|
51
|
+
// With no marker the on-disk flavor is Debug: the podspec installs the debug
|
|
52
|
+
// tarball (see resolve_podspec_source in scripts/cocoapods/rncore.rb).
|
|
53
|
+
if (configuration === 'Debug') {
|
|
49
54
|
console.log(
|
|
50
55
|
'No previous build detected, but Debug Configuration. No need to replace React-Core-prebuilt',
|
|
51
56
|
);
|
|
@@ -59,7 +64,7 @@ function replaceRNCoreConfiguration(
|
|
|
59
64
|
configuration /*: string */,
|
|
60
65
|
version /*: string */,
|
|
61
66
|
podsRoot /*: string */,
|
|
62
|
-
) {
|
|
67
|
+
) /*: void */ {
|
|
63
68
|
// Filename comes from rncore.rb
|
|
64
69
|
const tarballURLPath = `${podsRoot}/ReactNativeCore-artifacts/reactnative-core-${version.toLowerCase()}-${configuration.toLowerCase()}.tar.gz`;
|
|
65
70
|
|
|
@@ -73,18 +78,6 @@ function replaceRNCoreConfiguration(
|
|
|
73
78
|
const tmpExtractDir = path.join(tmpDir, 'React-Core-prebuilt');
|
|
74
79
|
fs.mkdirSync(tmpExtractDir, {recursive: true});
|
|
75
80
|
|
|
76
|
-
// Preserve Expo-generated modulemap before replacing directories
|
|
77
|
-
const useFrameworksModulemapName = 'React-use-frameworks.modulemap';
|
|
78
|
-
const useFrameworksModulemapPath = path.join(
|
|
79
|
-
finalLocation,
|
|
80
|
-
useFrameworksModulemapName,
|
|
81
|
-
);
|
|
82
|
-
let savedModulemap = null;
|
|
83
|
-
if (fs.existsSync(useFrameworksModulemapPath)) {
|
|
84
|
-
console.log('Preserving', useFrameworksModulemapName);
|
|
85
|
-
savedModulemap = fs.readFileSync(useFrameworksModulemapPath);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
81
|
try {
|
|
89
82
|
console.log('Extracting the tarball to temp dir', tarballURLPath);
|
|
90
83
|
const result = spawnSync(
|
|
@@ -110,98 +103,30 @@ function replaceRNCoreConfiguration(
|
|
|
110
103
|
);
|
|
111
104
|
}
|
|
112
105
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const src = path.join(tmpExtractDir, direntName);
|
|
134
|
-
const dst = path.join(finalLocation, direntName);
|
|
135
|
-
const mvResult = spawnSync('mv', [src, dst], {stdio: 'inherit'});
|
|
136
|
-
if (mvResult.status !== 0) {
|
|
137
|
-
// Fallback: copy recursively then remove source
|
|
138
|
-
console.log(`mv failed for ${direntName}, falling back to cp -R`);
|
|
139
|
-
const cpResult = spawnSync('cp', ['-R', src, dst], {
|
|
140
|
-
stdio: 'inherit',
|
|
141
|
-
});
|
|
142
|
-
if (cpResult.status !== 0) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
`cp fallback failed with exit code ${cpResult.status}`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
106
|
+
// Replace only the compiled framework. Headers/ is flattened from
|
|
107
|
+
// ReactNativeHeaders by the podspec prepare_command, and the prebuild
|
|
108
|
+
// compose job emits one set of those headers for both configurations, so a
|
|
109
|
+
// config switch leaves them identical. Leaving them alone keeps
|
|
110
|
+
// Headers/module.modulemap — which consumers activate through
|
|
111
|
+
// -fmodule-map-file — in place for the whole build; deleting and recreating
|
|
112
|
+
// it mid-build lets a concurrent dependency scan miss it, and the React
|
|
113
|
+
// module then precompiles without it (#57803).
|
|
114
|
+
const dest = path.join(finalLocation, 'React.xcframework');
|
|
115
|
+
console.log('Replacing', dest);
|
|
116
|
+
fs.rmSync(dest, {force: true, recursive: true});
|
|
117
|
+
const mvResult = spawnSync('mv', [xcfwPath, dest], {stdio: 'inherit'});
|
|
118
|
+
if (mvResult.status !== 0) {
|
|
119
|
+
// Fallback: copy recursively then remove source
|
|
120
|
+
console.log('mv failed for React.xcframework, falling back to cp -R');
|
|
121
|
+
const cpResult = spawnSync('cp', ['-R', xcfwPath, dest], {
|
|
122
|
+
stdio: 'inherit',
|
|
123
|
+
});
|
|
124
|
+
if (cpResult.status !== 0) {
|
|
125
|
+
throw new Error(`cp fallback failed with exit code ${cpResult.status}`);
|
|
147
126
|
}
|
|
148
127
|
}
|
|
149
|
-
|
|
150
|
-
// The podspec prepare_command flattens ReactNativeHeaders' headers into a
|
|
151
|
-
// top-level Headers/ dir, but it does not re-run on a config swap. Mirror
|
|
152
|
-
// it here: re-flatten the headers (identical across slices) and drop the
|
|
153
|
-
// now-redundant xcframework so $(PODS_ROOT)/React-Core-prebuilt/Headers
|
|
154
|
-
// keeps resolving <react/...>, <yoga/...>, etc.
|
|
155
|
-
//
|
|
156
|
-
// Fail closed when the swapped-in tarball lacks ReactNativeHeaders: the
|
|
157
|
-
// directory purge above already deleted the previous Headers/, so
|
|
158
|
-
// continuing silently would leave the injected -fmodule-map-file flag
|
|
159
|
-
// dangling and break every <react/...> include only on a config switch —
|
|
160
|
-
// with no pointer to the version-skewed artifact that caused it.
|
|
161
|
-
const rnhXcfw = path.join(finalLocation, 'ReactNativeHeaders.xcframework');
|
|
162
|
-
if (!fs.existsSync(rnhXcfw)) {
|
|
163
|
-
throw new Error(
|
|
164
|
-
`ReactNativeHeaders.xcframework not found in the extracted tarball at ${finalLocation}. ` +
|
|
165
|
-
'The downloaded artifact predates the headers-spec layout (or is incomplete); ' +
|
|
166
|
-
'use a prebuilt tarball matching this react-native version.',
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
const slice = fs
|
|
170
|
-
.readdirSync(rnhXcfw, {withFileTypes: true})
|
|
171
|
-
.find(
|
|
172
|
-
dirent =>
|
|
173
|
-
dirent.isDirectory() &&
|
|
174
|
-
fs.existsSync(path.join(rnhXcfw, dirent.name.toString(), 'Headers')),
|
|
175
|
-
);
|
|
176
|
-
if (!slice) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
`No slice with a Headers directory found inside ${rnhXcfw}.`,
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
const headersDest = path.join(finalLocation, 'Headers');
|
|
182
|
-
fs.rmSync(headersDest, {force: true, recursive: true});
|
|
183
|
-
const cpHeaders = spawnSync(
|
|
184
|
-
'cp',
|
|
185
|
-
['-R', path.join(rnhXcfw, slice.name.toString(), 'Headers'), headersDest],
|
|
186
|
-
{stdio: 'inherit'},
|
|
187
|
-
);
|
|
188
|
-
if (cpHeaders.status !== 0) {
|
|
189
|
-
throw new Error(
|
|
190
|
-
`Flattening ReactNativeHeaders failed with exit code ${cpHeaders.status}`,
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
fs.rmSync(rnhXcfw, {force: true, recursive: true});
|
|
194
128
|
} finally {
|
|
195
|
-
// Clean up temp directory
|
|
196
129
|
fs.rmSync(tmpDir, {force: true, recursive: true});
|
|
197
|
-
|
|
198
|
-
// Restore Expo-generated modulemap after directory replacement.
|
|
199
|
-
// Runs in finally so it is not skipped if mv/cp partially fails.
|
|
200
|
-
if (savedModulemap != null) {
|
|
201
|
-
const restoredPath = path.join(finalLocation, useFrameworksModulemapName);
|
|
202
|
-
fs.writeFileSync(restoredPath, savedModulemap);
|
|
203
|
-
console.log('Restored', useFrameworksModulemapName);
|
|
204
|
-
}
|
|
205
130
|
}
|
|
206
131
|
}
|
|
207
132
|
|
|
@@ -210,6 +135,10 @@ function updateLastBuildConfiguration(configuration /*: string */) {
|
|
|
210
135
|
fs.writeFileSync(LAST_BUILD_FILENAME, configuration);
|
|
211
136
|
}
|
|
212
137
|
|
|
138
|
+
function markReplacementInProgress() /*: void */ {
|
|
139
|
+
fs.writeFileSync(LAST_BUILD_FILENAME, REPLACEMENT_IN_PROGRESS);
|
|
140
|
+
}
|
|
141
|
+
|
|
213
142
|
function main(
|
|
214
143
|
configuration /*: string */,
|
|
215
144
|
version /*: string */,
|
|
@@ -219,37 +148,48 @@ function main(
|
|
|
219
148
|
validateVersion(version);
|
|
220
149
|
|
|
221
150
|
if (!shouldReplaceRnCoreConfiguration(configuration)) {
|
|
151
|
+
// A fresh install leaves no marker; record the flavor we skipped on.
|
|
152
|
+
if (!fs.existsSync(LAST_BUILD_FILENAME)) {
|
|
153
|
+
updateLastBuildConfiguration(configuration);
|
|
154
|
+
}
|
|
222
155
|
return;
|
|
223
156
|
}
|
|
224
157
|
|
|
158
|
+
// Invalidate before touching the framework so an interrupted swap is
|
|
159
|
+
// detectable on the next run.
|
|
160
|
+
markReplacementInProgress();
|
|
225
161
|
replaceRNCoreConfiguration(configuration, version, podsRoot);
|
|
226
162
|
updateLastBuildConfiguration(configuration);
|
|
227
163
|
console.log('Done replacing React Native prebuilt');
|
|
228
164
|
}
|
|
229
165
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
166
|
+
if (require.main === module) {
|
|
167
|
+
// This script is executed in the Pods folder, which is usually not synched to Github, so it should be ok
|
|
168
|
+
const argv = yargs
|
|
169
|
+
.option('c', {
|
|
170
|
+
alias: 'configuration',
|
|
171
|
+
description:
|
|
172
|
+
'Configuration to use to download the right React-Core prebuilt version. Allowed values are "Debug" and "Release".',
|
|
173
|
+
})
|
|
174
|
+
.option('r', {
|
|
175
|
+
alias: 'reactNativeVersion',
|
|
176
|
+
description:
|
|
177
|
+
'The Version of React Native associated with the React-Core prebuilt tarball.',
|
|
178
|
+
})
|
|
179
|
+
.option('p', {
|
|
180
|
+
alias: 'podsRoot',
|
|
181
|
+
description: 'The path to the Pods root folder',
|
|
182
|
+
})
|
|
183
|
+
.usage('Usage: $0 -c Debug -r <version> -p <path/to/react-native>').argv;
|
|
184
|
+
|
|
185
|
+
// $FlowFixMe[prop-missing]
|
|
186
|
+
const configuration = argv.configuration;
|
|
187
|
+
// $FlowFixMe[prop-missing]
|
|
188
|
+
const version = argv.reactNativeVersion;
|
|
189
|
+
// $FlowFixMe[prop-missing]
|
|
190
|
+
const podsRoot = argv.podsRoot;
|
|
191
|
+
|
|
192
|
+
main(configuration, version, podsRoot);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {replaceRNCoreConfiguration};
|
|
@@ -44,8 +44,10 @@
|
|
|
44
44
|
* directing you to `--deintegrate`).
|
|
45
45
|
*
|
|
46
46
|
* Options:
|
|
47
|
-
* --version <ver> React Native version
|
|
48
|
-
*
|
|
47
|
+
* --version <ver> React Native version. Pinned into
|
|
48
|
+
* .spm-injected.json and reused by later runs
|
|
49
|
+
* until a new one is passed (default: the
|
|
50
|
+
* resolved node_modules/react-native version).
|
|
49
51
|
* --yes Skip the dirty-pbxproj confirmation prompt.
|
|
50
52
|
* [add] --xcodeproj <path> Which .xcodeproj to inject into (when several).
|
|
51
53
|
* [add] --product-name <name> Which app target to inject into (when several).
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
* must contain debug/ and release/ cache slots.
|
|
56
58
|
* [advanced] --download <auto|skip|force> Artifact policy (default: auto).
|
|
57
59
|
* [advanced] --skip-codegen Skip the react-native codegen step.
|
|
60
|
+
* [advanced] --config-command <json> Override the autolinking config command.
|
|
58
61
|
*
|
|
59
62
|
* Steps performed (add/update):
|
|
60
63
|
* 1. react-native codegen → build/generated/ios/ + install SPM codegen template
|
|
@@ -83,14 +86,20 @@ const {
|
|
|
83
86
|
} = require('./spm/generate-spm-autolinking');
|
|
84
87
|
const {
|
|
85
88
|
generateAutolinkingConfig,
|
|
89
|
+
parseConfigCommandJson,
|
|
90
|
+
readEnvConfigCommand,
|
|
91
|
+
resolveEnvConfigCommand,
|
|
86
92
|
} = require('./spm/generate-spm-autolinking-config');
|
|
87
93
|
const {main: generatePackage} = require('./spm/generate-spm-package');
|
|
88
94
|
const {findSourcePath} = require('./spm/generate-spm-package');
|
|
89
95
|
const {
|
|
96
|
+
SPM_INJECTED_MARKER,
|
|
90
97
|
cleanupDanglingJavaScriptCoreRef,
|
|
91
98
|
cleanupLeftoverPodsGroup,
|
|
92
99
|
findInjectedXcodeproj,
|
|
93
100
|
injectSpmIntoExistingXcodeproj,
|
|
101
|
+
readArtifactsVersionOverride,
|
|
102
|
+
readPinnedConfigCommand,
|
|
94
103
|
removeSpmInjection,
|
|
95
104
|
} = require('./spm/generate-spm-xcodeproj');
|
|
96
105
|
const {scaffoldAll} = require('./spm/scaffold-package-swift');
|
|
@@ -147,7 +156,7 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
|
|
|
147
156
|
.option('version', {
|
|
148
157
|
type: 'string',
|
|
149
158
|
describe:
|
|
150
|
-
'React Native version (e.g. 0.80.0). Defaults to the version in node_modules/react-native/package.json',
|
|
159
|
+
'React Native version (e.g. 0.80.0). Sticks: later runs reuse it until you pass a new one. Defaults to the version in node_modules/react-native/package.json',
|
|
151
160
|
})
|
|
152
161
|
.option('yes', {
|
|
153
162
|
type: 'boolean',
|
|
@@ -187,6 +196,11 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
|
|
|
187
196
|
default: false,
|
|
188
197
|
describe: '[advanced] Skip the react-native codegen step',
|
|
189
198
|
})
|
|
199
|
+
.option('config-command', {
|
|
200
|
+
type: 'string',
|
|
201
|
+
describe:
|
|
202
|
+
'[advanced] JSON array of the argv used to generate autolinking.json, overriding the default @react-native-community/cli config command. Also settable via RCT_SPM_AUTOLINKING_CONFIG_COMMAND. Either way `add`/`update` remembers the value in .spm-injected.json, so later runs and Xcode builds reuse it. Example: \'["npx","expo-modules-autolinking","react-native-config","--json","--platform","ios"]\'',
|
|
203
|
+
})
|
|
190
204
|
.usage(
|
|
191
205
|
'Usage: $0 [action] [options]\n\nSets up Swift Package Manager support in a React Native app.',
|
|
192
206
|
)
|
|
@@ -214,6 +228,10 @@ function parseArgs(argv /*: Array<string> */) /*: SetupArgs */ {
|
|
|
214
228
|
version: parsed.version ?? null,
|
|
215
229
|
artifacts: parsed.artifacts ?? null,
|
|
216
230
|
skipCodegen: parsed['skip-codegen'],
|
|
231
|
+
configCommand:
|
|
232
|
+
parsed['config-command'] != null
|
|
233
|
+
? parseConfigCommandJson(parsed['config-command'], '--config-command')
|
|
234
|
+
: null,
|
|
217
235
|
downloadPolicy: parsed.download,
|
|
218
236
|
productName: parsed['product-name'] ?? null,
|
|
219
237
|
xcodeprojPath: parsed.xcodeproj ?? null,
|
|
@@ -362,19 +380,31 @@ function resolveReactNativeRoot(
|
|
|
362
380
|
return reactNativeRoot;
|
|
363
381
|
}
|
|
364
382
|
|
|
383
|
+
// Explicit `--version` → the version an earlier `--version` pinned into the
|
|
384
|
+
// injection marker → node_modules/react-native/package.json. The pin makes
|
|
385
|
+
// `--version` stick for later flagless runs, which would otherwise re-point the
|
|
386
|
+
// project at a different artifact slot than the one it was wired to.
|
|
365
387
|
function determineVersion(
|
|
366
388
|
args /*: SetupArgs */,
|
|
367
389
|
reactNativeRoot /*: string */,
|
|
390
|
+
appRoot /*: string */,
|
|
368
391
|
) /*: string */ {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
392
|
+
if (args.version != null) {
|
|
393
|
+
return args.version;
|
|
394
|
+
}
|
|
395
|
+
const pinned = readArtifactsVersionOverride(appRoot);
|
|
396
|
+
if (pinned != null) {
|
|
397
|
+
log(
|
|
398
|
+
`Using version ${pinned} pinned in ${SPM_INJECTED_MARKER} by an earlier ` +
|
|
399
|
+
'--version. Pass --version to change it.',
|
|
374
400
|
);
|
|
375
|
-
|
|
401
|
+
return pinned;
|
|
376
402
|
}
|
|
377
|
-
|
|
403
|
+
// $FlowFixMe[incompatible-type] JSON.parse returns any
|
|
404
|
+
const pkgJson /*: {version: string} */ = JSON.parse(
|
|
405
|
+
fs.readFileSync(path.join(reactNativeRoot, 'package.json'), 'utf8'),
|
|
406
|
+
);
|
|
407
|
+
return pkgJson.version;
|
|
378
408
|
}
|
|
379
409
|
|
|
380
410
|
function runCodegenStep(
|
|
@@ -427,7 +457,7 @@ async function runScaffold(
|
|
|
427
457
|
// a comment — that's how SPM's manifest hash bumps on slot transitions.
|
|
428
458
|
let cacheSlotLabel /*: ?string */ = null;
|
|
429
459
|
try {
|
|
430
|
-
const rawVersion =
|
|
460
|
+
const rawVersion = determineVersion(args, reactNativeRoot, appRoot);
|
|
431
461
|
const slotVersion = await resolveCacheSlotVersion(rawVersion);
|
|
432
462
|
cacheSlotLabel = `${slotVersion}/dual-flavor`;
|
|
433
463
|
} catch {
|
|
@@ -833,6 +863,7 @@ async function setupXcodeproj(
|
|
|
833
863
|
// (injectSpmIntoExistingXcodeproj preserves it — see
|
|
834
864
|
// generate-spm-xcodeproj.js).
|
|
835
865
|
artifactsVersionOverride: args.version ?? null,
|
|
866
|
+
configCommand: resolveConfigCommandToPin(args),
|
|
836
867
|
});
|
|
837
868
|
if (result.status !== 'injected') {
|
|
838
869
|
logError(`SPM injection failed: ${result.reason}`);
|
|
@@ -892,6 +923,88 @@ function logNextSteps(
|
|
|
892
923
|
log('To remove SPM later: `npx react-native spm deinit`');
|
|
893
924
|
}
|
|
894
925
|
|
|
926
|
+
// The autolinking config command for this run: an explicit `--config-command`
|
|
927
|
+
// first, then the value a previous `add`/`update` pinned into the injection
|
|
928
|
+
// marker. undefined means "no explicit command", which is what makes
|
|
929
|
+
// generateAutolinkingConfig fall back to RCT_SPM_AUTOLINKING_CONFIG_COMMAND and
|
|
930
|
+
// then to the built-in default — so the pin has to be WITHHELD while the env
|
|
931
|
+
// var is set, or a stale pin would outrank a developer's env override.
|
|
932
|
+
function resolveExplicitConfigCommand(
|
|
933
|
+
args /*: SetupArgs */,
|
|
934
|
+
appRoot /*: string */,
|
|
935
|
+
) /*: Array<string> | void */ {
|
|
936
|
+
if (args.configCommand != null) {
|
|
937
|
+
return args.configCommand;
|
|
938
|
+
}
|
|
939
|
+
if (readEnvConfigCommand() != null) {
|
|
940
|
+
return undefined;
|
|
941
|
+
}
|
|
942
|
+
const pinned = readPinnedConfigCommand(appRoot);
|
|
943
|
+
if (pinned == null) {
|
|
944
|
+
return undefined;
|
|
945
|
+
}
|
|
946
|
+
log(
|
|
947
|
+
`Autolinking config command (pinned in ${SPM_INJECTED_MARKER}): ` +
|
|
948
|
+
pinned.join(' '),
|
|
949
|
+
);
|
|
950
|
+
return pinned;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// The command to record in the injection marker. The env var is resolved here
|
|
954
|
+
// too, because the Xcode build phase inherits neither the flag nor the shell
|
|
955
|
+
// that set it — an env-only override that went unpinned would leave the build
|
|
956
|
+
// re-deriving autolinking.json with the default command. null pins nothing and
|
|
957
|
+
// preserves any earlier pin. An invalid env value throws, as the flag does,
|
|
958
|
+
// though `add` has already failed closed on it by this point.
|
|
959
|
+
function resolveConfigCommandToPin(
|
|
960
|
+
args /*: SetupArgs */,
|
|
961
|
+
) /*: ?Array<string> */ {
|
|
962
|
+
return args.configCommand ?? resolveEnvConfigCommand();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Generate autolinking.json, failing closed on a config-command error.
|
|
966
|
+
//
|
|
967
|
+
// generateAutolinkingConfig throws ONLY when the config command itself fails —
|
|
968
|
+
// a non-zero exit, unparseable output, or a config missing
|
|
969
|
+
// project.ios.sourceDir. Swallowing that (the old behavior) let the run proceed
|
|
970
|
+
// and emit an empty Autolinked package, which only surfaced much later as an
|
|
971
|
+
// inscrutable `unable to resolve module dependency` at build time. Instead we
|
|
972
|
+
// set process.exitCode = 2 (a hard Xcode build-phase error, matching the
|
|
973
|
+
// RemoteVersionError path) and return null so the caller stops.
|
|
974
|
+
//
|
|
975
|
+
// A genuinely native-module-free app does NOT reach the error path: its command
|
|
976
|
+
// exits 0 with valid, empty-dependency JSON, so generateAutolinkingConfig
|
|
977
|
+
// returns normally and the empty-package path downstream stays valid.
|
|
978
|
+
function generateAutolinkingConfigOrFailClosed(
|
|
979
|
+
opts /*: {
|
|
980
|
+
projectRoot: string,
|
|
981
|
+
configCommand?: Array<string>,
|
|
982
|
+
generate?: typeof generateAutolinkingConfig,
|
|
983
|
+
} */,
|
|
984
|
+
) /*: ?AutolinkingConfigResult */ {
|
|
985
|
+
const generate = opts.generate ?? generateAutolinkingConfig;
|
|
986
|
+
try {
|
|
987
|
+
return generate({
|
|
988
|
+
projectRoot: opts.projectRoot,
|
|
989
|
+
configCommand: opts.configCommand,
|
|
990
|
+
});
|
|
991
|
+
} catch (e) {
|
|
992
|
+
logError(
|
|
993
|
+
`Failed to generate autolinking.json: ${e.message}\n` +
|
|
994
|
+
'The autolinking config command failed. If this app replaces ' +
|
|
995
|
+
'@react-native-community/cli autolinking (e.g. an Expo app), set ' +
|
|
996
|
+
'RCT_SPM_AUTOLINKING_CONFIG_COMMAND (or pass --config-command) to a ' +
|
|
997
|
+
'JSON argv array whose command prints the React Native CLI config, ' +
|
|
998
|
+
'e.g. \'["npx","expo-modules-autolinking","react-native-config",' +
|
|
999
|
+
'"--json","--platform","ios"]\'. An earlier `add`/`update` may also ' +
|
|
1000
|
+
`have pinned a command in ${SPM_INJECTED_MARKER}; re-run with ` +
|
|
1001
|
+
'--config-command to replace a stale one.',
|
|
1002
|
+
);
|
|
1003
|
+
process.exitCode = 2;
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
895
1008
|
async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
|
|
896
1009
|
let appRoot = process.cwd();
|
|
897
1010
|
const projectRoot = findProjectRoot(appRoot);
|
|
@@ -981,22 +1094,22 @@ async function main(argv /*:: ?: Array<string> */) /*: Promise<void> */ {
|
|
|
981
1094
|
let autolinkingConfigResult /*: ?AutolinkingConfigResult */ = null;
|
|
982
1095
|
if (needsCliConfig) {
|
|
983
1096
|
log('Generating autolinking.json (CLI config)...');
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
);
|
|
1097
|
+
autolinkingConfigResult = generateAutolinkingConfigOrFailClosed({
|
|
1098
|
+
projectRoot,
|
|
1099
|
+
configCommand: resolveExplicitConfigCommand(args, appRoot),
|
|
1100
|
+
});
|
|
1101
|
+
if (autolinkingConfigResult == null) {
|
|
1102
|
+
// Fail closed: the config command errored and the helper already set
|
|
1103
|
+
// process.exitCode = 2. Stop rather than emit an empty Autolinked package.
|
|
1104
|
+
return;
|
|
993
1105
|
}
|
|
1106
|
+
log(`Wrote ${path.relative(appRoot, autolinkingConfigResult.outputPath)}`);
|
|
994
1107
|
}
|
|
995
1108
|
const reactNativeRoot = resolveReactNativeRoot(
|
|
996
1109
|
autolinkingConfigResult,
|
|
997
1110
|
projectRoot,
|
|
998
1111
|
);
|
|
999
|
-
const version = determineVersion(args, reactNativeRoot);
|
|
1112
|
+
const version = determineVersion(args, reactNativeRoot, appRoot);
|
|
1000
1113
|
log(`React Native version: ${version}`);
|
|
1001
1114
|
|
|
1002
1115
|
// Resolve remote SPM mode ONCE up front. remotePackageConfig throws
|
|
@@ -1158,8 +1271,13 @@ if (require.main === module) {
|
|
|
1158
1271
|
module.exports = {
|
|
1159
1272
|
main,
|
|
1160
1273
|
detectStandardRnLayoutRedirect,
|
|
1274
|
+
determineVersion,
|
|
1161
1275
|
findInjectedXcodeproj,
|
|
1276
|
+
generateAutolinkingConfigOrFailClosed,
|
|
1277
|
+
parseArgs,
|
|
1162
1278
|
resolveAction,
|
|
1279
|
+
resolveConfigCommandToPin,
|
|
1280
|
+
resolveExplicitConfigCommand,
|
|
1163
1281
|
shouldAutoDeintegrate,
|
|
1164
1282
|
ensureBothArtifactFlavors,
|
|
1165
1283
|
};
|
|
@@ -69,6 +69,14 @@
|
|
|
69
69
|
* // dir add/remove there re-triggers the sync. Relative/empty/non-string
|
|
70
70
|
* // entries are dropped with a warning; a non-array is ignored.
|
|
71
71
|
* watchPaths: ['/abs/path/Package.swift', '/abs/dir'],
|
|
72
|
+
* // Build-time shell phases on the app target — SwiftPM has no
|
|
73
|
+
* // `script_phase`. Only id/name/script are required; `id` is the
|
|
74
|
+
* // ledger key and UUID seed, `position` defaults to 'end'. Recorded
|
|
75
|
+
* // to `.spm-plugin-script-phases.json`, from which the `spm add`/
|
|
76
|
+
* // `update` injector emits a PBXShellScriptBuildPhase per entry.
|
|
77
|
+
* // Malformed entries and duplicate ids are fatal.
|
|
78
|
+
* scriptPhases: [{id, name, script, position, inputPaths, outputPaths,
|
|
79
|
+
* alwaysOutOfDate}],
|
|
72
80
|
* };
|
|
73
81
|
* };
|
|
74
82
|
*
|
|
@@ -76,12 +84,14 @@
|
|
|
76
84
|
* the merge, so regeneration stays deterministic and idempotent.
|
|
77
85
|
*/
|
|
78
86
|
|
|
87
|
+
const {isValidScriptPhaseId, isValidScriptPhaseName} = require('./spm-utils');
|
|
79
88
|
const path = require('path');
|
|
80
89
|
|
|
81
90
|
/*:: import type {
|
|
82
91
|
AutolinkedDep,
|
|
83
92
|
PluginContext,
|
|
84
93
|
PluginResult,
|
|
94
|
+
PluginScriptPhase,
|
|
85
95
|
DiscoveredPlugin,
|
|
86
96
|
} from './spm-types';
|
|
87
97
|
*/
|
|
@@ -117,7 +127,7 @@ function discoverPlugins(
|
|
|
117
127
|
);
|
|
118
128
|
}
|
|
119
129
|
const pluginPath = path.resolve(dep.root, rel);
|
|
120
|
-
let fn
|
|
130
|
+
let fn /*: unknown */ = null;
|
|
121
131
|
try {
|
|
122
132
|
// $FlowFixMe[unsupported-syntax] dynamic require by computed path
|
|
123
133
|
fn = require(pluginPath);
|
|
@@ -149,6 +159,12 @@ function discoverPlugins(
|
|
|
149
159
|
return found;
|
|
150
160
|
}
|
|
151
161
|
|
|
162
|
+
const isNonEmptyString = (value /*: unknown */) /*: boolean */ =>
|
|
163
|
+
typeof value === 'string' && value.length > 0;
|
|
164
|
+
|
|
165
|
+
const isOptionalStringList = (value /*: unknown */) /*: boolean */ =>
|
|
166
|
+
value == null || (Array.isArray(value) && value.every(isNonEmptyString));
|
|
167
|
+
|
|
152
168
|
/**
|
|
153
169
|
* Invoke discovered plugins and merge their results. Each plugin gets the same
|
|
154
170
|
* context. Fail-closed: a throwing plugin aborts (named), and a malformed
|
|
@@ -169,13 +185,15 @@ function invokePlugins(
|
|
|
169
185
|
const flavoredFrameworks /*: Array<{id: string, frameworkName: string, linkage: 'dynamic', flavors: {debug: string, release: string}}> */ =
|
|
170
186
|
[];
|
|
171
187
|
const watchPaths /*: Array<string> */ = [];
|
|
188
|
+
const scriptPhases /*: Array<PluginScriptPhase> */ = [];
|
|
172
189
|
const seenPackages /*: Set<string> */ = new Set();
|
|
173
190
|
const seenProducts /*: Set<string> */ = new Set();
|
|
174
191
|
const seenFrameworkIds /*: Set<string> */ = new Set();
|
|
175
192
|
const seenFrameworkNames /*: Set<string> */ = new Set();
|
|
193
|
+
const seenScriptPhaseIds /*: Set<string> */ = new Set();
|
|
176
194
|
|
|
177
195
|
for (const {depName, pluginPath, plugin} of plugins) {
|
|
178
|
-
let result
|
|
196
|
+
let result /*: unknown */ = null;
|
|
179
197
|
try {
|
|
180
198
|
result = plugin(context);
|
|
181
199
|
} catch (e) {
|
|
@@ -203,11 +221,18 @@ function invokePlugins(
|
|
|
203
221
|
const rawFrameworks = result.flavoredFrameworks ?? [];
|
|
204
222
|
// $FlowFixMe[incompatible-use]
|
|
205
223
|
const rawWatch = result.watchPaths ?? [];
|
|
224
|
+
// $FlowFixMe[incompatible-use]
|
|
225
|
+
const rawScriptPhases = result.scriptPhases ?? [];
|
|
206
226
|
if (!Array.isArray(rawFrameworks)) {
|
|
207
227
|
throw new Error(
|
|
208
228
|
`react-native spm: '${depName}' returned a non-array flavoredFrameworks.`,
|
|
209
229
|
);
|
|
210
230
|
}
|
|
231
|
+
if (!Array.isArray(rawScriptPhases)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`react-native spm: '${depName}' returned a non-array scriptPhases.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
211
236
|
// Watch paths are best-effort staleness hints, so
|
|
212
237
|
// a non-array is ignored (warn, never fatal).
|
|
213
238
|
if (!Array.isArray(rawWatch)) {
|
|
@@ -314,6 +339,60 @@ function invokePlugins(
|
|
|
314
339
|
}
|
|
315
340
|
watchPaths.push(w);
|
|
316
341
|
}
|
|
342
|
+
// Fatal like flavoredFrameworks, not dropped like watchPaths: a missing
|
|
343
|
+
// phase produces a GREEN build whose generated content was never written.
|
|
344
|
+
for (const phase of rawScriptPhases) {
|
|
345
|
+
if (
|
|
346
|
+
phase == null ||
|
|
347
|
+
!isValidScriptPhaseId(phase.id) ||
|
|
348
|
+
!isNonEmptyString(phase.script) ||
|
|
349
|
+
(phase.position != null &&
|
|
350
|
+
phase.position !== 'beforeCompile' &&
|
|
351
|
+
phase.position !== 'end') ||
|
|
352
|
+
!isOptionalStringList(phase.inputPaths) ||
|
|
353
|
+
!isOptionalStringList(phase.outputPaths) ||
|
|
354
|
+
(phase.alwaysOutOfDate != null &&
|
|
355
|
+
typeof phase.alwaysOutOfDate !== 'boolean')
|
|
356
|
+
) {
|
|
357
|
+
const named = typeof phase?.id === 'string' ? ` '${phase.id}'` : '';
|
|
358
|
+
throw new Error(
|
|
359
|
+
`react-native spm: '${depName}' returned an invalid scriptPhase${named} ` +
|
|
360
|
+
'(need {id (matching /^[@A-Za-z0-9_./-]+$/, and not __proto__, ' +
|
|
361
|
+
'constructor or prototype), name, script} plus optional ' +
|
|
362
|
+
'{position: "beforeCompile" | "end", inputPaths, outputPaths, ' +
|
|
363
|
+
'alwaysOutOfDate}).',
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (!isValidScriptPhaseName(phase.name)) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`react-native spm: '${depName}' returned an invalid scriptPhase name for ` +
|
|
369
|
+
`'${phase.id}' (a name must be a non-empty single-line string — it is ` +
|
|
370
|
+
"the phase's display name in Xcode).",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (seenScriptPhaseIds.has(phase.id)) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`react-native spm: duplicate script phase id '${phase.id}'.`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
seenScriptPhaseIds.add(phase.id);
|
|
379
|
+
const normalized /*: PluginScriptPhase */ = {
|
|
380
|
+
id: phase.id,
|
|
381
|
+
name: phase.name,
|
|
382
|
+
script: phase.script,
|
|
383
|
+
position: phase.position ?? 'end',
|
|
384
|
+
};
|
|
385
|
+
if (phase.inputPaths != null) {
|
|
386
|
+
normalized.inputPaths = [...phase.inputPaths];
|
|
387
|
+
}
|
|
388
|
+
if (phase.outputPaths != null) {
|
|
389
|
+
normalized.outputPaths = [...phase.outputPaths];
|
|
390
|
+
}
|
|
391
|
+
if (phase.alwaysOutOfDate != null) {
|
|
392
|
+
normalized.alwaysOutOfDate = phase.alwaysOutOfDate;
|
|
393
|
+
}
|
|
394
|
+
scriptPhases.push(normalized);
|
|
395
|
+
}
|
|
317
396
|
}
|
|
318
397
|
|
|
319
398
|
return {
|
|
@@ -322,6 +401,7 @@ function invokePlugins(
|
|
|
322
401
|
generatedSources,
|
|
323
402
|
flavoredFrameworks,
|
|
324
403
|
watchPaths,
|
|
404
|
+
scriptPhases,
|
|
325
405
|
};
|
|
326
406
|
}
|
|
327
407
|
|