diff --git a/crates/berry-core/src/parse.rs b/crates/berry-core/src/parse.rs index 3f1c085..30e5edc 100644 --- a/crates/berry-core/src/parse.rs +++ b/crates/berry-core/src/parse.rs @@ -93,21 +93,33 @@ pub fn parse_package_entry(input: &str) -> IResult<&str, (Vec, Packa /// Parse a package descriptor line like: "debug@npm:1.0.0":, eslint-config-turbo@latest:, or ? "conditional@npm:1.0.0": pub fn parse_descriptor_line(input: &str) -> IResult<&str, Vec> { - // Handle optional '? ' prefix for conditional packages - let (rest, _) = opt(tag("? ")).parse(input)?; + // Check for optional '? ' prefix for wrapped-line descriptors + let (rest, has_line_wrap_marker) = opt(tag("? ")).parse(input)?; + let is_wrapped_line = has_line_wrap_marker.is_some(); // Handle both quoted and unquoted descriptors - let (rest, descriptor_string) = alt(( - delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version": - // Handle very long descriptor lines that wrap: "very long descriptor..."\n: - delimited( - char('"'), - take_until("\""), - terminated(char('"'), preceded(newline, char(':'))), - ), - terminated(take_until(":"), char(':')), // Unquoted: package@latest: - )) - .parse(rest)?; + // Optimisation: when descriptors are prefixed with ? they are often "wrapped", so we check for that first + let (rest, descriptor_string) = if is_wrapped_line { + // For wrapped-line descriptors, try newline-wrapped format first + alt(( + // Handle very long descriptor lines that wrap: "very long descriptor..."\n: + delimited( + char('"'), + take_until("\""), + terminated(char('"'), preceded(newline, char(':'))), + ), + delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version": + terminated(take_until(":"), char(':')), // Unquoted: package@latest: + )) + .parse(rest)? + } else { + // For normal descriptors, skip the newline-wrapped check entirely (performance optimisation) + alt(( + delimited(char('"'), take_until("\":"), tag("\":")), // Quoted: "package@npm:version": + terminated(take_until(":"), char(':')), // Unquoted: package@latest: + )) + .parse(rest)? + }; // Parse comma-separated descriptors using fold_many0 to avoid allocations let (remaining, descriptor_data) = { @@ -1734,4 +1746,86 @@ __metadata: assert_eq!(package.bin.len(), 1); assert_eq!(package.bin.get("resolve"), Some(&"bin/resolve".to_string())); } + + #[test] + fn test_parse_descriptor_line_wrapped_long_line() { + let input = r#"? "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.0" +:"#; + let result = parse_descriptor_line(input); + + assert!( + result.is_ok(), + "Should successfully parse long, wrapped-line descriptor" + ); + let (remaining, descriptors) = result.unwrap(); + assert_eq!(remaining, ""); + assert_eq!(descriptors.len(), 3, "Should parse 3 descriptors"); + + // Verify first descriptor + assert_eq!(descriptors[0].ident().name(), "runtime"); + assert_eq!(descriptors[0].ident().scope(), Some("@babel")); + assert_eq!(descriptors[0].range(), "npm:^7.0.0"); + + // Verify second descriptor + assert_eq!(descriptors[1].ident().name(), "runtime"); + assert_eq!(descriptors[1].range(), "npm:^7.1.2"); + + // Verify third descriptor + assert_eq!(descriptors[2].ident().name(), "runtime"); + assert_eq!(descriptors[2].range(), "npm:^7.10.0"); + } + + #[test] + fn test_parse_descriptor_line_short_no_wrap() { + // Test that short lines with `?` prefix still work (without newline wrap) + let input = r#"? "resolve@patch:resolve@npm%3A^1.0.0#optional!builtin":"#; + let result = parse_descriptor_line(input); + + assert!( + result.is_ok(), + "Should successfully parse short conditional package without wrap" + ); + let (remaining, descriptors) = result.unwrap(); + assert_eq!(remaining, ""); + assert_eq!(descriptors.len(), 1); + assert_eq!(descriptors[0].ident().name(), "resolve"); + } + + #[test] + fn test_parse_package_entry_long_wrapped_line() { + // Test a complete package entry with the long wrapped descriptor pattern + let input = r#"? "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.0, @babel/runtime@npm:^7.12.0" +: + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 + languageName: node + linkType: hard + +"#; + let result = parse_package_entry(input); + + assert!( + result.is_ok(), + "Should successfully parse complete package entry with long wrapped descriptor" + ); + let (remaining, (descriptors, package)) = result.unwrap(); + assert_eq!(remaining, ""); + assert_eq!(descriptors.len(), 4, "Should parse 4 descriptors"); + + // Verify descriptors + assert_eq!(descriptors[0].ident().name(), "runtime"); + assert_eq!(descriptors[0].ident().scope(), Some("@babel")); + assert_eq!(descriptors[0].range(), "npm:^7.0.0"); + assert_eq!(descriptors[3].range(), "npm:^7.12.0"); + + // Verify the parsed package + assert_eq!(package.version, Some("7.28.4".to_string())); + assert_eq!( + package.resolution, + Some("@babel/runtime@npm:7.28.4".to_string()) + ); + assert_eq!(package.language_name.as_ref(), "node"); + assert_eq!(package.link_type, LinkType::Hard); + } } diff --git a/fixtures/long-descriptors.yarn.lock b/fixtures/long-descriptors.yarn.lock new file mode 100644 index 0000000..d3ac00b --- /dev/null +++ b/fixtures/long-descriptors.yarn.lock @@ -0,0 +1,49 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c + +? "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.1.5, @babel/runtime@npm:^7.10.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.4, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.4, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.7.7, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2" +: + version: 7.28.4 + resolution: "@babel/runtime@npm:7.28.4" + checksum: 10/6c9a70452322ea80b3c9b2a412bcf60771819213a67576c8cec41e88a95bb7bf01fc983754cda35dc19603eef52df22203ccbf7777b9d6316932f9fb77c25163 + languageName: node + linkType: hard + +? "resolve@patch:resolve@npm%3A^1.0.0#optional!builtin, resolve@patch:resolve@npm%3A^1.1.4#optional!builtin, resolve@patch:resolve@npm%3A^1.1.x#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.15.1#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.3.2#optional!builtin" +: + version: 1.22.10 + resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.16.0" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10/dc5c99fb47807d3771be3135ac6bdb892186973d0895ab17838f0b85bb575e03111214aa16cb68b6416df3c1dd658081a066dd7a9af6e668c28b0025080b615c + languageName: node + linkType: hard + +"is-core-module@npm:^2.16.0": + version: 2.16.0 + resolution: "is-core-module@npm:2.16.0" + checksum: 10/abcdef1234567890 + languageName: node + linkType: hard + +"path-parse@npm:^1.0.7": + version: 1.0.7 + resolution: "path-parse@npm:1.0.7" + checksum: 10/fedcba0987654321 + languageName: node + linkType: hard + +"supports-preserve-symlinks-flag@npm:^1.0.0": + version: 1.0.0 + resolution: "supports-preserve-symlinks-flag@npm:1.0.0" + checksum: 10/0123456789abcdef + languageName: node + linkType: hard