support regex for include/exclude and merge cli/config options

Fixes: https://github.com/silverwind/updates/issues/57
Fixes: https://github.com/silverwind/updates/issues/66
This commit is contained in:
silverwind 2023-08-26 01:17:17 +02:00
parent ae3b22a400
commit de94050877
Signed by: silverwind
GPG Key ID: 2E62B41C93869443
5 changed files with 81 additions and 36 deletions

@ -34,14 +34,15 @@ Put a `updates.config.js` or `updates.config.mjs` in the root of your project, u
export default {
exclude: [
"semver",
/^react/,
],
};
```
### Config File Options
- `include` *Array[String]*: Array of dependencies to include
- `exclude` *Array[String]*: Array of dependencies to exclude
- `include` *Array[String|Regexp]*: Array of dependencies to include
- `exclude` *Array[String|Regexp]*: Array of dependencies to exclude
- `types` *Array[String]*: Array of dependency types
- `registry` *String*: URL to npm registry

24
package-lock.json generated

@ -1477,18 +1477,6 @@
"@esbuild/win32-x64": "0.18.19"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
@ -1814,6 +1802,18 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",

@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`exclude version deps 1`] = `
exports[`exclude 1`] = `
{
"dependencies": {
"updates": {
@ -12,6 +12,18 @@ exports[`exclude version deps 1`] = `
}
`;
exports[`exclude 2`] = `
{
"dependencies": {
"react": {
"info": "https://github.com/facebook/react/tree/HEAD/packages/react",
"new": "18.2.0",
"old": "18.0",
},
},
}
`;
exports[`greatest 1`] = `
{
"dependencies": {
@ -78,7 +90,7 @@ exports[`greatest 1`] = `
}
`;
exports[`include version deps #2 1`] = `
exports[`include 1`] = `
{
"dependencies": {
"noty": {
@ -90,7 +102,19 @@ exports[`include version deps #2 1`] = `
}
`;
exports[`include version deps 1`] = `
exports[`include 2 1`] = `
{
"dependencies": {
"noty": {
"info": "https://github.com/needim/noty",
"new": "3.2.0-beta",
"old": "3.1.0",
},
},
}
`;
exports[`include 3 1`] = `
{
"dependencies": {
"noty": {

@ -611,6 +611,18 @@ async function getCerts(extra = []) {
return [...(await import("node:tls")).rootCertificates, ...extra];
}
// 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)}$`));
}
for (const arg of configArgs || []) {
ret.add(arg instanceof RegExp ? arg : new RegExp(`^${esc(arg)}$`));
}
return ret;
}
async function main() {
for (const stream of [process.stdout, process.stderr]) {
stream?._handle?.setBlocking?.(true);
@ -622,13 +634,13 @@ async function main() {
stdout.write(`usage: updates [options]
Options:
-l, --language <lang> Language to check, either 'js' or 'py'
-u, --update Update versions and write package file
-f, --file <path> Use given package file or module directory
-i, --include <pkg,...> Include only given packages, supports '/regex/' syntax
-e, --exclude <pkg,...> Exclude given packages, supports '/regex/' syntax
-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
@ -636,7 +648,7 @@ async function main() {
-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
-f, --file <path> Use given package file or module directory
-l, --language <lang> Language to check, either 'js' or 'py'
-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
@ -646,7 +658,10 @@ async function main() {
Examples:
$ updates
$ updates -u && npm i
$ updates -u
$ updates -e '/^react-(dom)?/'
$ updates -f package.json
$ updates -f pyproject.toml
`);
exit(0);
}
@ -763,22 +778,25 @@ async function main() {
finish(new Error(`Error parsing ${packageFile}: ${err.message}`));
}
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);
let includeCli, excludeCli;
if (args.include && args.include !== true) { // cli
includeCli = (Array.isArray(args.include) ? args.include : [args.include]).flatMap(item => item.split(","));
}
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);
excludeCli = (Array.isArray(args.exclude) ? args.exclude : [args.exclude]).flatMap(item => item.split(","));
}
const include = matchersToRegexSet(includeCli, config?.include);
const exclude = matchersToRegexSet(excludeCli, config?.exclude);
function canInclude(name, language) {
if (language === "py" && name === "python") return false;
if (exclude?.has?.(name) === true) return false;
if (include?.has?.(name) === false) return false;
for (const re of exclude) {
if (re.test(name)) return false;
}
for (const re of include) {
if (!re.test(name)) return false;
}
return true;
}
@ -806,7 +824,7 @@ async function main() {
}
if (!Object.keys(deps).length && !Object.keys(maybeUrlDeps).length) {
if (include || exclude) {
if (include.size || exclude.size) {
finish(new Error(`No dependencies match the given include/exclude filters`));
} else {
finish("No dependencies present, nothing to do");

@ -167,9 +167,11 @@ test("greatest", makeTest("-j -g"));
test("prerelease", makeTest("-j -g -p"));
test("release", makeTest("-j -R"));
test("patch", makeTest("-j -P"));
test("include version deps", makeTest("-j -i noty"));
test("include version deps #2", makeTest("-j -i noty -i noty,noty"));
test("exclude version deps", makeTest("-j -e gulp-sourcemaps,prismjs,svgstore,html-webpack-plugin,noty,jpeg-buffer-orientation,styled-components,@babel/preset-env,versions/updates,react"));
test("include", makeTest("-j -i noty"));
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("pypi", makeTest(
`-j -f ${fileURLToPath(new URL("fixtures/pyproject.toml", import.meta.url))}`,