1 | 'use strict';
|
2 | const {spawn} = require('child_process');
|
3 | const path = require('path');
|
4 | const {format} = require('util');
|
5 | const importLazy = require('import-lazy')(require);
|
6 |
|
7 | const configstore = importLazy('configstore');
|
8 | const chalk = importLazy('chalk');
|
9 | const semverDiff = importLazy('semver-diff');
|
10 | const latestVersion = importLazy('latest-version');
|
11 | const isNpm = importLazy('is-npm');
|
12 | const isInstalledGlobally = importLazy('is-installed-globally');
|
13 | const isYarnGlobal = importLazy('is-yarn-global');
|
14 | const hasYarn = importLazy('has-yarn');
|
15 | const boxen = importLazy('boxen');
|
16 | const xdgBasedir = importLazy('xdg-basedir');
|
17 | const isCi = importLazy('is-ci');
|
18 |
|
19 | const ONE_DAY = 1000 * 60 * 60 * 24;
|
20 |
|
21 | class UpdateNotifier {
|
22 | constructor(options = {}) {
|
23 | this.options = options;
|
24 | options.pkg = options.pkg || {};
|
25 | options.distTag = options.distTag || 'latest';
|
26 |
|
27 |
|
28 |
|
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 |
|
53 |
|
54 | lastUpdateCheck: Date.now()
|
55 | });
|
56 | } catch (_) {
|
57 |
|
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 |
|
84 | this.update.current = this.packageVersion;
|
85 |
|
86 |
|
87 | this.config.delete('update');
|
88 | }
|
89 |
|
90 |
|
91 | if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
|
92 | return;
|
93 | }
|
94 |
|
95 |
|
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 |
|
167 | module.exports = options => {
|
168 | const updateNotifier = new UpdateNotifier(options);
|
169 | updateNotifier.check();
|
170 | return updateNotifier;
|
171 | };
|
172 |
|
173 | module.exports.UpdateNotifier = UpdateNotifier;
|