diff --git a/package.json b/package.json index 8ca0423802..de1ece4e13 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "detect-indent": "^4.0.0", "diff": "^2.2.1", "ini": "^1.3.4", + "inquirer": "^1.2.2", "invariant": "^2.2.0", "is-builtin-module": "^1.0.0", "is-ci": "^1.0.9", diff --git a/src/cli/aliases.js b/src/cli/aliases.js index 41d72d735c..e650929284 100644 --- a/src/cli/aliases.js +++ b/src/cli/aliases.js @@ -28,7 +28,7 @@ export default ({ rm: 'remove', show: 'info', uninstall: 'remove', - update: 'upgrade', + udpate: 'upgrade', verison: 'version', view: 'info', }: { [key: string]: ?string }); diff --git a/src/cli/commands/index.js b/src/cli/commands/index.js index f8f4fbb747..bdb83ec696 100644 --- a/src/cli/commands/index.js +++ b/src/cli/commands/index.js @@ -31,6 +31,7 @@ import * as upgrade from './upgrade.js'; export {upgrade}; import * as version from './version.js'; export {version}; import * as versions from './versions.js'; export {versions}; import * as why from './why.js'; export {why}; +import * as upgradeInteractive from './upgrade-interactive.js'; export {upgradeInteractive}; import buildUseless from './_useless.js'; diff --git a/src/cli/commands/upgrade-interactive.js b/src/cli/commands/upgrade-interactive.js new file mode 100644 index 0000000000..bc87067f2e --- /dev/null +++ b/src/cli/commands/upgrade-interactive.js @@ -0,0 +1,156 @@ +/* @flow */ + +import type {Reporter} from '../../reporters/index.js'; +import type Config from '../../config.js'; +import inquirer from 'inquirer'; +import repeat from 'repeating'; +import {MessageError} from '../../errors.js'; +import PackageRequest from '../../package-request.js'; +import {Add} from './add.js'; +import {Install} from './install.js'; +import Lockfile from '../../lockfile/wrapper.js'; + +export const requireLockfile = true; + +export function setFlags(commander: Object) { + // TODO: support some flags that install command has + commander.usage('update'); +} + +type Dependency = { + name: string, + current: string, + wanted: string, + latest: string, + hint: ?string +}; + +type InquirerResponses<K, T> = {[key: K]: Array<T>}; + +// Prompt user with Inquirer +async function prompt(choices): Promise<Array<Dependency>> { + const answers: InquirerResponses<'packages', Dependency> = await inquirer.prompt([{ + name: 'packages', + type: 'checkbox', + message: 'Choose which packages to update.', + choices, + // Couldn't make it work, I guess I'm missing something here + // $FlowFixMe: https://github.com/facebook/flow/blob/f41e66e27b227235750792c34f5a80f38bde6320/lib/node.js#L1197 + pageSize: process.stdout.rows - 2, + validate: (answer) => !!answer.length || 'You must choose at least one package.', + }]); + return answers.packages; +} + +export async function run( + config: Config, + reporter: Reporter, + flags: Object, + args: Array<string>, +): Promise<void> { + const lockfile = await Lockfile.fromDirectory(config.cwd); + const install = new Install(flags, config, reporter, lockfile); + const [deps] = await install.fetchRequestFromCwd(); + + const allDeps = (await Promise.all(deps.map(async ({pattern, hint}): Promise<Dependency> => { + const locked = lockfile.getLocked(pattern); + if (!locked) { + throw new MessageError(reporter.lang('lockfileOutdated')); + } + + const {name, version: current} = locked; + let latest = ''; + let wanted = ''; + + const normalized = PackageRequest.normalizePattern(pattern); + + if (PackageRequest.getExoticResolver(pattern) || + PackageRequest.getExoticResolver(normalized.range)) { + latest = wanted = 'exotic'; + } else { + ({latest, wanted} = await config.registries[locked.registry].checkOutdated(config, name, normalized.range)); + } + + return ({name, current, wanted, latest, hint}); + }))); + + const isDepOld = ({latest, current}) => latest !== current; + const isDepExpected = ({current, wanted}) => current === wanted; + + const outdatedDeps = allDeps + .filter(isDepOld) + .sort((depA, depB) => { + if (isDepExpected(depA) && !isDepExpected(depB)) { + return 1; + } + return -1; + }); + + const getNameFromHint = (hint) => hint ? `${hint}Dependencies` : 'dependencies'; + + const maxLengthArr = {name: 0, current: 0, latest: 0}; + outdatedDeps.forEach((dep) => + ['name', 'current', 'latest'].forEach((key) => { + maxLengthArr[key] = Math.max(maxLengthArr[key], dep[key].length); + }), + ); + + // Depends on maxLengthArr + const addPadding = (dep) => (key) => + `${dep[key]}${repeat(' ', maxLengthArr[key] - dep[key].length)}`; + + const colorizeName = ({current, wanted}) => + (current === wanted) ? reporter.format.yellow : reporter.format.red; + + const makeRow = (dep) => { + const padding = addPadding(dep); + const name = colorizeName(dep)(padding('name')); + const current = reporter.format.blue(padding('current')); + const latest = reporter.format.green(padding('latest')); + return `${name} ${current} ❯ ${latest}`; + }; + + const groupedDeps = outdatedDeps.reduce((acc, dep) => { + const {hint, name, latest} = dep; + const key = getNameFromHint(hint); + const xs = acc[key] || []; + acc[key] = xs.concat({ + name: makeRow(dep), + value: dep, + short: `${name}@${latest}`, + }); + return acc; + }, {}); + + const flatten = (xs) => xs.reduce( + (ys, y) => ys.concat(Array.isArray(y) ? flatten(y) : y), [], + ); + + const choices = Object.keys(groupedDeps).map((key) => [ + new inquirer.Separator(reporter.format.bold.underline.green(key)), + groupedDeps[key], + new inquirer.Separator(' '), + ]); + + const answers = await prompt(flatten(choices)); + + const getName = ({name}) => name; + const isHint = (x) => ({hint}) => hint === x; + + await [null, 'dev', 'optional', 'peer'].reduce(async (promise, hint) => { + // Wait for previous promise to resolve + await promise; + // Reset dependency flags + flags.dev = hint === 'dev'; + flags.peer = hint === 'peer'; + flags.optional = hint === 'optional'; + + const deps = answers.filter(isHint(hint)).map(getName); + if (deps.length) { + reporter.info(reporter.lang('updateInstalling', getNameFromHint(hint))); + const add = new Add(deps, flags, config, reporter, lockfile); + return await add.init(); + } + return Promise.resolve(); + }, Promise.resolve()); +} diff --git a/src/reporters/lang/en.js b/src/reporters/lang/en.js index 0bca72f2f6..00321371c5 100644 --- a/src/reporters/lang/en.js +++ b/src/reporters/lang/en.js @@ -210,11 +210,11 @@ const messages = { cantRequestOffline: 'Can\'t make a request in offline mode', requestManagerNotSetupHAR: 'RequestManager was not setup to capture HAR files', requestError: 'Request $0 returned a $1', - tarballNotInNetworkOrCache: '$0: Tarball is not in network and can not be located in cache ($1)', fetchBadHash: 'Bad hash. Expected $0 but got $1 ', fetchErrorCorrupt: '$0. Mirror tarball appears to be corrupt. You can resolve this by running:\n\n $ rm -rf $1\n $ yarn install', errorDecompressingTarball: '$0. Error decompressing $1, it appears to be corrupt.', + updateInstalling: 'Installing $0...', }; export type LanguageKeys = $Keys<typeof messages>; diff --git a/yarn.lock b/yarn.lock index c21a32f47b..5a7a15d50e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1441,7 +1441,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.6: +concat-stream@^1.4.6, concat-stream@^1.4.7: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" dependencies: @@ -2006,6 +2006,14 @@ extend@^3.0.0, extend@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" +external-editor@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.0.tgz#c7fe15954b09af852b89aaec82a2707a0dc5597a" + dependencies: + extend "^3.0.0" + spawn-sync "^1.0.15" + temp "^0.8.3" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -2612,6 +2620,25 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" +inquirer: + version "1.2.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.2.tgz#f725c1316f0020e7f3d538c8c5ad0c2732c1c451" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + external-editor "^1.1.0" + figures "^1.3.5" + lodash "^4.3.0" + mute-stream "0.0.6" + pinkie-promise "^2.0.0" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + inquirer@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" @@ -2766,6 +2793,10 @@ is-primitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -3668,7 +3699,7 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" -mute-stream@~0.0.4: +mute-stream@~0.0.4, mute-stream@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" @@ -3903,6 +3934,10 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -4431,10 +4466,21 @@ run-async@^0.1.0: dependencies: once "^1.3.0" +run-async@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.2.0.tgz#8783abd83c7bb86f41ee0602fc82404b3bd6e8b9" + dependencies: + is-promise "^2.1.0" + pinkie-promise "^2.0.0" + rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + sane@~1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/sane/-/sane-1.4.1.tgz#88f763d74040f5f0c256b6163db399bf110ac715" @@ -4560,6 +4606,13 @@ sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + spdx-correct@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"