From bc04703ce795f450bde3c6261991a75e5c89b32b Mon Sep 17 00:00:00 2001 From: Ayush Sharma Date: Thu, 20 Sep 2018 09:53:29 +0200 Subject: [PATCH] feat: verdaccio update notification on CLI (#988) (#998) --- flow-typed/npm/chalk_v2.x.x.js | 192 +++++++++--------- package.json | 2 +- src/lib/cli.js | 6 + src/lib/constants.js | 4 + src/lib/update-banner.js | 61 ++++++ .../__snapshots__/update-banner.spec.js.snap | 117 +++++++++++ test/unit/api/update-banner.spec.js | 87 ++++++++ 7 files changed, 367 insertions(+), 102 deletions(-) create mode 100644 src/lib/update-banner.js create mode 100644 test/unit/api/__snapshots__/update-banner.spec.js.snap create mode 100644 test/unit/api/update-banner.spec.js diff --git a/flow-typed/npm/chalk_v2.x.x.js b/flow-typed/npm/chalk_v2.x.x.js index 50c500899..43548785f 100644 --- a/flow-typed/npm/chalk_v2.x.x.js +++ b/flow-typed/npm/chalk_v2.x.x.js @@ -1,108 +1,98 @@ -// flow-typed signature: fa51178772ad1f35158cb4238bc3f1eb -// flow-typed version: da30fe6876/chalk_v2.x.x/flow_>=v0.25.x +// flow-typed signature: db5b2cdde8db39d47e27cc8ab84f89bf +// flow-typed version: d662d43161/chalk_v2.x.x/flow_>=v0.25.x -type $npm$chalk$StyleElement = { - open: string, - close: string -}; - -type $npm$chalk$Chain = $npm$chalk$Style & ((...text: any[]) => string); - -type $npm$chalk$Style = { - // General - reset: $npm$chalk$Chain, - bold: $npm$chalk$Chain, - dim: $npm$chalk$Chain, - italic: $npm$chalk$Chain, - underline: $npm$chalk$Chain, - inverse: $npm$chalk$Chain, - strikethrough: $npm$chalk$Chain, - - // Text colors - black: $npm$chalk$Chain, - red: $npm$chalk$Chain, - redBright: $npm$chalk$Chain, - green: $npm$chalk$Chain, - greenBright: $npm$chalk$Chain, - yellow: $npm$chalk$Chain, - yellowBright: $npm$chalk$Chain, - blue: $npm$chalk$Chain, - blueBright: $npm$chalk$Chain, - magenta: $npm$chalk$Chain, - magentaBright: $npm$chalk$Chain, - cyan: $npm$chalk$Chain, - cyanBright: $npm$chalk$Chain, - white: $npm$chalk$Chain, - whiteBright: $npm$chalk$Chain, - gray: $npm$chalk$Chain, - grey: $npm$chalk$Chain, - - // Background colors - bgBlack: $npm$chalk$Chain, - bgBlackBright: $npm$chalk$Chain, - bgRed: $npm$chalk$Chain, - bgRedBright: $npm$chalk$Chain, - bgGreen: $npm$chalk$Chain, - bgGreenBright: $npm$chalk$Chain, - bgYellow: $npm$chalk$Chain, - bgYellowBright: $npm$chalk$Chain, - bgBlue: $npm$chalk$Chain, - bgBlueBright: $npm$chalk$Chain, - bgMagenta: $npm$chalk$Chain, - bgMagentaBright: $npm$chalk$Chain, - bgCyan: $npm$chalk$Chain, - bgCyanBright: $npm$chalk$Chain, - bgWhite: $npm$chalk$Chain, - bgWhiteBright: $npm$chalk$Chain -}; +// From: https://github.com/chalk/chalk/blob/master/index.js.flow declare module "chalk" { - declare var enabled: boolean; - declare var supportsColor: boolean; + declare type TemplateStringsArray = $ReadOnlyArray; - // General - declare var reset: $npm$chalk$Chain; - declare var bold: $npm$chalk$Chain; - declare var dim: $npm$chalk$Chain; - declare var italic: $npm$chalk$Chain; - declare var underline: $npm$chalk$Chain; - declare var inverse: $npm$chalk$Chain; - declare var strikethrough: $npm$chalk$Chain; + declare type Level = $Values<{ + None: 0, + Basic: 1, + Ansi256: 2, + TrueColor: 3 + }>; - // Text colors - declare var black: $npm$chalk$Chain; - declare var red: $npm$chalk$Chain; - declare var redBright: $npm$chalk$Chain; - declare var green: $npm$chalk$Chain; - declare var greenBright: $npm$chalk$Chain; - declare var yellow: $npm$chalk$Chain; - declare var yellowBright: $npm$chalk$Chain; - declare var blue: $npm$chalk$Chain; - declare var blueBright: $npm$chalk$Chain; - declare var magenta: $npm$chalk$Chain; - declare var magentaBright: $npm$chalk$Chain; - declare var cyan: $npm$chalk$Chain; - declare var cyanBright: $npm$chalk$Chain; - declare var white: $npm$chalk$Chain; - declare var whiteBright: $npm$chalk$Chain; - declare var gray: $npm$chalk$Chain; - declare var grey: $npm$chalk$Chain; + declare type ChalkOptions = {| + enabled?: boolean, + level?: Level + |}; - // Background colors - declare var bgBlack: $npm$chalk$Chain; - declare var bgBlackBright: $npm$chalk$Chain; - declare var bgRed: $npm$chalk$Chain; - declare var bgRedBright: $npm$chalk$Chain; - declare var bgGreen: $npm$chalk$Chain; - declare var bgGreenBright: $npm$chalk$Chain; - declare var bgYellow: $npm$chalk$Chain; - declare var bgYellowBright: $npm$chalk$Chain; - declare var bgBlue: $npm$chalk$Chain; - declare var bgBlueBright: $npm$chalk$Chain; - declare var bgMagenta: $npm$chalk$Chain; - declare var bgMagentaBright: $npm$chalk$Chain; - declare var bgCyan: $npm$chalk$Chain; - declare var bgCyanBright: $npm$chalk$Chain; - declare var bgWhite: $npm$chalk$Chain; - declare var bgWhiteBright: $npm$chalk$Chain; + declare type ColorSupport = {| + level: Level, + hasBasic: boolean, + has256: boolean, + has16m: boolean + |}; + + declare interface Chalk { + (...text: string[]): string, + (text: TemplateStringsArray, ...placeholders: string[]): string, + constructor(options?: ChalkOptions): Chalk, + enabled: boolean, + level: Level, + rgb(r: number, g: number, b: number): Chalk, + hsl(h: number, s: number, l: number): Chalk, + hsv(h: number, s: number, v: number): Chalk, + hwb(h: number, w: number, b: number): Chalk, + bgHex(color: string): Chalk, + bgKeyword(color: string): Chalk, + bgRgb(r: number, g: number, b: number): Chalk, + bgHsl(h: number, s: number, l: number): Chalk, + bgHsv(h: number, s: number, v: number): Chalk, + bgHwb(h: number, w: number, b: number): Chalk, + hex(color: string): Chalk, + keyword(color: string): Chalk, + + +reset: Chalk, + +bold: Chalk, + +dim: Chalk, + +italic: Chalk, + +underline: Chalk, + +inverse: Chalk, + +hidden: Chalk, + +strikethrough: Chalk, + + +visible: Chalk, + + +black: Chalk, + +red: Chalk, + +green: Chalk, + +yellow: Chalk, + +blue: Chalk, + +magenta: Chalk, + +cyan: Chalk, + +white: Chalk, + +gray: Chalk, + +grey: Chalk, + +blackBright: Chalk, + +redBright: Chalk, + +greenBright: Chalk, + +yellowBright: Chalk, + +blueBright: Chalk, + +magentaBright: Chalk, + +cyanBright: Chalk, + +whiteBright: Chalk, + + +bgBlack: Chalk, + +bgRed: Chalk, + +bgGreen: Chalk, + +bgYellow: Chalk, + +bgBlue: Chalk, + +bgMagenta: Chalk, + +bgCyan: Chalk, + +bgWhite: Chalk, + +bgBlackBright: Chalk, + +bgRedBright: Chalk, + +bgGreenBright: Chalk, + +bgYellowBright: Chalk, + +bgBlueBright: Chalk, + +bgMagentaBright: Chalk, + +bgCyanBright: Chalk, + +bgWhiteBrigh: Chalk, + + supportsColor: ColorSupport + } + + declare module.exports: Chalk; } diff --git a/package.json b/package.json index 2540ccd27..b74bbe481 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "pretest": "npm run code:build", "test": "npm run test:unit", "test:clean": "npx jest --clearCache", - "test:unit": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC jest --config ./jest.config.js --maxWorkers 2", + "test:unit": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC FORCE_COLOR=1 jest --config ./jest.config.js --maxWorkers 2", "test:functional": "cross-env NODE_ENV=testOldEnv jest --config ./test/jest.config.functional.js --testPathPattern ./test/functional/index*", "test:e2e": "cross-env BABEL_ENV=testOldEnv jest --config ./test/jest.config.e2e.js", "test:size": "bundlesize", diff --git a/src/lib/cli.js b/src/lib/cli.js index 9397320d5..e7082a961 100644 --- a/src/lib/cli.js +++ b/src/lib/cli.js @@ -8,6 +8,7 @@ import semver from 'semver'; import chalk from 'chalk'; import {startVerdaccio, listenDefaultCallback} from './bootstrap'; import findConfigFile from './config-path'; +import {verdaccioUpdateBanner} from './update-banner'; if (process.getuid && process.getuid() === 0) { global.console.warn(chalk.bgYellow('Verdaccio doesn\'t need superuser privileges. Don\'t run it under root.')); @@ -31,6 +32,11 @@ const pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-var const pkgVersion = module.exports.version; const pkgName = module.exports.name; +/** + * Checking verdaccio version on NPM + */ +verdaccioUpdateBanner(pkgVersion); + commander .option('-l, --listen <[host:]port>', 'host:port number to listen on (default: localhost:4873)') .option('-c, --config ', 'use this configuration file (default: ./config.yaml)') diff --git a/src/lib/constants.js b/src/lib/constants.js index 6a3ce3389..ea367f7e9 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -104,3 +104,7 @@ export const PACKAGE_ACCESS = { SCOPE: '@*/*', ALL: '**', }; + +export const UPDATE_BANNER = { + CHANGELOG_URL: 'https://github.com/verdaccio/verdaccio/releases/tag/' +} \ No newline at end of file diff --git a/src/lib/update-banner.js b/src/lib/update-banner.js new file mode 100644 index 000000000..6d3f0f529 --- /dev/null +++ b/src/lib/update-banner.js @@ -0,0 +1,61 @@ +// @prettier +// @flow + +import request from 'request'; +import semver from 'semver'; +import chalk from 'chalk'; +import _ from 'lodash'; + +import { UPDATE_BANNER, DEFAULT_REGISTRY, HTTP_STATUS } from './constants'; + +const VERDACCIO_LATEST_REGISTRY_URL = `${DEFAULT_REGISTRY}/verdaccio/latest`; + +/** + * Creates NPM update banner using chalk + */ +export function createBanner(currentVersion: string, newVersion: string, releaseType: string): string { + const changelog = `${UPDATE_BANNER.CHANGELOG_URL}v${newVersion}` + const versionUpdate = `${chalk.bold.red(currentVersion)} → ${chalk.bold.green(newVersion)}` + const banner = chalk` + {white.bold A new ${_.upperCase(releaseType)} version of Verdaccio is available. ${versionUpdate} } + {white.bold Run ${chalk.green.bold('npm install -g verdaccio')} to update}. + {white.bold Registry: ${DEFAULT_REGISTRY}} + {blue.bold Changelog: ${changelog}} + `; + return banner; +} + +/** + * creates error banner + */ +export function createErrorBanner(message: string): string { + const banner = chalk` + {red.bold Unable to check verdaccio version on ${DEFAULT_REGISTRY}} + {red.bold Error: ${message}} + `; + return banner; +} + +/** + * Show verdaccio update banner on start + */ +export function verdaccioUpdateBanner(pkgVersion: string) { + request(VERDACCIO_LATEST_REGISTRY_URL, function (error: ?Object = null, response: Object = {}) { + if (!error && response.statusCode === HTTP_STATUS.OK && response.body) { + // In case, NPM does not returns version, keeping version equals to + // verdaccio version. + const { version = pkgVersion } = JSON.parse(response.body); + const releaseType = semver.diff(version, pkgVersion); + + if (releaseType && semver.gt(version, pkgVersion)) { + const banner = createBanner(pkgVersion, version, releaseType); + /* eslint-disable-next-line */ + console.log(banner); + } + } else { + const errorBanner = createErrorBanner(JSON.stringify(error)); + /* eslint-disable-next-line */ + console.log(errorBanner); + } + }); +} \ No newline at end of file diff --git a/test/unit/api/__snapshots__/update-banner.spec.js.snap b/test/unit/api/__snapshots__/update-banner.spec.js.snap new file mode 100644 index 000000000..86704e953 --- /dev/null +++ b/test/unit/api/__snapshots__/update-banner.spec.js.snap @@ -0,0 +1,117 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verdaccio update banner should print major update banner 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + " + A new MAJOR version of Verdaccio is available. 3.0.0 → 4.5.6  + Run npm install -g verdaccio to update. + Registry: https://registry.npmjs.org + Changelog: https://github.com/verdaccio/verdaccio/releases/tag/v4.5.6 + ", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`; + +exports[`Verdaccio update banner should print minor update banner 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + " + A new MINOR version of Verdaccio is available. 4.0.0 → 4.5.6  + Run npm install -g verdaccio to update. + Registry: https://registry.npmjs.org + Changelog: https://github.com/verdaccio/verdaccio/releases/tag/v4.5.6 + ", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`; + +exports[`Verdaccio update banner should print patch update banner 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + " + A new PATCH version of Verdaccio is available. 4.5.0 → 4.5.6  + Run npm install -g verdaccio to update. + Registry: https://registry.npmjs.org + Changelog: https://github.com/verdaccio/verdaccio/releases/tag/v4.5.6 + ", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`; + +exports[`Verdaccio update banner when default registry returns with error 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + " + Unable to check verdaccio version on https://registry.npmjs.org + Error: {\\"message\\":\\"internal server error\\",\\"statusCode\\":500} + ", + ], + ], + "results": Array [ + Object { + "isThrow": false, + "value": undefined, + }, + ], +} +`; + +exports[`create banner should create a major update banner 1`] = ` +" + A new MAJOR version of Verdaccio is available. 1.0.0 → 2.0.0  + Run npm install -g verdaccio to update. + Registry: https://registry.npmjs.org + Changelog: https://github.com/verdaccio/verdaccio/releases/tag/v2.0.0 + " +`; + +exports[`create banner should create a minor update banner 1`] = ` +" + A new MINOR version of Verdaccio is available. 1.0.0 → 1.1.0  + Run npm install -g verdaccio to update. + Registry: https://registry.npmjs.org + Changelog: https://github.com/verdaccio/verdaccio/releases/tag/v1.1.0 + " +`; + +exports[`create banner should create a patch update banner 1`] = ` +" + A new PATCH version of Verdaccio is available. 1.0.0 → 1.0.1  + Run npm install -g verdaccio to update. + Registry: https://registry.npmjs.org + Changelog: https://github.com/verdaccio/verdaccio/releases/tag/v1.0.1 + " +`; + +exports[`createErrorBanner should create an error banner 1`] = ` +" + Unable to check verdaccio version on https://registry.npmjs.org + Error: message + " +`; diff --git a/test/unit/api/update-banner.spec.js b/test/unit/api/update-banner.spec.js new file mode 100644 index 000000000..3581ca891 --- /dev/null +++ b/test/unit/api/update-banner.spec.js @@ -0,0 +1,87 @@ +// @prettier +// @flow +import { + createBanner, + createErrorBanner +} from '../../../src/lib/update-banner'; + +import {HTTP_STATUS, API_ERROR} from '../../../src/lib/constants'; + +jest.resetModules(); +jest.doMock('request', () => (url: string, resolver: Function) => { + const response = { + body: JSON.stringify({version: '4.5.6' }), + statusCode: HTTP_STATUS.OK + } + resolver(null, response); +}); + +const banner = require('../../../src/lib/update-banner'); + +describe('Verdaccio update banner', () => { + let log; + + beforeEach(() => { + // mocking global console.log method + global.console.log = jest.fn(); + log = global.console.log + }); + + test('should print major update banner', () => { + banner.verdaccioUpdateBanner('3.0.0'); + expect(log).toMatchSnapshot(); + }); + + test('should print minor update banner', () => { + banner.verdaccioUpdateBanner('4.0.0'); + expect(log).toMatchSnapshot(); + }); + + test('should print patch update banner', () => { + banner.verdaccioUpdateBanner('4.5.0'); + expect(log).toMatchSnapshot(); + }); + + test('when local version is equals to npm version', () => { + banner.verdaccioUpdateBanner('4.5.6'); + expect(log).not.toHaveBeenCalledWith(); + }); + + test('when local version is greater than npm version', () => { + banner.verdaccioUpdateBanner('4.5.7'); + expect(log).not.toHaveBeenCalledWith(); + }); + + test('when default registry returns with error', () => { + jest.resetModules(); + jest.doMock('request', () => (url: string, resolver: Function) => { + const error = { + message: API_ERROR.INTERNAL_SERVER_ERROR, + statusCode: HTTP_STATUS.INTERNAL_ERROR + } + resolver(error, null); + }); + const banner = require('../../../src/lib/update-banner'); + banner.verdaccioUpdateBanner('4.5.7'); + expect(log).toMatchSnapshot(); + }) + +}); + +describe('createErrorBanner', () => { + test('should create an error banner', () => { + expect(createErrorBanner('message')).toMatchSnapshot(); + }); +}); + +describe('create banner', () => { + test('should create a major update banner', () => { + expect(createBanner('1.0.0', '2.0.0', 'major')).toMatchSnapshot(); + }); + test('should create a minor update banner', () => { + expect(createBanner('1.0.0', '1.1.0', 'minor')).toMatchSnapshot(); + }); + test('should create a patch update banner', () => { + expect(createBanner('1.0.0', '1.0.1', 'patch')).toMatchSnapshot(); + }); +});