1'use strict'; 2const spawn = require('child_process').spawn; 3const path = require('path'); 4const format = require('util').format; 5const importLazy = require('import-lazy')(require); 6 7const configstore = importLazy('configstore'); 8const chalk = importLazy('chalk'); 9const semverDiff = importLazy('semver-diff'); 10const latestVersion = importLazy('latest-version'); 11const isNpm = importLazy('is-npm'); 12const isInstalledGlobally = importLazy('is-installed-globally'); 13const boxen = importLazy('boxen'); 14const xdgBasedir = importLazy('xdg-basedir'); 15const isCi = importLazy('is-ci'); 16const ONE_DAY = 1000 * 60 * 60 * 24; 17 18class UpdateNotifier { 19 constructor(options) { 20 options = options || {}; 21 this.options = options; 22 options.pkg = options.pkg || {}; 23 24 // Reduce pkg to the essential keys. with fallback to deprecated options 25 // TODO: Remove deprecated options at some point far into the future 26 options.pkg = { 27 name: options.pkg.name || options.packageName, 28 version: options.pkg.version || options.packageVersion 29 }; 30 31 if (!options.pkg.name || !options.pkg.version) { 32 throw new Error('pkg.name and pkg.version required'); 33 } 34 35 this.packageName = options.pkg.name; 36 this.packageVersion = options.pkg.version; 37 this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY; 38 this.hasCallback = typeof options.callback === 'function'; 39 this.callback = options.callback || (() => {}); 40 this.disabled = 'NO_UPDATE_NOTIFIER' in process.env || 41 process.argv.indexOf('--no-update-notifier') !== -1 || 42 isCi(); 43 this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript; 44 45 if (!this.disabled && !this.hasCallback) { 46 try { 47 const ConfigStore = configstore(); 48 this.config = new ConfigStore(`update-notifier-${this.packageName}`, { 49 optOut: false, 50 // Init with the current time so the first check is only 51 // after the set interval, so not to bother users right away 52 lastUpdateCheck: Date.now() 53 }); 54 } catch (err) { 55 // Expecting error code EACCES or EPERM 56 const msg = 57 chalk().yellow(format(' %s update check failed ', options.pkg.name)) + 58 format('\n Try running with %s or get access ', chalk().cyan('sudo')) + 59 '\n to the local update config store via \n' + 60 chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config)); 61 62 process.on('exit', () => { 63 console.error('\n' + boxen()(msg, {align: 'center'})); 64 }); 65 } 66 } 67 } 68 check() { 69 if (this.hasCallback) { 70 this.checkNpm() 71 .then(update => this.callback(null, update)) 72 .catch(err => this.callback(err)); 73 return; 74 } 75 76 if ( 77 !this.config || 78 this.config.get('optOut') || 79 this.disabled 80 ) { 81 return; 82 } 83 84 this.update = this.config.get('update'); 85 86 if (this.update) { 87 this.config.delete('update'); 88 } 89 90 // Only check for updates on a set interval 91 if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) { 92 return; 93 } 94 95 // Spawn a detached process, passing the options as an environment property 96 spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], { 97 detached: true, 98 stdio: 'ignore' 99 }).unref(); 100 } 101 checkNpm() { 102 return latestVersion()(this.packageName).then(latestVersion => { 103 return { 104 latest: latestVersion, 105 current: this.packageVersion, 106 type: semverDiff()(this.packageVersion, latestVersion) || 'latest', 107 name: this.packageName 108 }; 109 }); 110 } 111 notify(opts) { 112 const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm(); 113 if (!process.stdout.isTTY || suppressForNpm || !this.update) { 114 return this; 115 } 116 117 opts = Object.assign({isGlobal: isInstalledGlobally()}, opts); 118 119 opts.message = opts.message || 'Update available ' + chalk().dim(this.update.current) + chalk().reset(' → ') + 120 chalk().green(this.update.latest) + ' \nRun ' + chalk().cyan('npm i ' + (opts.isGlobal ? '-g ' : '') + this.packageName) + ' to update'; 121 122 opts.boxenOpts = opts.boxenOpts || { 123 padding: 1, 124 margin: 1, 125 align: 'center', 126 borderColor: 'yellow', 127 borderStyle: 'round' 128 }; 129 130 const message = '\n' + boxen()(opts.message, opts.boxenOpts); 131 132 if (opts.defer === false) { 133 console.error(message); 134 } else { 135 process.on('exit', () => { 136 console.error(message); 137 }); 138 139 process.on('SIGINT', () => { 140 console.error(''); 141 process.exit(); 142 }); 143 } 144 145 return this; 146 } 147} 148 149module.exports = options => { 150 const updateNotifier = new UpdateNotifier(options); 151 updateNotifier.check(); 152 return updateNotifier; 153}; 154 155module.exports.UpdateNotifier = UpdateNotifier; 156