main
js 527 lines 16.2 KB
Raw
1 'use strict';
2
3 /* eslint-disable no-for-of-loops/no-for-of-loops */
4
5 const fs = require('fs');
6 const fse = require('fs-extra');
7 const {spawnSync} = require('child_process');
8 const path = require('path');
9 const tmp = require('tmp');
10 const shell = require('shelljs');
11 const {
12 ReactVersion,
13 stablePackages,
14 experimentalPackages,
15 canaryChannelLabel,
16 rcNumber,
17 } = require('../../ReactVersions');
18 const yargs = require('yargs');
19 const Bundles = require('./bundles');
20
21 // Runs the build script for both stable and experimental release channels,
22 // by configuring an environment variable.
23
24 const sha = String(spawnSync('git', ['rev-parse', 'HEAD']).stdout).slice(0, 8);
25
26 let dateString = String(
27 spawnSync('git', [
28 'show',
29 '-s',
30 '--no-show-signature',
31 '--format=%cd',
32 '--date=format:%Y%m%d',
33 sha,
34 ]).stdout
35 ).trim();
36
37 // On CI environment, this string is wrapped with quotes '...'s
38 if (dateString.startsWith("'")) {
39 dateString = dateString.slice(1, 9);
40 }
41
42 // Build the artifacts using a placeholder React version. We'll then do a string
43 // replace to swap it with the correct version per release channel.
44 //
45 // The placeholder version is the same format that the "next" channel uses
46 const PLACEHOLDER_REACT_VERSION =
47 ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString;
48
49 // TODO: We should inject the React version using a build-time parameter
50 // instead of overwriting the source files.
51 fs.writeFileSync(
52 './packages/shared/ReactVersion.js',
53 `export default '${PLACEHOLDER_REACT_VERSION}';\n`
54 );
55
56 const argv = yargs.wrap(yargs.terminalWidth()).options({
57 releaseChannel: {
58 alias: 'r',
59 describe: 'Build the given release channel.',
60 requiresArg: true,
61 type: 'string',
62 choices: ['experimental', 'stable'],
63 },
64 index: {
65 alias: 'i',
66 describe: 'Worker id.',
67 requiresArg: true,
68 type: 'number',
69 },
70 total: {
71 alias: 't',
72 describe: 'Total number of workers.',
73 requiresArg: true,
74 type: 'number',
75 },
76 ci: {
77 describe: 'Run tests in CI',
78 requiresArg: false,
79 type: 'boolean',
80 default: false,
81 },
82 type: {
83 describe: `Build the given bundle type. (${Object.values(
84 Bundles.bundleTypes
85 )})`,
86 requiresArg: false,
87 type: 'string',
88 },
89 pretty: {
90 describe: 'Force pretty output.',
91 requiresArg: false,
92 type: 'boolean',
93 },
94 'sync-fbsource': {
95 describe: 'Include to sync build to fbsource.',
96 requiresArg: false,
97 type: 'string',
98 },
99 'sync-www': {
100 describe: 'Include to sync build to www.',
101 requiresArg: false,
102 type: 'string',
103 },
104 'unsafe-partial': {
105 describe: 'Do not clean ./build first.',
106 requiresArg: false,
107 type: 'boolean',
108 },
109 }).argv;
110
111 async function main() {
112 if (argv.ci === true) {
113 buildForChannel(argv.releaseChannel, argv.total, argv.index);
114 switch (argv.releaseChannel) {
115 case 'stable': {
116 processStable('./build');
117 break;
118 }
119 case 'experimental': {
120 processExperimental('./build');
121 break;
122 }
123 default:
124 throw new Error(`Unknown release channel ${argv.releaseChannel}`);
125 }
126 } else {
127 const releaseChannel = argv.releaseChannel;
128 if (releaseChannel === 'stable') {
129 buildForChannel('stable', '', '');
130 processStable('./build');
131 } else if (releaseChannel === 'experimental') {
132 buildForChannel('experimental', '', '');
133 processExperimental('./build');
134 } else {
135 // Running locally, no concurrency. Move each channel's build artifacts into
136 // a temporary directory so that they don't conflict.
137 buildForChannel('stable', '', '');
138 const stableDir = tmp.dirSync().name;
139 crossDeviceRenameSync('./build', stableDir);
140 processStable(stableDir);
141 buildForChannel('experimental', '', '');
142 const experimentalDir = tmp.dirSync().name;
143 crossDeviceRenameSync('./build', experimentalDir);
144 processExperimental(experimentalDir);
145
146 // Then merge the experimental folder into the stable one. processExperimental
147 // will have already removed conflicting files.
148 //
149 // In CI, merging is handled by the GitHub Download Artifacts plugin.
150 mergeDirsSync(experimentalDir + '/', stableDir + '/');
151
152 // Now restore the combined directory back to its original name
153 crossDeviceRenameSync(stableDir, './build');
154 }
155 }
156 }
157
158 function buildForChannel(channel, total, index) {
159 const {status} = spawnSync(
160 'node',
161 ['./scripts/rollup/build.js', ...process.argv.slice(2)],
162 {
163 stdio: ['pipe', process.stdout, process.stderr],
164 env: {
165 ...process.env,
166 RELEASE_CHANNEL: channel,
167 CI_TOTAL: total,
168 CI_INDEX: index,
169 },
170 }
171 );
172
173 if (status !== 0) {
174 // Error of spawned process is already piped to this stderr
175 process.exit(status);
176 }
177 }
178
179 function processStable(buildDir) {
180 if (fs.existsSync(buildDir + '/node_modules')) {
181 // Identical to `oss-stable` but with real, semver versions. This is what
182 // will get published to @latest.
183 shell.cp('-r', buildDir + '/node_modules', buildDir + '/oss-stable-semver');
184 if (canaryChannelLabel === 'rc') {
185 // During the RC phase, we also generate an RC build that pins to exact
186 // versions but does not include a SHA, e.g. `19.0.0-rc.0`. This is purely
187 // for signaling purposes — aside from the version, it's no different from
188 // the corresponding canary.
189 shell.cp('-r', buildDir + '/node_modules', buildDir + '/oss-stable-rc');
190 }
191
192 const defaultVersionIfNotFound = '0.0.0' + '-' + sha + '-' + dateString;
193 const versionsMap = new Map();
194 for (const moduleName in stablePackages) {
195 const version = stablePackages[moduleName];
196 versionsMap.set(
197 moduleName,
198 version + '-' + canaryChannelLabel + '-' + sha + '-' + dateString,
199 defaultVersionIfNotFound
200 );
201 }
202 updatePackageVersions(
203 buildDir + '/node_modules',
204 versionsMap,
205 defaultVersionIfNotFound,
206 true
207 );
208 fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-stable');
209 updatePlaceholderReactVersionInCompiledArtifacts(
210 buildDir + '/oss-stable',
211 ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString
212 );
213
214 if (canaryChannelLabel === 'rc') {
215 const rcVersionsMap = new Map();
216 for (const moduleName in stablePackages) {
217 const version = stablePackages[moduleName];
218 rcVersionsMap.set(moduleName, version + `-rc.${rcNumber}`);
219 }
220 updatePackageVersions(
221 buildDir + '/oss-stable-rc',
222 rcVersionsMap,
223 defaultVersionIfNotFound,
224 // For RCs, we pin to exact versions, like we do for canaries.
225 true
226 );
227 updatePlaceholderReactVersionInCompiledArtifacts(
228 buildDir + '/oss-stable-rc',
229 ReactVersion
230 );
231 }
232
233 const rnVersionString =
234 ReactVersion + '-native-fb-' + sha + '-' + dateString;
235 if (fs.existsSync(buildDir + '/facebook-react-native')) {
236 updatePlaceholderReactVersionInCompiledArtifacts(
237 buildDir + '/facebook-react-native',
238 rnVersionString
239 );
240
241 // Also save a file with the version number.
242 fs.writeFileSync(
243 buildDir + '/facebook-react-native/VERSION_NATIVE_FB',
244 rnVersionString
245 );
246 }
247
248 if (fs.existsSync(buildDir + '/react-native')) {
249 updatePlaceholderReactVersionInCompiledArtifacts(
250 buildDir + '/react-native',
251 rnVersionString,
252 filename => filename.endsWith('.fb.js')
253 );
254
255 updatePlaceholderReactVersionInCompiledArtifacts(
256 buildDir + '/react-native',
257 ReactVersion,
258 filename => !filename.endsWith('.fb.js') && filename.endsWith('.js')
259 );
260 }
261
262 // Now do the semver ones
263 const semverVersionsMap = new Map();
264 for (const moduleName in stablePackages) {
265 const version = stablePackages[moduleName];
266 semverVersionsMap.set(moduleName, version);
267 }
268 updatePackageVersions(
269 buildDir + '/oss-stable-semver',
270 semverVersionsMap,
271 defaultVersionIfNotFound,
272 // Use ^ only for non-prerelease versions
273 false
274 );
275 updatePlaceholderReactVersionInCompiledArtifacts(
276 buildDir + '/oss-stable-semver',
277 ReactVersion
278 );
279 }
280
281 if (fs.existsSync(buildDir + '/facebook-www')) {
282 for (const fileName of fs.readdirSync(buildDir + '/facebook-www')) {
283 const filePath = buildDir + '/facebook-www/' + fileName;
284 const stats = fs.statSync(filePath);
285 if (!stats.isDirectory()) {
286 fs.renameSync(filePath, filePath.replace('.js', '.classic.js'));
287 }
288 }
289 const versionString =
290 ReactVersion + '-www-classic-' + sha + '-' + dateString;
291 updatePlaceholderReactVersionInCompiledArtifacts(
292 buildDir + '/facebook-www',
293 versionString
294 );
295 // Also save a file with the version number
296 fs.writeFileSync(buildDir + '/facebook-www/VERSION_CLASSIC', versionString);
297 }
298
299 if (fs.existsSync(buildDir + '/sizes')) {
300 fs.renameSync(buildDir + '/sizes', buildDir + '/sizes-stable');
301 }
302 }
303
304 function processExperimental(buildDir, version) {
305 if (fs.existsSync(buildDir + '/node_modules')) {
306 const defaultVersionIfNotFound =
307 '0.0.0' + '-experimental-' + sha + '-' + dateString;
308 const versionsMap = new Map();
309 for (const moduleName in stablePackages) {
310 versionsMap.set(moduleName, defaultVersionIfNotFound);
311 }
312 for (const moduleName of experimentalPackages) {
313 versionsMap.set(moduleName, defaultVersionIfNotFound);
314 }
315 updatePackageVersions(
316 buildDir + '/node_modules',
317 versionsMap,
318 defaultVersionIfNotFound,
319 true
320 );
321 fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-experimental');
322 updatePlaceholderReactVersionInCompiledArtifacts(
323 buildDir + '/oss-experimental',
324 // TODO: The npm version for experimental releases does not include the
325 // React version, but the runtime version does so that DevTools can do
326 // feature detection. Decide what to do about this later.
327 ReactVersion + '-experimental-' + sha + '-' + dateString
328 );
329 }
330
331 if (fs.existsSync(buildDir + '/facebook-www')) {
332 for (const fileName of fs.readdirSync(buildDir + '/facebook-www')) {
333 const filePath = buildDir + '/facebook-www/' + fileName;
334 const stats = fs.statSync(filePath);
335 if (!stats.isDirectory()) {
336 fs.renameSync(filePath, filePath.replace('.js', '.modern.js'));
337 }
338 }
339 const versionString =
340 ReactVersion + '-www-modern-' + sha + '-' + dateString;
341 updatePlaceholderReactVersionInCompiledArtifacts(
342 buildDir + '/facebook-www',
343 versionString
344 );
345
346 // Also save a file with the version number
347 fs.writeFileSync(buildDir + '/facebook-www/VERSION_MODERN', versionString);
348 }
349
350 const rnVersionString = ReactVersion + '-native-fb-' + sha + '-' + dateString;
351 if (fs.existsSync(buildDir + '/facebook-react-native')) {
352 updatePlaceholderReactVersionInCompiledArtifacts(
353 buildDir + '/facebook-react-native',
354 rnVersionString
355 );
356 // NOTE: VERSION_NATIVE_FB is written in processStable
357 }
358
359 if (fs.existsSync(buildDir + '/react-native')) {
360 updatePlaceholderReactVersionInCompiledArtifacts(
361 buildDir + '/react-native',
362 rnVersionString,
363 filename => filename.endsWith('.fb.js')
364 );
365
366 updatePlaceholderReactVersionInCompiledArtifacts(
367 buildDir + '/react-native',
368 ReactVersion,
369 filename => !filename.endsWith('.fb.js') && filename.endsWith('.js')
370 );
371 }
372
373 if (fs.existsSync(buildDir + '/sizes')) {
374 fs.renameSync(buildDir + '/sizes', buildDir + '/sizes-experimental');
375 }
376 if (fs.existsSync(buildDir + '/bundle-sizes.json')) {
377 fs.renameSync(
378 buildDir + '/bundle-sizes.json',
379 buildDir + '/bundle-sizes-experimental.json'
380 );
381 }
382
383 // Delete all other artifacts that weren't handled above. We assume they are
384 // duplicates of the corresponding artifacts in the stable channel. Ideally,
385 // the underlying build script should not have produced these files in the
386 // first place.
387 for (const pathName of fs.readdirSync(buildDir)) {
388 if (
389 pathName !== 'oss-experimental' &&
390 pathName !== 'facebook-www' &&
391 pathName !== 'sizes-experimental' &&
392 // Not a duplicate: this worker's shard timings, merged into the build
393 // weights cache by process_artifacts_combined.
394 pathName !== '__shard_timings__'
395 ) {
396 fs.rmSync(path.join(buildDir, pathName), {
397 recursive: true,
398 force: true,
399 });
400 }
401 }
402 }
403
404 function crossDeviceRenameSync(source, destination) {
405 return fse.moveSync(source, destination, {overwrite: true});
406 }
407
408 /*
409 * Grabs the built packages in ${tmp_build_dir}/node_modules and updates the
410 * `version` key in their package.json to 0.0.0-${date}-${commitHash} for the commit
411 * you're building. Also updates the dependencies and peerDependencies
412 * to match this version for all of the 'React' packages
413 * (packages available in this repo).
414 */
415 function updatePackageVersions(
416 modulesDir,
417 versionsMap,
418 defaultVersionIfNotFound,
419 pinToExactVersion
420 ) {
421 for (const moduleName of fs.readdirSync(modulesDir)) {
422 let version = versionsMap.get(moduleName);
423 if (version === undefined) {
424 // TODO: If the module is not in the version map, we should exclude it
425 // from the build artifacts.
426 version = defaultVersionIfNotFound;
427 }
428 const packageJSONPath = path.join(modulesDir, moduleName, 'package.json');
429 const stats = fs.statSync(packageJSONPath);
430 if (stats.isFile()) {
431 const packageInfo = JSON.parse(fs.readFileSync(packageJSONPath));
432
433 // Update version
434 packageInfo.version = version;
435
436 if (packageInfo.dependencies) {
437 for (const dep of Object.keys(packageInfo.dependencies)) {
438 const depVersion = versionsMap.get(dep);
439 if (depVersion !== undefined) {
440 packageInfo.dependencies[dep] = pinToExactVersion
441 ? depVersion
442 : '^' + depVersion;
443 }
444 }
445 }
446 if (packageInfo.peerDependencies) {
447 if (
448 !pinToExactVersion &&
449 (moduleName === 'use-sync-external-store' ||
450 moduleName === 'use-subscription')
451 ) {
452 // use-sync-external-store supports older versions of React, too, so
453 // we don't override to the latest version. We should figure out some
454 // better way to handle this.
455 // TODO: Remove this special case.
456 } else {
457 for (const dep of Object.keys(packageInfo.peerDependencies)) {
458 const depVersion = versionsMap.get(dep);
459 if (depVersion !== undefined) {
460 packageInfo.peerDependencies[dep] = pinToExactVersion
461 ? depVersion
462 : '^' + depVersion;
463 }
464 }
465 }
466 }
467
468 // Write out updated package.json
469 fs.writeFileSync(packageJSONPath, JSON.stringify(packageInfo, null, 2));
470 }
471 }
472 }
473
474 function updatePlaceholderReactVersionInCompiledArtifacts(
475 artifactsDirectory,
476 newVersion,
477 filteringClosure
478 ) {
479 // Update the version of React in the compiled artifacts by searching for
480 // the placeholder string and replacing it with a new one.
481 if (filteringClosure == null) {
482 filteringClosure = filename => filename.endsWith('.js');
483 }
484
485 const artifactFilenames = String(
486 spawnSync('grep', [
487 '-lr',
488 PLACEHOLDER_REACT_VERSION,
489 '--',
490 artifactsDirectory,
491 ]).stdout
492 )
493 .trim()
494 .split('\n')
495 .filter(filteringClosure);
496
497 for (const artifactFilename of artifactFilenames) {
498 const originalText = fs.readFileSync(artifactFilename, 'utf8');
499 const replacedText = originalText.replaceAll(
500 PLACEHOLDER_REACT_VERSION,
501 newVersion
502 );
503 fs.writeFileSync(artifactFilename, replacedText);
504 }
505 }
506
507 /**
508 * cross-platform alternative to `rsync -ar`
509 * @param {string} source
510 * @param {string} destination
511 */
512 function mergeDirsSync(source, destination) {
513 for (const sourceFileBaseName of fs.readdirSync(source)) {
514 const sourceFileName = path.join(source, sourceFileBaseName);
515 const targetFileName = path.join(destination, sourceFileBaseName);
516
517 const sourceFile = fs.statSync(sourceFileName);
518 if (sourceFile.isDirectory()) {
519 fse.ensureDirSync(targetFileName);
520 mergeDirsSync(sourceFileName, targetFileName);
521 } else {
522 fs.copyFileSync(sourceFileName, targetFileName);
523 }
524 }
525 }
526
527 main();