updates/updates.js

912 lines
28 KiB
JavaScript
Raw Normal View History

2017-12-09 07:45:06 +00:00
#!/usr/bin/env node
2021-04-20 18:48:48 +00:00
import ansiRegex from "ansi-regex";
import fetchEnhanced from "fetch-enhanced";
import minimist from "minimist";
2022-10-26 13:12:01 +00:00
import nodeFetch from "node-fetch"; // seems twice as fast than undici for the 1500 deps case
2021-04-20 18:48:48 +00:00
import rat from "registry-auth-token";
import rc from "rc";
import ru from "registry-auth-token/registry-url.js";
import semver from "semver";
import textTable from "text-table";
import {cwd, stdout, argv, env, exit, versions} from "node:process";
2022-11-10 21:15:17 +00:00
import hostedGitInfo from "hosted-git-info";
2023-06-16 20:45:23 +00:00
import {join, dirname, basename} from "node:path";
2022-12-09 11:46:05 +00:00
import {lstatSync, readFileSync, truncateSync, writeFileSync, accessSync} from "node:fs";
import {platform} from "node:os";
import {rootCertificates} from "node:tls";
2022-07-29 23:54:31 +00:00
import {timerel} from "timerel";
2023-05-23 19:56:51 +00:00
import supportsColor from "supports-color";
2023-05-23 20:40:16 +00:00
import {magenta, red, green, disableColor} from "glowie";
2023-06-16 20:45:23 +00:00
import parseTOML from "@iarna/toml/parse-string.js";
import {getProperty} from "dot-prop";
2023-06-17 23:30:21 +00:00
import pAll from "p-all";
2023-06-18 00:02:32 +00:00
import memize from "memize";
2020-10-05 21:42:57 +00:00
2022-11-10 21:15:17 +00:00
const {fromUrl} = hostedGitInfo;
2022-12-23 21:19:37 +00:00
let fetch;
if (globalThis.fetch && !versions?.node) { // avoid node experimental warning
2022-12-23 21:19:37 +00:00
fetch = globalThis.fetch;
} else {
fetch = fetchEnhanced(nodeFetch, {undici: false});
}
const MAX_SOCKETS = 96;
2019-12-07 18:43:13 +00:00
const sep = "\0";
2018-08-23 20:28:33 +00:00
// regexes for url dependencies. does only github and only hash or exact semver
2019-12-07 18:50:24 +00:00
// https://regex101.com/r/gCZzfK/2
const stripRe = /^.*?:\/\/(.*?@)?(github\.com[:/])/i;
2020-04-20 09:03:01 +00:00
const partsRe = /^([^/]+)\/([^/#]+)?.*?\/([0-9a-f]+|v?[0-9]+\.[0-9]+\.[0-9]+)$/i;
const hashRe = /^[0-9a-f]{7,}$/i;
const versionRe = /[0-9]+(\.[0-9]+)?(\.[0-9]+)?/g;
2020-03-08 13:45:20 +00:00
const esc = str => str.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
2023-06-18 00:02:32 +00:00
const gitInfo = memize(fromUrl);
const registryAuthToken = memize(rat);
const registryUrl = memize(ru);
const normalizeUrl = memize(url => url.endsWith("/") ? url.substring(0, url.length - 1) : url);
2020-08-06 22:11:23 +00:00
const patchSemvers = new Set(["patch"]);
const minorSemvers = new Set(["patch", "minor"]);
const majorSemvers = new Set(["patch", "minor", "major"]);
2023-06-18 00:02:32 +00:00
const packageVersion = import.meta.VERSION || "0.0.0";
2023-04-23 21:31:52 +00:00
let config = {};
2020-08-06 22:11:23 +00:00
2020-03-25 23:06:31 +00:00
const args = minimist(argv.slice(2), {
boolean: [
"E", "error-on-outdated",
2019-07-12 14:37:24 +00:00
"U", "error-on-unchanged",
"h", "help",
"j", "json",
"n", "no-color",
"u", "update",
"v", "version",
2020-10-20 19:04:33 +00:00
"V", "verbose",
],
string: [
"d", "allow-downgrade",
"f", "file",
"g", "greatest",
"l", "language",
"m", "minor",
"P", "patch",
"p", "prerelease",
"R", "release",
"r", "registry",
2018-10-16 15:18:06 +00:00
"t", "types",
"githubapi", // undocumented, only for tests
2023-06-16 20:45:23 +00:00
"pypiapi", // undocumented, only for tests
],
alias: {
d: "allow-downgrade",
E: "error-on-outdated",
2019-07-12 14:37:24 +00:00
U: "error-on-unchanged",
2018-10-16 15:18:06 +00:00
e: "exclude",
f: "file",
g: "greatest",
h: "help",
i: "include",
j: "json",
2023-06-16 20:45:23 +00:00
l: "language",
m: "minor",
n: "no-color",
P: "patch",
p: "prerelease",
r: "registry",
R: "release",
s: "semver",
S: "sockets",
2018-10-16 15:18:06 +00:00
t: "types",
u: "update",
v: "version",
2020-10-20 19:04:33 +00:00
V: "verbose",
2018-04-30 06:31:42 +00:00
},
});
2023-05-23 20:40:16 +00:00
if (args["no-color"] || !supportsColor.stdout) disableColor();
2020-08-13 18:37:05 +00:00
const greatest = parseMixedArg(args.greatest);
2018-10-04 05:09:13 +00:00
const prerelease = parseMixedArg(args.prerelease);
const release = parseMixedArg(args.release);
const patch = parseMixedArg(args.patch);
const minor = parseMixedArg(args.minor);
const allowDowngrade = parseMixedArg(args["allow-downgrade"]);
2017-12-03 11:15:02 +00:00
2022-02-27 15:46:38 +00:00
const npmrc = rc("npm", {registry: "https://registry.npmjs.org"});
const authTokenOpts = {npmrc, recursive: true};
2020-03-08 18:12:16 +00:00
const githubApiUrl = args.githubapi ? normalizeUrl(args.githubapi) : "https://api.github.com";
2023-06-16 20:45:23 +00:00
const pypiApiUrl = args.pypiapi ? normalizeUrl(args.pypiapi) : "https://pypi.org";
2022-03-07 15:54:59 +00:00
const maxSockets = typeof args.sockets === "number" ? parseInt(args.sockets) : MAX_SOCKETS;
2020-09-18 15:53:31 +00:00
2023-06-18 00:02:32 +00:00
function extractCerts(str) {
return Array.from(str.matchAll(/(----BEGIN CERT[^]+?IFICATE----)/g), m => m[0]);
2022-06-03 21:08:42 +00:00
}
2023-06-16 20:45:23 +00:00
function findUpSync(filename, dir, stopDir) {
2020-03-08 14:59:50 +00:00
const path = join(dir, filename);
try {
accessSync(path);
return path;
2020-03-25 23:06:31 +00:00
} catch {}
2020-03-08 14:59:50 +00:00
const parent = dirname(dir);
if ((stopDir && path === stopDir) || parent === dir) {
return null;
} else {
2023-06-16 20:45:23 +00:00
return findUpSync(filename, parent, stopDir);
2020-03-08 14:59:50 +00:00
}
}
function getAuthAndRegistry(name, registry) {
if (!name.startsWith("@")) {
return [registryAuthToken(registry, authTokenOpts), registry];
} else {
const scope = (/@[a-z0-9][\w-.]+/.exec(name) || [])[0];
2020-03-08 18:12:16 +00:00
const url = normalizeUrl(registryUrl(scope, npmrc));
if (url !== registry) {
try {
const newAuth = registryAuthToken(url, authTokenOpts);
2022-08-24 23:13:46 +00:00
if (newAuth?.token) return [newAuth, url];
2023-01-06 21:59:17 +00:00
} catch {}
2019-06-27 17:15:01 +00:00
}
2023-01-06 21:59:17 +00:00
return [registryAuthToken(registry, authTokenOpts), registry];
}
2019-06-27 17:15:01 +00:00
}
2023-06-18 00:02:32 +00:00
const getFetchOpts = memize((agentOpts, authType, authToken) => {
return {
...(Object.keys(agentOpts).length && {agentOpts}),
headers: {
"user-agent": `updates/${packageVersion}`,
...(authToken && {Authorization: `${authType} ${authToken}`}),
}
};
});
2023-06-16 20:45:23 +00:00
async function fetchNpmInfo(name, type, originalRegistry, agentOpts) {
2020-09-18 18:55:18 +00:00
const [auth, registry] = getAuthAndRegistry(name, originalRegistry);
const packageName = type === "resolutions" ? resolutionsBasePackage(name) : name;
const urlName = packageName.replace(/\//g, "%2f");
const url = `${registry}/${urlName}`;
2022-08-24 23:13:46 +00:00
2020-10-20 19:04:33 +00:00
if (args.verbose) console.error(`${magenta("fetch")} ${url}`);
2023-06-18 00:02:32 +00:00
const res = await fetch(url, getFetchOpts(agentOpts, auth?.type, auth?.token));
2022-06-03 20:55:00 +00:00
if (res?.ok) {
2020-10-20 19:04:33 +00:00
if (args.verbose) console.error(`${green("done")} ${url}`);
return [await res.json(), type, registry, name];
} else {
2022-06-03 20:55:00 +00:00
if (res?.status && res?.statusText) {
throw new Error(`Received ${res.status} ${res.statusText} for ${name} from ${registry}`);
} else {
throw new Error(`Unable to fetch ${name} from ${registry}`);
}
}
2020-09-18 18:55:18 +00:00
}
2023-06-16 20:45:23 +00:00
async function fetchPypiInfo(name, type, agentOpts) {
const url = `${pypiApiUrl}/pypi/${name}/json`;
if (args.verbose) console.error(`${magenta("fetch")} ${url}`);
2023-06-18 00:02:32 +00:00
const res = await fetch(url, getFetchOpts(agentOpts));
2023-06-16 20:45:23 +00:00
if (res?.ok) {
if (args.verbose) console.error(`${green("done")} ${url}`);
return [await res.json(), type, null, name];
} else {
if (res?.status && res?.statusText) {
throw new Error(`Received ${res.status} ${res.statusText} for ${name} from PyPi`);
} else {
throw new Error(`Unable to fetch ${name} from PyPi`);
}
}
}
function getInfoUrl({repository, homepage, info}, registry, name) {
if (info) { // pypi
repository =
info.project_urls.repository ||
info.project_urls.Repository ||
info.project_urls.repo ||
info.project_urls.Repo ||
info.project_urls.source ||
info.project_urls.Source ||
info.project_urls["source code"] ||
info.project_urls["Source Code"] ||
info.project_urls.homepage ||
info.project_urls.Homepage ||
`https://pypi.org/project/${name}/`;
}
let infoUrl;
2019-06-27 17:15:01 +00:00
if (registry === "https://npm.pkg.github.com") {
return `https://github.com/${name.replace(/^@/, "")}`;
} else if (repository) {
const url = typeof repository === "string" ? repository : repository.url;
2022-11-10 21:15:17 +00:00
const info = gitInfo(url);
2023-06-20 13:42:56 +00:00
const browse = info?.browse?.();
if (browse) {
infoUrl = browse; // https://github.com/babel/babel
}
if (infoUrl && repository.directory && info.treepath) {
// https://github.com/babel/babel/tree/HEAD/packages/babel-cli
2020-10-04 14:55:16 +00:00
// HEAD seems to always go to the default branch on GitHub but ideally
// package.json should have a field for source branch
infoUrl = `${infoUrl}/${info.treepath}/HEAD/${repository.directory}`;
}
2022-06-03 20:55:00 +00:00
if (!infoUrl && repository?.url && /^https?:/.test(repository.url)) {
infoUrl = repository.url;
}
2023-06-20 13:42:56 +00:00
if (!infoUrl && url) {
infoUrl = url;
}
}
2022-06-03 21:06:14 +00:00
let url = infoUrl || homepage || "";
if (url) {
const u = new URL(url);
2022-08-24 23:13:46 +00:00
// force https for github.com
if (u.protocol === "http:" && u.hostname === "github.com") {
u.protocol = "https:";
url = String(u);
}
}
return url;
2020-09-18 18:55:18 +00:00
}
2022-10-26 13:03:43 +00:00
function finish(obj, deps = {}) {
const output = {};
2018-07-24 17:41:44 +00:00
const hadError = obj instanceof Error;
if (typeof obj === "string") {
output.message = obj;
2018-07-24 17:41:44 +00:00
} else if (hadError) {
2023-04-18 21:31:42 +00:00
output.error = obj.stack || obj.message;
}
2020-03-08 18:12:16 +00:00
for (const value of Object.values(deps)) {
if ("oldPrint" in value) {
value.old = value.oldPrint;
delete value.oldPrint;
}
if ("newPrint" in value) {
value.new = value.newPrint;
delete value.newPrint;
}
if ("oldOriginal" in value) {
value.old = value.oldOriginal;
delete value.oldOriginal;
}
2020-03-08 18:12:16 +00:00
}
if (args.json) {
2018-07-24 17:41:44 +00:00
if (!hadError) {
output.results = {};
for (const [key, value] of Object.entries(deps)) {
2019-12-07 18:43:13 +00:00
const [type, name] = key.split(sep);
if (!output.results[type]) output.results[type] = {};
output.results[type][name] = value;
}
2018-07-24 17:41:44 +00:00
}
2018-10-25 20:36:41 +00:00
console.info(JSON.stringify(output));
2017-12-03 12:17:31 +00:00
} else {
2018-07-24 17:41:44 +00:00
if (Object.keys(deps).length && !hadError) {
2018-03-03 19:47:48 +00:00
console.info(formatDeps(deps));
}
if (output.message || output.error) {
2019-05-29 20:42:25 +00:00
if (output.message) {
console.info(output.message);
} else if (output.error) {
2022-10-26 17:27:27 +00:00
console.info(red(output.error));
2019-05-29 20:42:25 +00:00
}
2017-12-03 12:17:31 +00:00
}
}
if (args["error-on-outdated"]) {
2020-03-25 23:06:31 +00:00
exit(Object.keys(deps).length ? 2 : 0);
2019-07-12 14:37:24 +00:00
} else if (args["error-on-unchanged"]) {
2020-03-25 23:06:31 +00:00
exit(Object.keys(deps).length ? 0 : 2);
2018-10-15 04:52:37 +00:00
} else {
2022-10-26 13:03:43 +00:00
exit(output.error ? 1 : 0);
2018-10-15 04:52:37 +00:00
}
}
2020-03-25 23:06:31 +00:00
// preserve file metadata on windows
2019-08-20 17:54:07 +00:00
function write(file, content) {
2020-03-25 23:06:31 +00:00
const isWindows = platform() === "win32";
if (isWindows) truncateSync(file, 0);
writeFileSync(file, content, isWindows ? {flag: "r+"} : undefined);
2019-08-20 17:54:07 +00:00
}
2017-12-03 13:12:30 +00:00
function highlightDiff(a, b, added) {
if (a === b) return a;
2017-12-03 13:12:30 +00:00
const aParts = a.split(/\./);
const bParts = b.split(/\./);
2020-03-09 16:36:15 +00:00
const color = added ? green : red;
2018-07-30 16:55:32 +00:00
const versionPartRe = /^[0-9a-zA-Z-.]+$/;
2017-12-03 13:12:30 +00:00
2022-06-03 21:03:46 +00:00
let res = "";
2017-12-03 13:12:30 +00:00
for (let i = 0; i < aParts.length; i++) {
if (aParts[i] !== bParts[i]) {
2018-07-10 21:04:08 +00:00
if (versionPartRe.test(aParts[i])) {
2018-07-25 09:53:50 +00:00
res += color(aParts.slice(i).join("."));
2017-12-03 13:12:30 +00:00
} else {
2018-04-30 08:42:39 +00:00
res += aParts[i].split("").map(char => {
2018-08-23 20:28:33 +00:00
return versionPartRe.test(char) ? color(char) : char;
2020-03-08 10:00:46 +00:00
}).join("") + color(`.${aParts.slice(i + 1).join(".")}`);
2017-12-03 13:12:30 +00:00
}
break;
} else {
2020-03-08 10:00:46 +00:00
res += `${aParts[i]}.`;
}
2017-12-03 13:12:30 +00:00
}
return res;
}
2022-10-26 12:53:59 +00:00
function formatDeps(deps) {
2020-03-08 16:06:58 +00:00
const arr = [["NAME", "OLD", "NEW", "AGE", "INFO"]];
2019-02-04 18:12:15 +00:00
for (const [key, data] of Object.entries(deps)) {
arr.push([
2023-01-06 22:06:25 +00:00
key.split(sep)[1],
2020-03-08 18:12:16 +00:00
highlightDiff(data.old, data.new, false),
highlightDiff(data.new, data.old, true),
2020-03-08 16:06:58 +00:00
data.age || "",
data.info,
]);
}
2019-02-04 18:12:15 +00:00
2020-03-08 13:40:48 +00:00
return textTable(arr, {
2022-06-03 20:41:48 +00:00
hsep: " ",
2022-06-03 21:03:46 +00:00
stringLength: str => str.replace(ansiRegex(), "").length,
2017-12-03 11:15:02 +00:00
});
}
2022-10-26 13:03:43 +00:00
function updatePackageJson(pkgStr, deps) {
2017-12-03 11:15:02 +00:00
let newPkgStr = pkgStr;
for (const key of Object.keys(deps)) {
2022-08-24 23:13:46 +00:00
const name = key.split(sep)[1];
2022-11-10 21:59:20 +00:00
const old = deps[key].oldOriginal || deps[key].old;
2023-06-19 23:09:54 +00:00
const re = new RegExp(`"${esc(name)}": *"${esc(old)}"`, "g");
newPkgStr = newPkgStr.replace(re, `"${name}": "${deps[key].new}"`);
2018-08-23 20:28:33 +00:00
}
2017-12-03 11:15:02 +00:00
return newPkgStr;
}
2023-06-19 23:09:54 +00:00
function updateProjectToml(pkgStr, deps) {
let newPkgStr = pkgStr;
for (const key of Object.keys(deps)) {
const name = key.split(sep)[1];
const old = deps[key].oldOriginal || deps[key].old;
const re = new RegExp(`${esc(name)} *= *"${esc(old)}"`, "g");
newPkgStr = newPkgStr.replace(re, `${name} = "${deps[key].new}"`);
}
return newPkgStr;
}
2017-12-03 11:15:02 +00:00
function updateRange(range, version) {
return range.replace(/[0-9]+\.[0-9]+\.[0-9]+(-.+)?/g, version);
2017-12-03 11:15:02 +00:00
}
function isVersionPrerelease(version) {
const parsed = semver.parse(version);
if (!parsed) return false;
return Boolean(parsed.prerelease.length);
}
function isRangePrerelease(range) {
// can not use semver.coerce here because it ignores prerelease tags
return /[0-9]+\.[0-9]+\.[0-9]+-.+/.test(range);
}
function rangeToVersion(range) {
try {
return semver.coerce(range).version;
2020-03-25 23:06:31 +00:00
} catch {
return null;
}
}
2020-03-15 17:23:42 +00:00
function findVersion(data, versions, {range, semvers, usePre, useRel, useGreatest} = {}) {
let tempVersion = rangeToVersion(range);
2018-12-22 15:24:24 +00:00
let tempDate = 0;
2020-03-15 17:23:42 +00:00
semvers = new Set(semvers);
usePre = isRangePrerelease(range) || usePre;
2019-01-20 21:26:30 +00:00
if (usePre) {
2020-03-15 17:23:42 +00:00
semvers.add("prerelease");
if (semvers.has("patch")) semvers.add("prepatch");
if (semvers.has("minor")) semvers.add("preminor");
if (semvers.has("major")) semvers.add("premajor");
2019-01-20 21:26:30 +00:00
}
for (const version of versions) {
const parsed = semver.parse(version);
2020-03-15 17:23:42 +00:00
if (parsed.prerelease.length && (!usePre || useRel)) continue;
2019-01-20 21:26:30 +00:00
const diff = semver.diff(tempVersion, parsed.version);
2020-03-15 17:23:42 +00:00
if (!diff || !semvers.has(diff)) continue;
2018-12-22 15:24:24 +00:00
// some registries like github don't have data.time available, fall back to greatest on them
2020-03-15 17:23:42 +00:00
if (useGreatest || !("time" in data)) {
2019-01-20 21:26:30 +00:00
if (semver.gte(semver.coerce(parsed.version).version, tempVersion)) {
tempVersion = parsed.version;
}
2018-12-22 15:24:24 +00:00
} else {
const date = (new Date(data.time[version])).getTime();
if (date >= 0 && date > tempDate) {
tempVersion = parsed.version;
tempDate = date;
}
}
}
return tempVersion || null;
}
2023-06-16 20:45:23 +00:00
function coerce(version) {
return semver.coerce(version).version;
}
function findNewVersion(data, {language, range, useGreatest, useRel, usePre, semvers} = {}) {
if (range === "*") return null; // ignore wildcard
if (range.includes("||")) return null; // ignore or-chains
const versions = Object.keys(language === "py" ? data.releases : data.versions)
.filter(version => semver.valid(version));
const version = findVersion(data, versions, {range, semvers, usePre, useRel, useGreatest});
2023-06-16 20:45:23 +00:00
if (useGreatest) {
return version;
} else {
2023-06-16 20:45:23 +00:00
let latestTag;
2023-06-20 13:42:56 +00:00
let originalLatestTag;
2023-06-16 20:45:23 +00:00
if (language === "py") {
2023-06-20 13:42:56 +00:00
originalLatestTag = data.info.version; // may not be a 3-part semver
latestTag = coerce(data.info.version); // add .0 to 6.0 so semver eats it
2023-06-16 20:45:23 +00:00
} else {
latestTag = data["dist-tags"].latest;
}
const oldVersion = coerce(range);
const oldIsPre = isRangePrerelease(range);
const newIsPre = isVersionPrerelease(version);
const latestIsPre = isVersionPrerelease(latestTag);
const isGreater = semver.gt(version, oldVersion);
// update to new prerelease
2023-06-16 20:45:23 +00:00
if (!useRel && usePre || (oldIsPre && newIsPre)) {
return version;
}
// downgrade from prerelease to release on --release-only
2023-06-16 20:45:23 +00:00
if (useRel && !isGreater && oldIsPre && !newIsPre) {
return version;
}
2019-02-25 20:50:12 +00:00
// update from prerelease to release
if (oldIsPre && !newIsPre && isGreater) {
return version;
}
// do not downgrade from prerelease to release
if (oldIsPre && !newIsPre && !isGreater) {
2019-01-20 21:26:30 +00:00
return null;
}
// check if latestTag is allowed by semvers
const diff = semver.diff(oldVersion, latestTag);
2023-06-16 20:45:23 +00:00
if (diff && diff !== "prerelease" && !semvers.has(diff.replace(/^pre/, ""))) {
return version;
}
// prevent upgrading to prerelease with --release-only
2023-06-16 20:45:23 +00:00
if (useRel && isVersionPrerelease(latestTag)) {
return version;
}
2019-11-19 19:52:51 +00:00
// prevent downgrade to older version except with --allow-downgrade
if (semver.lt(latestTag, oldVersion) && !latestIsPre) {
2022-06-03 20:55:00 +00:00
if (allowDowngrade === true || allowDowngrade?.has?.(data.name)) {
return latestTag;
} else {
return null;
}
}
// in all other cases, return latest dist-tag
2023-06-20 13:42:56 +00:00
return originalLatestTag ?? latestTag;
}
}
2020-03-08 16:06:58 +00:00
// TODO: refactor this mess
async function checkUrlDep([key, dep], {useGreatest} = {}) {
const stripped = dep.old.replace(stripRe, "");
2019-12-06 05:37:39 +00:00
const [_, user, repo, oldRef] = partsRe.exec(stripped) || [];
if (!user || !repo || !oldRef) return;
if (hashRe.test(oldRef)) {
const opts = {maxSockets};
const token = env.UPDATES_GITHUB_API_TOKEN || env.GITHUB_API_TOKEN || env.GH_TOKEN || env.HOMEBREW_GITHUB_API_TOKEN;
if (token) {
opts.headers = {Authorization: `Bearer ${token}`};
}
2023-04-18 21:33:40 +00:00
const url = `${githubApiUrl}/repos/${user}/${repo}/commits`;
if (args.verbose) console.error(`${magenta("fetch")} ${url}`);
const res = await fetch(url, opts);
if (args.verbose && res?.ok) console.error(`${green("done")} ${url}`);
if (!res || !res.ok) return;
const data = await res.json();
2020-03-08 16:06:58 +00:00
let {sha: newRef, commit} = data[0];
if (!newRef || !newRef.length) return;
2020-03-08 16:06:58 +00:00
2022-06-03 21:03:46 +00:00
const newDate = commit?.committer?.date ?? commit?.author?.date;
newRef = newRef.substring(0, oldRef.length);
if (oldRef !== newRef) {
const newRange = dep.old.replace(oldRef, newRef);
2020-03-08 16:06:58 +00:00
return {key, newRange, user, repo, oldRef, newRef, newDate};
}
2020-03-08 16:13:23 +00:00
} else { // TODO: newDate support
2020-03-08 18:12:16 +00:00
const res = await fetch(`${githubApiUrl}/repos/${user}/${repo}/git/refs/tags`);
if (!res || !res.ok) return;
const data = await res.json();
const tags = data.map(entry => entry.ref.replace(/^refs\/tags\//, ""));
const oldRefBare = oldRef.replace(/^v/, "");
if (!semver.valid(oldRefBare)) return;
if (!useGreatest) {
const lastTag = tags[tags.length - 1];
const lastTagBare = lastTag.replace(/^v/, "");
if (!semver.valid(lastTagBare)) return;
if (semver.neq(oldRefBare, lastTagBare)) {
2022-08-24 23:13:46 +00:00
return {key, newRange: lastTag, user, repo, oldRef, newRef: lastTag};
}
} else {
let greatestTag = oldRef;
let greatestTagBare = oldRef.replace(/^v/, "");
for (const tag of tags) {
const tagBare = tag.replace(/^v/, "");
if (!semver.valid(tagBare)) continue;
if (!greatestTag || semver.gt(tagBare, greatestTagBare)) {
greatestTag = tag;
greatestTagBare = tagBare;
}
}
if (semver.neq(oldRefBare, greatestTagBare)) {
2022-08-24 23:13:46 +00:00
return {key, newRange: greatestTag, user, repo, oldRef, newRef: greatestTag};
}
}
}
}
function resolutionsBasePackage(name) {
const packages = name.match(/(@[^/]+\/)?([^/]+)/g) || [];
return packages[packages.length - 1];
}
function normalizeRange(range) {
const versionMatches = range.match(versionRe);
2022-12-21 10:04:42 +00:00
if (versionMatches?.length !== 1) return range;
return range.replace(versionRe, semver.coerce(versionMatches[0]));
}
function parseMixedArg(arg) {
if (arg === undefined) {
return false;
} else if (arg === "") {
return true;
} else if (typeof arg === "string") {
2020-03-15 17:26:40 +00:00
return arg.includes(",") ? new Set(arg.split(",")) : new Set([arg]);
} else if (Array.isArray(arg)) {
2020-03-15 17:26:40 +00:00
return new Set(arg);
} else {
return false;
}
}
2019-10-18 00:28:11 +00:00
async function main() {
2022-10-26 15:31:23 +00:00
for (const stream of [process.stdout, process.stderr]) {
stream?._handle?.setBlocking?.(true);
}
2023-06-16 20:45:23 +00:00
let {help, version, language, file, types, update} = args;
if (help) {
2022-10-26 15:31:23 +00:00
stdout.write(`usage: updates [options]
Options:
2023-06-18 00:13:10 +00:00
-l, --language <lang> Language to check, either 'js' or 'py'
2023-06-16 20:45:23 +00:00
-u, --update Update versions and write package file
2022-10-26 15:31:23 +00:00
-p, --prerelease [<pkg,...>] Consider prerelease versions
-R, --release [<pkg,...>] Only use release versions, may downgrade
-g, --greatest [<pkg,...>] Prefer greatest over latest version
-i, --include <pkg,...> Include only given packages
-e, --exclude <pkg,...> Exclude given packages
-t, --types <type,...> Check only given dependency types
-P, --patch [<pkg,...>] Consider only up to semver-patch
-m, --minor [<pkg,...>] Consider only up to semver-minor
-d, --allow-downgrade [<pkg,...>] Allow version downgrades when using latest version
-E, --error-on-outdated Exit with code 2 when updates are available and 0 when not
-U, --error-on-unchanged Exit with code 0 when updates are available and 2 when not
-r, --registry <url> Override npm registry URL
2023-06-16 20:45:23 +00:00
-f, --file <path> Use given package file or module directory
2022-10-26 15:31:23 +00:00
-S, --sockets <num> Maximum number of parallel HTTP sockets opened. Default: ${MAX_SOCKETS}
-j, --json Output a JSON object
-n, --no-color Disable color output
-v, --version Print the version
-V, --verbose Print verbose output to stderr
-h, --help Print this help
Examples:
$ updates
2023-04-18 21:29:08 +00:00
$ updates -u && npm i
2022-10-26 15:31:23 +00:00
`);
exit(0);
}
2023-06-16 20:45:23 +00:00
if (version) {
2023-06-18 00:02:32 +00:00
console.info(packageVersion);
2022-10-26 15:31:23 +00:00
exit(0);
}
2023-06-16 20:52:31 +00:00
if (language && !["js", "py"].includes(language)) {
2023-06-16 20:45:23 +00:00
throw new Error(`Invalid language: ${language}`);
}
if (file) {
const filename = basename(file);
if (filename === "package.json") {
language = "js";
} else if (filename === "pyproject.toml") {
language = "py";
}
}
if (!language) language = "js";
let packageFileName;
if (language === "py") {
packageFileName = "pyproject.toml";
} else if (language === "js") {
packageFileName = "package.json";
}
2022-10-26 13:03:43 +00:00
let packageFile;
2023-06-16 20:45:23 +00:00
if (file) {
2022-10-26 13:03:43 +00:00
let stat;
try {
2023-06-16 20:45:23 +00:00
stat = lstatSync(file);
2022-10-26 13:03:43 +00:00
} catch (err) {
2023-06-16 20:45:23 +00:00
finish(new Error(`Unable to open ${file}: ${err.message}`));
2022-10-26 13:03:43 +00:00
}
if (stat?.isFile()) {
2023-06-16 20:45:23 +00:00
packageFile = file;
2022-10-26 13:03:43 +00:00
} else if (stat?.isDirectory()) {
2023-06-16 20:45:23 +00:00
packageFile = join(file, packageFileName);
2022-10-26 13:03:43 +00:00
} else {
2023-06-16 20:45:23 +00:00
finish(new Error(`${file} is neither a file nor directory`));
2022-10-26 13:03:43 +00:00
}
} else {
2022-10-26 15:26:56 +00:00
const pwd = cwd();
2023-06-16 20:45:23 +00:00
packageFile = findUpSync(packageFileName, pwd);
if (!packageFile) return finish(new Error(`Unable to find ${packageFileName} in ${pwd} or any of its parent directories`));
}
2023-06-16 20:45:23 +00:00
const packageDir = dirname(packageFile);
try {
2023-06-16 20:45:23 +00:00
config = (await import(join(packageDir, "updates.config.js"))).default;
} catch {
try {
2023-06-16 20:45:23 +00:00
config = (await import(join(packageDir, "updates.config.mjs"))).default;
} catch {}
2022-10-26 13:03:43 +00:00
}
2023-04-24 16:29:25 +00:00
const agentOpts = {};
2023-06-18 00:02:32 +00:00
if (language === "js") {
if (npmrc["strict-ssl"] === false) {
agentOpts.rejectUnauthorized = false;
}
2023-04-24 16:29:25 +00:00
if ("cafile" in npmrc) {
agentOpts.ca = rootCertificates.concat(extractCerts(readFileSync(npmrc.cafile, "utf8")));
}
if ("ca" in npmrc) {
const cas = Array.isArray(npmrc.ca) ? npmrc.ca : [npmrc.ca];
agentOpts.ca = rootCertificates.concat(cas.map(ca => extractCerts(ca)));
}
}
2022-10-26 13:03:43 +00:00
let dependencyTypes;
2023-06-16 20:45:23 +00:00
if (types) {
dependencyTypes = Array.isArray(types) ? types : types.split(",");
2023-04-23 22:01:24 +00:00
} else if ("types" in config && Array.isArray(config.types)) {
dependencyTypes = config.types;
2022-10-26 13:03:43 +00:00
} else {
2023-06-16 20:45:23 +00:00
if (language === "js") {
dependencyTypes = [
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
"resolutions",
];
} else {
dependencyTypes = [
"tool.poetry.dependencies",
"tool.poetry.group.dev.dependencies",
];
}
2022-10-26 13:03:43 +00:00
}
let pkg, pkgStr;
try {
pkgStr = readFileSync(packageFile, "utf8");
} catch (err) {
finish(new Error(`Unable to open ${packageFile}: ${err.message}`));
2022-10-26 13:03:43 +00:00
}
try {
2023-06-16 20:45:23 +00:00
if (language === "js") {
pkg = JSON.parse(pkgStr);
} else {
pkg = parseTOML(pkgStr);
}
2022-10-26 13:03:43 +00:00
} catch (err) {
finish(new Error(`Error parsing ${packageFile}: ${err.message}`));
2022-10-26 13:03:43 +00:00
}
let include, exclude;
if (args.include && args.include !== true) {
include = new Set(((Array.isArray(args.include) ? args.include : [args.include]).flatMap(item => item.split(","))));
} else if ("include" in config && Array.isArray(config.include)) {
include = new Set(config.include);
}
if (args.exclude && args.exclude !== true) {
exclude = new Set(((Array.isArray(args.exclude) ? args.exclude : [args.exclude]).flatMap(item => item.split(","))));
} else if ("exclude" in config && Array.isArray(config.exclude)) {
exclude = new Set(config.exclude);
}
2022-10-26 13:03:43 +00:00
2023-06-16 20:45:23 +00:00
function canInclude(name, language) {
if (language === "py" && name === "python") return false;
2022-10-26 13:03:43 +00:00
if (exclude?.has?.(name) === true) return false;
if (include?.has?.(name) === false) return false;
return true;
}
const deps = {}, maybeUrlDeps = {};
for (const depType of dependencyTypes) {
2023-06-16 20:45:23 +00:00
let obj;
if (language === "js") {
obj = pkg[depType] || {};
} else {
obj = getProperty(pkg, depType) || {};
}
for (const [name, value] of Object.entries(obj)) {
if (semver.validRange(value) && canInclude(name, language)) {
deps[`${depType}${sep}${name}`] = {
old: normalizeRange(value),
oldOriginal: value,
};
2023-06-16 20:45:23 +00:00
} else if (language === "js" && canInclude(name, language)) {
maybeUrlDeps[`${depType}${sep}${name}`] = {
old: value,
};
2022-10-26 13:03:43 +00:00
}
}
}
if (!Object.keys(deps).length && !Object.keys(maybeUrlDeps).length) {
if (include || exclude) {
finish(new Error(`No dependencies match the given include/exclude filters`));
} else {
2022-10-26 17:28:52 +00:00
finish("No dependencies present, nothing to do");
}
2022-10-26 13:03:43 +00:00
}
2023-06-16 20:45:23 +00:00
let registry;
if (language === "js") {
registry = normalizeUrl(args.registry || config.registry || npmrc.registry);
}
2023-04-24 16:29:25 +00:00
2023-06-17 23:30:21 +00:00
const entries = await pAll(Object.keys(deps).map(key => () => {
2019-12-07 18:43:13 +00:00
const [type, name] = key.split(sep);
2023-06-16 20:45:23 +00:00
if (language === "js") {
return fetchNpmInfo(name, type, registry, agentOpts);
} else {
return fetchPypiInfo(name, type, agentOpts);
}
2023-06-17 23:30:21 +00:00
}), {concurrency: maxSockets});
2019-10-18 00:28:11 +00:00
for (const [data, type, registry, name] of entries) {
2022-08-24 23:13:46 +00:00
if (data?.error) throw new Error(data.error);
2019-10-18 00:28:11 +00:00
2020-03-15 17:26:40 +00:00
const useGreatest = typeof greatest === "boolean" ? greatest : greatest.has(data.name);
const usePre = typeof prerelease === "boolean" ? prerelease : prerelease.has(data.name);
const useRel = typeof release === "boolean" ? release : release.has(data.name);
2019-10-18 00:28:11 +00:00
let semvers;
2022-06-03 20:55:00 +00:00
if (patch === true || patch?.has?.(data.name)) {
2020-08-06 22:11:23 +00:00
semvers = patchSemvers;
2022-06-03 20:55:00 +00:00
} else if (minor === true || minor?.has?.(data.name)) {
2020-08-06 22:11:23 +00:00
semvers = minorSemvers;
2019-10-18 00:28:11 +00:00
} else {
2020-08-06 22:11:23 +00:00
semvers = majorSemvers;
2019-10-18 00:28:11 +00:00
}
const key = `${type}${sep}${name}`;
2019-10-18 00:28:11 +00:00
const oldRange = deps[key].old;
2023-06-16 20:45:23 +00:00
const newVersion = findNewVersion(data, {
usePre, useRel, useGreatest, semvers, range: oldRange, language,
});
2019-10-18 00:28:11 +00:00
const newRange = updateRange(oldRange, newVersion);
if (!newVersion || oldRange === newRange) {
delete deps[key];
} else {
deps[key].new = newRange;
2023-06-20 13:42:56 +00:00
if (language === "js") {
deps[key].info = getInfoUrl(data?.versions?.[newVersion], registry, data.name);
} else {
deps[key].info = getInfoUrl(data, registry, data.info.name);
}
2023-06-16 20:45:23 +00:00
if (data.time?.[newVersion]) {
deps[key].age = timerel(data.time[newVersion], {noAffix: true});
2023-06-20 13:42:56 +00:00
} else if (data.releases?.[newVersion]?.[0]?.upload_time_iso_8601) {
deps[key].age = timerel(data.releases[newVersion][0].upload_time_iso_8601, {noAffix: true});
2023-06-16 20:45:23 +00:00
}
2019-10-18 00:28:11 +00:00
}
}
if (Object.keys(maybeUrlDeps).length) {
2023-04-18 21:22:58 +00:00
const results = await Promise.all(Object.entries(maybeUrlDeps).map(([key, dep]) => {
2022-08-24 23:13:46 +00:00
const name = key.split(sep)[1];
2020-03-15 17:26:40 +00:00
const useGreatest = typeof greatest === "boolean" ? greatest : greatest.has(name);
return checkUrlDep([key, dep], {useGreatest});
}));
2023-04-18 21:22:58 +00:00
for (const res of (results || []).filter(Boolean)) {
2020-03-08 16:06:58 +00:00
const {key, newRange, user, repo, oldRef, newRef, newDate} = res;
deps[key] = {
old: maybeUrlDeps[key].old,
new: newRange,
oldPrint: hashRe.test(oldRef) ? oldRef.substring(0, 7) : oldRef,
newPrint: hashRe.test(newRef) ? newRef.substring(0, 7) : newRef,
info: `https://github.com/${user}/${repo}`,
2022-07-29 23:54:31 +00:00
...(newDate ? {age: timerel(newDate, {noAffix: true})} : {}),
};
}
}
2019-10-18 00:28:11 +00:00
if (!Object.keys(deps).length) {
finish("All dependencies are up to date.");
2019-10-18 00:28:11 +00:00
}
2023-06-16 20:45:23 +00:00
if (!update) {
2022-10-26 13:03:43 +00:00
finish(undefined, deps);
2019-10-18 00:28:11 +00:00
}
try {
2023-06-19 23:09:54 +00:00
if (language === "js") {
write(packageFile, updatePackageJson(pkgStr, deps));
} else {
write(packageFile, updateProjectToml(pkgStr, deps));
}
2019-10-18 00:28:11 +00:00
} catch (err) {
finish(new Error(`Error writing ${packageFile}: ${err.message}`));
}
2023-06-21 14:34:55 +00:00
finish(green(`${basename(packageFile)} updated`), deps);
2019-10-18 00:28:11 +00:00
}
main().catch(finish);