Skip to content

Commit 2edc36f

Browse files
committed
feat(cli): update check
1 parent a27aabe commit 2edc36f

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

packages/cli/src/check-update.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { join } from 'path';
2+
import fs from 'fs';
3+
import fsp from 'fs/promises';
4+
import { tmpdir } from 'os';
5+
import https from 'https';
6+
import http from 'http';
7+
import { env } from 'process';
8+
import picocolors from 'picocolors';
9+
import type { PackageJson } from 'type-fest';
10+
11+
const UPDATE_CHECK_INTERVAL = 3600000;
12+
const UPDATE_CHECK_DIST_TAG = 'latest';
13+
14+
const compareVersions = (a: string, b: string) => a.localeCompare(b, 'en-US', { numeric: true });
15+
16+
const getFile = async (name: string, scope: string | undefined, distTag: string) => {
17+
const subDir = join(tmpdir(), 'update-check');
18+
19+
if (!fs.existsSync(subDir)) {
20+
await fsp.mkdir(subDir, { recursive: true });
21+
}
22+
return join(subDir, `${scope ? `${scope}-` : ''}${name}-${distTag}.json`);
23+
};
24+
25+
const evaluateCache = async (file: string, time: number, interval: number) => {
26+
if (fs.existsSync(file)) {
27+
const content = await fsp.readFile(file, 'utf8');
28+
const { lastUpdate, latest } = JSON.parse(content);
29+
const nextCheck = lastUpdate + interval;
30+
31+
// As long as the time of the next check is in
32+
// the future, we don't need to run it yet
33+
if (nextCheck > time) {
34+
return {
35+
shouldCheck: false,
36+
latest
37+
};
38+
}
39+
}
40+
41+
return {
42+
shouldCheck: true,
43+
latest: null
44+
};
45+
};
46+
47+
const updateCache = (file: string, latest: string | null, lastUpdate: number) => {
48+
const content = JSON.stringify({
49+
latest,
50+
lastUpdate
51+
});
52+
53+
return fsp.writeFile(file, content, 'utf8');
54+
};
55+
56+
const loadPackage = (url: URL) => new Promise<PackageJson>((resolve, reject) => {
57+
const options: https.RequestOptions = {
58+
host: url.hostname,
59+
path: url.pathname,
60+
port: url.port,
61+
headers: {
62+
accept: 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
63+
},
64+
timeout: 2000
65+
};
66+
67+
const { get } = url.protocol === 'https:' ? https : http;
68+
get(options, response => {
69+
const { statusCode } = response;
70+
71+
if (statusCode !== 200) {
72+
const error = new Error(`Request failed with code ${statusCode}`);
73+
Object.defineProperty(error, 'code', { value: statusCode });
74+
75+
reject(error);
76+
77+
// Consume response data to free up RAM
78+
response.resume();
79+
return;
80+
}
81+
82+
let rawData = '';
83+
response.setEncoding('utf8');
84+
85+
response.on('data', chunk => {
86+
rawData += chunk;
87+
});
88+
89+
response.on('end', () => {
90+
try {
91+
const parsedData = JSON.parse(rawData);
92+
resolve(parsedData);
93+
} catch (e) {
94+
reject(e);
95+
}
96+
});
97+
}).on('error', reject).on('timeout', reject);
98+
});
99+
100+
const getMostRecent = async (distTag: string): Promise<string | null> => {
101+
const url = new URL(`https://registry.npmjs.org/nolyfill/${distTag}`);
102+
103+
try {
104+
const spec = await loadPackage(url);
105+
return spec.version ?? null;
106+
} catch {
107+
return null;
108+
}
109+
};
110+
111+
export const checkForUpdates = async (name: string, currentVersion: string): Promise<void> => {
112+
// Do not check for updates if the `NO_UPDATE_CHECK` variable is set.
113+
if (env.NO_UPDATE_CHECK) return;
114+
115+
const time = Date.now();
116+
117+
const isScoped = name.startsWith('/');
118+
const parts = name.split('/');
119+
const file = await getFile(
120+
isScoped ? parts[1] : name,
121+
isScoped ? parts[0] : undefined,
122+
UPDATE_CHECK_DIST_TAG
123+
);
124+
125+
let latest: string | null = null;
126+
let shouldCheck = true;
127+
128+
({ shouldCheck, latest } = await evaluateCache(file, time, UPDATE_CHECK_INTERVAL));
129+
130+
if (shouldCheck) {
131+
latest = (await getMostRecent(UPDATE_CHECK_DIST_TAG)) ?? null;
132+
// If we pulled an update, we need to update the cache
133+
await updateCache(file, latest, time);
134+
}
135+
136+
if (latest) {
137+
const comparision = compareVersions(currentVersion, latest);
138+
139+
if (comparision === -1) {
140+
console.log(
141+
picocolors.bgRed(picocolors.white(' UPDATE ')),
142+
`The latest version of "nolyfill" is ${latest}`
143+
);
144+
}
145+
}
146+
};

packages/cli/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { overridesPackageJson } from './json';
1010
import type { PKG } from './types';
1111
import { handleSigTerm } from './handle-sigterm';
1212
import { findPackagesCoveredByNolyfill } from './find-coverable-packages';
13+
import { checkForUpdates } from './check-update';
1314

1415
interface CliOptions {
1516
/** see full error messages, mostly for debugging */
@@ -118,6 +119,7 @@ const program = new Command('nolyfill');
118119
}
119120
});
120121

122+
await checkForUpdates('nolyfill', version);
121123
await program.parseAsync(process.argv);
122124
} catch (e) {
123125
handleError(e as Error, !!program.opts<CliOptions>().debug);

0 commit comments

Comments
 (0)