├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── check.js ├── example.js ├── index.js ├── license ├── package.json ├── readme.md ├── screenshot.png ├── test ├── fs-error.js ├── notify.js └── update-notifier.js └── update-notifier.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /check.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-process-exit */ 2 | import process from 'node:process'; 3 | import UpdateNotifier from './update-notifier.js'; 4 | 5 | const updateNotifier = new UpdateNotifier(JSON.parse(process.argv[2])); 6 | 7 | try { 8 | // Exit process when offline 9 | setTimeout(process.exit, 1000 * 30); 10 | 11 | const update = await updateNotifier.fetchInfo(); 12 | 13 | // Only update the last update check time on success 14 | updateNotifier.config.set('lastUpdateCheck', Date.now()); 15 | 16 | if (update.type && update.type !== 'latest') { 17 | updateNotifier.config.set('update', update); 18 | } 19 | 20 | // Call process exit explicitly to terminate the child process, 21 | // otherwise the child process will run forever, according to the Node.js docs 22 | process.exit(); 23 | } catch (error) { 24 | console.error(error); 25 | process.exit(1); 26 | } 27 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | import updateNotifier from './index.js'; 2 | 3 | // Run: $ node example 4 | 5 | // You have to run this file two times the first time 6 | // This is because it never reports updates on the first run 7 | // If you want to test your own usage, ensure you set an older version 8 | 9 | updateNotifier({ 10 | pkg: { 11 | name: 'public-ip', 12 | version: '0.9.2', 13 | }, 14 | updateCheckInterval: 0, 15 | }) 16 | .notify(); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import UpdateNotifier from './update-notifier.js'; 2 | 3 | export default function updateNotifier(options) { 4 | const updateNotifier = new UpdateNotifier(options); 5 | updateNotifier.check(); 6 | return updateNotifier; 7 | } 8 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright Google 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "update-notifier", 3 | "version": "7.3.1", 4 | "description": "Update notifications for your CLI app", 5 | "license": "BSD-2-Clause", 6 | "repository": "sindresorhus/update-notifier", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": "./index.js", 15 | "sideEffects": false, 16 | "engines": { 17 | "node": ">=18" 18 | }, 19 | "scripts": { 20 | "test": "xo && NODE_OPTIONS='--loader=esmock --no-warnings' ava" 21 | }, 22 | "files": [ 23 | "index.js", 24 | "update-notifier.js", 25 | "check.js" 26 | ], 27 | "keywords": [ 28 | "npm", 29 | "update", 30 | "updater", 31 | "notify", 32 | "notifier", 33 | "check", 34 | "checker", 35 | "cli", 36 | "module", 37 | "package", 38 | "version" 39 | ], 40 | "dependencies": { 41 | "boxen": "^8.0.1", 42 | "chalk": "^5.3.0", 43 | "configstore": "^7.0.0", 44 | "is-in-ci": "^1.0.0", 45 | "is-installed-globally": "^1.0.0", 46 | "is-npm": "^6.0.0", 47 | "latest-version": "^9.0.0", 48 | "pupa": "^3.1.0", 49 | "semver": "^7.6.3", 50 | "xdg-basedir": "^5.1.0" 51 | }, 52 | "devDependencies": { 53 | "ava": "^6.1.3", 54 | "clear-module": "^4.1.2", 55 | "esmock": "^2.6.7", 56 | "fixture-stdout": "^0.2.1", 57 | "strip-ansi": "^7.1.0", 58 | "xo": "^0.59.3" 59 | }, 60 | "ava": { 61 | "timeout": "20s", 62 | "serial": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # update-notifier 2 | 3 | > Update notifications for your CLI app 4 | 5 | ![](screenshot.png) 6 | 7 | Inform users of updates for your package in a non-intrusive way. 8 | 9 | #### Contents 10 | 11 | - [Install](#install) 12 | - [Usage](#usage) 13 | - [How](#how) 14 | - [API](#api) 15 | - [About](#about) 16 | - [Users](#users) 17 | 18 | ## Install 19 | 20 | ```sh 21 | npm install update-notifier 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Basic 27 | 28 | ```js 29 | import updateNotifier from 'update-notifier'; 30 | import packageJson from './package.json' assert {type: 'json'}; 31 | 32 | updateNotifier({pkg: packageJson}).notify(); 33 | ``` 34 | 35 | ### Advanced 36 | 37 | ```js 38 | import updateNotifier from 'update-notifier'; 39 | import packageJson from './package.json' assert {type: 'json'}; 40 | 41 | // Checks for available update and returns an instance 42 | const notifier = updateNotifier({pkg: packageJson}); 43 | 44 | // Notify using the built-in convenience method 45 | notifier.notify(); 46 | 47 | // `notifier.update` contains some useful info about the update 48 | console.log(notifier.update); 49 | /* 50 | { 51 | latest: '1.0.1', 52 | current: '1.0.0', 53 | type: 'patch', // Possible values: latest, major, minor, patch, prerelease, build 54 | name: 'pageres' 55 | } 56 | */ 57 | ``` 58 | 59 | ### Options and custom message 60 | 61 | ```js 62 | const notifier = updateNotifier({ 63 | pkg, 64 | updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week 65 | }); 66 | 67 | if (notifier.update) { 68 | console.log(`Update available: ${notifier.update.latest}`); 69 | } 70 | ``` 71 | 72 | ## How it works 73 | 74 | Whenever you initiate the update notifier and it's not within the interval threshold, it will asynchronously check with npm in the background for available updates, then persist the result. The next time the notifier is initiated, the result will be loaded into the `.update` property. This prevents any impact on your package startup performance. 75 | The update check is done in an unref'ed [child process](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options). This means that if you call `process.exit`, the check will still be performed in its own process. 76 | 77 | The first time the user runs your app, it will check for an update, and even if an update is available, it will wait the specified `updateCheckInterval` before notifying the user. This is done to not be annoying to the user, but might surprise you as an implementer if you're testing whether it works. Check out [`example.js`](example.js) to quickly test out `update-notifier` and see how you can test that it works in your app. 78 | 79 | ## API 80 | 81 | ### notifier = updateNotifier(options) 82 | 83 | Checks if there is an available update. Accepts options defined below. Returns an instance with an `.update` property if there is an available update, otherwise `undefined`. 84 | 85 | ### options 86 | 87 | Type: `object` 88 | 89 | #### pkg 90 | 91 | Type: `object` 92 | 93 | ##### name 94 | 95 | *Required*\ 96 | Type: `string` 97 | 98 | ##### version 99 | 100 | *Required*\ 101 | Type: `string` 102 | 103 | #### updateCheckInterval 104 | 105 | Type: `number`\ 106 | Default: `1000 * 60 * 60 * 24` *(1 day)* 107 | 108 | How often to check for updates. 109 | 110 | #### shouldNotifyInNpmScript 111 | 112 | Type: `boolean`\ 113 | Default: `false` 114 | 115 | Allows notification to be shown when running as an npm script. 116 | 117 | #### distTag 118 | 119 | Type: `string`\ 120 | Default: `'latest'` 121 | 122 | Which [dist-tag](https://docs.npmjs.com/adding-dist-tags-to-packages) to use to find the latest version. 123 | 124 | ### notifier.fetchInfo() 125 | 126 | Check update information. 127 | 128 | Returns an `object` with: 129 | 130 | - `latest` *(string)* - Latest version. 131 | - `current` *(string)* - Current version. 132 | - `type` *(string)* - Type of the current update. Possible values: `latest`, `major`, `minor`, `patch`, `prerelease`, `build`. 133 | - `name` *(string)* - Package name. 134 | 135 | ### notifier.notify(options?) 136 | 137 | Convenience method to display a notification message. *(See screenshot)* 138 | 139 | Only notifies if there is an update and the process is [TTY](https://nodejs.org/api/process.html#process_a_note_on_process_i_o). 140 | 141 | #### options 142 | 143 | Type: `object` 144 | 145 | ##### defer 146 | 147 | Type: `boolean`\ 148 | Default: `true` 149 | 150 | Defer showing the notification until after the process has exited. 151 | 152 | ##### message 153 | 154 | Type: `string`\ 155 | Default: [See above screenshot](#update-notifier-) 156 | 157 | Message that will be shown when an update is available. 158 | 159 | Available placeholders: 160 | 161 | - `{packageName}` - Package name. 162 | - `{currentVersion}` - Current version. 163 | - `{latestVersion}` - Latest version. 164 | - `{updateCommand}` - Update command. 165 | 166 | ```js 167 | notifier.notify({message: 'Run `{updateCommand}` to update.'}); 168 | 169 | // Output: 170 | // Run `npm install update-notifier-tester@1.0.0` to update. 171 | ``` 172 | 173 | ##### isGlobal 174 | 175 | Type: `boolean`\ 176 | Default: Auto-detect 177 | 178 | Include the `-g` argument in the default message's `npm i` recommendation. You may want to change this if your CLI package can be installed as a dependency of another project, and don't want to recommend a global installation. This option is ignored if you supply your own `message` (see above). 179 | 180 | ##### boxenOptions 181 | 182 | Type: `object`\ 183 | Default: `{padding: 1, margin: 1, textAlignment: 'center', borderColor: 'yellow', borderStyle: 'round'}` *(See screenshot)* 184 | 185 | Options object that will be passed to [`boxen`](https://github.com/sindresorhus/boxen). 186 | 187 | ### User settings 188 | 189 | Users of your module have the ability to opt-out of the update notifier by changing the `optOut` property to `true` in `~/.config/configstore/update-notifier-[your-module-name].json`. The path is available in `notifier.config.path`. 190 | 191 | Users can also opt-out by [setting the environment variable](https://github.com/sindresorhus/guides/blob/main/set-environment-variables.md) `NO_UPDATE_NOTIFIER` with any value or by using the `--no-update-notifier` flag on a per run basis. 192 | 193 | The check is also skipped automatically: 194 | - [in CI](https://github.com/sindresorhus/is-in-ci) 195 | - in unit tests (when the `NODE_ENV` environment variable is `test`) 196 | 197 | ## About 198 | 199 | The idea for this module came from the desire to apply the browser update strategy to CLI tools, where everyone is always on the latest version. We first tried automatic updating, which we discovered wasn't popular. This is the second iteration of that idea, but limited to just update notifications. 200 | 201 | ## Users 202 | 203 | There are a bunch of projects using it: 204 | 205 | - [npm](https://github.com/npm/npm) - Package manager for JavaScript 206 | - [Yeoman](https://yeoman.io) - Modern workflows for modern webapps 207 | - [AVA](https://avajs.dev) - Simple concurrent test runner 208 | - [XO](https://github.com/xojs/xo) - JavaScript happiness style linter 209 | - [Node GH](https://github.com/node-gh/gh) - GitHub command line tool 210 | 211 | [And 5000+ more…](https://www.npmjs.org/browse/depended/update-notifier) 212 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/update-notifier/fbe4d6748b76da4e0b955ed39b7911e8804e58eb/screenshot.png -------------------------------------------------------------------------------- /test/fs-error.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import clearModule from 'clear-module'; 3 | import test from 'ava'; 4 | 5 | for (const name of ['..', 'configstore', 'xdg-basedir']) { 6 | clearModule(name); 7 | } 8 | 9 | // Set configstore.config to something that requires root access 10 | process.env.XDG_CONFIG_HOME = '/usr'; 11 | const {default: updateNotifier} = await import('../index.js'); 12 | 13 | test('fail gracefully', t => { 14 | t.notThrows(() => { 15 | updateNotifier({ 16 | packageName: 'npme', 17 | packageVersion: '3.7.0', 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/notify.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {inherits} from 'node:util'; 3 | import FixtureStdout from 'fixture-stdout'; 4 | import stripAnsi from 'strip-ansi'; 5 | import test from 'ava'; 6 | import esmock from 'esmock'; 7 | 8 | const stderr = new FixtureStdout({ 9 | stream: process.stderr, 10 | }); 11 | 12 | function Control(shouldNotifyInNpmScript) { 13 | this._packageName = 'update-notifier-tester'; 14 | this.update = { 15 | current: '0.0.2', 16 | latest: '1.0.0', 17 | }; 18 | this._shouldNotifyInNpmScript = shouldNotifyInNpmScript; 19 | } 20 | 21 | const setupTest = async isNpmReturnValue => { 22 | process.stdout.isTTY = true; 23 | 24 | const UpdateNotifier = await esmock('../update-notifier.js', { 25 | 'is-npm': {isNpmOrYarn: isNpmReturnValue || false}, 26 | }); 27 | 28 | inherits(Control, UpdateNotifier); 29 | }; 30 | 31 | let errorLogs = ''; 32 | 33 | test.beforeEach(async () => { 34 | await setupTest(); 35 | 36 | stderr.capture(s => { 37 | errorLogs += s; 38 | return false; 39 | }); 40 | }); 41 | 42 | test.afterEach(() => { 43 | stderr.release(); 44 | errorLogs = ''; 45 | }); 46 | 47 | test('use pretty boxen message by default', t => { 48 | const notifier = new Control(); 49 | notifier.notify({defer: false, isGlobal: true}); 50 | 51 | console.log('d', errorLogs); 52 | 53 | t.is(stripAnsi(errorLogs), ` 54 | ╭───────────────────────────────────────────────────╮ 55 | │ │ 56 | │ Update available 0.0.2 → 1.0.0 │ 57 | │ Run npm i -g update-notifier-tester to update │ 58 | │ │ 59 | ╰───────────────────────────────────────────────────╯ 60 | 61 | `); 62 | }); 63 | 64 | test('supports custom message', t => { 65 | const notifier = new Control(); 66 | notifier.notify({ 67 | defer: false, 68 | isGlobal: true, 69 | message: 'custom message', 70 | }); 71 | 72 | t.true(stripAnsi(errorLogs).includes('custom message')); 73 | }); 74 | 75 | test('supports message with placeholders', t => { 76 | const notifier = new Control(); 77 | notifier.notify({ 78 | defer: false, 79 | isGlobal: true, 80 | message: [ 81 | 'Package Name: {packageName}', 82 | 'Current Version: {currentVersion}', 83 | 'Latest Version: {latestVersion}', 84 | 'Update Command: {updateCommand}', 85 | ].join('\n'), 86 | }); 87 | 88 | t.is(stripAnsi(errorLogs), ` 89 | ╭─────────────────────────────────────────────────────╮ 90 | │ │ 91 | │ Package Name: update-notifier-tester │ 92 | │ Current Version: 0.0.2 │ 93 | │ Latest Version: 1.0.0 │ 94 | │ Update Command: npm i -g update-notifier-tester │ 95 | │ │ 96 | ╰─────────────────────────────────────────────────────╯ 97 | 98 | `); 99 | }); 100 | 101 | test('exclude -g argument when `isGlobal` option is `false`', t => { 102 | const notifier = new Control(); 103 | notifier.notify({defer: false, isGlobal: false}); 104 | t.not(stripAnsi(errorLogs).indexOf('Run npm i update-notifier-tester to update'), -1); 105 | }); 106 | 107 | test('shouldNotifyInNpmScript should default to false', t => { 108 | const notifier = new Control(); 109 | notifier.notify({defer: false}); 110 | t.not(stripAnsi(errorLogs).indexOf('Update available'), -1); 111 | }); 112 | 113 | test('suppress output when running as npm script', async t => { 114 | await setupTest(true); 115 | const notifier = new Control(); 116 | notifier.notify({defer: false}); 117 | t.false(stripAnsi(errorLogs).includes('Update available')); 118 | }); 119 | 120 | test('should output if running as npm script and shouldNotifyInNpmScript option set', async t => { 121 | await setupTest(true); 122 | const notifier = new Control(true); 123 | notifier.notify({defer: false}); 124 | t.true(stripAnsi(errorLogs).includes('Update available')); 125 | }); 126 | 127 | test('should not output if current version is the latest', async t => { 128 | await setupTest(true); 129 | const notifier = new Control(true); 130 | notifier.update.current = '1.0.0'; 131 | notifier.notify({defer: false}); 132 | t.false(stripAnsi(errorLogs).includes('Update available')); 133 | }); 134 | 135 | test('should not output if current version is more recent than the reported latest', async t => { 136 | await setupTest(true); 137 | const notifier = new Control(true); 138 | notifier.update.current = '1.0.1'; 139 | notifier.notify({defer: false}); 140 | t.false(stripAnsi(errorLogs).includes('Update available')); 141 | }); 142 | -------------------------------------------------------------------------------- /test/update-notifier.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import fs from 'node:fs'; 3 | import test from 'ava'; 4 | import esmock from 'esmock'; 5 | 6 | const generateSettings = (options = {}) => ({ 7 | pkg: { 8 | name: 'update-notifier-tester', 9 | version: '0.0.2', 10 | }, 11 | distTag: options.distTag, 12 | }); 13 | 14 | let argv; 15 | let configstorePath; 16 | 17 | test.beforeEach(() => { 18 | // Prevents NODE_ENV 'test' default behavior which disables `update-notifier` 19 | process.env.NODE_ENV = 'ava-test'; 20 | 21 | argv = [...process.argv]; 22 | }); 23 | 24 | test.afterEach(() => { 25 | delete process.env.NO_UPDATE_NOTIFIER; 26 | process.argv = argv; 27 | 28 | setTimeout(() => { 29 | try { 30 | fs.unlinkSync(configstorePath); 31 | } catch {} 32 | }, 10_000); 33 | }); 34 | 35 | test('fetch info', async t => { 36 | const updateNotifier = await esmock('../index.js', undefined, {'is-in-ci': false}); 37 | configstorePath = updateNotifier(generateSettings()).config.path; 38 | const update = await updateNotifier(generateSettings()).fetchInfo(); 39 | console.log(update); 40 | t.is(update.latest, '0.0.2'); 41 | }); 42 | 43 | test('fetch info with dist-tag', async t => { 44 | const updateNotifier = await esmock('../index.js', undefined, {'is-in-ci': false}); 45 | configstorePath = updateNotifier(generateSettings()).config.path; 46 | const update = await updateNotifier(generateSettings({distTag: '0.0.3-rc1'})).fetchInfo(); 47 | t.is(update.latest, '0.0.3-rc1'); 48 | }); 49 | 50 | test('don\'t initialize configStore when NO_UPDATE_NOTIFIER is set', async t => { 51 | const updateNotifier = await esmock('../index.js', undefined, {'is-in-ci': false}); 52 | configstorePath = updateNotifier(generateSettings()).config.path; 53 | process.env.NO_UPDATE_NOTIFIER = '1'; 54 | const notifier = updateNotifier(generateSettings()); 55 | t.is(notifier.config, undefined); 56 | }); 57 | 58 | test('don\'t initialize configStore when --no-update-notifier is set', async t => { 59 | const updateNotifier = await esmock('../index.js', undefined, {'is-in-ci': false}); 60 | configstorePath = updateNotifier(generateSettings()).config.path; 61 | process.argv.push('--no-update-notifier'); 62 | const notifier = updateNotifier(generateSettings()); 63 | t.is(notifier.config, undefined); 64 | }); 65 | 66 | test('don\'t initialize configStore when NODE_ENV === "test"', async t => { 67 | process.env.NODE_ENV = 'test'; 68 | const updateNotifier = await esmock('../index.js', undefined, {'is-in-ci': false}); 69 | const notifier = updateNotifier(generateSettings()); 70 | t.is(notifier.config, undefined); 71 | }); 72 | -------------------------------------------------------------------------------- /update-notifier.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {spawn} from 'node:child_process'; 3 | import {fileURLToPath} from 'node:url'; 4 | import path from 'node:path'; 5 | import {format} from 'node:util'; 6 | import ConfigStore from 'configstore'; 7 | import chalk from 'chalk'; 8 | // Only import what we need for performance 9 | import semverDiff from 'semver/functions/diff.js'; 10 | import semverGt from 'semver/functions/gt.js'; 11 | import latestVersion from 'latest-version'; 12 | import {isNpmOrYarn} from 'is-npm'; 13 | import isInstalledGlobally from 'is-installed-globally'; 14 | import boxen from 'boxen'; 15 | import {xdgConfig} from 'xdg-basedir'; 16 | import isInCi from 'is-in-ci'; 17 | import pupa from 'pupa'; 18 | 19 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 20 | 21 | const ONE_DAY = 1000 * 60 * 60 * 24; 22 | 23 | export default class UpdateNotifier { 24 | // Public 25 | config; 26 | update; 27 | 28 | // Semi-private (used for tests) 29 | _packageName; // eslint-disable-line lines-between-class-members 30 | _shouldNotifyInNpmScript; 31 | 32 | #options; // eslint-disable-line lines-between-class-members 33 | #packageVersion; 34 | #updateCheckInterval; 35 | #isDisabled; 36 | 37 | constructor(options = {}) { 38 | this.#options = options; 39 | options.pkg ??= {}; 40 | options.distTag ??= 'latest'; 41 | 42 | // Reduce pkg to the essential keys. with fallback to deprecated options 43 | // TODO: Remove deprecated options at some point far into the future 44 | options.pkg = { 45 | name: options.pkg.name ?? options.packageName, 46 | version: options.pkg.version ?? options.packageVersion, 47 | }; 48 | 49 | if (!options.pkg.name || !options.pkg.version) { 50 | throw new Error('pkg.name and pkg.version required'); 51 | } 52 | 53 | this._packageName = options.pkg.name; 54 | this.#packageVersion = options.pkg.version; 55 | this.#updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY; 56 | this.#isDisabled = 'NO_UPDATE_NOTIFIER' in process.env 57 | || process.env.NODE_ENV === 'test' 58 | || process.argv.includes('--no-update-notifier') 59 | || isInCi; 60 | this._shouldNotifyInNpmScript = options.shouldNotifyInNpmScript; 61 | 62 | if (!this.#isDisabled) { 63 | try { 64 | this.config = new ConfigStore(`update-notifier-${this._packageName}`, { 65 | optOut: false, 66 | // Init with the current time so the first check is only 67 | // after the set interval, so not to bother users right away 68 | lastUpdateCheck: Date.now(), 69 | }); 70 | } catch { 71 | // Expecting error code EACCES or EPERM 72 | const message 73 | = chalk.yellow(format(' %s update check failed ', options.pkg.name)) 74 | + format('\n Try running with %s or get access ', chalk.cyan('sudo')) 75 | + '\n to the local update config store via \n' 76 | + chalk.cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgConfig)); 77 | 78 | process.on('exit', () => { 79 | console.error(boxen(message, {textAlignment: 'center'})); 80 | }); 81 | } 82 | } 83 | } 84 | 85 | check() { 86 | if ( 87 | !this.config 88 | || this.config.get('optOut') 89 | || this.#isDisabled 90 | ) { 91 | return; 92 | } 93 | 94 | this.update = this.config.get('update'); 95 | 96 | if (this.update) { 97 | // Use the real latest version instead of the cached one 98 | this.update.current = this.#packageVersion; 99 | 100 | // Clear cached information 101 | this.config.delete('update'); 102 | } 103 | 104 | // Only check for updates on a set interval 105 | if (Date.now() - this.config.get('lastUpdateCheck') < this.#updateCheckInterval) { 106 | return; 107 | } 108 | 109 | // Spawn a detached process, passing the options as an environment property 110 | spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.#options)], { 111 | detached: true, 112 | stdio: 'ignore', 113 | }).unref(); 114 | } 115 | 116 | async fetchInfo() { 117 | const {distTag} = this.#options; 118 | const latest = await latestVersion(this._packageName, {version: distTag}); 119 | 120 | return { 121 | latest, 122 | current: this.#packageVersion, 123 | type: semverDiff(this.#packageVersion, latest) ?? distTag, 124 | name: this._packageName, 125 | }; 126 | } 127 | 128 | notify(options) { 129 | const suppressForNpm = !this._shouldNotifyInNpmScript && isNpmOrYarn; 130 | if (!process.stdout.isTTY || suppressForNpm || !this.update || !semverGt(this.update.latest, this.update.current)) { 131 | return this; 132 | } 133 | 134 | options = { 135 | isGlobal: isInstalledGlobally, 136 | ...options, 137 | }; 138 | 139 | const installCommand = options.isGlobal ? `npm i -g ${this._packageName}` : `npm i ${this._packageName}`; 140 | 141 | const defaultTemplate = 'Update available ' 142 | + chalk.dim('{currentVersion}') 143 | + chalk.reset(' → ') 144 | + chalk.green('{latestVersion}') 145 | + ' \nRun ' + chalk.cyan('{updateCommand}') + ' to update'; 146 | 147 | const template = options.message || defaultTemplate; 148 | 149 | options.boxenOptions ??= { 150 | padding: 1, 151 | margin: 1, 152 | textAlignment: 'center', 153 | borderColor: 'yellow', 154 | borderStyle: 'round', 155 | }; 156 | 157 | const message = boxen( 158 | pupa(template, { 159 | packageName: this._packageName, 160 | currentVersion: this.update.current, 161 | latestVersion: this.update.latest, 162 | updateCommand: installCommand, 163 | }), 164 | options.boxenOptions, 165 | ); 166 | 167 | if (options.defer === false) { 168 | console.error(message); 169 | } else { 170 | process.on('exit', () => { 171 | console.error(message); 172 | }); 173 | } 174 | 175 | return this; 176 | } 177 | } 178 | --------------------------------------------------------------------------------