From 22edb9f777d27369fd2c1fad378f74e237b6dfd3 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 26 Apr 2022 16:28:48 -0400 Subject: [PATCH] React `version` field should match package.json (#24445) The `version` field exported by the React package currently corresponds to the `@next` release for that build. This updates the build script to output the same version that is used in the package.json file. It works by doing a find-and-replace of the React version after the build has completed. This is a bit weird but it saves us from having to build the `@next` and `@latest` releases separately; they are identical except for the version numbers. --- .../react/src/__tests__/ReactVersion-test.js | 26 ++++++ scripts/rollup/build-all-release-channels.js | 85 ++++++++++++++----- 2 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 packages/react/src/__tests__/ReactVersion-test.js diff --git a/packages/react/src/__tests__/ReactVersion-test.js b/packages/react/src/__tests__/ReactVersion-test.js new file mode 100644 index 0000000000000..c163756e17c4f --- /dev/null +++ b/packages/react/src/__tests__/ReactVersion-test.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +// NOTE: Intentionally using the dynamic version of the `gate` pragma to opt out +// the negative test behavior. If this test happens to pass when running +// against files source, that's fine. But all we care about is the behavior of +// the build artifacts. +// TODO: The experimental builds have a different version at runtime than +// the package.json because DevTools uses it for feature detection. Consider +// some other way of handling that. +test('ReactVersion matches package.json', () => { + if (gate(flags => flags.build && flags.stable && !flags.www)) { + const React = require('react'); + const packageJSON = require('react/package.json'); + expect(React.version).toBe(packageJSON.version); + } +}); diff --git a/scripts/rollup/build-all-release-channels.js b/scripts/rollup/build-all-release-channels.js index 4468a778d4510..652d744eaaac3 100644 --- a/scripts/rollup/build-all-release-channels.js +++ b/scripts/rollup/build-all-release-channels.js @@ -38,6 +38,20 @@ if (dateString.startsWith("'")) { dateString = dateString.substr(1, 8); } +// Build the artifacts using a placeholder React version. We'll then do a string +// replace to swap it with the correct version per release channel. +// +// The placeholder version is the same format that the "next" channel uses +const PLACEHOLDER_REACT_VERSION = + ReactVersion + '-' + nextChannelLabel + '-' + sha + '-' + dateString; + +// TODO: We should inject the React version using a build-time parameter +// instead of overwriting the source files. +fs.writeFileSync( + './packages/shared/ReactVersion.js', + `export default '${PLACEHOLDER_REACT_VERSION}';\n` +); + if (process.env.CIRCLE_NODE_TOTAL) { // In CI, we use multiple concurrent processes. Allocate half the processes to // build the stable channel, and the other half for experimental. Override @@ -48,33 +62,21 @@ if (process.env.CIRCLE_NODE_TOTAL) { if (index < halfTotal) { const nodeTotal = halfTotal; const nodeIndex = index; - updateTheReactVersionThatDevToolsReads( - ReactVersion + '-' + sha + '-' + dateString - ); buildForChannel('stable', nodeTotal, nodeIndex); processStable('./build'); } else { const nodeTotal = total - halfTotal; const nodeIndex = index - halfTotal; - updateTheReactVersionThatDevToolsReads( - ReactVersion + '-experimental-' + sha + '-' + dateString - ); buildForChannel('experimental', nodeTotal, nodeIndex); processExperimental('./build'); } } else { // Running locally, no concurrency. Move each channel's build artifacts into // a temporary directory so that they don't conflict. - updateTheReactVersionThatDevToolsReads( - ReactVersion + '-' + sha + '-' + dateString - ); buildForChannel('stable', '', ''); const stableDir = tmp.dirSync().name; crossDeviceRenameSync('./build', stableDir); processStable(stableDir); - updateTheReactVersionThatDevToolsReads( - ReactVersion + '-experimental-' + sha + '-' + dateString - ); buildForChannel('experimental', '', ''); const experimentalDir = tmp.dirSync().name; crossDeviceRenameSync('./build', experimentalDir); @@ -129,6 +131,10 @@ function processStable(buildDir) { true ); fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-stable'); + updatePlaceholderReactVersionInCompiledArtifacts( + buildDir + '/oss-stable', + ReactVersion + '-' + nextChannelLabel + '-' + sha + '-' + dateString + ); // Now do the semver ones const semverVersionsMap = new Map(); @@ -142,6 +148,10 @@ function processStable(buildDir) { defaultVersionIfNotFound, false ); + updatePlaceholderReactVersionInCompiledArtifacts( + buildDir + '/oss-stable-semver', + ReactVersion + ); } if (fs.existsSync(buildDir + '/facebook-www')) { @@ -152,6 +162,10 @@ function processStable(buildDir) { fs.renameSync(filePath, filePath.replace('.js', '.classic.js')); } } + updatePlaceholderReactVersionInCompiledArtifacts( + buildDir + '/facebook-www', + ReactVersion + '-www-classic-' + sha + '-' + dateString + ); } if (fs.existsSync(buildDir + '/sizes')) { @@ -162,7 +176,7 @@ function processStable(buildDir) { function processExperimental(buildDir, version) { if (fs.existsSync(buildDir + '/node_modules')) { const defaultVersionIfNotFound = - '0.0.0' + '-' + 'experimental' + '-' + sha + '-' + dateString; + '0.0.0' + '-experimental-' + sha + '-' + dateString; const versionsMap = new Map(); for (const moduleName in stablePackages) { versionsMap.set(moduleName, defaultVersionIfNotFound); @@ -177,6 +191,13 @@ function processExperimental(buildDir, version) { true ); fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-experimental'); + updatePlaceholderReactVersionInCompiledArtifacts( + buildDir + '/oss-experimental', + // TODO: The npm version for experimental releases does not include the + // React version, but the runtime version does so that DevTools can do + // feature detection. Decide what to do about this later. + ReactVersion + '-experimental-' + sha + '-' + dateString + ); } if (fs.existsSync(buildDir + '/facebook-www')) { @@ -187,6 +208,10 @@ function processExperimental(buildDir, version) { fs.renameSync(filePath, filePath.replace('.js', '.modern.js')); } } + updatePlaceholderReactVersionInCompiledArtifacts( + buildDir + '/facebook-www', + ReactVersion + '-www-modern-' + sha + '-' + dateString + ); } if (fs.existsSync(buildDir + '/sizes')) { @@ -278,14 +303,32 @@ function updatePackageVersions( } } -function updateTheReactVersionThatDevToolsReads(version) { - // Overwrite the ReactVersion module before the build script runs so that it - // is included in the final bundles. This only runs in CI, so it's fine to - // edit the source file. - fs.writeFileSync( - './packages/shared/ReactVersion.js', - `export default '${version}';\n` - ); +function updatePlaceholderReactVersionInCompiledArtifacts( + artifactsDirectory, + newVersion +) { + // Update the version of React in the compiled artifacts by searching for + // the placeholder string and replacing it with a new one. + const artifactFilenames = String( + spawnSync('grep', [ + '-lr', + PLACEHOLDER_REACT_VERSION, + '--', + artifactsDirectory, + ]).stdout + ) + .trim() + .split('\n') + .filter(filename => filename.endsWith('.js')); + + for (const artifactFilename of artifactFilenames) { + const originalText = fs.readFileSync(artifactFilename, 'utf8'); + const replacedText = originalText.replace( + PLACEHOLDER_REACT_VERSION, + newVersion + ); + fs.writeFileSync(artifactFilename, replacedText); + } } /**