UNPKG

@oclif/plugin-warn-if-update-available

Version:
151 lines (150 loc) 6.31 kB
import { Ansis } from 'ansis'; import makeDebug from 'debug'; import { spawn } from 'node:child_process'; import { readFile, stat, writeFile } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const ansis = new Ansis(); async function readJSON(file) { return JSON.parse(await readFile(file, 'utf8')); } export function hasNotBeenMsSinceDate(ms, now, date) { const diff = now.getTime() - date.getTime(); return diff < ms; } export function convertToMs(frequency, unit) { switch (unit) { case 'days': { return frequency * 24 * 60 * 60 * 1000; } case 'hours': { return frequency * 60 * 60 * 1000; } case 'milliseconds': { return frequency; } case 'minutes': { return frequency * 60 * 1000; } case 'seconds': { return frequency * 1000; } default: { // default to minutes return frequency * 60 * 1000; } } } export function getEnvVarNumber(envVar, defaultValue) { const envVarRaw = process.env[envVar]; if (!envVarRaw) return defaultValue; const parsed = Number.parseInt(envVarRaw, 10); if (Number.isNaN(parsed)) return defaultValue; return parsed; } export function getEnvVarEnum(envVar, allowed, defaultValue) { const envVarRaw = process.env[envVar]; if (!envVarRaw) return defaultValue; if (!allowed.includes(envVarRaw)) return defaultValue; return envVarRaw; } export function semverGreaterThan(a, b) { return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }) > 0; } /** * Returns the newest version of the CLI from the cache if it is newer than the current version. * * Returns undefined early if: * - `update` command is being run * - `<CLI>_SKIP_NEW_VERSION_CHECK` is set to true * - the current version is a prerelease * - the warning was last shown to the user within the frequency and frequencyUnit */ export async function getNewerVersion({ argv, config, lastWarningFile, versionFile, }) { // do not show warning if running `update` command of <CLI>_SKIP_NEW_VERSION_CHECK=true if (argv[2] === 'update' || config.scopedEnvVarTrue('SKIP_NEW_VERSION_CHECK')) return; // TODO: handle prerelease channels if (config.version.includes('-')) return; const { frequency, frequencyUnit } = config.pjson.oclif['warn-if-update-available'] ?? {}; const warningFrequency = getEnvVarNumber(config.scopedEnvVarKey('NEW_VERSION_CHECK_FREQ'), frequency); const warningFrequencyUnit = getEnvVarEnum(config.scopedEnvVarKey('NEW_VERSION_CHECK_FREQ_UNIT'), ['days', 'hours', 'minutes', 'seconds', 'milliseconds'], frequencyUnit ?? 'minutes'); try { const { mtime } = await stat(lastWarningFile); // If the file was modified before the timeout, don't show the warning if (warningFrequency && warningFrequencyUnit && hasNotBeenMsSinceDate(convertToMs(warningFrequency, warningFrequencyUnit), new Date(), mtime)) return; } catch { // The last-warning file doesn't exist, which is okay since it will be created the first time the warning is shown } const distTags = await readJSON(versionFile); const tag = config.scopedEnvVar('NEW_VERSION_CHECK_TAG') ?? 'latest'; if (distTags[tag] && semverGreaterThan(distTags[tag].split('-')[0], config.version.split('-')[0])) return distTags[tag]; } const hook = async function ({ config }) { const debug = makeDebug('update-check'); const versionFile = join(config.cacheDir, 'version'); const lastWarningFile = join(config.cacheDir, 'last-warning'); // Destructure package.json configuration with defaults const { authorization = '', message = '<%= config.name %> update available from <%= chalk.greenBright(config.version) %> to <%= chalk.greenBright(latest) %>.', registry = config.npmRegistry ?? 'https://registry.npmjs.org', timeoutInDays = 60, } = config.pjson.oclif['warn-if-update-available'] ?? {}; const refreshNeeded = async () => { if (this.config.scopedEnvVarTrue('FORCE_VERSION_CACHE_UPDATE')) return true; if (this.config.scopedEnvVarTrue('SKIP_NEW_VERSION_CHECK')) return false; try { const { mtime } = await stat(versionFile); const staleAt = new Date(mtime.valueOf() + 1000 * 60 * 60 * 24 * timeoutInDays); return staleAt < new Date(); } catch (error) { debug(error); return true; } }; const spawnRefresh = async () => { const versionScript = resolve(dirname(fileURLToPath(import.meta.url)), '../../../lib/get-version'); debug('spawning version refresh'); debug(process.execPath, versionScript, config.name, versionFile, config.version, registry, authorization); spawn(process.execPath, [versionScript, config.name, versionFile, config.version, registry, authorization], { detached: !config.windows, stdio: 'ignore', }).unref(); }; try { const newerVersion = await getNewerVersion({ argv: process.argv, config, lastWarningFile, versionFile }); if (newerVersion) { // Default message if the user doesn't provide one const [lodash] = await Promise.all([ import('lodash'), // Update the modified time (mtime) of the last-warning file so that we can track the last time we // showed the warning. This makes it possible to respect the frequency and frequencyUnit options. writeFile(lastWarningFile, ''), ]); this.warn(lodash.default.template(message)({ ansis, // chalk and ansis have the same api. Keeping chalk for backwards compatibility. chalk: ansis, config, latest: newerVersion, })); } } catch (error) { const { code } = error; if (code !== 'ENOENT') throw error; } if (await refreshNeeded()) await spawnRefresh(); }; export default hook;