Skip to content

Commit 6c233ea

Browse files
committed
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
1 parent 6b2454e commit 6c233ea

File tree

6 files changed

+185
-4
lines changed

6 files changed

+185
-4
lines changed

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@
44
"packages/*"
55
],
66
"scripts": {
7-
"build": "cd packages/react-scripts && node scripts/build.js",
7+
"build": "cd packages/react-scripts && node bin/react-scripts.js build",
88
"changelog": "lerna-changelog",
99
"create-react-app": "node tasks/cra.js",
1010
"e2e": "tasks/e2e-simple.sh",
1111
"e2e:docker": "tasks/local-test.sh",
1212
"postinstall": "cd packages/react-error-overlay/ && yarn build:prod",
1313
"publish": "tasks/publish.sh",
14-
"start": "cd packages/react-scripts && node scripts/start.js",
14+
"start": "cd packages/react-scripts && node bin/react-scripts.js start",
1515
"screencast": "svg-term --cast hItN7sl5yfCPTHxvFg5glhhfp --out screencast.svg --window",
16-
"test": "cd packages/react-scripts && node scripts/test.js --env=jsdom",
16+
"test": "cd packages/react-scripts && node bin/react-scripts.js test --env=jsdom",
1717
"format": "prettier --trailing-comma es5 --single-quote --write 'packages/*/*.js' 'packages/*/!(node_modules)/**/*.js'",
1818
"precommit": "lint-staged"
1919
},
2020
"devDependencies": {
21-
"eslint": "^4.4.1",
21+
"eslint": "4.15.0",
2222
"husky": "^0.13.2",
2323
"lerna": "2.6.0",
2424
"lerna-changelog": "^0.6.0",

packages/react-scripts/bin/react-scripts.js

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
'use strict';
1010

11+
// Makes the script crash on unhandled rejections instead of silently
12+
// ignoring them. In the future, promise rejections that are not handled will
13+
// terminate the Node.js process with a non-zero exit code.
14+
process.on('unhandledRejection', err => {
15+
throw err;
16+
});
17+
1118
const spawn = require('react-dev-utils/crossSpawn');
1219
const args = process.argv.slice(2);
1320

packages/react-scripts/scripts/build.js

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ process.on('unhandledRejection', err => {
2121

2222
// Ensure environment variables are read.
2323
require('../config/env');
24+
// @remove-on-eject-begin
25+
// Do the preflight check (only happens before eject).
26+
const verifyPackageTree = require('./utils/verifyPackageTree');
27+
if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') {
28+
verifyPackageTree();
29+
}
30+
// @remove-on-eject-end
2431

2532
const path = require('path');
2633
const chalk = require('chalk');

packages/react-scripts/scripts/start.js

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ process.on('unhandledRejection', err => {
2121

2222
// Ensure environment variables are read.
2323
require('../config/env');
24+
// @remove-on-eject-begin
25+
// Do the preflight check (only happens before eject).
26+
const verifyPackageTree = require('./utils/verifyPackageTree');
27+
if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') {
28+
verifyPackageTree();
29+
}
30+
// @remove-on-eject-end
2431

2532
const fs = require('fs');
2633
const chalk = require('chalk');

packages/react-scripts/scripts/test.js

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ process.on('unhandledRejection', err => {
2222

2323
// Ensure environment variables are read.
2424
require('../config/env');
25+
// @remove-on-eject-begin
26+
// Do the preflight check (only happens before eject).
27+
const verifyPackageTree = require('./utils/verifyPackageTree');
28+
if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') {
29+
verifyPackageTree();
30+
}
31+
// @remove-on-eject-end
2532

2633
const jest = require('jest');
2734
const argv = process.argv.slice(2);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// @remove-file-on-eject
2+
/**
3+
* Copyright (c) 2015-present, Facebook, Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
const chalk = require('chalk');
12+
const fs = require('fs');
13+
const path = require('path');
14+
15+
// We assume that having wrong versions of these
16+
// in the tree will likely break your setup.
17+
// This is a relatively low-effort way to find common issues.
18+
function verifyPackageTree() {
19+
const depsToCheck = [
20+
// These are packages most likely to break in practice.
21+
// See https://github.com/facebookincubator/create-react-app/issues/1795 for reasons why.
22+
// I have not included Babel here because plugins typically don't import Babel (so it's not affected).
23+
'eslint',
24+
'jest',
25+
'webpack',
26+
'webpack-dev-server',
27+
];
28+
// Inlined from semver-regex, MIT license.
29+
// Don't want to make this a dependency after ejecting.
30+
const getSemverRegex = () =>
31+
/\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/gi;
32+
const ownPackageJson = require('../../package.json');
33+
const expectedVersionsByDep = {};
34+
// Gather wanted deps
35+
depsToCheck.forEach(dep => {
36+
const expectedVersion = ownPackageJson.dependencies[dep];
37+
if (!expectedVersion) {
38+
throw new Error('This dependency list is outdated, fix it.');
39+
}
40+
if (!getSemverRegex().test(expectedVersion)) {
41+
throw new Error(
42+
`The ${dep} package should be pinned, instead got version ${expectedVersion}.`
43+
);
44+
}
45+
expectedVersionsByDep[dep] = expectedVersion;
46+
});
47+
// Verify we don't have other versions up the tree
48+
let currentDir = __dirname;
49+
// eslint-disable-next-line no-constant-condition
50+
while (true) {
51+
const previousDir = currentDir;
52+
currentDir = path.resolve(currentDir, '..');
53+
if (currentDir === previousDir) {
54+
// We've reached the root.
55+
break;
56+
}
57+
const maybeNodeModules = path.resolve(currentDir, 'node_modules');
58+
if (!fs.existsSync(maybeNodeModules)) {
59+
continue;
60+
}
61+
depsToCheck.forEach(dep => {
62+
const maybeDep = path.resolve(maybeNodeModules, dep);
63+
if (!fs.existsSync(maybeDep)) {
64+
return;
65+
}
66+
const maybeDepPackageJson = path.resolve(maybeDep, 'package.json');
67+
if (!fs.existsSync(maybeDepPackageJson)) {
68+
return;
69+
}
70+
const depPackageJson = JSON.parse(
71+
fs.readFileSync(maybeDepPackageJson, 'utf8')
72+
);
73+
const expectedVersion = expectedVersionsByDep[dep];
74+
if (depPackageJson.version !== expectedVersion) {
75+
console.error(
76+
chalk.red(
77+
`\nThere might be a problem with the project dependency tree.\n` +
78+
`It is likely ${chalk.bold(
79+
'not'
80+
)} a bug in Create React App, but something you need to fix locally.\n\n`
81+
) +
82+
`The ${chalk.bold(
83+
ownPackageJson.name
84+
)} package provided by Create React App requires a dependency:\n\n` +
85+
chalk.green(
86+
` "${chalk.bold(dep)}": "${chalk.bold(expectedVersion)}"\n\n`
87+
) +
88+
`Don't try to install it manually: your package manager does it automatically.\n` +
89+
`However, a different version of ${chalk.bold(
90+
dep
91+
)} was detected higher up in the tree:\n\n` +
92+
` ${chalk.bold(chalk.red(maybeDep))} (version: ${chalk.bold(
93+
chalk.red(depPackageJson.version)
94+
)}) \n\n` +
95+
`Manually installing incompatible versions is known to cause hard-to-debug issues.\n` +
96+
`To fix the dependency tree, try following the steps below in the exact order:\n\n` +
97+
` ${chalk.cyan('1.')} Delete ${chalk.bold(
98+
'package-lock.json'
99+
)} (${chalk.underline('not')} ${chalk.bold(
100+
'package.json'
101+
)}!) and/or ${chalk.bold(
102+
'yarn.lock'
103+
)} in your project folder.\n\n` +
104+
` ${chalk.cyan('2.')} Delete ${chalk.bold(
105+
'node_modules'
106+
)} in your project folder.\n\n` +
107+
` ${chalk.cyan('3.')} Remove "${chalk.bold(
108+
dep
109+
)}" from ${chalk.bold('dependencies')} and/or ${chalk.bold(
110+
'devDependencies'
111+
)} in the ${chalk.bold(
112+
'package.json'
113+
)} file in your project folder.\n\n` +
114+
` ${chalk.cyan('4.')} Run ${chalk.bold(
115+
'npm install'
116+
)} or ${chalk.bold(
117+
'yarn'
118+
)}, depending on the package manager you use.\n\n` +
119+
`In most cases, this should be enough to fix the problem.\n` +
120+
`If this has not helped, there are a few other things you can try:\n\n` +
121+
` ${chalk.cyan('5.')} If you used ${chalk.bold(
122+
'npm'
123+
)}, install ${chalk.bold(
124+
'yarn'
125+
)} (http://yarnpkg.com/) and repeat the above steps with it instead.\n` +
126+
` This may help because npm has known issues with package hoisting which may get resolved in future versions.\n\n` +
127+
` ${chalk.cyan('6.')} Check if ${chalk.bold(
128+
maybeDep
129+
)} is outside your project directory.\n` +
130+
` For example, you might have accidentally installed something in your home folder.\n\n` +
131+
` ${chalk.cyan('7.')} Try running ${chalk.bold(
132+
`npm ls ${dep}`
133+
)} in your project folder.\n` +
134+
` This will tell you which ${chalk.underline(
135+
'other'
136+
)} package (apart from the expected ${chalk.bold(
137+
ownPackageJson.name
138+
)}) installed ${chalk.bold(dep)}.\n\n` +
139+
`If nothing else helps, add ${chalk.bold(
140+
'SKIP_PREFLIGHT_CHECK=true'
141+
)} to an ${chalk.bold('.env')} file in your project.\n` +
142+
`That would permanently disable this preflight check in case you want to proceed anyway.\n\n` +
143+
chalk.cyan(
144+
`P.S. We know this message is long but please read the steps above :-) We hope you find them helpful!\n`
145+
)
146+
);
147+
process.exit(1);
148+
}
149+
});
150+
}
151+
}
152+
153+
module.exports = verifyPackageTree;

0 commit comments

Comments
 (0)