Repository: facebook/react
Pull request: Add CI job to check npm dependencies

Checks that if one React package depends on another, the current version satisfies the given dependency range.

That way we don't forget to bump dependencies when we release a new version.

Changed files:
filename: .circleci/config.yml
patch: @@ -205,11 +205,24 @@ jobs:
     environment: *environment
     steps:
       - checkout
-      - attach_workspace: *attach_workspace
+      - attach_workspace:
+          at: .
       - run: yarn workspaces info | head -n -1 > workspace_info.txt
       - *restore_node_modules
       - run: yarn lint-build
 
+  yarn_check_release_dependencies:
+    docker: *docker
+    environment: *environment
+    steps:
+      - checkout
+      - attach_workspace:
+          at: .
+      - run: yarn workspaces info | head -n -1 > workspace_info.txt
+      - *restore_node_modules
+      - run: yarn check-release-dependencies
+
+
   check_error_codes:
     docker: *docker
     environment: *environment
@@ -427,6 +440,9 @@ workflows:
       - yarn_lint_build:
           requires:
             - yarn_build_combined
+      - yarn_check_release_dependencies:
+          requires:
+            - yarn_build_combined
       - check_error_codes:
           requires:
             - yarn_build_combined

filename: package.json
patch: @@ -145,7 +145,8 @@
     "publish-prereleases": "node ./scripts/release/publish-using-ci-workflow.js",
     "download-build": "node ./scripts/release/download-experimental-build.js",
     "download-build-for-head": "node ./scripts/release/download-experimental-build.js --commit=$(git rev-parse HEAD)",
-    "download-build-in-codesandbox-ci": "cd scripts/release && yarn install && cd ../../ && yarn download-build-for-head || yarn build-combined --type=node react/index react-dom scheduler"
+    "download-build-in-codesandbox-ci": "cd scripts/release && yarn install && cd ../../ && yarn download-build-for-head || yarn build-combined --type=node react/index react-dom scheduler",
+    "check-release-dependencies": "node ./scripts/release/check-release-dependencies"
   },
   "resolutions": {
     "react-is": "npm:react-is"

filename: scripts/release/check-release-dependencies.js
patch: @@ -0,0 +1,59 @@
+'use strict';
+
+/* eslint-disable no-for-of-loops/no-for-of-loops */
+
+const fs = require('fs');
+const semver = require('semver');
+
+const {stablePackages} = require('../../ReactVersions');
+
+function main() {
+  if (!fs.existsSync('./build/oss-stable-semver')) {
+    throw new Error('No build artifacts found');
+  }
+
+  const packages = new Map();
+  for (const packageName in stablePackages) {
+    if (!fs.existsSync(`build/oss-stable-semver/${packageName}/package.json`)) {
+      throw new Error(`${packageName}`);
+    } else {
+      const info = JSON.parse(
+        fs.readFileSync(`build/oss-stable-semver/${packageName}/package.json`)
+      );
+      packages.set(info.name, info);
+    }
+  }
+
+  for (const [packageName, info] of packages) {
+    if (info.dependencies) {
+      for (const [depName, depRange] of Object.entries(info.dependencies)) {
+        if (packages.has(depName)) {
+          const releaseVersion = packages.get(depName).version;
+          checkDependency(packageName, depName, releaseVersion, depRange);
+        }
+      }
+    }
+
+    if (info.peerDependencies) {
+      for (const [depName, depRange] of Object.entries(info.peerDependencies)) {
+        if (packages.has(depName)) {
+          const releaseVersion = packages.get(depName).version;
+          checkDependency(packageName, depName, releaseVersion, depRange);
+        }
+      }
+    }
+  }
+}
+
+function checkDependency(packageName, depName, version, range) {
+  if (!semver.satisfies(version, range)) {
+    throw new Error(
+      `${packageName} has an invalid dependency on ${depName}: ${range}` +
+        '\n\n' +
+        'Actual version is: ' +
+        version
+    );
+  }
+}
+
+main();