diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d0134a..ec3d88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,12 @@ ### Added - display the output as an ASCII tree +- new option `--filter`, to limit which versions to search for +- new ption `--dedup` to remove duplicate results - colorize output when using a TTY -- MAX_PKG_VISITS can be set with an env var -- remove duplicate results using --dedup +- env var MAX_PKG_VISITS can be set to bypass hypotetical infinite loops. + It stops searching children of a package when it was visited already more than + MAX_PKG_VISITS times - fixed a bug detecting dependencies on newer versions of yarn.lock - fix duplication caused by dependencies using patch protocol diff --git a/Cargo.lock b/Cargo.lock index ea769e0..5cd9c7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,6 +213,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" version = "1.0.197" @@ -338,6 +344,7 @@ dependencies = [ "fxhash", "once_cell", "pico-args", + "semver", "serde", "serde_json", "yarn-lock-parser", diff --git a/Cargo.toml b/Cargo.toml index 1aa38ed..a1481bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ atty = "0.2.14" fxhash = "0.2.1" once_cell = "1.19.0" pico-args = "0.5.0" +semver = "1.0.22" serde = { version = "1.0.197", features = ["derive", "rc"] } serde_json = "1.0.115" yarn-lock-parser = { version = "0.7.0" } diff --git a/README.md b/README.md index 4974ce8..b8785f9 100644 --- a/README.md +++ b/README.md @@ -36,26 +36,44 @@ OPTIONS: -h, --help Prints this help and exit -V, --version Prints version information -y, --yarn-lock-file Path to a yarn.lock file to parse + --filter [descriptors] Keep only matching versions + (e.g. --filter '>=1.3.0, <2.0.0') ARGS: package[@range] Package to search for, with or without range. The range must match one in yarn.lock ``` -Example output (searching for `lodash`) - +Example output (searching for `fs-minipass`) + +```bash +└─ vite@5.2.4 (via ^5.2.0) + ├─ fsevents@2.3.3 (via ~2.3.3) + │ └─ node-gyp@10.0.1 (via latest) + │ ├─ make-fetch-happen@13.0.0 (via ^13.0.0) + │ │ └─ cacache@18.0.2 (via ^18.0.0) + │ │ ├─ fs-minipass@3.0.3 (via ^3.0.0) + │ │ └─ tar@6.2.1 (via ^6.1.11) + │ │ └─ fs-minipass@2.1.0 (via ^2.0.0) + │ └─ tar@6.2.1 (via ^6.1.2) + │ └─ fs-minipass@2.1.0 (via ^2.0.0) + └─ rollup@4.13.0 (via ^4.13.0) + └─ fsevents@2.3.3 (via ~2.3.2) + └─ node-gyp@10.0.1 (via latest) ``` -├─ standard@^11.0.0 -│ └─ eslint@~4.18.0 -│ ├─ inquirer@^3.0.6 -│ │ └─ lodash@^4.3.0 -│ ├─ lodash@^4.17.4 -│ └─ table@4.0.2 -│ └─ lodash@^4.17.4 -│ -└─ webpack@3.6.0 - └─ async@^2.1.2 - └─ lodash@^4.14.0 + +Similar search, but filtered for `fs-minipass --filter '>=3.0, <4.0.0'` + +```bash +└─ vite@5.2.4 (via ^5.2.0) + ├─ fsevents@2.3.3 (via ~2.3.3) + │ └─ node-gyp@10.0.1 (via latest) + │ └─ make-fetch-happen@13.0.0 (via ^13.0.0) + │ └─ cacache@18.0.2 (via ^18.0.0) + │ └─ fs-minipass@3.0.3 (via ^3.0.0) + └─ rollup@4.13.0 (via ^4.13.0) + └─ fsevents@2.3.3 (via ~2.3.2) + └─ node-gyp@10.0.1 (via latest) ``` Defaults: @@ -65,35 +83,35 @@ Defaults: ## Benchmarks -Benchmarks run on Thinkpad T460s -- node 17.9.0 -- yarn 1.22.18 / yarn 3.2.0 +Benchmarks run on Framework Laptop 14 AMD Ryzen 7 7840U +- node 21.7.1 +- yarn 1.22.22 / yarn 4.1.0 - using [renovate 35.45.5 yarn.lock file](https://github.com/renovatebot/renovate/blob/32.45.5/yarn.lock) (v1 first, then updating it) (had to use -y because hyperfine would trigger stdin input) -``` -$ hyperfine 'yarn-why -y yarn.lock lodash' -Benchmark #1: yarn-why -y yarn.lock lodash - Time (mean ± σ): 9.4 ms ± 1.6 ms [User: 8.3 ms, System: 1.1 ms] - Range (min … max): 7.8 ms … 22.7 ms 191 runs - -$ hyperfine 'yarn why lodash' -Benchmark #1: yarn why lodash - Time (mean ± σ): 1.012 s ± 0.012 s [User: 1.686 s, System: 0.101 s] - Range (min … max): 0.994 s … 1.026 s 10 runs - -# again, after updating yarn.lock using `yarn 3.2.0` - -$ hyperfine 'yarn why lodash' -Benchmark #1: yarn why lodash - ⠏ Current estimate: 45.455 s █████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ETA 00:05:17 -^C # was taking too long, I stopped it - -hyperfine 'yarn-why -y yarn.lock lodash' -Benchmark #1: yarn-why -y yarn.lock lodash - Time (mean ± σ): 11.8 ms ± 0.8 ms [User: 10.6 ms, System: 1.3 ms] - Range (min … max): 9.8 ms … 14.5 ms 179 runs +```bash +$ hyperfine -w 3 './target/release/yarn-why lodash' # yarn.lock v1 +Benchmark 1: ./target/release/yarn-why lodash + Time (mean ± σ): 4.9 ms ± 0.6 ms [User: 3.8 ms, System: 1.1 ms] + Range (min … max): 2.3 ms … 7.8 ms 398 runs + +$ hyperfine -w 3 'yarn why lodash' # yarn v1.22.22 / yarn.lock v1 +Benchmark 1: yarn why lodash + Time (mean ± σ): 416.0 ms ± 76.7 ms [User: 691.7 ms, System: 75.0 ms] + Range (min … max): 367.9 ms … 608.7 ms 10 runs + +# again, after updating the same yarn.lock to v8 using `yarn 4.1.0` + +$ hyperfine -w 3 './target/release/yarn-why lodash' # yarn.lock v8 +Benchmark 1: ./target/release/yarn-why lodash + Time (mean ± σ): 6.0 ms ± 0.6 ms [User: 4.6 ms, System: 1.4 ms] + Range (min … max): 3.6 ms … 8.5 ms 340 runs + +$ hyperfine 'yarn why lodash' # yarn v4.1.0 / yarn.lock v8 +Benchmark 1: yarn why lodash + Time (mean ± σ): 295.0 ms ± 57.7 ms [User: 316.1 ms, System: 58.1 ms] + Range (min … max): 229.9 ms … 361.0 ms 10 runs ``` ## LICENSE diff --git a/src/main.rs b/src/main.rs index bbba302..2ec9a49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Result}; use once_cell::sync::OnceCell; +use semver::{Version, VersionReq}; use serde::ser::SerializeTuple; use serde::{Serialize, Serializer}; use serde_json::Result as SerdeJsonResult; @@ -35,6 +36,8 @@ OPTIONS: -h, --help Prints this help and exit -V, --version Prints version information -y, --yarn-lock-file Path to a yarn.lock file to parse + --filter [descriptors] Keep only matching versions + (e.g. --filter '>=1.3.0, <2.0.0') ARGS: package[@range] Package to search for, with or without range. @@ -57,6 +60,7 @@ struct Opt { no_max_depth: bool, query: Option, yarn_lock_path: Option, + filter: Option, } type Pkg<'a> = (&'a str, &'a str); @@ -200,6 +204,7 @@ fn main() -> Result<()> { .or(Some(10)), yarn_lock_path: pargs.opt_value_from_os_str(["-y", "--yarn-lock-path"], parse_path)?, query: pargs.free_from_str().ok(), + filter: pargs.opt_value_from_fn("--filter", VersionReq::parse)?, }; let remaining = pargs.finish(); @@ -272,6 +277,19 @@ fn main() -> Result<()> { let mut entries = parse_str(std::str::from_utf8(&yarn_lock_text)?)?; + if args.filter.is_some() { + let req = args.filter.as_ref().unwrap(); + entries.retain(|e| { + if e.name == query.as_str() { + let v = Version::parse(e.version); + // if we can't parse e.version, let's keep the entry + return v.is_err() || req.matches(&v.unwrap()); + } + + true + }) + } + // In yarn-lock-parser the dependencies were meant to contain // just (name, descriptor), with the descriptor being without the // protocol. Turns out it's not always the case, so we adjuts it here.