Repository: facebook/react
Pull request: Update build script to automatically generate RCs

RC releases are a special kind of prerelease build because unlike canaries we shouldn't publish new RCs from any commit on `main`, only when we intentionally bump the RC number. But they are still prerelases — like canary and experimental releases, they should use exact version numbers in their dependencies (no ^).

We only need to generate these builds during the RC phase, i.e. when the canary channel label is set to "rc".

Example of resulting package.json output:

```json
{
  "name": "react-dom",
  "version": "19.0.0-rc.0",
  "dependencies": {
    "scheduler": "0.25.0-rc.0"
  },
  "peerDependencies": {
    "react": "19.0.0-rc.0"
  }
}
```

https://react-builds.vercel.app/prs/29736/files/oss-stable-rc/react-dom/package.json

Changed files:
filename: ReactVersions.js
patch: @@ -28,6 +28,10 @@ const ReactVersion = '19.0.0';
 // npm dist tags used during publish, refer to .circleci/config.yml.
 const canaryChannelLabel = 'rc';
 
+// If the canaryChannelLabel is "rc", the build pipeline will use this to build
+// an RC version of the packages.
+const rcNumber = 0;
+
 const stablePackages = {
   'eslint-plugin-react-hooks': '5.1.0',
   'jest-react': '0.16.0',
@@ -53,6 +57,7 @@ const experimentalPackages = [];
 module.exports = {
   ReactVersion,
   canaryChannelLabel,
+  rcNumber,
   stablePackages,
   experimentalPackages,
 };

filename: scripts/release/shared-commands/download-build-artifacts.js
patch: @@ -50,6 +50,8 @@ const run = async ({build, cwd, releaseChannel}) => {
     sourceDir = 'oss-stable';
   } else if (releaseChannel === 'experimental') {
     sourceDir = 'oss-experimental';
+  } else if (releaseChannel === 'rc') {
+    sourceDir = 'oss-stable-rc';
   } else if (releaseChannel === 'latest') {
     sourceDir = 'oss-stable-semver';
   } else {

filename: scripts/release/shared-commands/parse-params.js
patch: @@ -50,10 +50,11 @@ module.exports = async () => {
   if (
     channel !== 'experimental' &&
     channel !== 'stable' &&
+    channel !== 'rc' &&
     channel !== 'latest'
   ) {
     console.error(
-      theme.error`Invalid release channel (-r) "${channel}". Must be "stable", "experimental", or "latest".`
+      theme.error`Invalid release channel (-r) "${channel}". Must be "stable", "experimental", "rc", or "latest".`
     );
     process.exit(1);
   }

filename: scripts/rollup/build-all-release-channels.js
patch: @@ -13,6 +13,7 @@ const {
   stablePackages,
   experimentalPackages,
   canaryChannelLabel,
+  rcNumber,
 } = require('../../ReactVersions');
 
 // Runs the build script for both stable and experimental release channels,
@@ -118,6 +119,13 @@ function processStable(buildDir) {
     // Identical to `oss-stable` but with real, semver versions. This is what
     // will get published to @latest.
     shell.cp('-r', buildDir + '/node_modules', buildDir + '/oss-stable-semver');
+    if (canaryChannelLabel === 'rc') {
+      // During the RC phase, we also generate an RC build that pins to exact
+      // versions but does not include a SHA, e.g. `19.0.0-rc.0`. This is purely
+      // for signaling purposes — aside from the version, it's no different from
+      // the corresponding canary.
+      shell.cp('-r', buildDir + '/node_modules', buildDir + '/oss-stable-rc');
+    }
 
     const defaultVersionIfNotFound = '0.0.0' + '-' + sha + '-' + dateString;
     const versionsMap = new Map();
@@ -141,6 +149,25 @@ function processStable(buildDir) {
       ReactVersion + '-' + canaryChannelLabel + '-' + sha + '-' + dateString
     );
 
+    if (canaryChannelLabel === 'rc') {
+      const rcVersionsMap = new Map();
+      for (const moduleName in stablePackages) {
+        const version = stablePackages[moduleName];
+        rcVersionsMap.set(moduleName, version + `-rc.${rcNumber}`);
+      }
+      updatePackageVersions(
+        buildDir + '/oss-stable-rc',
+        rcVersionsMap,
+        defaultVersionIfNotFound,
+        // For RCs, we pin to exact versions, like we do for canaries.
+        true
+      );
+      updatePlaceholderReactVersionInCompiledArtifacts(
+        buildDir + '/oss-stable-rc',
+        ReactVersion
+      );
+    }
+
     // Now do the semver ones
     const semverVersionsMap = new Map();
     for (const moduleName in stablePackages) {
@@ -151,6 +178,7 @@ function processStable(buildDir) {
       buildDir + '/oss-stable-semver',
       semverVersionsMap,
       defaultVersionIfNotFound,
+      // Use ^ only for non-prerelease versions
       false
     );
     updatePlaceholderReactVersionInCompiledArtifacts(