@@ -437,9 +437,12 @@ mod tests {
437437
438438 #[ test]
439439 fn remap_single_level ( ) {
440- // outer: output.js → intermediate.js
440+ // outer: output.js → intermediate.js + other.js (second source has no upstream)
441+ // AAAA maps gen(0,0) → intermediate.js(0,0)
442+ // KCAA maps gen(0,5) → other.js(0,0) (source delta +1)
443+ // ;ADCA maps gen(1,0) → intermediate.js(1,0) (source delta -1, line delta +1)
441444 let outer = SourceMap :: from_json (
442- r#"{"version":3,"sources":["intermediate.js"],"names":[],"mappings":"AAAA;AACA "}"# ,
445+ r#"{"version":3,"sources":["intermediate.js","other.js" ],"names":[],"mappings":"AAAA,KCAA;ADCA "}"# ,
443446 )
444447 . unwrap ( ) ;
445448
@@ -457,7 +460,9 @@ mod tests {
457460 }
458461 } ) ;
459462
460- assert_eq ! ( result. sources, vec![ "original.js" ] ) ;
463+ assert ! ( result. sources. contains( & "original.js" . to_string( ) ) ) ;
464+ // other.js passes through since loader returns None
465+ assert ! ( result. sources. contains( & "other.js" . to_string( ) ) ) ;
461466
462467 // Line 0 col 0 in outer → line 0 col 0 in intermediate → line 1 col 0 in original
463468 let loc = result. original_position_for ( 0 , 0 ) . unwrap ( ) ;
@@ -569,4 +574,225 @@ mod tests {
569574 }
570575
571576 // ── Clone needed for SourceMap in tests ──────────────────────
577+
578+ #[ test]
579+ fn concat_updates_source_content_on_duplicate ( ) {
580+ // First map has no sourcesContent, second has it for same source
581+ let a = SourceMap :: from_json (
582+ r#"{"version":3,"sources":["shared.js"],"names":[],"mappings":"AAAA"}"# ,
583+ )
584+ . unwrap ( ) ;
585+ let b = SourceMap :: from_json (
586+ r#"{"version":3,"sources":["shared.js"],"sourcesContent":["var x = 1;"],"names":[],"mappings":"AAAA"}"# ,
587+ )
588+ . unwrap ( ) ;
589+
590+ let mut builder = ConcatBuilder :: new ( None ) ;
591+ builder. add_map ( & a, 0 ) ;
592+ builder. add_map ( & b, 1 ) ;
593+
594+ let result = builder. build ( ) ;
595+ assert_eq ! ( result. sources. len( ) , 1 ) ;
596+ assert_eq ! (
597+ result. sources_content,
598+ vec![ Some ( "var x = 1;" . to_string( ) ) ]
599+ ) ;
600+ }
601+
602+ #[ test]
603+ fn concat_deduplicates_names ( ) {
604+ let a = SourceMap :: from_json (
605+ r#"{"version":3,"sources":["a.js"],"names":["sharedName"],"mappings":"AAAAA"}"# ,
606+ )
607+ . unwrap ( ) ;
608+ let b = SourceMap :: from_json (
609+ r#"{"version":3,"sources":["b.js"],"names":["sharedName"],"mappings":"AAAAA"}"# ,
610+ )
611+ . unwrap ( ) ;
612+
613+ let mut builder = ConcatBuilder :: new ( None ) ;
614+ builder. add_map ( & a, 0 ) ;
615+ builder. add_map ( & b, 1 ) ;
616+
617+ let result = builder. build ( ) ;
618+ // Names should be deduplicated
619+ assert_eq ! ( result. names. len( ) , 1 ) ;
620+ assert_eq ! ( result. names[ 0 ] , "sharedName" ) ;
621+ }
622+
623+ #[ test]
624+ fn concat_with_ignore_list ( ) {
625+ let a = SourceMap :: from_json (
626+ r#"{"version":3,"sources":["vendor.js"],"names":[],"mappings":"AAAA","ignoreList":[0]}"# ,
627+ )
628+ . unwrap ( ) ;
629+
630+ let mut builder = ConcatBuilder :: new ( None ) ;
631+ builder. add_map ( & a, 0 ) ;
632+
633+ let result = builder. build ( ) ;
634+ assert_eq ! ( result. ignore_list, vec![ 0 ] ) ;
635+ }
636+
637+ #[ test]
638+ fn concat_with_generated_only_mappings ( ) {
639+ // Map with a generated-only segment (1-field segment, no source info)
640+ let a = SourceMap :: from_json (
641+ r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"A,AAAA"}"# ,
642+ )
643+ . unwrap ( ) ;
644+
645+ let mut builder = ConcatBuilder :: new ( None ) ;
646+ builder. add_map ( & a, 0 ) ;
647+
648+ let result = builder. build ( ) ;
649+ // Should have both mappings, including the generated-only one
650+ assert ! ( result. mapping_count( ) >= 1 ) ;
651+ }
652+
653+ #[ test]
654+ fn remap_generated_only_passthrough ( ) {
655+ // Outer map with a generated-only segment and two sources (second has no upstream)
656+ // A = generated-only segment at col 0
657+ // ,AAAA = gen(0,4)→a.js(0,0)
658+ // ,KCAA = gen(0,9)→other.js(0,0) (source delta +1)
659+ let outer = SourceMap :: from_json (
660+ r#"{"version":3,"sources":["a.js","other.js"],"names":[],"mappings":"A,AAAA,KCAA"}"# ,
661+ )
662+ . unwrap ( ) ;
663+
664+ let inner = SourceMap :: from_json (
665+ r#"{"version":3,"sources":["original.js"],"names":[],"mappings":"AAAA"}"# ,
666+ )
667+ . unwrap ( ) ;
668+
669+ let result = remap ( & outer, |source| {
670+ if source == "a.js" {
671+ Some ( inner. clone ( ) )
672+ } else {
673+ None
674+ }
675+ } ) ;
676+
677+ // Result should have mappings for the generated-only, remapped, and passthrough
678+ assert ! ( result. mapping_count( ) >= 2 ) ;
679+ assert ! ( result. sources. contains( & "original.js" . to_string( ) ) ) ;
680+ assert ! ( result. sources. contains( & "other.js" . to_string( ) ) ) ;
681+ }
682+
683+ #[ test]
684+ fn remap_no_upstream_mapping_with_name ( ) {
685+ // Outer has named mapping but upstream lookup finds no match at that position
686+ let outer = SourceMap :: from_json (
687+ r#"{"version":3,"sources":["compiled.js"],"names":["myFunc"],"mappings":"AAAAA"}"# ,
688+ )
689+ . unwrap ( ) ;
690+
691+ // Inner map maps different position (line 5, not line 0)
692+ let inner = SourceMap :: from_json (
693+ r#"{"version":3,"sources":["original.ts"],"names":[],"mappings":";;;;AAAA"}"# ,
694+ )
695+ . unwrap ( ) ;
696+
697+ let result = remap ( & outer, |_| Some ( inner. clone ( ) ) ) ;
698+
699+ // The outer mapping at (0,0) maps to (0,0) in compiled.js
700+ // Inner doesn't have a mapping at (0,0), so it falls through
701+ // The name from outer should be preserved
702+ let loc = result. original_position_for ( 0 , 0 ) . unwrap ( ) ;
703+ assert ! ( loc. name. is_some( ) ) ;
704+ assert_eq ! ( result. name( loc. name. unwrap( ) ) , "myFunc" ) ;
705+ }
706+
707+ #[ test]
708+ fn remap_no_upstream_with_sources_content_and_name ( ) {
709+ let outer = SourceMap :: from_json (
710+ r#"{"version":3,"sources":["a.js"],"sourcesContent":["var a;"],"names":["fn1"],"mappings":"AAAAA"}"# ,
711+ )
712+ . unwrap ( ) ;
713+
714+ // No upstream — everything passes through
715+ let result = remap ( & outer, |_| None ) ;
716+
717+ assert_eq ! ( result. sources, vec![ "a.js" ] ) ;
718+ assert_eq ! (
719+ result. sources_content,
720+ vec![ Some ( "var a;" . to_string( ) ) ]
721+ ) ;
722+ let loc = result. original_position_for ( 0 , 0 ) . unwrap ( ) ;
723+ assert ! ( loc. name. is_some( ) ) ;
724+ assert_eq ! ( result. name( loc. name. unwrap( ) ) , "fn1" ) ;
725+ }
726+
727+ #[ test]
728+ fn remap_no_upstream_no_name ( ) {
729+ let outer = SourceMap :: from_json (
730+ r#"{"version":3,"sources":["a.js"],"sourcesContent":["var a;"],"names":[],"mappings":"AAAA"}"# ,
731+ )
732+ . unwrap ( ) ;
733+
734+ let result = remap ( & outer, |_| None ) ;
735+ let loc = result. original_position_for ( 0 , 0 ) . unwrap ( ) ;
736+ assert ! ( loc. name. is_none( ) ) ;
737+ }
738+
739+ #[ test]
740+ fn remap_no_upstream_mapping_no_name ( ) {
741+ // Outer has a mapping with NO name pointing to compiled.js
742+ // AAAA = gen(0,0) → compiled.js(0,0), no name (4-field segment)
743+ let outer = SourceMap :: from_json (
744+ r#"{"version":3,"sources":["compiled.js"],"names":[],"mappings":"AAAA"}"# ,
745+ )
746+ . unwrap ( ) ;
747+
748+ // Inner map only has mappings at line 5, not at line 0
749+ // So original_position_for(0, 0) returns None → takes the None branch
750+ // Since the outer mapping has no name, this hits the else at lines 268-272
751+ let inner = SourceMap :: from_json (
752+ r#"{"version":3,"sources":["original.ts"],"names":[],"mappings":";;;;AAAA"}"# ,
753+ )
754+ . unwrap ( ) ;
755+
756+ let result = remap ( & outer, |_| Some ( inner. clone ( ) ) ) ;
757+
758+ // Falls through to the None branch (no upstream match at position)
759+ // Since outer has no name, the mapping is added without a name
760+ let loc = result. original_position_for ( 0 , 0 ) . unwrap ( ) ;
761+ assert_eq ! ( result. source( loc. source) , "compiled.js" ) ;
762+ assert_eq ! ( loc. line, 0 ) ;
763+ assert_eq ! ( loc. column, 0 ) ;
764+ assert ! ( loc. name. is_none( ) ) ;
765+ }
766+
767+ #[ test]
768+ fn remap_upstream_found_no_name ( ) {
769+ // Outer has a named mapping, but upstream has NO name
770+ // The upstream mapping is found but has no name_index
771+ // Since upstream has no name, the name resolution falls to the outer name
772+ // This is already covered by remap_preserves_names
773+ //
774+ // What we need instead: outer has NO name AND upstream has NO name
775+ // → name_idx is None → hits the add_mapping branch (line 246-252)
776+ let outer = SourceMap :: from_json (
777+ r#"{"version":3,"sources":["intermediate.js"],"names":[],"mappings":"AAAA"}"# ,
778+ )
779+ . unwrap ( ) ;
780+
781+ // Inner maps intermediate.js(0,0) → original.js(0,0) with NO name
782+ let inner = SourceMap :: from_json (
783+ r#"{"version":3,"sources":["original.js"],"names":[],"mappings":"AAAA"}"# ,
784+ )
785+ . unwrap ( ) ;
786+
787+ let result = remap ( & outer, |_| Some ( inner. clone ( ) ) ) ;
788+
789+ assert_eq ! ( result. sources, vec![ "original.js" ] ) ;
790+ let loc = result. original_position_for ( 0 , 0 ) . unwrap ( ) ;
791+ assert_eq ! ( result. source( loc. source) , "original.js" ) ;
792+ assert_eq ! ( loc. line, 0 ) ;
793+ assert_eq ! ( loc. column, 0 ) ;
794+ // Neither outer nor upstream has a name, so result has no name
795+ assert ! ( loc. name. is_none( ) ) ;
796+ assert ! ( result. names. is_empty( ) ) ;
797+ }
572798}
0 commit comments