1 | 'use strict';
|
2 |
|
3 | const compareVersions = require('compare-versions');
|
4 | const path = require('path');
|
5 |
|
6 | const CHECK_STATUS_FAILED = Symbol('check failed');
|
7 | const CHECK_STATUS_FOUND_NEW = Symbol('check found new');
|
8 | const CHECK_STATUS_NOT_CONNECTED = Symbol('check not connected');
|
9 | const CHECK_STATUS_OK = Symbol('check ok');
|
10 |
|
11 | const MESSAGE_TIMEOUT = 5000;
|
12 | const REQUEST_TIMEOUT = 10000;
|
13 |
|
14 | const CHECK_RATE = 7 * 24 * 60 * 60 * 1000;
|
15 |
|
16 |
|
17 | function 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 |
|
28 | }
|
29 |
|
30 | return true;
|
31 | }
|
32 |
|
33 |
|
34 | function storeLastCheckDate (app, updateCheckConfig) {
|
35 | try {
|
36 | updateCheckConfig.timestamp = Date.now();
|
37 | app.config.save();
|
38 | }
|
39 | catch (_error) {
|
40 |
|
41 | }
|
42 | }
|
43 |
|
44 |
|
45 | function 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 |
|
77 | function createDelayPromise (timeout = 1000) {
|
78 | return new Promise((resolve, _reject) => {
|
79 | setTimeout(resolve, timeout);
|
80 | });
|
81 | }
|
82 |
|
83 |
|
84 | module.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 |
|
92 | return new Promise((mainResolve) => {
|
93 |
|
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 |
|
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 |
|
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 |
|
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 |
|
157 | }
|
158 | });
|
159 | });
|
160 | };
|