diff --git a/Makefile b/Makefile index 8c0ab19..73b2583 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: node_modules npx eslint --color . .PHONY: lint-fix -lint: node_modules +lint-fix: node_modules npx eslint --color . --fix .PHONY: test diff --git a/README.md b/README.md index f67d88b..618f512 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,11 @@ deno run -A npm:updates ## Options -See `--help`. Options that take multiple arguments can take them either via comma-separated value or by specifying the option multiple times. If an option has a optional `pkg` argument but none is given, the option will be applied to all packages instead. +See `--help`. Options that take multiple arguments can take them either via comma-separated value or by specifying the option multiple times. + +If an option has a optional `pkg` argument but none is given, the option will be applied to all packages instead. + +All `pkg` options support glob matching via [picomatch](https://github.com/micromatch/picomatch) or regex (on CLI, wrap the regex in slashes, e.g. `'/^foo/'`). ## Config File diff --git a/package-lock.json b/package-lock.json index 2423254..156b666 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "minimist": "1.2.8", "node-fetch": "3.3.2", "p-all": "5.0.0", + "picomatch": "2.3.1", "rc": "1.2.8", "registry-auth-token": "4.2.2", "restana": "4.9.7", @@ -3486,6 +3487,18 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-types": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", diff --git a/package.json b/package.json index a6ae4b7..865cc3b 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "minimist": "1.2.8", "node-fetch": "3.3.2", "p-all": "5.0.0", + "picomatch": "2.3.1", "rc": "1.2.8", "registry-auth-token": "4.2.2", "restana": "4.9.7", diff --git a/snapshots/updates.test.js.snap b/snapshots/updates.test.js.snap index 9f7e9e0..804d9b9 100644 --- a/snapshots/updates.test.js.snap +++ b/snapshots/updates.test.js.snap @@ -24,6 +24,30 @@ exports[`exclude 2`] = ` } `; +exports[`exclude 3`] = ` +{ + "dependencies": { + "gulp-sourcemaps": { + "info": "https://github.com/gulp-sourcemaps/gulp-sourcemaps", + "new": "2.6.5", + "old": "2.0.0", + }, + }, +} +`; + +exports[`exclude 4`] = ` +{ + "dependencies": { + "gulp-sourcemaps": { + "info": "https://github.com/floridoo/gulp-sourcemaps", + "new": "2.0.1", + "old": "2.0.0", + }, + }, +} +`; + exports[`greatest 1`] = ` { "dependencies": { diff --git a/updates.js b/updates.js index 014b96f..81998c2 100755 --- a/updates.js +++ b/updates.js @@ -17,6 +17,7 @@ import {magenta, red, green, disableColor} from "glowie"; import {getProperty} from "dot-prop"; import pAll from "p-all"; import memize from "memize"; +import picomatch from "picomatch"; let fetch; if (globalThis.fetch && !versions?.node) { // avoid node experimental warning @@ -96,11 +97,11 @@ const args = minimist(argv.slice(2), { if (args["no-color"] || !supportsColor.stdout) disableColor(); -const greatest = parseMixedArg(args.greatest); -const prerelease = parseMixedArg(args.prerelease); -const release = parseMixedArg(args.release); -const patch = parseMixedArg(args.patch); -const minor = parseMixedArg(args.minor); +const greatest = argSetToRegexes(parseMixedArg(args.greatest)); +const prerelease = argSetToRegexes(parseMixedArg(args.prerelease)); +const release = argSetToRegexes(parseMixedArg(args.release)); +const patch = argSetToRegexes(parseMixedArg(args.patch)); +const minor = argSetToRegexes(parseMixedArg(args.minor)); const allowDowngrade = parseMixedArg(args["allow-downgrade"]); const npmrc = rc("npm", {registry: "https://registry.npmjs.org"}); @@ -109,6 +110,13 @@ const githubApiUrl = args.githubapi ? normalizeUrl(args.githubapi) : "https://ap const pypiApiUrl = args.pypiapi ? normalizeUrl(args.pypiapi) : "https://pypi.org"; const maxSockets = typeof args.sockets === "number" ? parseInt(args.sockets) : MAX_SOCKETS; +function matchesAny(str, set) { + for (const re of (set instanceof Set ? set : [])) { + if (re.test(str)) return true; + } + return false; +} + const registryUrl = memize((scope, npmrc) => { const url = npmrc[`${scope}:registry`] || npmrc.registry; return url.endsWith("/") ? url : `${url}/`; @@ -502,7 +510,7 @@ function findNewVersion(data, {language, range, useGreatest, useRel, usePre, sem // prevent downgrade to older version except with --allow-downgrade if (lt(latestTag, oldVersion) && !latestIsPre) { - if (allowDowngrade === true || allowDowngrade?.has?.(data.name)) { + if (allowDowngrade === true || matchesAny(data.name, allowDowngrade)) { return latestTag; } else { return null; @@ -611,14 +619,35 @@ async function getCerts(extra = []) { return [...(await import("node:tls")).rootCertificates, ...extra]; } +// convert arg from cli or config to regex +function argToRegex(arg, cli) { + if (cli) { + return /\/.+\//.test(arg) ? new RegExp(arg.slice(1, -1)) : picomatch.makeRe(arg); + } else { + return arg instanceof RegExp ? arg : picomatch.makeRe(arg); + } +} + +// parse cli arg into regex set +function argSetToRegexes(arg) { + if (arg instanceof Set) { + const ret = new Set(); + for (const entry of arg) { + ret.add(argToRegex(entry, true)); + } + return ret; + } + return arg; +} + // parse include/exclude into a Set of regexes function matchersToRegexSet(cliArgs, configArgs) { const ret = new Set(); for (const arg of cliArgs || []) { - ret.add(new RegExp(/\/.+\//.test(arg) ? arg.slice(1, -1) : `^${esc(arg)}$`)); + ret.add(argToRegex(arg, true)); } for (const arg of configArgs || []) { - ret.add(arg instanceof RegExp ? arg : new RegExp(`^${esc(arg)}$`)); + ret.add(argToRegex(arg, false)); } return ret; } @@ -636,8 +665,8 @@ async function main() { Options: -u, --update Update versions and write package file -f, --file Use given package file or module directory - -i, --include Include only given packages, supports '/regex/' syntax - -e, --exclude Exclude given packages, supports '/regex/' syntax + -i, --include Include only given packages + -e, --exclude Exclude given packages -p, --prerelease [] Consider prerelease versions -R, --release [] Only use release versions, may downgrade -g, --greatest [] Prefer greatest over latest version @@ -850,14 +879,14 @@ async function main() { for (const [data, type, registry, name] of entries) { if (data?.error) throw new Error(data.error); - 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); + const useGreatest = typeof greatest === "boolean" ? greatest : matchesAny(data.name, greatest); + const usePre = typeof prerelease === "boolean" ? prerelease : matchesAny(data.name, prerelease); + const useRel = typeof release === "boolean" ? release : matchesAny(data.name, release); let semvers; - if (patch === true || patch?.has?.(data.name)) { + if (patch === true || matchesAny(data.name, patch)) { semvers = patchSemvers; - } else if (minor === true || minor?.has?.(data.name)) { + } else if (minor === true || matchesAny(data.name, minor)) { semvers = minorSemvers; } else { semvers = majorSemvers; @@ -892,7 +921,7 @@ async function main() { if (Object.keys(maybeUrlDeps).length) { const results = await Promise.all(Object.entries(maybeUrlDeps).map(([key, dep]) => { const name = key.split(sep)[1]; - const useGreatest = typeof greatest === "boolean" ? greatest : greatest.has(name); + const useGreatest = typeof greatest === "boolean" ? greatest : matchesAny(name, greatest); return checkUrlDep([key, dep], {useGreatest}); })); diff --git a/updates.test.js b/updates.test.js index 04a06f4..72ba769 100644 --- a/updates.test.js +++ b/updates.test.js @@ -172,6 +172,8 @@ test("include 2", makeTest("-j -i noty -i noty,noty")); test("include 3", makeTest("-j -i /^noty/")); test("exclude", makeTest("-j -e gulp-sourcemaps,prismjs,svgstore,html-webpack-plugin,noty,jpeg-buffer-orientation,styled-components,@babel/preset-env,versions/updates,react")); test("exclude", makeTest("-j -e gulp-sourcemaps -i /react/")); +test("exclude", makeTest("-j -i gulp*")); +test("exclude", makeTest("-j -i /^gulp/ -P gulp*")); test("pypi", makeTest( `-j -f ${fileURLToPath(new URL("fixtures/pyproject.toml", import.meta.url))}`,