Skip to content

Commit e639358

Browse files
mysticateanot-an-aardvark
authored andcommitted
Update: add question to confirm downgrade (fixes eslint#8870) (eslint#8911)
1 parent 601039d commit e639358

File tree

4 files changed

+193
-18
lines changed

4 files changed

+193
-18
lines changed

lib/config/config-initializer.js

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
const util = require("util"),
1313
inquirer = require("inquirer"),
1414
ProgressBar = require("progress"),
15+
semver = require("semver"),
1516
autoconfig = require("./autoconfig.js"),
1617
ConfigFile = require("./config-file"),
1718
ConfigOps = require("./config-ops"),
1819
getSourceCodeOfFiles = require("../util/source-code-util").getSourceCodeOfFiles,
20+
ModuleResolver = require("../util/module-resolver"),
1921
npmUtil = require("../util/npm-util"),
2022
recConfig = require("../../conf/eslint-recommended"),
2123
log = require("../logging");
@@ -56,12 +58,35 @@ function writeFile(config, format) {
5658
}
5759
}
5860

61+
/**
62+
* Get the peer dependencies of the given module.
63+
* This adds the gotten value to cache at the first time, then reuses it.
64+
* In a process, this function is called twice, but `npmUtil.fetchPeerDependencies` needs to access network which is relatively slow.
65+
* @param {string} moduleName The module name to get.
66+
* @returns {Object} The peer dependencies of the given module.
67+
* This object is the object of `peerDependencies` field of `package.json`.
68+
*/
69+
function getPeerDependencies(moduleName) {
70+
let result = getPeerDependencies.cache.get(moduleName);
71+
72+
if (!result) {
73+
log.info(`Checking peerDependencies of ${moduleName}`);
74+
75+
result = npmUtil.fetchPeerDependencies(moduleName);
76+
getPeerDependencies.cache.set(moduleName, result);
77+
}
78+
79+
return result;
80+
}
81+
getPeerDependencies.cache = new Map();
82+
5983
/**
6084
* Synchronously install necessary plugins, configs, parsers, etc. based on the config
6185
* @param {Object} config config object
86+
* @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
6287
* @returns {void}
6388
*/
64-
function installModules(config) {
89+
function installModules(config, installESLint) {
6590
const modules = {};
6691

6792
// Create a list of modules which should be installed based on config
@@ -73,11 +98,10 @@ function installModules(config) {
7398
if (config.extends && config.extends.indexOf("eslint:") === -1) {
7499
const moduleName = `eslint-config-${config.extends}`;
75100

76-
log.info(`Checking peerDependencies of ${moduleName}`);
77101
modules[moduleName] = "latest";
78102
Object.assign(
79103
modules,
80-
npmUtil.fetchPeerDependencies(`${moduleName}@latest`)
104+
getPeerDependencies(`${moduleName}@latest`)
81105
);
82106
}
83107

@@ -86,15 +110,17 @@ function installModules(config) {
86110
return;
87111
}
88112

89-
// Add eslint to list in case user does not have it installed locally
90-
modules.eslint = modules.eslint || "latest";
91-
92-
// Mark to show messages if it's new installation of eslint.
93-
const installStatus = npmUtil.checkDevDeps(["eslint"]);
113+
if (installESLint === false) {
114+
delete modules.eslint;
115+
} else {
116+
const installStatus = npmUtil.checkDevDeps(["eslint"]);
94117

95-
if (installStatus.eslint === false) {
96-
log.info("Local ESLint installation not found.");
97-
config.installedESLint = true;
118+
// Mark to show messages if it's new installation of eslint.
119+
if (installStatus.eslint === false) {
120+
log.info("Local ESLint installation not found.");
121+
modules.eslint = modules.eslint || "latest";
122+
config.installedESLint = true;
123+
}
98124
}
99125

100126
// Install packages
@@ -265,9 +291,10 @@ function processAnswers(answers) {
265291
/**
266292
* process user's style guide of choice and return an appropriate config object.
267293
* @param {string} guide name of the chosen style guide
294+
* @param {boolean} [installESLint=true] If `false` is given, it does not install eslint.
268295
* @returns {Object} config object
269296
*/
270-
function getConfigForStyleGuide(guide) {
297+
function getConfigForStyleGuide(guide, installESLint) {
271298
const guides = {
272299
google: { extends: "google" },
273300
airbnb: { extends: "airbnb" },
@@ -279,11 +306,74 @@ function getConfigForStyleGuide(guide) {
279306
throw new Error("You referenced an unsupported guide.");
280307
}
281308

282-
installModules(guides[guide]);
309+
installModules(guides[guide], installESLint);
283310

284311
return guides[guide];
285312
}
286313

314+
/**
315+
* Get the version of the local ESLint.
316+
* @returns {string|null} The version. If the local ESLint was not found, returns null.
317+
*/
318+
function getLocalESLintVersion() {
319+
try {
320+
const resolver = new ModuleResolver();
321+
const eslintPath = resolver.resolve("eslint", process.cwd());
322+
const eslint = require(eslintPath);
323+
324+
return eslint.linter.version || null;
325+
} catch (_err) {
326+
return null;
327+
}
328+
}
329+
330+
/**
331+
* Get the shareable config name of the chosen style guide.
332+
* @param {Object} answers The answers object.
333+
* @returns {string} The shareable config name.
334+
*/
335+
function getStyleGuideName(answers) {
336+
if (answers.styleguide === "airbnb" && !answers.airbnbReact) {
337+
return "airbnb-base";
338+
}
339+
return answers.styleguide;
340+
}
341+
342+
/**
343+
* Check whether the local ESLint version conflicts with the required version of the chosen shareable config.
344+
* @param {Object} answers The answers object.
345+
* @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config.
346+
*/
347+
function hasESLintVersionConflict(answers) {
348+
349+
// Get the local ESLint version.
350+
const localESLintVersion = getLocalESLintVersion();
351+
352+
if (!localESLintVersion) {
353+
return false;
354+
}
355+
356+
// Get the required range of ESLint version.
357+
const configName = getStyleGuideName(answers);
358+
const moduleName = `eslint-config-${configName}@latest`;
359+
const requiredESLintVersionRange = getPeerDependencies(moduleName).eslint;
360+
361+
if (!requiredESLintVersionRange) {
362+
return false;
363+
}
364+
365+
answers.localESLintVersion = localESLintVersion;
366+
answers.requiredESLintVersionRange = requiredESLintVersionRange;
367+
368+
// Check the version.
369+
if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) {
370+
answers.installESLint = false;
371+
return false;
372+
}
373+
374+
return true;
375+
}
376+
287377
/* istanbul ignore next: no need to test inquirer*/
288378
/**
289379
* Ask use a few questions on command prompt
@@ -346,6 +436,21 @@ function promptUser() {
346436
when(answers) {
347437
return ((answers.source === "guide" && answers.packageJsonExists) || answers.source === "auto");
348438
}
439+
},
440+
{
441+
type: "confirm",
442+
name: "installESLint",
443+
message(answers) {
444+
const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange)
445+
? "upgrade"
446+
: "downgrade";
447+
448+
return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`;
449+
},
450+
default: true,
451+
when(answers) {
452+
return answers.source === "guide" && answers.packageJsonExists && hasESLintVersionConflict(answers);
453+
}
349454
}
350455
]).then(earlyAnswers => {
351456

@@ -355,11 +460,14 @@ function promptUser() {
355460
log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again.");
356461
return void 0;
357462
}
463+
if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) {
464+
log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`);
465+
}
358466
if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) {
359467
earlyAnswers.styleguide = "airbnb-base";
360468
}
361469

362-
config = getConfigForStyleGuide(earlyAnswers.styleguide);
470+
config = getConfigForStyleGuide(earlyAnswers.styleguide, earlyAnswers.installESLint);
363471
writeFile(config, earlyAnswers.format);
364472

365473
return void 0;
@@ -479,6 +587,7 @@ function promptUser() {
479587

480588
const init = {
481589
getConfigForStyleGuide,
590+
hasESLintVersionConflict,
482591
processAnswers,
483592
/* istanbul ignore next */initializeConfig() {
484593
return promptUser();

lib/util/npm-util.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function installSyncSaveDev(packages) {
5959
/**
6060
* Fetch `peerDependencies` of the given package by `npm show` command.
6161
* @param {string} packageName The package name to fetch peerDependencies.
62-
* @returns {string[]} Gotten peerDependencies.
62+
* @returns {Object} Gotten peerDependencies.
6363
*/
6464
function fetchPeerDependencies(packageName) {
6565
const fetchedText = spawn.sync(

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"pluralize": "^4.0.0",
6868
"progress": "^2.0.0",
6969
"require-uncached": "^1.0.3",
70+
"semver": "^5.3.0",
7071
"strip-json-comments": "~2.0.1",
7172
"table": "^4.0.1",
7273
"text-table": "~0.2.0"
@@ -104,7 +105,6 @@
104105
"npm-license": "^0.3.3",
105106
"phantomjs-prebuilt": "^2.1.14",
106107
"proxyquire": "^1.8.0",
107-
"semver": "^5.3.0",
108108
"shelljs": "^0.7.7",
109109
"shelljs-nodecli": "~0.1.1",
110110
"sinon": "^2.3.2",

tests/lib/config/config-initializer.js

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,30 @@ describe("configInitializer", () => {
3333
npmCheckStub,
3434
npmInstallStub,
3535
npmFetchPeerDependenciesStub,
36-
init;
36+
init,
37+
localESLintVersion = null;
3738

3839
const log = {
3940
info: sinon.spy(),
4041
error: sinon.spy()
4142
};
4243
const requireStubs = {
43-
"../logging": log
44+
"../logging": log,
45+
"../util/module-resolver": class ModuleResolver {
46+
47+
/**
48+
* @returns {string} The path to local eslint to test.
49+
*/
50+
resolve() { // eslint-disable-line class-methods-use-this
51+
if (localESLintVersion) {
52+
return `local-eslint-${localESLintVersion}`;
53+
}
54+
throw new Error("Cannot find module");
55+
}
56+
},
57+
"local-eslint-3.18.0": { linter: { version: "3.18.0" }, "@noCallThru": true },
58+
"local-eslint-3.19.0": { linter: { version: "3.19.0" }, "@noCallThru": true },
59+
"local-eslint-4.0.0": { linter: { version: "4.0.0" }, "@noCallThru": true }
4460
};
4561

4662
/**
@@ -245,6 +261,56 @@ describe("configInitializer", () => {
245261
]
246262
);
247263
});
264+
265+
describe("hasESLintVersionConflict (Note: peerDependencies always `eslint: \"^3.19.0\"` by stubs)", () => {
266+
describe("if local ESLint is not found,", () => {
267+
before(() => {
268+
localESLintVersion = null;
269+
});
270+
271+
it("should return false.", () => {
272+
const result = init.hasESLintVersionConflict({ styleguide: "airbnb" });
273+
274+
assert.equal(result, false);
275+
});
276+
});
277+
278+
describe("if local ESLint is 3.19.0,", () => {
279+
before(() => {
280+
localESLintVersion = "3.19.0";
281+
});
282+
283+
it("should return false.", () => {
284+
const result = init.hasESLintVersionConflict({ styleguide: "airbnb" });
285+
286+
assert.equal(result, false);
287+
});
288+
});
289+
290+
describe("if local ESLint is 4.0.0,", () => {
291+
before(() => {
292+
localESLintVersion = "4.0.0";
293+
});
294+
295+
it("should return true.", () => {
296+
const result = init.hasESLintVersionConflict({ styleguide: "airbnb" });
297+
298+
assert.equal(result, true);
299+
});
300+
});
301+
302+
describe("if local ESLint is 3.18.0,", () => {
303+
before(() => {
304+
localESLintVersion = "3.18.0";
305+
});
306+
307+
it("should return true.", () => {
308+
const result = init.hasESLintVersionConflict({ styleguide: "airbnb" });
309+
310+
assert.equal(result, true);
311+
});
312+
});
313+
});
248314
});
249315

250316
describe("auto", () => {

0 commit comments

Comments
 (0)