• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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