From 11db9af4df4caa7d0664e732b807115c3b05b7a3 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 13 Jan 2018 15:55:48 +0000 Subject: [PATCH] Add preflight check to guard against wrong versions of webpack/eslint/jest higher up the tree (#3771) * Run real scripts in local development * Add preflight check warning * I know what I am doing * Move preflight check into individual scripts This ensures we don't try to filter NODE_PATH twice, accidentally removing the now-absolute path. * Slightly tweak the wording * Fix lint --- package.json | 8 +- packages/react-scripts/bin/react-scripts.js | 7 + packages/react-scripts/scripts/build.js | 7 + packages/react-scripts/scripts/start.js | 7 + packages/react-scripts/scripts/test.js | 7 + .../scripts/utils/verifyPackageTree.js | 153 ++++++++++++++++++ 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 packages/react-scripts/scripts/utils/verifyPackageTree.js diff --git a/package.json b/package.json index 9a32bc1350f..bc78dfee6b6 100644 --- a/package.json +++ b/package.json @@ -4,21 +4,21 @@ "packages/*" ], "scripts": { - "build": "cd packages/react-scripts && node scripts/build.js", + "build": "cd packages/react-scripts && node bin/react-scripts.js build", "changelog": "lerna-changelog", "create-react-app": "node tasks/cra.js", "e2e": "tasks/e2e-simple.sh", "e2e:docker": "tasks/local-test.sh", "postinstall": "cd packages/react-error-overlay/ && yarn build:prod", "publish": "tasks/publish.sh", - "start": "cd packages/react-scripts && node scripts/start.js", + "start": "cd packages/react-scripts && node bin/react-scripts.js start", "screencast": "svg-term --cast hItN7sl5yfCPTHxvFg5glhhfp --out screencast.svg --window", - "test": "cd packages/react-scripts && node scripts/test.js --env=jsdom", + "test": "cd packages/react-scripts && node bin/react-scripts.js test --env=jsdom", "format": "prettier --trailing-comma es5 --single-quote --write 'packages/*/*.js' 'packages/*/!(node_modules)/**/*.js'", "precommit": "lint-staged" }, "devDependencies": { - "eslint": "^4.4.1", + "eslint": "4.15.0", "husky": "^0.13.2", "lerna": "2.6.0", "lerna-changelog": "^0.6.0", diff --git a/packages/react-scripts/bin/react-scripts.js b/packages/react-scripts/bin/react-scripts.js index 9de034ff0d9..9e41f5da2b9 100755 --- a/packages/react-scripts/bin/react-scripts.js +++ b/packages/react-scripts/bin/react-scripts.js @@ -8,6 +8,13 @@ 'use strict'; +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on('unhandledRejection', err => { + throw err; +}); + const spawn = require('react-dev-utils/crossSpawn'); const args = process.argv.slice(2); diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index 930897008e1..29712418273 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -21,6 +21,13 @@ process.on('unhandledRejection', err => { // Ensure environment variables are read. require('../config/env'); +// @remove-on-eject-begin +// Do the preflight check (only happens before eject). +const verifyPackageTree = require('./utils/verifyPackageTree'); +if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') { + verifyPackageTree(); +} +// @remove-on-eject-end const path = require('path'); const chalk = require('chalk'); diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 3ff1b91f435..1adb8e03ce8 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -21,6 +21,13 @@ process.on('unhandledRejection', err => { // Ensure environment variables are read. require('../config/env'); +// @remove-on-eject-begin +// Do the preflight check (only happens before eject). +const verifyPackageTree = require('./utils/verifyPackageTree'); +if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') { + verifyPackageTree(); +} +// @remove-on-eject-end const fs = require('fs'); const chalk = require('chalk'); diff --git a/packages/react-scripts/scripts/test.js b/packages/react-scripts/scripts/test.js index b30113fe662..ee592d9b115 100644 --- a/packages/react-scripts/scripts/test.js +++ b/packages/react-scripts/scripts/test.js @@ -22,6 +22,13 @@ process.on('unhandledRejection', err => { // Ensure environment variables are read. require('../config/env'); +// @remove-on-eject-begin +// Do the preflight check (only happens before eject). +const verifyPackageTree = require('./utils/verifyPackageTree'); +if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') { + verifyPackageTree(); +} +// @remove-on-eject-end const jest = require('jest'); const argv = process.argv.slice(2); diff --git a/packages/react-scripts/scripts/utils/verifyPackageTree.js b/packages/react-scripts/scripts/utils/verifyPackageTree.js new file mode 100644 index 00000000000..87f6acc1157 --- /dev/null +++ b/packages/react-scripts/scripts/utils/verifyPackageTree.js @@ -0,0 +1,153 @@ +// @remove-file-on-eject +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const chalk = require('chalk'); +const fs = require('fs'); +const path = require('path'); + +// We assume that having wrong versions of these +// in the tree will likely break your setup. +// This is a relatively low-effort way to find common issues. +function verifyPackageTree() { + const depsToCheck = [ + // These are packages most likely to break in practice. + // See https://github.com/facebookincubator/create-react-app/issues/1795 for reasons why. + // I have not included Babel here because plugins typically don't import Babel (so it's not affected). + 'eslint', + 'jest', + 'webpack', + 'webpack-dev-server', + ]; + // Inlined from semver-regex, MIT license. + // Don't want to make this a dependency after ejecting. + const getSemverRegex = () => + /\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/gi; + const ownPackageJson = require('../../package.json'); + const expectedVersionsByDep = {}; + // Gather wanted deps + depsToCheck.forEach(dep => { + const expectedVersion = ownPackageJson.dependencies[dep]; + if (!expectedVersion) { + throw new Error('This dependency list is outdated, fix it.'); + } + if (!getSemverRegex().test(expectedVersion)) { + throw new Error( + `The ${dep} package should be pinned, instead got version ${expectedVersion}.` + ); + } + expectedVersionsByDep[dep] = expectedVersion; + }); + // Verify we don't have other versions up the tree + let currentDir = __dirname; + // eslint-disable-next-line no-constant-condition + while (true) { + const previousDir = currentDir; + currentDir = path.resolve(currentDir, '..'); + if (currentDir === previousDir) { + // We've reached the root. + break; + } + const maybeNodeModules = path.resolve(currentDir, 'node_modules'); + if (!fs.existsSync(maybeNodeModules)) { + continue; + } + depsToCheck.forEach(dep => { + const maybeDep = path.resolve(maybeNodeModules, dep); + if (!fs.existsSync(maybeDep)) { + return; + } + const maybeDepPackageJson = path.resolve(maybeDep, 'package.json'); + if (!fs.existsSync(maybeDepPackageJson)) { + return; + } + const depPackageJson = JSON.parse( + fs.readFileSync(maybeDepPackageJson, 'utf8') + ); + const expectedVersion = expectedVersionsByDep[dep]; + if (depPackageJson.version !== expectedVersion) { + console.error( + chalk.red( + `\nThere might be a problem with the project dependency tree.\n` + + `It is likely ${chalk.bold( + 'not' + )} a bug in Create React App, but something you need to fix locally.\n\n` + ) + + `The ${chalk.bold( + ownPackageJson.name + )} package provided by Create React App requires a dependency:\n\n` + + chalk.green( + ` "${chalk.bold(dep)}": "${chalk.bold(expectedVersion)}"\n\n` + ) + + `Don't try to install it manually: your package manager does it automatically.\n` + + `However, a different version of ${chalk.bold( + dep + )} was detected higher up in the tree:\n\n` + + ` ${chalk.bold(chalk.red(maybeDep))} (version: ${chalk.bold( + chalk.red(depPackageJson.version) + )}) \n\n` + + `Manually installing incompatible versions is known to cause hard-to-debug issues.\n` + + `To fix the dependency tree, try following the steps below in the exact order:\n\n` + + ` ${chalk.cyan('1.')} Delete ${chalk.bold( + 'package-lock.json' + )} (${chalk.underline('not')} ${chalk.bold( + 'package.json' + )}!) and/or ${chalk.bold( + 'yarn.lock' + )} in your project folder.\n\n` + + ` ${chalk.cyan('2.')} Delete ${chalk.bold( + 'node_modules' + )} in your project folder.\n\n` + + ` ${chalk.cyan('3.')} Remove "${chalk.bold( + dep + )}" from ${chalk.bold('dependencies')} and/or ${chalk.bold( + 'devDependencies' + )} in the ${chalk.bold( + 'package.json' + )} file in your project folder.\n\n` + + ` ${chalk.cyan('4.')} Run ${chalk.bold( + 'npm install' + )} or ${chalk.bold( + 'yarn' + )}, depending on the package manager you use.\n\n` + + `In most cases, this should be enough to fix the problem.\n` + + `If this has not helped, there are a few other things you can try:\n\n` + + ` ${chalk.cyan('5.')} If you used ${chalk.bold( + 'npm' + )}, install ${chalk.bold( + 'yarn' + )} (http://yarnpkg.com/) and repeat the above steps with it instead.\n` + + ` This may help because npm has known issues with package hoisting which may get resolved in future versions.\n\n` + + ` ${chalk.cyan('6.')} Check if ${chalk.bold( + maybeDep + )} is outside your project directory.\n` + + ` For example, you might have accidentally installed something in your home folder.\n\n` + + ` ${chalk.cyan('7.')} Try running ${chalk.bold( + `npm ls ${dep}` + )} in your project folder.\n` + + ` This will tell you which ${chalk.underline( + 'other' + )} package (apart from the expected ${chalk.bold( + ownPackageJson.name + )}) installed ${chalk.bold(dep)}.\n\n` + + `If nothing else helps, add ${chalk.bold( + 'SKIP_PREFLIGHT_CHECK=true' + )} to an ${chalk.bold('.env')} file in your project.\n` + + `That would permanently disable this preflight check in case you want to proceed anyway.\n\n` + + chalk.cyan( + `P.S. We know this message is long but please read the steps above :-) We hope you find them helpful!\n` + ) + ); + process.exit(1); + } + }); + } +} + +module.exports = verifyPackageTree;