@rushstack/lockfile-explorer
Version:
Rush Lockfile Explorer: The UI for solving version conflicts quickly in a large monorepo
113 lines • 4.13 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { homedir } from 'node:os';
import semver from 'semver';
import { JsonFile } from '@rushstack/node-core-library';
const REGISTRY_BASE_URL = 'https://registry.npmjs.org';
const FETCH_TIMEOUT_MS = 5000;
const DEFAULT_CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
const CACHE_VERSION = 1;
const CACHE_FOLDER = `${homedir()}/.rushstack/update-checks`;
async function _tryFetchLatestVersionAsync(packageName) {
const url = `${REGISTRY_BASE_URL}/${encodeURIComponent(packageName)}/latest`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) {
return undefined;
}
const { version } = (await response.json());
return typeof version === 'string' ? version : undefined;
}
catch (_a) {
// Network errors, timeouts, and parse failures are all silent.
return undefined;
}
finally {
clearTimeout(timeout);
}
}
async function _readCacheAsync(filePath) {
try {
const data = await JsonFile.loadAsync(filePath);
const { cacheVersion, ...rest } = data;
if (cacheVersion === CACHE_VERSION) {
return rest;
}
}
catch (_a) {
// Ignore
}
}
async function _writeCacheAsync(filePath, cache) {
try {
const cacheData = {
cacheVersion: CACHE_VERSION,
checkedAt: Date.now(),
...cache
};
await JsonFile.saveAsync(cacheData, filePath, {
ensureFolderExists: true
});
}
catch (_a) {
// Cache write failures are silent — a stale or missing cache just means
// we'll re-fetch on the next invocation.
}
}
/**
* Checks npm for a newer version of a package and caches the result locally so that
* the registry is not queried on every invocation.
*
* @internal
*/
export class PackageUpdateChecker {
constructor(options) {
const { packageName, currentVersion, skip = false, forceCheck = false, cacheExpiryMs = DEFAULT_CACHE_EXPIRY_MS } = options;
this._packageName = packageName;
this._currentVersion = currentVersion;
this._skip = skip;
this._forceCheck = forceCheck;
this._cacheExpiryMs = cacheExpiryMs;
}
/**
* Performs the update check and returns the result, or `undefined` if the check
* was skipped or the registry could not be reached.
*/
async tryGetUpdateAsync() {
if (this._skip) {
return undefined;
}
const cacheFilePath = this._getCacheFilePath();
let latestVersion;
if (!this._forceCheck) {
const cached = await _readCacheAsync(cacheFilePath);
if (cached !== undefined) {
const { checkedAt, latestVersion: latestVersionFromCache } = cached;
const ageMs = Date.now() - checkedAt;
if (ageMs < this._cacheExpiryMs) {
latestVersion = latestVersionFromCache;
}
}
}
if (latestVersion === undefined) {
// Cache is missing or stale — fetch from the registry.
latestVersion = await _tryFetchLatestVersionAsync(this._packageName);
if (latestVersion === undefined) {
return undefined;
}
await _writeCacheAsync(cacheFilePath, { latestVersion });
}
return {
latestVersion,
isOutdated: semver.gt(latestVersion, this._currentVersion)
};
}
_getCacheFilePath() {
// Replace characters that are unsafe in file names (e.g. the "/" in scoped package names).
const sanitizedName = this._packageName.replace(/[^a-zA-Z0-9._-]/g, '_');
return `${CACHE_FOLDER}/${sanitizedName}.json`;
}
}
//# sourceMappingURL=PackageUpdateChecker.js.map