Repository: facebook/react
Pull request: Prepare semver (`@latest`) releases in CI

Now that we track package versions in source, `@latest` builds should be fully reproducible for a given commit. We can prepare the packages in CI and store them as artifacts, the same way we do for `@next` and `@experimental`.

Eventually this can replace the interactive script that we currently use to swap out the version numbers.

The other nice thing about this approach is that we can run tests in CI to verify that the packages are releasable, instead of waiting until right before publish.

I named the output directory `oss-stable-semver`, to distinguish from the `@next` prereleases that are located at `oss-stable`. I don't love this naming. I'd prefer to use the name of the corresponding npm dist tag. I'll do that in a follow-up, though, since the `oss-stable` name is referenced in a handful of places.

Current naming (after this PR):

- `oss-experimental` → `@experimental`
- `oss-stable` → `@next`
- `oss-stable-semver` → `@latest`

Proposed naming (not yet implemented, requires more work):

- `oss-experimental` → `@experimental`
- `oss-next` → `@next`
- `oss-latest` → `@latest`

Changed files:
filename: scripts/rollup/build-all-release-channels.js
patch: @@ -98,9 +98,29 @@ function processStable(buildDir) {
     updatePackageVersions(
       buildDir + '/node_modules',
       versionsMap,
-      defaultVersionIfNotFound
+      defaultVersionIfNotFound,
+      true
     );
     fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-stable');
+
+    // Identical to `oss-stable` but with real, semver versions. This is what
+    // will get published to @latest.
+    spawnSync('cp', [
+      '-r',
+      buildDir + '/oss-stable',
+      buildDir + '/oss-stable-semver',
+    ]);
+    const semverVersionsMap = new Map();
+    for (const moduleName in stablePackages) {
+      const version = stablePackages[moduleName];
+      semverVersionsMap.set(moduleName, version);
+    }
+    updatePackageVersions(
+      buildDir + '/oss-stable-semver',
+      semverVersionsMap,
+      defaultVersionIfNotFound,
+      false
+    );
   }
 
   if (fs.existsSync(buildDir + '/facebook-www')) {
@@ -131,7 +151,8 @@ function processExperimental(buildDir, version) {
     updatePackageVersions(
       buildDir + '/node_modules',
       versionsMap,
-      defaultVersionIfNotFound
+      defaultVersionIfNotFound,
+      true
     );
     fs.renameSync(buildDir + '/node_modules', buildDir + '/oss-experimental');
   }
@@ -179,7 +200,8 @@ function crossDeviceRenameSync(source, destination) {
 function updatePackageVersions(
   modulesDir,
   versionsMap,
-  defaultVersionIfNotFound
+  defaultVersionIfNotFound,
+  pinToExactVersion
 ) {
   for (const moduleName of fs.readdirSync(modulesDir)) {
     let version = versionsMap.get(moduleName);
@@ -199,14 +221,18 @@ function updatePackageVersions(
       if (packageInfo.dependencies) {
         for (const dep of Object.keys(packageInfo.dependencies)) {
           if (versionsMap.has(dep)) {
-            packageInfo.dependencies[dep] = version;
+            packageInfo.dependencies[dep] = pinToExactVersion
+              ? version
+              : '^' + version;
           }
         }
       }
       if (packageInfo.peerDependencies) {
         for (const dep of Object.keys(packageInfo.peerDependencies)) {
           if (versionsMap.has(dep)) {
-            packageInfo.peerDependencies[dep] = version;
+            packageInfo.peerDependencies[dep] = pinToExactVersion
+              ? version
+              : '^' + version;
           }
         }
       }