UNPKG

5.08 kBJavaScriptView Raw
1'use strict';
2const {spawn} = require('child_process');
3const path = require('path');
4const {format} = require('util');
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 isYarnGlobal = importLazy('is-yarn-global');
14const hasYarn = importLazy('has-yarn');
15const boxen = importLazy('boxen');
16const xdgBasedir = importLazy('xdg-basedir');
17const isCi = importLazy('is-ci');
18
19const ONE_DAY = 1000 * 60 * 60 * 24;
20
21class UpdateNotifier {
22 constructor(options = {}) {
23 this.options = options;
24 options.pkg = options.pkg || {};
25 options.distTag = options.distTag || 'latest';
26
27 // Reduce pkg to the essential keys. with fallback to deprecated options
28 // TODO: Remove deprecated options at some point far into the future
29 options.pkg = {
30 name: options.pkg.name || options.packageName,
31 version: options.pkg.version || options.packageVersion
32 };
33
34 if (!options.pkg.name || !options.pkg.version) {
35 throw new Error('pkg.name and pkg.version required');
36 }
37
38 this.packageName = options.pkg.name;
39 this.packageVersion = options.pkg.version;
40 this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
41 this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
42 process.env.NODE_ENV === 'test' ||
43 process.argv.includes('--no-update-notifier') ||
44 isCi();
45 this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
46
47 if (!this.disabled) {
48 try {
49 const ConfigStore = configstore();
50 this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
51 optOut: false,
52 // Init with the current time so the first check is only
53 // after the set interval, so not to bother users right away
54 lastUpdateCheck: Date.now()
55 });
56 } catch (_) {
57 // Expecting error code EACCES or EPERM
58 const message =
59 chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
60 format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
61 '\n to the local update config store via \n' +
62 chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));
63
64 process.on('exit', () => {
65 console.error('\n' + boxen()(message, {align: 'center'}));
66 });
67 }
68 }
69 }
70
71 check() {
72 if (
73 !this.config ||
74 this.config.get('optOut') ||
75 this.disabled
76 ) {
77 return;
78 }
79
80 this.update = this.config.get('update');
81
82 if (this.update) {
83 // Use the real latest version instead of the cached one
84 this.update.current = this.packageVersion;
85
86 // Clear cached information
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
102 async fetchInfo() {
103 const {distTag} = this.options;
104 const latest = await latestVersion()(this.packageName, {version: distTag});
105
106 return {
107 latest,
108 current: this.packageVersion,
109 type: semverDiff()(this.packageVersion, latest) || distTag,
110 name: this.packageName
111 };
112 }
113
114 notify(options) {
115 const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
116 if (!process.stdout.isTTY || suppressForNpm || !this.update || this.update.current === this.update.latest) {
117 return this;
118 }
119
120 options = Object.assign({
121 isGlobal: isInstalledGlobally(),
122 isYarnGlobal: isYarnGlobal()()
123 }, options);
124
125 let installCommand;
126
127 if (options.isYarnGlobal) {
128 installCommand = `yarn global add ${this.packageName}`;
129 } else if (options.isGlobal) {
130 installCommand = `npm i -g ${this.packageName}`;
131 } else if (hasYarn()()) {
132 installCommand = `yarn add ${this.packageName}`;
133 } else {
134 installCommand = `npm i ${this.packageName}`;
135 }
136
137 options.message = options.message || 'Update available ' + chalk().dim(this.update.current) + chalk().reset(' → ') +
138 chalk().green(this.update.latest) + ' \nRun ' + chalk().cyan(installCommand) + ' to update';
139
140 options.boxenOptions = options.boxenOptions || {
141 padding: 1,
142 margin: 1,
143 align: 'center',
144 borderColor: 'yellow',
145 borderStyle: 'round'
146 };
147
148 const message = '\n' + boxen()(options.message, options.boxenOptions);
149
150 if (options.defer === false) {
151 console.error(message);
152 } else {
153 process.on('exit', () => {
154 console.error(message);
155 });
156
157 process.on('SIGINT', () => {
158 console.error('');
159 process.exit();
160 });
161 }
162
163 return this;
164 }
165}
166
167module.exports = options => {
168 const updateNotifier = new UpdateNotifier(options);
169 updateNotifier.check();
170 return updateNotifier;
171};
172
173module.exports.UpdateNotifier = UpdateNotifier;