npm-upgrade
Version:
Interactive CLI utility to easily update outdated NPM dependencies
339 lines (281 loc) • 10.6 kB
JavaScript
import {writeFileSync} from 'fs';
import _ from 'lodash';
import {flow, map, partition} from 'lodash/fp';
import open from 'open';
import semver from 'semver';
import detectIndent from 'detect-indent';
import ncu from 'npm-check-updates';
import shell from 'shelljs';
import {colorizeDiff} from 'npm-check-updates/lib/version-util';
import catchAsyncError from '../catchAsyncError';
import {makeFilterFunction} from '../filterUtils';
import {DEPS_GROUPS, loadGlobalPackages, loadPackageJson, setModuleVersion,
getModuleInfo, getModuleHomepage} from '../packageUtils';
import {fetchRemoteDb, findModuleChangelogUrl} from '../changelogUtils';
import {createSimpleTable} from '../cliTable';
import {strong, success, attention} from '../cliStyles';
import askUser from '../askUser';
import {toSentence} from '../stringUtils';
import {askIgnoreFields} from './ignore';
import Config from '../Config';
const pkg = require('../../package.json');
function createUpdatedModulesTable(modules) {
return createSimpleTable(
_.map(modules, ({name, from, to}) => [
strong(name),
from, '→', colorizeDiff(from, to)
])
);
}
export const command = 'check [filter]';
export const aliases = '*';
export const describe = 'Check for outdated modules';
const VERSION_TARGETS = ['greatest', 'latest', 'minor', 'patch'];
export function builder(yargs) {
DEPS_GROUPS
.forEach(({name, field, flag}) =>
yargs.option(name, {
type: 'boolean',
alias: flag,
describe: `check only "${field}"`
})
);
yargs.option('target', {
type: 'string',
alias: 't',
describe: 'Target version to upgrade to',
choices: VERSION_TARGETS,
default: 'latest'
});
}
/* eslint complexity: "off" */
export const handler = catchAsyncError(async opts => {
const {filter} = opts;
// Checking all the deps if all of them are omitted
if (_.every(DEPS_GROUPS, ({name}) => !opts[name])) {
_.each(DEPS_GROUPS, ({name}) => (opts[name] = true));
opts.global = false;
} else if (opts.global) {
// Make global flag mutually exclusive with other flags
_.each(DEPS_GROUPS, ({name}) => { opts[name] = false });
opts.global = true;
}
// Loading `package.json` from the current directory
const {path: packageFile, content: packageJson, source: packageSource} = opts.global ?
loadGlobalPackages() : loadPackageJson();
// Fetching remote changelogs db in background
fetchRemoteDb();
const depsGroupsToCheck = _.filter(DEPS_GROUPS, ({name}) => !!opts[name]);
const depsGroupsToCheckStr = (depsGroupsToCheck.length === DEPS_GROUPS.length) ?
'' : `${toSentence(_.map(depsGroupsToCheck, ({name}) => strong(name)))} `;
const filteredWith = filter ? `filtered with ${strong(filter)} ` : '';
const versionTarget = opts.target;
const targetInfo = versionTarget === 'latest' ? '' : `(target: ${strong(versionTarget)}) `;
console.log(
`Checking for outdated ${depsGroupsToCheckStr}dependencies ${filteredWith}${targetInfo}${opts.global ? '' :
(`for "${strong(packageFile)}"`)}...`
);
const ncuDepGroups = DEPS_GROUPS
.filter(({name}) => opts[name])
.map(({ncuValue}) => ncuValue)
.join(',');
const filteredPackageJson = filterDepsInPackageJson(packageJson, makeFilterFunction(filter));
const currentVersions = ncu.getCurrentDependencies(filteredPackageJson, {dep: ncuDepGroups});
const latestVersions = await ncu.queryVersions(currentVersions, {target: versionTarget, timeout: 0});
const upgradedVersions = ncu.upgradeDependencies(currentVersions, latestVersions);
if (_.isEmpty(upgradedVersions)) {
return console.log(success('All dependencies are up-to-date!'));
}
// Getting the list of ignored modules
const config = new Config();
config.ignore = config.ignore || {};
// Making arrays of outdated modules
const [ignoredModules, modulesToUpdate] = flow(
map.convert({'cap': false})((newVersion, moduleName) => ({
name: moduleName,
from: currentVersions[moduleName],
to: newVersion
})),
partition(module => (
_.has(config.ignore, module.name) &&
semver.satisfies(latestVersions[module.name], config.ignore[module.name].versions)
))
)(upgradedVersions);
// Moving `@types/*` modules right below their original modules
sortModules(modulesToUpdate);
sortModules(ignoredModules);
// Creating pretty-printed CLI tables with update info
if (_.isEmpty(modulesToUpdate)) {
console.log(
success('\nAll active modules are up-to-date!')
);
} else {
console.log(
`\n${strong('New versions of active modules available:')}\n\n${createUpdatedModulesTable(modulesToUpdate)}`
);
}
if (!_.isEmpty(ignoredModules)) {
const rows = _.map(ignoredModules, ({name, from, to}) => [
strong(name),
from, '→', colorizeDiff(from, to),
attention(config.ignore[name].versions),
config.ignore[name].reason
]);
// Adding table header
rows.unshift(_.map(
['', 'From', '', 'To', 'Ignored versions', 'Reason'],
header => strong(header)
));
console.log(`\n${strong('Ignored updates:')}\n\n${createSimpleTable(rows)}`);
}
const updatedModules = [];
let isUpdateFinished = false;
while (modulesToUpdate.length && !isUpdateFinished) {
const outdatedModule = modulesToUpdate.shift();
const {name, from, to} = outdatedModule;
let {changelogUrl, homepage} = outdatedModule;
// Adds new line
console.log('');
const answer = await askUser({
type: 'list',
message: `${changelogUrl === undefined ? 'U' : 'So, u'}pdate "${name}" ${opts.global ? 'globally' :
'in package.json'} from ${from} to ${colorizeDiff(from, to)}?`,
choices: _.compact([
{name: 'Yes', value: true},
{name: 'No', value: false},
// Don't show this option if we couldn't find module's changelog url
(changelogUrl !== null) &&
{name: 'Show changelog', value: 'changelog'},
// Show this if we haven't found changelog
(changelogUrl === null && homepage !== null) &&
{name: 'Open homepage', value: 'homepage'},
{name: 'Ignore', value: 'ignore'},
{name: 'Finish update process', value: 'finish'}
]),
// Automatically setting cursor to "Open homepage" after we haven't found changelog
default: (changelogUrl === null && homepage === undefined) ? 2 : 0
});
switch (answer) {
case 'changelog':
// Ask user about this module again
modulesToUpdate.unshift(outdatedModule);
if (changelogUrl === undefined) {
console.log('Trying to find changelog URL...');
changelogUrl =
outdatedModule.changelogUrl = await findModuleChangelogUrl(name);
}
if (changelogUrl) {
console.log(`Opening ${strong(changelogUrl)}...`);
open(changelogUrl);
} else {
console.log(
`Sorry, we haven't found any changelog URL for ${strong(name)} module.\n` +
`It would be great if you could fill an issue about this here: ${strong(pkg.bugs.url)}\n` +
'Thanks a lot!'
);
}
break;
case 'homepage':
// Ask user about this module again
modulesToUpdate.unshift(outdatedModule);
if (homepage === undefined) {
console.log('Trying to find homepage URL...');
homepage = outdatedModule.homepage = getModuleHomepage(await getModuleInfo(name));
}
if (homepage) {
console.log(`Opening ${strong(homepage)}...`);
open(homepage);
} else {
console.log(`Sorry, there is no info about homepage URL in the ${strong(name)}'s package.json`);
}
break;
case 'ignore': {
const {versions, reason} = await askIgnoreFields(latestVersions[name]);
config.ignore[name] = {versions, reason};
break;
}
case 'finish':
isUpdateFinished = true;
break;
case true:
updatedModules.push(outdatedModule);
setModuleVersion(name, to, packageJson);
delete config.ignore[name];
break;
}
}
// Adds new line
console.log('');
// Saving config
config.save();
if (!updatedModules.length) {
console.log('Nothing to update');
return;
}
// Showing the list of modules that are going to be updated
console.log(
`\n${strong('These packages will be updated:')}\n\n` +
createUpdatedModulesTable(updatedModules) +
'\n'
);
if (opts.global) {
const shouldUpdateGlobalPackages = await askUser(
{type: 'confirm', message: 'Update global modules?', default: true}
);
if (!shouldUpdateGlobalPackages) {return}
console.log(`Automatically upgrading ${updatedModules.length} module${updatedModules.length !== 1 ? 's' : ''}...`);
return shell.exec(`npm install --global ${updatedModules.map(({name, to}) => `${name}@${to}`).join(' ')}`);
}
const shouldUpdatePackageFile = await askUser(
{type: 'confirm', message: 'Update package.json?', default: true}
);
if (shouldUpdatePackageFile) {
const {indent} = detectIndent(packageSource);
writeFileSync(
packageFile,
// Adding newline to the end of file
`${JSON.stringify(packageJson, null, indent)}\n`
);
}
});
function filterDepsInPackageJson(packageJson, moduleNameFilterFn) {
const result = _.cloneDeep(packageJson);
for (const depsGroup of DEPS_GROUPS) {
const deps = result[depsGroup.field];
if (deps) {
for (const moduleName of Object.keys(deps)) {
if (!moduleNameFilterFn(moduleName)) {
delete deps[moduleName];
}
}
}
}
return result;
}
function sortModules(modules) {
const processedModules = new Set();
for (let i = 0, len = modules.length; i < len; i++) {
const module = modules[i];
if (processedModules.has(module)) {
continue;
}
const normalizedName = module.name.replace(/^@types\//, '');
if (module.name === normalizedName) {
continue;
}
// Searching for corresponding module
const originalModuleIndex = modules.findIndex(({name}) => name === normalizedName);
if (originalModuleIndex === -1 || i === originalModuleIndex + 1) {
continue;
}
if (originalModuleIndex > i) {
modules.splice(originalModuleIndex + 1, 0, module);
modules.splice(i, 1);
processedModules.add(module);
i--;
} else {
modules.splice(i, 1);
modules.splice(originalModuleIndex + 1, 0, module);
}
}
}