diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index c202b4b..0000000 --- a/.github/funding.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: sindresorhus -tidelift: npm/update-notifier diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..346585c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,21 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - 20 + - 18 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9d7745e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: node_js -node_js: - - '14' - - '12' - - '10' diff --git a/check.js b/check.js index fc0ee9c..75d2f8a 100644 --- a/check.js +++ b/check.js @@ -1,12 +1,10 @@ /* eslint-disable unicorn/no-process-exit */ -'use strict'; -let updateNotifier = require('.'); +import process from 'node:process'; +import UpdateNotifier from './update-notifier.js'; -const options = JSON.parse(process.argv[2]); +const updateNotifier = new UpdateNotifier(JSON.parse(process.argv[2])); -updateNotifier = new updateNotifier.UpdateNotifier(options); - -(async () => { +try { // Exit process when offline setTimeout(process.exit, 1000 * 30); @@ -22,7 +20,7 @@ updateNotifier = new updateNotifier.UpdateNotifier(options); // Call process exit explicitly to terminate the child process, // otherwise the child process will run forever, according to the Node.js docs process.exit(); -})().catch(error => { +} catch (error) { console.error(error); process.exit(1); -}); +} diff --git a/contributing.md b/contributing.md deleted file mode 100644 index 0be6f2f..0000000 --- a/contributing.md +++ /dev/null @@ -1 +0,0 @@ -See the [contributing docs](https://github.com/yeoman/yeoman/blob/master/contributing.md) diff --git a/example.js b/example.js index c9ff4de..649ef88 100644 --- a/example.js +++ b/example.js @@ -1,5 +1,4 @@ -'use strict'; -const updateNotifier = require('.'); +import updateNotifier from './index.js'; // Run: $ node example @@ -10,7 +9,8 @@ const updateNotifier = require('.'); updateNotifier({ pkg: { name: 'public-ip', - version: '0.9.2' + version: '0.9.2', }, - updateCheckInterval: 0 -}).notify(); + updateCheckInterval: 0, +}) + .notify(); diff --git a/index.js b/index.js index 5e89216..e216e65 100644 --- a/index.js +++ b/index.js @@ -1,188 +1,7 @@ -'use strict'; -const {spawn} = require('child_process'); -const path = require('path'); -const {format} = require('util'); -const importLazy = require('import-lazy')(require); +import UpdateNotifier from './update-notifier.js'; -const configstore = importLazy('configstore'); -const chalk = importLazy('chalk'); -const semver = importLazy('semver'); -const semverDiff = importLazy('semver-diff'); -const latestVersion = importLazy('latest-version'); -const isNpm = importLazy('is-npm'); -const isInstalledGlobally = importLazy('is-installed-globally'); -const isYarnGlobal = importLazy('is-yarn-global'); -const hasYarn = importLazy('has-yarn'); -const boxen = importLazy('boxen'); -const xdgBasedir = importLazy('xdg-basedir'); -const isCi = importLazy('is-ci'); -const pupa = importLazy('pupa'); - -const ONE_DAY = 1000 * 60 * 60 * 24; - -class UpdateNotifier { - constructor(options = {}) { - this.options = options; - options.pkg = options.pkg || {}; - options.distTag = options.distTag || 'latest'; - - // Reduce pkg to the essential keys. with fallback to deprecated options - // TODO: Remove deprecated options at some point far into the future - options.pkg = { - name: options.pkg.name || options.packageName, - version: options.pkg.version || options.packageVersion - }; - - if (!options.pkg.name || !options.pkg.version) { - throw new Error('pkg.name and pkg.version required'); - } - - this.packageName = options.pkg.name; - this.packageVersion = options.pkg.version; - this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY; - this.disabled = 'NO_UPDATE_NOTIFIER' in process.env || - process.env.NODE_ENV === 'test' || - process.argv.includes('--no-update-notifier') || - isCi(); - this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript; - - if (!this.disabled) { - try { - const ConfigStore = configstore(); - this.config = new ConfigStore(`update-notifier-${this.packageName}`, { - optOut: false, - // Init with the current time so the first check is only - // after the set interval, so not to bother users right away - lastUpdateCheck: Date.now() - }); - } catch { - // Expecting error code EACCES or EPERM - const message = - chalk().yellow(format(' %s update check failed ', options.pkg.name)) + - format('\n Try running with %s or get access ', chalk().cyan('sudo')) + - '\n to the local update config store via \n' + - chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config)); - - process.on('exit', () => { - console.error(boxen()(message, {align: 'center'})); - }); - } - } - } - - check() { - if ( - !this.config || - this.config.get('optOut') || - this.disabled - ) { - return; - } - - this.update = this.config.get('update'); - - if (this.update) { - // Use the real latest version instead of the cached one - this.update.current = this.packageVersion; - - // Clear cached information - this.config.delete('update'); - } - - // Only check for updates on a set interval - if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) { - return; - } - - // Spawn a detached process, passing the options as an environment property - spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], { - detached: true, - stdio: 'ignore' - }).unref(); - } - - async fetchInfo() { - const {distTag} = this.options; - const latest = await latestVersion()(this.packageName, {version: distTag}); - - return { - latest, - current: this.packageVersion, - type: semverDiff()(this.packageVersion, latest) || distTag, - name: this.packageName - }; - } - - notify(options) { - const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn; - if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) { - return this; - } - - options = { - isGlobal: isInstalledGlobally(), - isYarnGlobal: isYarnGlobal()(), - ...options - }; - - let installCommand; - if (options.isYarnGlobal) { - installCommand = `yarn global add ${this.packageName}`; - } else if (options.isGlobal) { - installCommand = `npm i -g ${this.packageName}`; - } else if (hasYarn()()) { - installCommand = `yarn add ${this.packageName}`; - } else { - installCommand = `npm i ${this.packageName}`; - } - - const defaultTemplate = 'Update available ' + - chalk().dim('{currentVersion}') + - chalk().reset(' → ') + - chalk().green('{latestVersion}') + - ' \nRun ' + chalk().cyan('{updateCommand}') + ' to update'; - - const template = options.message || defaultTemplate; - - options.boxenOptions = options.boxenOptions || { - padding: 1, - margin: 1, - align: 'center', - borderColor: 'yellow', - borderStyle: 'round' - }; - - const message = boxen()( - pupa()(template, { - packageName: this.packageName, - currentVersion: this.update.current, - latestVersion: this.update.latest, - updateCommand: installCommand - }), - options.boxenOptions - ); - - if (options.defer === false) { - console.error(message); - } else { - process.on('exit', () => { - console.error(message); - }); - - process.on('SIGINT', () => { - console.error(''); - process.exit(); - }); - } - - return this; - } -} - -module.exports = options => { +export default function updateNotifier(options) { const updateNotifier = new UpdateNotifier(options); updateNotifier.check(); return updateNotifier; -}; - -module.exports.UpdateNotifier = UpdateNotifier; +} diff --git a/package.json b/package.json index b7a183b..d1dd489 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,27 @@ { "name": "update-notifier", - "version": "5.0.1", + "version": "7.3.1", "description": "Update notifications for your CLI app", "license": "BSD-2-Clause", - "repository": "yeoman/update-notifier", - "funding": "https://github.com/yeoman/update-notifier?sponsor=1", + "repository": "sindresorhus/update-notifier", + "funding": "https://github.com/sponsors/sindresorhus", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, + "type": "module", + "exports": "./index.js", + "sideEffects": false, "engines": { - "node": ">=10" + "node": ">=18" }, "scripts": { - "test": "xo && ava --timeout=20s --serial" + "test": "xo && NODE_OPTIONS='--loader=esmock --no-warnings' ava" }, "files": [ "index.js", + "update-notifier.js", "check.js" ], "keywords": [ @@ -34,27 +38,27 @@ "version" ], "dependencies": { - "boxen": "^4.2.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.2", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.2", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "configstore": "^7.0.0", + "is-in-ci": "^1.0.0", + "is-installed-globally": "^1.0.0", + "is-npm": "^6.0.0", + "latest-version": "^9.0.0", + "pupa": "^3.1.0", + "semver": "^7.6.3", + "xdg-basedir": "^5.1.0" }, "devDependencies": { - "ava": "^2.4.0", - "clear-module": "^4.1.1", + "ava": "^6.1.3", + "clear-module": "^4.1.2", + "esmock": "^2.6.7", "fixture-stdout": "^0.2.1", - "mock-require": "^3.0.3", - "strip-ansi": "^6.0.0", - "xo": "^0.34.1" + "strip-ansi": "^7.1.0", + "xo": "^0.59.3" + }, + "ava": { + "timeout": "20s", + "serial": true } } diff --git a/readme.md b/readme.md index afab894..ef814b2 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,10 @@ -# update-notifier [](https://travis-ci.org/yeoman/update-notifier) +# update-notifier > Update notifications for your CLI app  -Inform users of your package of updates in a non-intrusive way. +Inform users of updates for your package in a non-intrusive way. #### Contents @@ -17,29 +17,29 @@ Inform users of your package of updates in a non-intrusive way. ## Install -``` -$ npm install update-notifier +```sh +npm install update-notifier ``` ## Usage -### Simple +### Basic ```js -const updateNotifier = require('update-notifier'); -const pkg = require('./package.json'); +import updateNotifier from 'update-notifier'; +import packageJson from './package.json' assert {type: 'json'}; -updateNotifier({pkg}).notify(); +updateNotifier({pkg: packageJson}).notify(); ``` -### Comprehensive +### Advanced ```js -const updateNotifier = require('update-notifier'); -const pkg = require('./package.json'); +import updateNotifier from 'update-notifier'; +import packageJson from './package.json' assert {type: 'json'}; // Checks for available update and returns an instance -const notifier = updateNotifier({pkg}); +const notifier = updateNotifier({pkg: packageJson}); // Notify using the built-in convenience method notifier.notify(); @@ -69,10 +69,10 @@ if (notifier.update) { } ``` -## How +## How it works 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. -The update check is done in a 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. +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. 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. @@ -127,10 +127,10 @@ Check update information. Returns an `object` with: -- `latest` _(String)_ - Latest version. -- `current` _(String)_ - Current version. -- `type` _(String)_ - Type of current update. Possible values: `latest`, `major`, `minor`, `patch`, `prerelease`, `build`. -- `name` _(String)_ - Package name. +- `latest` *(string)* - Latest version. +- `current` *(string)* - Current version. +- `type` *(string)* - Type of the current update. Possible values: `latest`, `major`, `minor`, `patch`, `prerelease`, `build`. +- `name` *(string)* - Package name. ### notifier.notify(options?) @@ -147,12 +147,12 @@ Type: `object` Type: `boolean`\ Default: `true` -Defer showing the notification to after the process has exited. +Defer showing the notification until after the process has exited. ##### message Type: `string`\ -Default: [See above screenshot](https://github.com/yeoman/update-notifier#update-notifier-) +Default: [See above screenshot](#update-notifier-) Message that will be shown when an update is available. @@ -180,7 +180,7 @@ Include the `-g` argument in the default message's `npm i` recommendation. You m ##### boxenOptions Type: `object`\ -Default: `{padding: 1, margin: 1, align: 'center', borderColor: 'yellow', borderStyle: 'round'}` *(See screenshot)* +Default: `{padding: 1, margin: 1, textAlignment: 'center', borderColor: 'yellow', borderStyle: 'round'}` *(See screenshot)* Options object that will be passed to [`boxen`](https://github.com/sindresorhus/boxen). @@ -188,11 +188,11 @@ Options object that will be passed to [`boxen`](https://github.com/sindresorhus/ 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`. -Users can also opt-out by [setting the environment variable](https://github.com/sindresorhus/guides/blob/master/set-environment-variables.md) `NO_UPDATE_NOTIFIER` with any value or by using the `--no-update-notifier` flag on a per run basis. +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. The check is also skipped automatically: - - on CI - - in unit tests (when the `NODE_ENV` environment variable is `test`) +- [in CI](https://github.com/sindresorhus/is-in-ci) +- in unit tests (when the `NODE_ENV` environment variable is `test`) ## About @@ -200,24 +200,12 @@ The idea for this module came from the desire to apply the browser update strate ## Users -There are a bunch projects using it: +There are a bunch of projects using it: - [npm](https://github.com/npm/npm) - Package manager for JavaScript - [Yeoman](https://yeoman.io) - Modern workflows for modern webapps -- [AVA](https://ava.li) - Simple concurrent test runner +- [AVA](https://avajs.dev) - Simple concurrent test runner - [XO](https://github.com/xojs/xo) - JavaScript happiness style linter - [Node GH](https://github.com/node-gh/gh) - GitHub command line tool -[And 2700+ more…](https://www.npmjs.org/browse/depended/update-notifier) - ---- - -