From c6bd30b4b01c2e5422d82b2568ab5f45f79350bc Mon Sep 17 00:00:00 2001 From: Rifat Nabi Date: Tue, 25 Oct 2016 21:24:38 +0600 Subject: [PATCH 1/4] Add interactive update --- __tests__/cli/aliases.js | 2 +- package.json | 1 + src/cli/aliases.js | 2 +- src/cli/commands/index.js | 1 + src/cli/commands/update.js | 155 +++++++++++++++++++++++++++++++++++++ src/reporters/lang/en.js | 1 + yarn.lock | 57 +++++++++++++- 7 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 src/cli/commands/update.js diff --git a/__tests__/cli/aliases.js b/__tests__/cli/aliases.js index 822dd783a0..db609b217f 100644 --- a/__tests__/cli/aliases.js +++ b/__tests__/cli/aliases.js @@ -26,7 +26,7 @@ test('shorthands and affordances', () => { expect(aliases['rm']).toBe('remove'); expect(aliases['show']).toBe('info'); expect(aliases['uninstall']).toBe('remove'); - expect(aliases['update']).toBe('upgrade'); + expect(aliases['udpate']).toBe('update'); expect(aliases['verison']).toBe('version'); expect(aliases['view']).toBe('info'); }); diff --git a/package.json b/package.json index b275b4ec0c..1196a91181 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "diff": "^2.2.1", "eslint-plugin-react": "5.2.2", "ini": "^1.3.4", + "inquirer": "^1.2.2", "invariant": "^2.2.0", "is-builtin-module": "^1.0.0", "leven": "^2.0.0", diff --git a/src/cli/aliases.js b/src/cli/aliases.js index 80b22770d5..5bb1e9a4f6 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: 'update', verison: 'version', view: 'info', }: { [key: string]: ?string }); diff --git a/src/cli/commands/index.js b/src/cli/commands/index.js index f8f4fbb747..338ebc794b 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 update from './update.js'; export {update}; import buildUseless from './_useless.js'; diff --git a/src/cli/commands/update.js b/src/cli/commands/update.js new file mode 100644 index 0000000000..9f7656bae4 --- /dev/null +++ b/src/cli/commands/update.js @@ -0,0 +1,155 @@ +/* @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 = {[key: K]: Array}; + +// Prompt user with Inquirer +async function prompt(choices): Promise> { + 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, +): Promise { + const lockfile = await Lockfile.fromDirectory(config.cwd); + const install = new Install(flags, config, reporter, lockfile); + const [deps] = await install.fetchRequestFromCwd(); + + const outdatedDeps = (await Promise.all(deps.map(async ({pattern, hint}): Promise => { + 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}); + }))) + .filter(({latest, current}) => latest !== current) + .sort((depA, depB) => { + const isExpected = (dep) => dep.current === dep.wanted; + if (isExpected(depA) && !isExpected(depB)) { + return 1; + } + return -1; + }); + + const getNameFromHint = (hint) => hint ? `${hint}Dependencies` : 'dependencies'; + + const maxLengthArr = outdatedDeps.reduce( + (acc, dep) => + ['name', 'current', 'latest']. + reduce((obj, key) => { + obj[key] = Math.max(obj[key], dep[key].length); + return obj; + }, acc), + {name: 0, current: 0, latest: 0}, + ); + + // 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 45977ecacd..af204c6979 100644 --- a/src/reporters/lang/en.js +++ b/src/reporters/lang/en.js @@ -207,6 +207,7 @@ 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', + updateInstalling: 'Installing $0...', }; export type LanguageKeys = $Keys; diff --git a/yarn.lock b/yarn.lock index aeb7aa32dc..b1f7cab174 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" From 57c804a80db0750190ea0ccb5559fe70ef885a94 Mon Sep 17 00:00:00 2001 From: Rifat Nabi Date: Thu, 27 Oct 2016 23:01:58 +0600 Subject: [PATCH 2/4] Simplify some obscure code --- src/cli/commands/update.js | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/cli/commands/update.js b/src/cli/commands/update.js index 9f7656bae4..562540ae37 100644 --- a/src/cli/commands/update.js +++ b/src/cli/commands/update.js @@ -52,7 +52,7 @@ export async function run( const install = new Install(flags, config, reporter, lockfile); const [deps] = await install.fetchRequestFromCwd(); - const outdatedDeps = (await Promise.all(deps.map(async ({pattern, hint}): Promise => { + const allOutdatedDeps = (await Promise.all(deps.map(async ({pattern, hint}): Promise => { const locked = lockfile.getLocked(pattern); if (!locked) { throw new MessageError(reporter.lang('lockfileOutdated')); @@ -72,26 +72,27 @@ export async function run( } return ({name, current, wanted, latest, hint}); - }))) - .filter(({latest, current}) => latest !== current) - .sort((depA, depB) => { - const isExpected = (dep) => dep.current === dep.wanted; - if (isExpected(depA) && !isExpected(depB)) { - return 1; - } - return -1; - }); + }))); + + const isDepOld = ({latest, current}) => latest !== current; + const isDepExpected = ({current, wanted}) => current === wanted; + + const outdatedDeps = allOutdatedDeps + .filter(isDepOld) + .sort((depA, depB) => { + if (isDepExpected(depA) && !isDepExpected(depB)) { + return 1; + } + return -1; + }); const getNameFromHint = (hint) => hint ? `${hint}Dependencies` : 'dependencies'; - const maxLengthArr = outdatedDeps.reduce( - (acc, dep) => - ['name', 'current', 'latest']. - reduce((obj, key) => { - obj[key] = Math.max(obj[key], dep[key].length); - return obj; - }, acc), - {name: 0, current: 0, latest: 0}, + 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 From 14fe80f0aa97bd0ac5145e967a44bfdc52efb504 Mon Sep 17 00:00:00 2001 From: Rifat Nabi Date: Fri, 28 Oct 2016 22:21:34 +0600 Subject: [PATCH 3/4] Fix wrong const name --- src/cli/commands/update.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/update.js b/src/cli/commands/update.js index 562540ae37..bc87067f2e 100644 --- a/src/cli/commands/update.js +++ b/src/cli/commands/update.js @@ -52,7 +52,7 @@ export async function run( const install = new Install(flags, config, reporter, lockfile); const [deps] = await install.fetchRequestFromCwd(); - const allOutdatedDeps = (await Promise.all(deps.map(async ({pattern, hint}): Promise => { + const allDeps = (await Promise.all(deps.map(async ({pattern, hint}): Promise => { const locked = lockfile.getLocked(pattern); if (!locked) { throw new MessageError(reporter.lang('lockfileOutdated')); @@ -77,7 +77,7 @@ export async function run( const isDepOld = ({latest, current}) => latest !== current; const isDepExpected = ({current, wanted}) => current === wanted; - const outdatedDeps = allOutdatedDeps + const outdatedDeps = allDeps .filter(isDepOld) .sort((depA, depB) => { if (isDepExpected(depA) && !isDepExpected(depB)) { From b6f3aec6b6fb4c2186cbf70e2d3701ac755f04b4 Mon Sep 17 00:00:00 2001 From: Sebastian McKenzie Date: Fri, 11 Nov 2016 18:40:22 +0000 Subject: [PATCH 4/4] rename update command to upgrade-interactive --- __tests__/cli/aliases.js | 2 +- src/cli/aliases.js | 2 +- src/cli/commands/index.js | 2 +- src/cli/commands/{update.js => upgrade-interactive.js} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/cli/commands/{update.js => upgrade-interactive.js} (100%) diff --git a/__tests__/cli/aliases.js b/__tests__/cli/aliases.js index aabf2e380f..aac94ca965 100644 --- a/__tests__/cli/aliases.js +++ b/__tests__/cli/aliases.js @@ -26,7 +26,7 @@ test('shorthands and affordances', () => { expect(aliases['rm']).toBe('remove'); expect(aliases['show']).toBe('info'); expect(aliases['uninstall']).toBe('remove'); - expect(aliases['udpate']).toBe('update'); + expect(aliases['update']).toBe('upgrade'); expect(aliases['verison']).toBe('version'); expect(aliases['view']).toBe('info'); }); diff --git a/src/cli/aliases.js b/src/cli/aliases.js index da5a849aea..e650929284 100644 --- a/src/cli/aliases.js +++ b/src/cli/aliases.js @@ -28,7 +28,7 @@ export default ({ rm: 'remove', show: 'info', uninstall: 'remove', - udpate: 'update', + udpate: 'upgrade', verison: 'version', view: 'info', }: { [key: string]: ?string }); diff --git a/src/cli/commands/index.js b/src/cli/commands/index.js index 338ebc794b..bdb83ec696 100644 --- a/src/cli/commands/index.js +++ b/src/cli/commands/index.js @@ -31,7 +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 update from './update.js'; export {update}; +import * as upgradeInteractive from './upgrade-interactive.js'; export {upgradeInteractive}; import buildUseless from './_useless.js'; diff --git a/src/cli/commands/update.js b/src/cli/commands/upgrade-interactive.js similarity index 100% rename from src/cli/commands/update.js rename to src/cli/commands/upgrade-interactive.js