-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
index.ts
157 lines (140 loc) · 4.25 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import semver from 'semver';
import XDGAppPaths from 'xdg-app-paths';
import { dirname, parse as parsePath, resolve as resolvePath } from 'path';
import type { Output } from '../output';
import { existsSync, outputJSONSync, readJSONSync } from 'fs-extra';
import type { PackageJson } from '@vercel/build-utils';
import { spawn } from 'child_process';
interface GetLatestVersionOptions {
cacheDir?: string;
distTag?: string;
notifyInterval?: number;
output?: Output;
pkg: PackageJson;
updateCheckInterval?: number;
}
interface PackageInfoCache {
expireAt: number;
notifyAt: number;
version: string;
}
interface GetLatestWorkerPayload {
cacheFile?: string;
distTag?: string;
name?: string;
updateCheckInterval?: number;
}
/**
* Determines if it needs to check for a newer CLI version and returns the last
* detected version. The version could be stale, but still newer than the
* current version.
*
* @returns {String|undefined} If a newer version is found, then the lastest
* version, otherwise `undefined`.
*/
export default function getLatestVersion({
cacheDir = XDGAppPaths('com.vercel.cli').cache(),
distTag = 'latest',
notifyInterval = 1000 * 60 * 60 * 24 * 3, // 3 days
output,
pkg,
updateCheckInterval = 1000 * 60 * 60 * 24, // 1 day
}: GetLatestVersionOptions): string | undefined {
if (
!pkg ||
typeof pkg !== 'object' ||
!pkg.name ||
typeof pkg.name !== 'string'
) {
throw new TypeError('Expected package to be an object with a package name');
}
const cacheFile = resolvePath(
cacheDir,
'package-updates',
`${pkg.name}-${distTag}.json`
);
let cache: PackageInfoCache | undefined;
try {
cache = readJSONSync(cacheFile);
} catch (err: any) {
// cache does not exist or malformed
if (err.code !== 'ENOENT') {
output?.debug(`Error reading latest package cache file: ${err}`);
}
}
if (!cache || !cache.expireAt || cache.expireAt < Date.now()) {
spawnWorker(
{
cacheFile,
distTag,
name: pkg.name,
updateCheckInterval,
},
output
);
}
if (cache) {
const shouldNotify = !cache.notifyAt || cache.notifyAt < Date.now();
let updateAvailable = false;
if (cache.version && pkg.version) {
updateAvailable = semver.lt(pkg.version, cache.version);
}
if (shouldNotify && updateAvailable) {
cache.notifyAt = Date.now() + notifyInterval;
outputJSONSync(cacheFile, cache);
return cache.version;
}
}
}
/**
* Spawn the worker, wait for the worker to report it's ready, then signal the
* worker to fetch the latest version.
*/
function spawnWorker(
payload: GetLatestWorkerPayload,
output: Output | undefined
) {
// we need to find the update worker script since the location is
// different based on production vs tests
let dir = dirname(__filename);
let script = resolvePath(dir, 'dist', 'get-latest-worker.js');
const { root } = parsePath(dir);
while (!existsSync(script)) {
dir = dirname(dir);
if (dir === root) {
// didn't find it, bail
output?.debug('Failed to find the get latest worker script!');
return;
}
script = resolvePath(dir, 'dist', 'get-latest-worker.js');
}
// spawn the worker with an IPC channel
output?.debug(`Spawning ${script}`);
const args = [script];
if (output?.debugEnabled) {
args.push('--debug');
}
const worker = spawn(process.execPath, args, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
windowsHide: true,
});
// we allow the child 2 seconds to let us know it's ready before we give up
const workerReadyTimer = setTimeout(() => worker.kill(), 2000);
// listen for an early on close error, but then we remove it when unref
const onClose = (code: number) => {
output?.debug(`Get latest worker exited (code ${code})`);
};
worker.on('close', onClose);
// generally, the parent won't be around long enough to handle a non-zero
// worker process exit code
worker.on('error', err => {
output?.log(`Failed to spawn get latest worker: ${err.stack}`);
});
// wait for the worker to start and notify us it is ready
worker.once('message', () => {
clearTimeout(workerReadyTimer);
worker.removeListener('close', onClose);
worker.send(payload);
worker.unref();
});
}