Repository: facebook/react
Pull request: React `version` field should match package.json

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.

Changed files:
filename: packages/react/src/__tests__/ReactVersion-test.js
patch: @@ -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);
+  }
+});

filename: scripts/rollup/build-all-release-channels.js
patch: @@ -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);
+  }
 }
 
 /**