updates/updates.js

309 lines
7.8 KiB
JavaScript
Raw Normal View History

2017-12-09 07:45:06 +00:00
#!/usr/bin/env node
2017-12-03 11:15:02 +00:00
"use strict";
2018-08-23 20:28:33 +00:00
process.env.NODE_ENV = "production";
const args = require("minimist")(process.argv.slice(2), {
boolean: [
"c", "color",
"h", "help",
"j", "json",
"n", "no-color",
"u", "update",
"v", "version",
],
string: [
2018-10-15 04:52:37 +00:00
"E", "exit-code",
"f", "file",
"g", "greatest",
"p", "prerelease",
"r", "registry",
],
default: {
"registry": "https://registry.npmjs.org/",
},
alias: {
c: "color",
e: "exclude",
2018-10-15 04:52:37 +00:00
E: "exit-code",
f: "file",
g: "greatest",
h: "help",
i: "include",
j: "json",
n: "no-color",
p: "prerelease",
r: "registry",
u: "update",
v: "version",
2018-04-30 06:31:42 +00:00
},
});
if (args.help) {
process.stdout.write(`usage: updates [options]
2017-12-03 11:15:02 +00:00
Options:
-u, --update Update packages and write package.json
-p, --prerelease [<pkg,...>] Consider prerelease versions
-g, --greatest [<pkg,...>] Prefer greatest over latest version
-i, --include <pkg,...> Only include given packages
-e, --exclude <pkg,...> Exclude given packages
2018-10-15 04:52:37 +00:00
-E, --exit-code Exit with code 2 on outdated packages
-r, --registry <url> Use a custom registry
-f, --file <path> Use specified package.json file
2018-09-02 19:44:11 +00:00
-j, --json Output a JSON object
-c, --color Force-enable color output
-n, --no-color Disable color output
-v, --version Print the version
-h, --help Print this help
2017-12-03 11:15:02 +00:00
Examples:
$ updates
2017-12-03 11:35:25 +00:00
$ updates -u
2018-06-11 18:04:58 +00:00
$ updates -u -e semver
`);
process.exit(0);
}
2017-12-03 11:15:02 +00:00
const path = require("path");
2017-12-06 18:11:40 +00:00
if (args.version) {
2018-03-03 19:47:48 +00:00
console.info(require(path.join(__dirname, "package.json")).version);
2017-12-06 18:11:40 +00:00
process.exit(0);
}
if (args["color"]) process.env.FORCE_COLOR = "1";
if (args["no-color"]) process.env.FORCE_COLOR = "0";
2017-12-09 05:49:35 +00:00
const greatest = parseMixedArg(args.greatest);
2018-10-04 05:09:13 +00:00
const prerelease = parseMixedArg(args.prerelease);
2017-12-03 11:15:02 +00:00
2018-10-15 04:52:37 +00:00
let exitCode = parseMixedArg(args["exit-code"]);
if (Array.isArray(exitCode) && exitCode.length) {
exitCode = true;
}
2018-07-30 16:46:10 +00:00
const registry = args.registry.endsWith("/") ? args.registry : args.registry + "/";
const packageFile = args.file || require("find-up").sync("package.json");
2017-12-03 11:15:02 +00:00
const dependencyTypes = [
2017-12-03 11:15:02 +00:00
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies"
];
const fs = require("fs");
let pkg, pkgStr;
const deps = {};
try {
pkgStr = fs.readFileSync(packageFile, "utf8");
} catch (err) {
finish(new Error(`Unable to open package.json: ${err.message}`));
}
try {
pkg = JSON.parse(pkgStr);
} catch (err) {
finish(new Error(`Error parsing package.json: ${err.message}`));
}
2018-07-30 16:55:32 +00:00
const semver = require("semver");
2018-04-30 07:11:31 +00:00
let include, exclude;
if (args.include) include = args.include.split(",");
if (args.exclude) exclude = args.exclude.split(",");
2018-04-30 06:31:42 +00:00
2018-08-23 20:28:33 +00:00
for (const key of dependencyTypes) {
2017-12-03 11:15:02 +00:00
if (pkg[key]) {
2018-04-30 08:42:39 +00:00
const names = Object.keys(pkg[key])
.filter(name => !include ? true : include.includes(name))
.filter(name => !exclude ? true : !exclude.includes(name));
2018-08-23 20:28:33 +00:00
for (const name of names) {
2017-12-09 05:47:15 +00:00
const old = pkg[key][name];
if (isValidSemverRange(old)) {
deps[name] = {old};
2017-12-03 11:15:02 +00:00
}
2018-08-23 20:28:33 +00:00
}
2017-12-03 11:15:02 +00:00
}
2018-08-23 20:28:33 +00:00
}
2017-12-03 11:15:02 +00:00
if (!Object.keys(deps).length) {
if (include || exclude) {
finish(new Error("No packages match the given filters"));
} else {
finish(new Error("No packages found"));
}
}
const fetch = require("make-fetch-happen");
2018-07-30 16:55:32 +00:00
const esc = require("escape-string-regexp");
const chalk = require("chalk");
2018-10-05 21:20:41 +00:00
const get = async name => {
// on scoped packages replace "/" with "%2f"
2018-10-05 21:04:52 +00:00
if (/@[a-z0-9][\w-.]+\/[a-z0-9][\w-.]*/gi.test(name)) {
name = name.replace(/\//g, "%2f");
2018-07-30 17:18:53 +00:00
}
2018-10-05 21:20:41 +00:00
return fetch(registry + name).then(r => r.json());
};
2018-10-05 21:20:41 +00:00
Promise.all(Object.keys(deps).map(name => get(name))).then(dati => {
2018-08-23 20:28:33 +00:00
for (const data of dati) {
const useGreatest = typeof greatest === "boolean" ? greatest : greatest.includes(data.name);
const usePre = typeof prerelease === "boolean" ? prerelease : prerelease.includes(data.name);
const newVersion = useGreatest ? findHighestVersion(Object.keys(data.versions), usePre) : data["dist-tags"].latest;
2018-04-30 08:42:39 +00:00
const oldRange = deps[data.name].old;
const newRange = updateRange(oldRange, newVersion);
if (!newVersion || oldRange === newRange) {
2018-04-30 08:42:39 +00:00
delete deps[data.name];
2017-12-03 12:17:31 +00:00
} else {
2018-04-30 08:42:39 +00:00
deps[data.name].new = newRange;
2017-12-03 12:17:31 +00:00
}
2018-08-23 20:28:33 +00:00
}
if (!Object.keys(deps).length) {
finish("All packages are up to date.");
2017-12-03 15:30:59 +00:00
}
if (!args.update) {
2018-08-23 20:28:33 +00:00
finish();
2017-12-03 11:15:02 +00:00
}
2018-08-23 20:28:33 +00:00
try {
fs.writeFileSync(packageFile, updatePkg(), "utf8");
} catch (err) {
finish(new Error(`Error writing package.json: ${err.message}`));
}
const msg = `
2018-07-21 07:47:37 +00:00
package.json updated
2018-08-23 20:28:33 +00:00
`;
finish(chalk.green(msg.substring(1)));
2018-07-24 17:41:44 +00:00
}).catch(finish);
2017-12-03 11:15:02 +00:00
function finish(obj, opts) {
opts = opts || {};
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) {
output.error = obj.message;
}
if (args.json) {
2018-07-24 17:41:44 +00:00
if (!hadError) {
output.results = deps;
}
2018-03-03 19:47:48 +00:00
console.info(JSON.stringify(output, null, 2));
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) {
2018-03-03 19:47:48 +00:00
console.info(output.message || output.error);
2017-12-03 12:17:31 +00:00
}
}
2018-10-15 04:52:37 +00:00
if (exitCode) {
process.exit(Object.keys(deps).length ? 2 : 0);
} else {
process.exit(opts.exitCode || (output.error ? 1 : 0));
}
}
2017-12-03 13:12:30 +00:00
function highlightDiff(a, b, added) {
const aParts = a.split(/\./);
const bParts = b.split(/\./);
2018-07-25 09:53:50 +00:00
const color = chalk[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
let res = "";
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;
2018-07-25 09:53:50 +00:00
}).join("") + color("." + aParts.slice(i + 1).join("."));
2017-12-03 13:12:30 +00:00
}
break;
} else {
res += aParts[i] + ".";
}
2017-12-03 13:12:30 +00:00
}
return res;
}
function formatDeps() {
2018-10-04 05:41:31 +00:00
const arr = [["NAME", "OLD", "NEW"]];
for (const [name, versions] of Object.entries(deps)) arr.push([
name,
highlightDiff(versions.old, versions.new, false),
highlightDiff(versions.new, versions.old, true),
]);
return require("text-table")(arr, {
hsep: " ".repeat(4),
stringLength: require("string-width"),
2017-12-03 11:15:02 +00:00
});
}
function updatePkg() {
2017-12-03 11:15:02 +00:00
let newPkgStr = pkgStr;
2018-08-23 20:28:33 +00:00
for (const dep of Object.keys(deps)) {
const re = new RegExp(`"${esc(dep)}": +"${esc(deps[dep].old)}"`, "g");
newPkgStr = newPkgStr.replace(re, `"${dep}": "${deps[dep].new}"`);
2018-08-23 20:28:33 +00:00
}
2017-12-03 11:15:02 +00:00
return newPkgStr;
}
// naive regex replace
function updateRange(range, version) {
return range.replace(/[0-9]+\.[0-9]+\.[0-9]+(-.+)?/g, version);
2017-12-03 11:15:02 +00:00
}
function isValidSemverRange(range) {
let valid = false;
try {
semver.Range(range);
valid = true;
} catch (err) {}
return valid;
}
// find the newest version, ignoring prerelease version unless they are requested
function findHighestVersion(versions, pre) {
2018-07-10 21:04:08 +00:00
let highest = "0.0.0";
while (versions.length) {
const parsed = semver.parse(versions.pop());
if (!pre && parsed.prerelease.length) continue;
2018-07-10 21:04:08 +00:00
if (semver.gt(parsed.version, highest)) {
highest = parsed.version;
}
}
return highest === "0.0.0" ? null : highest;
}
function parseMixedArg(arg) {
if (arg === "") {
return true;
} else if (typeof arg === "string") {
return arg.includes(",") ? arg.split(",") : [arg];
} else if (Array.isArray(arg)) {
return arg;
} else {
return false;
}
}