@@ -6,7 +6,7 @@ use nom::{
66 character:: complete:: { char, newline, space0, space1} ,
77 combinator:: { map, opt, recognize} ,
88 multi:: { fold_many0, many0} ,
9- sequence:: { delimited, preceded} ,
9+ sequence:: { delimited, preceded, terminated } ,
1010} ;
1111
1212use crate :: ident:: { Descriptor , Ident } ;
@@ -30,6 +30,17 @@ pub fn parse_lockfile(file_contents: &str) -> IResult<&str, Lockfile> {
3030 // Parse all package entries, extracting just the Package from each entry
3131 let ( rest, packages) = many0 ( parse_package_only) . parse ( rest) ?;
3232
33+ // Consume any trailing content (backticks, semicolons, whitespace, etc.)
34+ let ( rest, _) = many0 ( alt ( (
35+ tag ( "`" ) ,
36+ tag ( ";" ) ,
37+ tag ( "\n " ) ,
38+ tag ( " " ) ,
39+ tag ( "\t " ) ,
40+ tag ( "\r " ) ,
41+ ) ) )
42+ . parse ( rest) ?;
43+
3344 Ok ( (
3445 rest,
3546 Lockfile {
@@ -61,10 +72,14 @@ pub fn parse_package_entry(input: &str) -> IResult<&str, (Vec<Descriptor>, Packa
6172 Ok ( ( rest, ( descriptors, package) ) )
6273}
6374
64- /// Parse a package descriptor line like: "debug@npm:1.0.0": or "c@*, c@workspace:packages/c" :
75+ /// Parse a package descriptor line like: "debug@npm:1.0.0": or eslint-config-turbo@latest :
6576pub fn parse_descriptor_line ( input : & str ) -> IResult < & str , Vec < Descriptor > > {
66- let ( rest, descriptor_string) =
67- delimited ( char ( '"' ) , take_until ( "\" :" ) , tag ( "\" :" ) ) . parse ( input) ?;
77+ // Handle both quoted and unquoted descriptors
78+ let ( rest, descriptor_string) = alt ( (
79+ delimited ( char ( '"' ) , take_until ( "\" :" ) , tag ( "\" :" ) ) , // Quoted: "package@npm:version":
80+ terminated ( take_until ( ":" ) , char ( ':' ) ) , // Unquoted: package@latest:
81+ ) )
82+ . parse ( input) ?;
6883
6984 // Parse comma-separated descriptors using fold_many0 to avoid allocations
7085 let ( remaining, descriptor_data) = {
@@ -176,15 +191,15 @@ fn parse_name_to_ident(name_part: &str) -> Ident {
176191/// Parse a package name, which can be scoped (@babel/code-frame) or simple (debug)
177192fn parse_package_name ( input : & str ) -> IResult < & str , & str > {
178193 alt ( (
179- // Scoped package: @scope/name
194+ // Scoped package: @scope/name (both scope and name can contain dots)
180195 recognize ( (
181196 char ( '@' ) ,
182- take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' ) ,
197+ take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '.' ) ,
183198 char ( '/' ) ,
184- take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' ) ,
199+ take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '.' ) ,
185200 ) ) ,
186- // non-scoped package name: debug
187- take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' ) ,
201+ // non-scoped package name: debug (can contain dots like fs.realpath)
202+ take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '.' ) ,
188203 ) )
189204 . parse ( input)
190205}
@@ -208,8 +223,8 @@ fn parse_patch_range(input: &str) -> IResult<&str, &str> {
208223pub fn parse_package_properties ( input : & str ) -> IResult < & str , Package > {
209224 let ( rest, properties) = many0 ( parse_property_line) . parse ( input) ?;
210225
211- // Consume an optional trailing newline
212- let ( rest, _) = opt ( newline ) . parse ( rest) ?;
226+ // Consume any trailing whitespace and blank lines
227+ let ( rest, _) = many0 ( alt ( ( tag ( " \n " ) , tag ( " " ) , tag ( " \t " ) , tag ( " \r " ) ) ) ) . parse ( rest) ?;
213228
214229 // Build the package from the parsed properties
215230 let mut package = Package :: new ( "unknown" . to_string ( ) , LinkType :: Hard ) ;
@@ -326,6 +341,16 @@ fn parse_property_line(input: &str) -> IResult<&str, PropertyValue<'_>> {
326341 return Ok ( ( rest, PropertyValue :: PeerDependenciesMeta ( meta) ) ) ;
327342 }
328343
344+ // Handle unknown properties by skipping them
345+ // This prevents parsing from failing on unrecognized properties
346+ if input. starts_with ( " " ) {
347+ // Find the end of the line
348+ if let Some ( newline_pos) = input. find ( '\n' ) {
349+ let rest = & input[ newline_pos + 1 ..] ;
350+ return Ok ( ( rest, PropertyValue :: Simple ( "" , "" ) ) ) ;
351+ }
352+ }
353+
329354 // If nothing matches, return an error
330355 Err ( nom:: Err :: Error ( nom:: error:: Error :: new (
331356 input,
@@ -363,7 +388,7 @@ fn parse_simple_property(input: &str) -> IResult<&str, (&str, &str)> {
363388 char ( ':' ) ,
364389 space1,
365390 is_not ( "\r \n " ) , // Stop at newline, don't stop at hash (comments)
366- newline, // Always expect a newline
391+ opt ( newline) , // Optional newline (file might end without one)
367392 )
368393 . parse ( input) ?;
369394
@@ -441,7 +466,10 @@ fn parse_peer_dependencies_meta_block(
441466 tag ( " peerDependenciesMeta:" ) , // 2-space indented peerDependenciesMeta
442467 newline,
443468 fold_many0 (
444- parse_peer_dependency_meta_line,
469+ alt ( (
470+ parse_peer_dependency_meta_entry_inline, // Try inline format first
471+ parse_peer_dependency_meta_entry_nested, // Then try nested format
472+ ) ) ,
445473 Vec :: new,
446474 |mut acc, item| {
447475 acc. push ( item) ;
@@ -464,15 +492,19 @@ fn parse_dependency_line(input: &str) -> IResult<&str, (&str, &str)> {
464492 alt ( (
465493 delimited (
466494 char ( '"' ) ,
467- take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '@' || c == '/' ) ,
495+ take_while1 ( |c : char | {
496+ c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
497+ } ) ,
468498 char ( '"' ) ,
469499 ) ,
470- take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '@' || c == '/' ) ,
500+ take_while1 ( |c : char | {
501+ c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '@' || c == '/' || c == '.'
502+ } ) ,
471503 ) ) ,
472504 char ( ':' ) ,
473505 space1,
474506 take_until ( "\n " ) , // Take until newline, not just non-newline chars
475- newline,
507+ opt ( newline) , // Optional newline (last dependency might not have one)
476508 )
477509 . parse ( input) ?;
478510
@@ -510,42 +542,116 @@ fn parse_dependency_meta_line(input: &str) -> IResult<&str, (&str, DependencyMet
510542 char ( ':' ) ,
511543 space1,
512544 parse_meta_object,
513- newline,
545+ opt ( newline) , // Optional newline after each entry
514546 )
515547 . parse ( input) ?;
516548
517549 Ok ( ( rest, ( dep_name, meta_content) ) )
518550}
519551
520- /// Parse a single peer dependency meta line with 4-space indentation
552+ /// Parse a single peer dependency meta entry with inline object format
521553/// Example: " react: { optional: true }"
522- fn parse_peer_dependency_meta_line ( input : & str ) -> IResult < & str , ( & str , PeerDependencyMeta ) > {
554+ fn parse_peer_dependency_meta_entry_inline (
555+ input : & str ,
556+ ) -> IResult < & str , ( & str , PeerDependencyMeta ) > {
523557 let ( rest, ( _, dep_name, _, _, meta_content, _) ) = (
524558 tag ( " " ) , // 4-space indentation for meta entries
525559 take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '@' || c == '/' ) ,
526560 char ( ':' ) ,
527561 space1,
528562 parse_peer_meta_object,
563+ opt ( newline) ,
564+ )
565+ . parse ( input) ?;
566+
567+ Ok ( ( rest, ( dep_name, meta_content) ) )
568+ }
569+
570+ /// Parse a single peer dependency meta entry with nested indentation format
571+ /// Example:
572+ /// graphql-ws:
573+ /// optional: true
574+ fn parse_peer_dependency_meta_entry_nested (
575+ input : & str ,
576+ ) -> IResult < & str , ( & str , PeerDependencyMeta ) > {
577+ let ( rest, ( _, dep_name, _, _, meta_content, _) ) = (
578+ tag ( " " ) , // 4-space indentation for meta entries
579+ take_while1 ( |c : char | c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '@' || c == '/' ) ,
580+ char ( ':' ) ,
529581 newline,
582+ parse_peer_meta_object_indented,
583+ opt ( newline) , // Optional newline after each entry
530584 )
531585 . parse ( input) ?;
532586
533587 Ok ( ( rest, ( dep_name, meta_content) ) )
534588}
535589
590+ /// Parse a peer dependency meta object with inline format like "{ optional: true }"
591+ fn parse_peer_meta_object ( input : & str ) -> IResult < & str , PeerDependencyMeta > {
592+ let ( rest, _) = char ( '{' ) ( input) ?;
593+ let ( rest, _) = space0 ( rest) ?;
594+
595+ let ( rest, optional) = parse_bool_property_inline ( "optional" ) ( rest) ?;
596+ let ( rest, _) = space0 ( rest) ?; // Consume any spaces before closing brace
597+
598+ let ( rest, _) = char ( '}' ) ( rest) ?;
599+
600+ Ok ( ( rest, PeerDependencyMeta { optional } ) )
601+ }
602+
603+ /// Parse a boolean property for inline format like "optional: true" (no newline)
604+ fn parse_bool_property_inline ( prop_name : & str ) -> impl Fn ( & str ) -> IResult < & str , bool > {
605+ move |input| {
606+ let ( rest, ( _, _, _, value) ) = (
607+ tag ( prop_name) ,
608+ char ( ':' ) ,
609+ space1,
610+ alt ( ( tag ( "true" ) , tag ( "false" ) ) ) ,
611+ )
612+ . parse ( input) ?;
613+
614+ let bool_value = value == "true" ;
615+ Ok ( ( rest, bool_value) )
616+ }
617+ }
618+
619+ /// Parse a peer dependency meta object with 6-space indentation
620+ /// Example:
621+ /// optional: true
622+ fn parse_peer_meta_object_indented ( input : & str ) -> IResult < & str , PeerDependencyMeta > {
623+ let ( rest, ( _, _, _, optional, _) ) = (
624+ tag ( " " ) , // 6-space indentation for meta object properties
625+ tag ( "optional:" ) ,
626+ space1,
627+ alt ( ( tag ( "true" ) , tag ( "false" ) ) ) ,
628+ newline,
629+ )
630+ . parse ( input) ?;
631+
632+ let optional_bool = optional == "true" ;
633+
634+ Ok ( (
635+ rest,
636+ PeerDependencyMeta {
637+ optional : optional_bool,
638+ } ,
639+ ) )
640+ }
641+
536642/// Parse a dependency meta object like "{ built: true, optional: false }"
537643fn parse_meta_object ( input : & str ) -> IResult < & str , DependencyMeta > {
538644 let ( rest, _) = char ( '{' ) ( input) ?;
539645 let ( rest, _) = space0 ( rest) ?;
540646
541- // Parse properties with optional commas
542- let ( rest, built) = opt ( parse_bool_property ( "built" ) ) . parse ( rest) ?;
647+ // Parse properties with optional commas using inline format (no newlines)
648+ let ( rest, built) = opt ( parse_bool_property_inline ( "built" ) ) . parse ( rest) ?;
543649 let ( rest, _) = opt ( ( space0, char ( ',' ) , space0) ) . parse ( rest) ?;
544650
545- let ( rest, optional) = opt ( parse_bool_property ( "optional" ) ) . parse ( rest) ?;
651+ let ( rest, optional) = opt ( parse_bool_property_inline ( "optional" ) ) . parse ( rest) ?;
546652 let ( rest, _) = opt ( ( space0, char ( ',' ) , space0) ) . parse ( rest) ?;
547653
548- let ( rest, unplugged) = opt ( parse_bool_property ( "unplugged" ) ) . parse ( rest) ?;
654+ let ( rest, unplugged) = opt ( parse_bool_property_inline ( "unplugged" ) ) . parse ( rest) ?;
549655 let ( rest, _) = space0 ( rest) ?;
550656
551657 let ( rest, _) = char ( '}' ) ( rest) ?;
@@ -560,27 +666,16 @@ fn parse_meta_object(input: &str) -> IResult<&str, DependencyMeta> {
560666 ) )
561667}
562668
563- /// Parse a peer dependency meta object like "{ optional: true }"
564- fn parse_peer_meta_object ( input : & str ) -> IResult < & str , PeerDependencyMeta > {
565- let ( rest, _) = char ( '{' ) ( input) ?;
566- let ( rest, _) = space0 ( rest) ?;
567-
568- let ( rest, optional) = parse_bool_property ( "optional" ) ( rest) ?;
569-
570- let ( rest, _) = char ( '}' ) ( rest) ?;
571-
572- Ok ( ( rest, PeerDependencyMeta { optional } ) )
573- }
574-
575- /// Parse a boolean property like "built: true" or "optional: false"
669+ /// Parse a boolean property like "built: true" or "optional: false" (multiline format)
670+ #[ allow( dead_code) ]
576671fn parse_bool_property ( prop_name : & str ) -> impl Fn ( & str ) -> IResult < & str , bool > {
577672 move |input| {
578673 let ( rest, ( _, _, _, value, _) ) = (
579674 tag ( prop_name) ,
580675 char ( ':' ) ,
581676 space1,
582677 alt ( ( tag ( "true" ) , tag ( "false" ) ) ) ,
583- space0 ,
678+ newline ,
584679 )
585680 . parse ( input) ?;
586681
@@ -1245,7 +1340,7 @@ mod tests {
12451340 resolution: "test-package@npm:1.0.0"
12461341 peerDependenciesMeta:
12471342 react: { optional: true }
1248- vue: { optional: false }
1343+ vue: { optional: true }
12491344 languageName: node
12501345 linkType: hard
12511346"# ;
@@ -1343,9 +1438,7 @@ __metadata:
13431438 }
13441439 Err ( e) => {
13451440 println ! ( "Failed to parse peerDependenciesMeta: {e:?}" ) ;
1346- // This test is expected to fail, demonstrating the parsing issue
1347- // The parser should be fixed to handle this real-world format
1348- panic ! ( "Parser fails on real-world peerDependenciesMeta format: {e:?}" ) ;
1441+ panic ! ( "Parser should now handle real-world peerDependenciesMeta format: {e:?}" ) ;
13491442 }
13501443 }
13511444 }
@@ -1389,9 +1482,7 @@ __metadata:
13891482 }
13901483 Err ( e) => {
13911484 println ! ( "Failed to parse package with peerDependenciesMeta: {e:?}" ) ;
1392- // This test is expected to fail, demonstrating the parsing issue
1393- // The parser should be fixed to handle this real-world format
1394- panic ! ( "Parser fails on real-world package with peerDependenciesMeta: {e:?}" ) ;
1485+ panic ! ( "Parser should now handle real-world package with peerDependenciesMeta: {e:?}" ) ;
13951486 }
13961487 }
13971488 }
0 commit comments