UNPKG

5.42 kBJavaScriptView Raw
1'use strict';
2
3const compareVersions = require('compare-versions');
4const path = require('path');
5
6const CHECK_STATUS_FAILED = Symbol('check failed');
7const CHECK_STATUS_FOUND_NEW = Symbol('check found new');
8const CHECK_STATUS_NOT_CONNECTED = Symbol('check not connected');
9const CHECK_STATUS_OK = Symbol('check ok');
10
11const MESSAGE_TIMEOUT = 5000;
12const REQUEST_TIMEOUT = 10000;
13
14const CHECK_RATE = 7 * 24 * 60 * 60 * 1000;
15
16/* istanbul ignore next */
17function checkIfUpdateCheckIsDue (updateCheckConfig) {
18 try {
19 if (updateCheckConfig && updateCheckConfig.timestamp) {
20 const now = Date.now();
21 if (now - updateCheckConfig.timestamp <= CHECK_RATE) {
22 return false;
23 }
24 }
25 }
26 catch (_error) {
27 // Just check for updates when the file does not exist or is corrupted.
28 }
29
30 return true;
31}
32
33/* istanbul ignore next */
34function storeLastCheckDate (app, updateCheckConfig) {
35 try {
36 updateCheckConfig.timestamp = Date.now();
37 app.config.save();
38 }
39 catch (_error) {
40 // Do nothing, and try again on next run.
41 }
42}
43
44/* istanbul ignore next */
45function checkPackageForUpdates (packagePath) {
46 const currentPackageInfo = require(path.join(packagePath, 'package.json'));
47 const currentName = currentPackageInfo.name;
48 const currentVersion = currentPackageInfo.version;
49
50 const requestPromise = require('request-promise-native');
51 return requestPromise({
52 method: 'GET',
53 url: `https://registry.npmjs.org/${encodeURIComponent(currentName)}/x`,
54 timeout: REQUEST_TIMEOUT,
55 resolveWithFullResponse: true
56 })
57 .then(response => {
58 const packageInfo = JSON.parse(response.body);
59 const latestVersion = packageInfo.version;
60 const isLatest = compareVersions(currentVersion, latestVersion) >= 0;
61 return {
62 name: currentName,
63 status: isLatest ? CHECK_STATUS_OK : CHECK_STATUS_FOUND_NEW,
64 currentVersion,
65 latestVersion
66 };
67 })
68 .catch(error => {
69 return {
70 name: currentName,
71 status: error && error.code === 'ENOTFOUND' ? CHECK_STATUS_NOT_CONNECTED : CHECK_STATUS_FAILED
72 };
73 });
74}
75
76/* istanbul ignore next */
77function createDelayPromise (timeout = 1000) {
78 return new Promise((resolve, _reject) => {
79 setTimeout(resolve, timeout);
80 });
81}
82
83/* istanbul ignore next */
84module.exports = function addCheckForUpdates (app) {
85 const updateCheckConfig = app.config.registerConfig('fdtUpdateCheck', {
86 timestamp: null
87 });
88
89 app.cli.addPreController((_request, response) => {
90 let stopSpinner = null;
91 // Always return a Promise, this also catches any error so normal operation is always possible.
92 return new Promise((mainResolve) => {
93 // Don't check for updates when we already checked recently. We should play nice with the repository.
94 if (!checkIfUpdateCheckIsDue(updateCheckConfig)) {
95 return mainResolve();
96 }
97
98 stopSpinner = response.spinner('Doing periodic check for updates...');
99
100 const appPackageInfo = require(path.join(__dirname, '../package.json'));
101
102 // Get all modules from the development tools package.json dependencies which match the tools's package name.
103 const packagePathsToCheck = [
104 path.resolve(path.join(__dirname, '..')),
105 ...Object.keys(appPackageInfo.dependencies || {})
106 .filter(packageName => {
107 return packageName.indexOf(`${appPackageInfo.name}-module-`) === 0;
108 })
109 .map(packageName => path.dirname(require.resolve(packageName)))
110 ];
111
112 const updateCheckPromises = Promise.all(packagePathsToCheck.map(checkPackageForUpdates));
113
114 mainResolve(updateCheckPromises.then(updateCheckResults => {
115 const packagesWithUpdates = updateCheckResults.filter(r => r.status === CHECK_STATUS_FOUND_NEW);
116 const packagesWithFails = updateCheckResults.filter(r => r.status === CHECK_STATUS_FAILED);
117 const packagesWithNotConnected = updateCheckResults.filter(r => r.status === CHECK_STATUS_NOT_CONNECTED);
118
119 // Store last check timestamp, but only when there were no connection problems.
120 if (!packagesWithNotConnected.length) {
121 storeLastCheckDate(app, updateCheckConfig);
122 }
123
124 stopSpinner();
125 stopSpinner = null;
126
127 if (packagesWithUpdates.length) {
128 response.notice(`Found ${packagesWithUpdates.length} updates for this tool.`);
129 response.notice(`You can run \`npm update -g ${appPackageInfo.name}\` to install updates...`);
130
131 return createDelayPromise(MESSAGE_TIMEOUT);
132 }
133 else if (packagesWithFails.length) {
134 response.notice(`Could not check for updates on ${packagesWithFails.length}/${packagePathsToCheck.length} packages...`);
135 response.notice(`If this message keeps showing, you can run \`npm update -g ${appPackageInfo.name}\` to try and install updates...`);
136
137 return createDelayPromise(MESSAGE_TIMEOUT);
138 }
139 else if (packagesWithNotConnected.length) {
140 response.log('Could not connect to the packages repository, will try again next time.');
141 }
142 else {
143 response.log(`Checked all ${packagePathsToCheck.length} packages for updates, everything is up to date.`);
144 }
145 }));
146 }).catch(_error => {
147 // Catch any error. Update checking should never stop you from using this tool.
148 try {
149 if (stopSpinner) {
150 stopSpinner();
151 }
152 response.notice('Could not check for updates...');
153 response.notice('If this message keeps showing, run a global update of this tool using `npm update -g <packageName>` to try and install updates...');
154 }
155 catch (_error) {
156 // Do nothing
157 }
158 });
159 });
160};