diff --git a/src/lib.rs b/src/lib.rs index e83804f..aa82179 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,7 @@ pub enum Generator { /// yarn.lock entry. /// It only shows the name of the dependency and the version. #[derive(Debug, PartialEq, Eq, Default)] +#[non_exhaustive] pub struct Entry<'a> { pub name: &'a str, pub version: &'a str, @@ -53,6 +54,9 @@ pub struct Entry<'a> { pub integrity: &'a str, pub dependencies: Vec<(&'a str, &'a str)>, pub optional_dependencies: Vec<(&'a str, &'a str)>, + pub dependencies_meta: Vec<(&'a str, DepMeta)>, + pub peer_dependencies: Vec<(&'a str, &'a str)>, + pub peer_dependencies_meta: Vec<(&'a str, DepMeta)>, pub descriptors: Vec<(&'a str, &'a str)>, } @@ -190,6 +194,9 @@ enum EntryItem<'a> { Resolved(&'a str), Dependencies(Vec<(&'a str, &'a str)>), OptionalDependencies(Vec<(&'a str, &'a str)>), + PeerDependencies(Vec<(&'a str, &'a str)>), + DepsMeta(Vec<(&'a str, DepMeta)>), + PeersMeta(Vec<(&'a str, DepMeta)>), Integrity(&'a str), Unknown(&'a str), } @@ -220,9 +227,9 @@ fn entry_item(input: &str) -> Res<&str, EntryItem<'_>> { alt(( entry_version, parse_dependencies, - parse_optional_dependencies, integrity, entry_resolved, + parse_deps_meta, unknown_line, )) .parse(input) @@ -243,6 +250,9 @@ fn parse_entry(input: &str) -> Res<&str, Entry<'_>> { let mut resolved = ""; let mut dependencies = Vec::new(); let mut optional_dependencies = Vec::new(); + let mut dependencies_meta = Vec::new(); + let mut peer_dependencies = Vec::new(); + let mut peer_dependencies_meta = Vec::new(); let mut integrity = ""; for ei in entry_items { @@ -251,7 +261,10 @@ fn parse_entry(input: &str) -> Res<&str, Entry<'_>> { EntryItem::Resolved(r) => resolved = r, EntryItem::Dependencies(d) => dependencies = d, EntryItem::OptionalDependencies(d) => optional_dependencies = d, + EntryItem::PeerDependencies(d) => peer_dependencies = d, EntryItem::Integrity(c) => integrity = c, + EntryItem::DepsMeta(m) => dependencies_meta = m, + EntryItem::PeersMeta(m) => peer_dependencies_meta = m, EntryItem::Unknown(_) => (), } } @@ -272,6 +285,9 @@ fn parse_entry(input: &str) -> Res<&str, Entry<'_>> { integrity, dependencies, optional_dependencies, + dependencies_meta, + peer_dependencies, + peer_dependencies_meta, descriptors, }, )) @@ -283,7 +299,23 @@ fn dependency_version(input: &str) -> Res<&str, &str> { } fn parse_dependencies(input: &str) -> Res<&str, EntryItem<'_>> { - let (input, (indent, _, _)) = (space1, tag("dependencies:"), line_ending).parse(input)?; + enum DepsKind { + Deps, + // yarn v1 only + Optional, + // yarn berry only + Peer, + } + let (input, (indent, key, _)) = ( + space1, + alt(( + map(tag("dependencies:"), |_| DepsKind::Deps), + map(tag("optionalDependencies:"), |_| DepsKind::Optional), + map(tag("peerDependencies:"), |_| DepsKind::Peer), + )), + line_ending, + ) + .parse(input)?; let dependencies_parser = many1(move |i| { ( @@ -300,29 +332,97 @@ fn parse_dependencies(input: &str) -> Res<&str, EntryItem<'_>> { }); context("dependencies", dependencies_parser) .parse(input) - .map(|(i, res)| (i, EntryItem::Dependencies(res))) + .map(|(i, res)| { + ( + i, + match key { + DepsKind::Deps => EntryItem::Dependencies(res), + DepsKind::Optional => EntryItem::OptionalDependencies(res), + DepsKind::Peer => EntryItem::PeerDependencies(res), + }, + ) + }) } -fn parse_optional_dependencies(input: &str) -> Res<&str, EntryItem<'_>> { - let (input, (indent, _, _)) = - (space1, tag("optionalDependencies:"), line_ending).parse(input)?; +#[derive(Debug, PartialEq, Eq, Default)] +#[non_exhaustive] +pub struct DepMeta { + pub optional: Option, +} - let optional_dependencies_parser = many1(move |i| { - ( - tag(indent), // indented as much as the parent... - space1, // ... plus extra indentation - is_not(": "), // package name - one_of(": "), - space0, - dependency_version, // version - alt((line_ending, space0)), // newline or space - ) - .parse(i) - .map(|(i, (_, _, p, _, _, v, _))| (i, (p.trim_matches('"'), v))) - }); - context("optionalDependencies", optional_dependencies_parser) +fn parse_deps_meta(input: &str) -> Res<&str, EntryItem<'_>> { + let (input, indent_top) = space1(input)?; + enum MetaKind { + Deps, + Peers, + } + let (input, key) = terminated( + alt(( + map(tag("dependenciesMeta:"), |_| MetaKind::Deps), + map(tag("peerDependenciesMeta:"), |_| MetaKind::Peers), + )), + line_ending, + ) + .parse(input)?; + many1(|i| deps_meta_dep(i, indent_top)) + .parse(input) + .map(|(i, dm)| { + ( + i, + match key { + MetaKind::Deps => EntryItem::DepsMeta(dm), + MetaKind::Peers => EntryItem::PeersMeta(dm), + }, + ) + }) +} + +fn deps_meta_dep<'a>(input: &'a str, indent_top: &'a str) -> Res<&'a str, (&'a str, DepMeta)> { + let (input, indent_dep) = recognize((tag(indent_top), space1)).parse(input)?; + let (input, dep_name) = take_until(":")(input)?; + let (input, _) = (tag(":"), line_ending).parse(input)?; + many1(|i| peers_meta_dep_prop(i, indent_dep)) .parse(input) - .map(|(i, res)| (i, EntryItem::OptionalDependencies(res))) + .and_then(|(i, props)| { + let mut meta = DepMeta { optional: None }; + for (prop_key, prop_val) in props { + #[allow(clippy::single_match)] + match prop_key { + "optional" => { + meta.optional = Some(match prop_val { + "true" => true, + "false" => false, + _ => { + return Err(nom::Err::Failure(VerboseError::from_error_kind( + "bool property not 'true' or 'false'", + nom::error::ErrorKind::Fail, + ))) + } + }) + } + _ => {} + } + } + Ok((i, (dep_name, meta))) + }) +} + +fn peers_meta_dep_prop<'a>( + input: &'a str, + indent_dep: &'a str, +) -> Res<&'a str, (&'a str, &'a str)> { + let (input, _) = recognize((tag(indent_dep), space1)).parse(input)?; + let (input, (prop_key, _, prop_val)) = ( + take_until(":"), + tag(": "), + map(take_till_line_end, |v| { + v.strip_suffix("\r\n") + .or_else(|| v.strip_suffix("\n")) + .unwrap() + }), + ) + .parse(input)?; + Ok((input, (prop_key, prop_val))) } /** @@ -626,7 +726,8 @@ cli-table3@~0.6.1: descriptors: vec![("cli-table3", "~0.6.1")], dependencies: vec![("string-width", "^4.2.0")], optional_dependencies: vec![("@colors/colors", "1.5.0")], - integrity: "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==" + integrity: "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + ..Default::default() }, Entry { name: "@babel/helper-validator-identifier", @@ -769,6 +870,7 @@ __metadata: resolved: "@babel/plugin-transform-for-of@npm:7.16.7", descriptors: vec![("@babel/plugin-transform-for-of", "npm:^7.12.1")], dependencies: vec![("@babel/helper-plugin-utils", "^7.16.7")], + peer_dependencies: vec![("@babel/core", "^7.0.0-0")], integrity: "35c9264ee4bef814818123d70afe8b2f0a85753a0a9dc7b73f93a71cadc5d7de852f1a3e300a7c69a491705805704611de1e2ccceb5686f7828d6bca2e5a7306", ..Default::default() }, @@ -1183,14 +1285,14 @@ __metadata: "#, EntryItem::Dependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]), ); - } - #[test] - fn parse_optional_dependencies_work() { - fn assert(input: &str, expect: EntryItem) { - let res = parse_optional_dependencies(input).unwrap(); - assert_eq!(res.1, expect); - } + assert( + r#" peerDependencies: + foo: 1.0 || 2.0 + "bar": "0.3-alpha1" + "#, + EntryItem::PeerDependencies(vec![("foo", "1.0 || 2.0"), ("bar", "0.3-alpha1")]), + ); assert( r#" optionalDependencies: @@ -1286,6 +1388,63 @@ __metadata: ); } + #[test] + fn supports_dependencies_meta() { + let content = std::fs::read_to_string("tests/v2_deps_meta/yarn.lock").unwrap(); + let res = parse_str(&content).unwrap(); + + assert_eq!( + res.entries[2], + Entry { + name: "jsonfile", + version: "4.0.0", + resolved: "jsonfile@npm:4.0.0", + dependencies: vec![("graceful-fs", "^4.1.6")], + dependencies_meta: vec![( + "graceful-fs", + DepMeta { + optional: Some(true), + } + )], + integrity: "a40b7b64da41c84b0dc7ad753737ba240bb0dc50a94be20ec0b73459707dede69a6f89eb44b4d29e6994ed93ddf8c9b6e57f6b1f09dd707567959880ad6cee7f", + descriptors: vec![("jsonfile", "npm:4.0.0")], + ..Default::default() + } + ); + } + + #[test] + fn supports_peer_dependencies() { + let content = std::fs::read_to_string("tests/peer_dependencies/yarn.lock").unwrap(); + let res = parse_str(&content).unwrap(); + + assert_eq!( + res.entries[4], + Entry { + name: "react-router", + version: "7.2.0", + resolved: "react-router@npm:7.2.0", + descriptors: vec![("react-router", "npm:^7.2.0")], + integrity: "05c79d86639f146aafc64351bb042acd785dbb69c7874ad8e0a3f5f3e70890b1b3ee07d0e18f8cebaffd62bca47e58d0645b07d1cc428a73ba449ce378cbef01", + dependencies: vec![ + ("@types/cookie", "^0.6.0"), + ("cookie", "^1.0.1"), + ("set-cookie-parser", "^2.6.0"), + ("turbo-stream", "2.4.0"), + ], + peer_dependencies: vec![ + ("react", ">=18"), + ("react-dom", ">=18"), + ], + peer_dependencies_meta: vec![("react-dom", DepMeta { + optional: Some(true), + ..Default::default() + })], + ..Default::default() + } + ); + } + #[test] fn supports_version_url() { // https://github.com/oven-sh/bun/issues/17091 diff --git a/tests/peer_dependencies/yarn.lock b/tests/peer_dependencies/yarn.lock new file mode 100644 index 0000000..e690cff --- /dev/null +++ b/tests/peer_dependencies/yarn.lock @@ -0,0 +1,87 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 4 + cacheKey: 7 + +"@chastelock/testcase@workspace:.": + version: 0.0.0-use.local + resolution: "@chastelock/testcase@workspace:." + dependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + react-router: ^7.2.0 + languageName: unknown + linkType: soft + +"@types/cookie@npm:^0.6.0": + version: 0.6.0 + resolution: "@types/cookie@npm:0.6.0" + checksum: f4aa264a5af8c63cbb78c4c5de4743ffca9120af6c47ec15d77da97c1caf1cde381776b5c57a03fcd251eeaed81de684a437f3c4d60f7492b4982e0d0f7c99fa + languageName: node + linkType: hard + +"cookie@npm:^1.0.1": + version: 1.0.2 + resolution: "cookie@npm:1.0.2" + checksum: b0346db1a20fdff372728e55c13d5a4ea5b5c2b06ada0aa8817b2bcec7884eba01484f59e37fec8830d3db2e9178e7ff84eb13e183c87b4ccb3802d61ec81bf2 + languageName: node + linkType: hard + +"react-dom@npm:^19.0.0": + version: 19.0.0 + resolution: "react-dom@npm:19.0.0" + dependencies: + scheduler: ^0.25.0 + peerDependencies: + react: ^19.0.0 + checksum: 9bd4eef5c92fd3996b4a84ef03e4c03f0b526a574fe24042904470bcdf1304a96e3e46ca8cea89fede3382e8aae4479e1b52649060d1bc7faa6d9dbdcf6ee2c1 + languageName: node + linkType: hard + +"react-router@npm:^7.2.0": + version: 7.2.0 + resolution: "react-router@npm:7.2.0" + dependencies: + "@types/cookie": ^0.6.0 + cookie: ^1.0.1 + set-cookie-parser: ^2.6.0 + turbo-stream: 2.4.0 + peerDependencies: + react: ">=18" + react-dom: ">=18" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 05c79d86639f146aafc64351bb042acd785dbb69c7874ad8e0a3f5f3e70890b1b3ee07d0e18f8cebaffd62bca47e58d0645b07d1cc428a73ba449ce378cbef01 + languageName: node + linkType: hard + +"react@npm:^19.0.0": + version: 19.0.0 + resolution: "react@npm:19.0.0" + checksum: 945974326f0c9460c61f85bb4e3029a476302dd9d7262ae252bd9b39496d655315d1bcf9f376206d50f492b39b35b466a29fbd8003f611e4ab9dc407233cfae7 + languageName: node + linkType: hard + +"scheduler@npm:^0.25.0": + version: 0.25.0 + resolution: "scheduler@npm:0.25.0" + checksum: f6898ebe560ae91938c1b533956e510ee88d3ef861c8af006841ec4b477fb0ee194de8fd02642ad824e11c126d4fd6309c5ed3094aae033aacc5e2b517bfc222 + languageName: node + linkType: hard + +"set-cookie-parser@npm:^2.6.0": + version: 2.7.1 + resolution: "set-cookie-parser@npm:2.7.1" + checksum: e026e0a355e1d0462cfb54eb0f429cd167c42da0829396c705469b30c887e8ccb3d32faa22b7823283d21cb5470df67980bc507140bf4ea2c4350083b5102b84 + languageName: node + linkType: hard + +"turbo-stream@npm:2.4.0": + version: 2.4.0 + resolution: "turbo-stream@npm:2.4.0" + checksum: 27b8ca0bfe8ae7f00ca667fef6cf603517a1261ccf8db5f48c0e9e1b05a3ce4ccbd0639a2e4d0e7fe181d603ecc1d06bdf919fec73f53260652d8c651218f1f9 + languageName: node + linkType: hard diff --git a/tests/v2_deps_meta/yarn.lock b/tests/v2_deps_meta/yarn.lock new file mode 100644 index 0000000..04e8233 --- /dev/null +++ b/tests/v2_deps_meta/yarn.lock @@ -0,0 +1,33 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 4 + cacheKey: 7 + +"@chastelock/testcase@workspace:.": + version: 0.0.0-use.local + resolution: "@chastelock/testcase@workspace:." + dependencies: + jsonfile: 4.0.0 + languageName: unknown + linkType: soft + +"graceful-fs@npm:^4.1.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 99533f7c73bafc139bfc435a628272226cb46dc2b9b3dba7395dcb6c19936db28961ad9fcd648f407c32f91998fb2102a2df4dd13951a9699c5c28e2145713a9 + languageName: node + linkType: hard + +"jsonfile@npm:4.0.0": + version: 4.0.0 + resolution: "jsonfile@npm:4.0.0" + dependencies: + graceful-fs: ^4.1.6 + dependenciesMeta: + graceful-fs: + optional: true + checksum: a40b7b64da41c84b0dc7ad753737ba240bb0dc50a94be20ec0b73459707dede69a6f89eb44b4d29e6994ed93ddf8c9b6e57f6b1f09dd707567959880ad6cee7f + languageName: node + linkType: hard