diff --git a/.npm-upgrade.json b/.npm-upgrade.json index 3f35239..358fe63 100644 --- a/.npm-upgrade.json +++ b/.npm-upgrade.json @@ -4,5 +4,10 @@ "versions": ">=6.0.0", "reason": "^5 is required for 'eslint-config-th0r'" } + }, + "recentUpdates": { + "info": "3d", + "warning": "2d", + "caution": "1d" } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 866552b..1a580d1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ Update "@angular/common" in package.json from 2.4.8 to 2.4.10? (Use arrow keys) * `Ignore` will add this module to the ignored list (see details in [`Ignoring module`](#ignoring-module) section below). * `Finish update process` will ...hm... finish update process and save all the changes to `package.json`. +> [!NOTE] +> By default, `npm-upgrade` will show warnings for modules that have recent updates (by default, within the last 3 days). You can change this behavior by modifying `config.recentUpdates` in your `~/.npm-upgrade/config.json` file. For example, you can set it to `{"info": "1d", "warning": "2d", "caution": "3d"}` to show warnings for modules updated within the last 1 day, 2 days, and 3 days respectively. + A note on saving changes to `package.json`: when you choose `Yes` to update some module's version, `package.json` won't be immediately updated. It will be updated only after you will process all the outdated modules and confirm update **or** when you choose `Finish update process`. So if in the middle of the update process you've changed your mind just press `Ctrl+C` and `package.json` will remain untouched. If you want to check only some deps, you can use `filter` argument: diff --git a/src/Config.js b/src/Config.js index c0d55b0..b2cc43d 100644 --- a/src/Config.js +++ b/src/Config.js @@ -80,3 +80,9 @@ function cleanDeep(obj) { return obj; } + +export const RECENT_UPDATES_DEFAULT = { + info: '3d', + warning: '2d', + caution: '1d' +}; diff --git a/src/cliStyles.js b/src/cliStyles.js index 30ba434..f31970e 100644 --- a/src/cliStyles.js +++ b/src/cliStyles.js @@ -1,5 +1,9 @@ -import {white, green, yellow} from 'chalk'; +import {white, green, yellow, black} from 'chalk'; export const strong = white.bold; export const success = green.bold; export const attention = yellow.bold; + +export const upgradeCaution = black.bgRed; +export const upgradeWarning = black.bgYellow; +export const upgradeInfo = black.bgWhite; diff --git a/src/commands/check.js b/src/commands/check.js index ca2a1f3..992dce6 100644 --- a/src/commands/check.js +++ b/src/commands/check.js @@ -12,14 +12,15 @@ 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'; + getModuleInfo, getModuleHomepage, getVersionPublicationDate, + getModuleVersions} from '../packageUtils'; import {fetchRemoteDb, findModuleChangelogUrl} from '../changelogUtils'; import {createSimpleTable} from '../cliTable'; -import {strong, success, attention} from '../cliStyles'; +import {strong, success, attention, upgradeCaution, upgradeWarning, upgradeInfo} from '../cliStyles'; import askUser from '../askUser'; -import {toSentence} from '../stringUtils'; +import {toSentence, toTimespan} from '../stringUtils'; import {askIgnoreFields} from './ignore'; -import Config from '../Config'; +import Config, {RECENT_UPDATES_DEFAULT} from '../Config'; const pkg = require('../../package.json'); @@ -140,6 +141,24 @@ export const handler = catchAsyncError(async opts => { console.log(`\n${strong('Ignored updates:')}\n\n${createSimpleTable(rows)}`); } + let infoTime = toTimespan(config.recentUpdates?.info ?? RECENT_UPDATES_DEFAULT.info); + let warningTime = toTimespan(config.recentUpdates?.warning ?? RECENT_UPDATES_DEFAULT.warning); + let cautionTime = toTimespan(config.recentUpdates?.caution ?? RECENT_UPDATES_DEFAULT.caution); + + // If timespan are not valid, print an error and set to default values + if (infoTime < warningTime || infoTime < cautionTime || warningTime < cautionTime) { + console.error('Invalid timespan values in config.recentUpdates. Using default values.'); + infoTime = toTimespan(RECENT_UPDATES_DEFAULT.info); + warningTime = toTimespan(RECENT_UPDATES_DEFAULT.warning); + cautionTime = toTimespan(RECENT_UPDATES_DEFAULT.caution); + } + + // Preload published dates in the background before the loop + const publishedDatesCache = {}; + modulesToUpdate.forEach(({name, to}) => { + publishedDatesCache[`${name}@${to}`] = getVersionPublicationDate(name, to); + }); + const updatedModules = []; let isUpdateFinished = false; while (modulesToUpdate.length && !isUpdateFinished) { @@ -150,6 +169,29 @@ export const handler = catchAsyncError(async opts => { // Adds new line console.log(''); + // This checks if the package was released less than N days ago, throws a warning if true + const publishedDate = new Date(await publishedDatesCache[`${name}@${to}`]); + // This is N days prior to execution time. + const recommendedDatePrior = new Date(Date.now() - infoTime); + const isRecent = publishedDate.getTime() > recommendedDatePrior.getTime(); + if (isRecent) { + const timeSincePublication = new Date(Date.now()).getTime() - publishedDate.getTime(); + const warningLevel = (isRecent + && timeSincePublication < cautionTime) ? 'caution' + : (timeSincePublication < warningTime) ? 'warning' + : 'info'; + let message = (warningLevel === 'caution') + ? upgradeCaution('CAUTION') : (warningLevel === 'warning') + ? upgradeWarning('WARN') : upgradeInfo('INFO'); + + const versions = await getModuleVersions(name); + const resolvedVersion = semver.maxSatisfying(Object.keys(versions), to); + message += ` ${name}@${resolvedVersion} was released less than ${Math.ceil( + timeSincePublication / toTimespan('1d') + )} days ago, be careful when upgrading.`; + console.log(message); + } + const answer = await askUser({ type: 'list', message: `${changelogUrl === undefined ? 'U' : 'So, u'}pdate "${name}" ${opts.global ? 'globally' : diff --git a/src/packageUtils.js b/src/packageUtils.js index 5a67999..6230f92 100644 --- a/src/packageUtils.js +++ b/src/packageUtils.js @@ -5,6 +5,8 @@ import pacote from 'pacote'; import shell from 'shelljs'; import _ from 'lodash'; +import got from 'got'; +import {maxSatisfying, validRange} from 'semver'; export const DEPS_GROUPS = [ {name: 'global', field: 'dependencies', flag: 'g', ncuValue: 'prod'}, @@ -108,3 +110,15 @@ export const getModuleInfo = _.memoize(async moduleName => fullMetadata: true }) ); + +export const getModuleVersions = _.memoize(async moduleName => { + const moduleData = await got(`https://registry.npmjs.org/${moduleName}/`).json(); + return moduleData.time; +}); + +// This function returns the publication date of a specific module version. +export const getVersionPublicationDate = _.memoize(async (moduleName, version) => { + const versions = await getModuleVersions(moduleName); + const resolvedVersion = maxSatisfying(Object.keys(versions), validRange(version)); + return versions[resolvedVersion] || null; +}, (moduleName, version) => `${moduleName}@${version}`); diff --git a/src/stringUtils.js b/src/stringUtils.js index b784582..550de80 100644 --- a/src/stringUtils.js +++ b/src/stringUtils.js @@ -5,3 +5,26 @@ export function toSentence(items) { return items.slice(0, -1).join(', ') + ' and ' + items[items.length - 1]; } + +export function toTimespan(string) { + const match = string.match(/^(\d+)([smhd])$/); + if (!match) { + return null; + } + + const value = parseInt(match[1], 10); + const unit = match[2]; + + switch (unit) { + case 's': + return value * 1000; + case 'm': + return value * 60 * 1000; + case 'h': + return value * 60 * 60 * 1000; + case 'd': + return value * 24 * 60 * 60 * 1000; + default: + return null; + } +}