1use std::path::{Path, PathBuf};
4
5use crate::{GeneratedLocation, Mapping, OriginalLocation, ParseError, SourceMap};
6
7pub fn find_common_prefix<'a>(paths: impl Iterator<Item = &'a str>) -> Option<String> {
15 let abs_paths: Vec<&str> = paths.filter(|p| p.starts_with('/')).collect();
16 if abs_paths.len() < 2 {
17 return None;
18 }
19
20 let first_all: Vec<&str> = abs_paths[0].split('/').collect();
22 let first_dir = &first_all[..first_all.len().saturating_sub(1)];
23 let mut common_len = first_dir.len();
24
25 for path in &abs_paths[1..] {
26 let components: Vec<&str> = path.split('/').collect();
27 let dir = &components[..components.len().saturating_sub(1)];
28 let mut match_len = 0;
29 for (a, b) in first_dir.iter().zip(dir.iter()) {
30 if a != b {
31 break;
32 }
33 match_len += 1;
34 }
35 common_len = common_len.min(match_len);
36 }
37
38 if common_len < 2 {
40 return None;
41 }
42
43 let prefix = first_dir[..common_len].join("/");
44 if prefix.is_empty() || prefix == "/" {
45 return None;
46 }
47
48 Some(format!("{prefix}/"))
49}
50
51pub fn make_relative_path(base: &str, target: &str) -> String {
63 if base == target {
64 return ".".to_string();
65 }
66
67 let base_parts: Vec<&str> = base.split('/').collect();
68 let target_parts: Vec<&str> = target.split('/').collect();
69
70 let base_dir = &base_parts[..base_parts.len().saturating_sub(1)];
72 let target_dir = &target_parts[..target_parts.len().saturating_sub(1)];
73 let target_file = target_parts.last().unwrap_or(&"");
74
75 let mut common = 0;
77 for (a, b) in base_dir.iter().zip(target_dir.iter()) {
78 if a != b {
79 break;
80 }
81 common += 1;
82 }
83
84 let ups = base_dir.len() - common;
85 let mut result = String::new();
86
87 for _ in 0..ups {
88 result.push_str("../");
89 }
90
91 for part in &target_dir[common..] {
92 result.push_str(part);
93 result.push('/');
94 }
95
96 result.push_str(target_file);
97
98 if result.is_empty() {
99 ".".to_string()
100 } else {
101 result
102 }
103}
104
105pub fn is_sourcemap(json: &str) -> bool {
114 let Ok(val) = serde_json::from_str::<serde_json::Value>(json) else {
115 return false;
116 };
117
118 let Some(obj) = val.as_object() else {
119 return false;
120 };
121
122 if obj.contains_key("sections") {
124 return true;
125 }
126
127 let has_version = obj.contains_key("version");
129 let has_mappings = obj.contains_key("mappings");
130 let has_source_field = obj.contains_key("sources")
131 || obj.contains_key("names")
132 || obj.contains_key("sourceRoot")
133 || obj.contains_key("sourcesContent");
134
135 has_version && has_mappings && has_source_field
136}
137
138pub fn resolve_source_map_url(minified_url: &str, source_map_ref: &str) -> Option<String> {
156 if source_map_ref.starts_with("data:") {
158 return None;
159 }
160
161 if source_map_ref.starts_with("http://")
163 || source_map_ref.starts_with("https://")
164 || source_map_ref.starts_with('/')
165 {
166 return Some(source_map_ref.to_string());
167 }
168
169 if let Some(last_slash) = minified_url.rfind('/') {
171 let base = &minified_url[..=last_slash];
172 let combined = format!("{base}{source_map_ref}");
173 Some(normalize_path_components(&combined))
174 } else {
175 Some(source_map_ref.to_string())
177 }
178}
179
180pub fn resolve_source_map_path(minified_path: &Path, source_map_ref: &str) -> Option<PathBuf> {
185 let parent = minified_path.parent()?;
186 let joined = parent.join(source_map_ref);
187
188 Some(normalize_pathbuf(&joined))
190}
191
192fn normalize_path_components(url: &str) -> String {
194 let (prefix, path) = if let Some(idx) = url.find("://") {
196 let after_proto = &url[idx + 3..];
197 if let Some(slash_idx) = after_proto.find('/') {
198 let split_at = idx + 3 + slash_idx;
199 (&url[..split_at], &url[split_at..])
200 } else {
201 return url.to_string();
202 }
203 } else {
204 ("", url)
205 };
206
207 let mut segments: Vec<&str> = Vec::new();
208 for segment in path.split('/') {
209 match segment {
210 ".." => {
211 if segments.len() > 1 {
213 segments.pop();
214 }
215 }
216 "." | "" if !segments.is_empty() => {
217 }
219 _ => {
220 segments.push(segment);
221 }
222 }
223 }
224
225 let normalized = segments.join("/");
226 format!("{prefix}{normalized}")
227}
228
229fn normalize_pathbuf(path: &Path) -> PathBuf {
231 let mut components = Vec::new();
232 for component in path.components() {
233 match component {
234 std::path::Component::ParentDir => {
235 components.pop();
236 }
237 std::path::Component::CurDir => {}
238 _ => {
239 components.push(component);
240 }
241 }
242 }
243 components.iter().collect()
244}
245
246const BASE64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
249
250pub fn to_data_url(json: &str) -> String {
262 let encoded = base64_encode(json.as_bytes());
263 format!("data:application/json;base64,{encoded}")
264}
265
266fn base64_encode(input: &[u8]) -> String {
268 let mut result = String::with_capacity(input.len().div_ceil(3) * 4);
269 let chunks = input.chunks(3);
270
271 for chunk in chunks {
272 let b0 = chunk[0] as u32;
273 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
274 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
275
276 let triple = (b0 << 16) | (b1 << 8) | b2;
277
278 result.push(BASE64_CHARS[((triple >> 18) & 0x3F) as usize] as char);
279 result.push(BASE64_CHARS[((triple >> 12) & 0x3F) as usize] as char);
280
281 if chunk.len() > 1 {
282 result.push(BASE64_CHARS[((triple >> 6) & 0x3F) as usize] as char);
283 } else {
284 result.push('=');
285 }
286
287 if chunk.len() > 2 {
288 result.push(BASE64_CHARS[(triple & 0x3F) as usize] as char);
289 } else {
290 result.push('=');
291 }
292 }
293
294 result
295}
296
297pub struct RewriteOptions<'a> {
301 pub with_names: bool,
303 pub with_source_contents: bool,
305 pub strip_prefixes: &'a [&'a str],
308}
309
310impl Default for RewriteOptions<'_> {
311 fn default() -> Self {
312 Self {
313 with_names: true,
314 with_source_contents: true,
315 strip_prefixes: &[],
316 }
317 }
318}
319
320pub fn rewrite_sources(sm: &SourceMap, options: &RewriteOptions<'_>) -> SourceMap {
330 let auto_prefix = if options.strip_prefixes.contains(&"~") {
332 find_common_prefix(sm.sources.iter().map(|s| s.as_str()))
333 } else {
334 None
335 };
336
337 let explicit_prefixes: Vec<&str> = options
338 .strip_prefixes
339 .iter()
340 .filter(|&&p| p != "~")
341 .copied()
342 .collect();
343
344 let sources: Vec<String> = sm
346 .sources
347 .iter()
348 .map(|s| {
349 let mut result = s.as_str();
350
351 if let Some(ref prefix) = auto_prefix
353 && let Some(stripped) = result.strip_prefix(prefix.as_str())
354 {
355 result = stripped;
356 }
357
358 for prefix in &explicit_prefixes {
360 if let Some(stripped) = result.strip_prefix(prefix) {
361 result = stripped;
362 break;
363 }
364 }
365
366 result.to_string()
367 })
368 .collect();
369
370 let sources_content = if options.with_source_contents {
372 sm.sources_content.clone()
373 } else {
374 vec![None; sm.sources_content.len()]
375 };
376
377 let (names, mappings) = if options.with_names {
379 (sm.names.clone(), sm.all_mappings().to_vec())
380 } else {
381 let cleared_mappings: Vec<Mapping> = sm
382 .all_mappings()
383 .iter()
384 .map(|m| Mapping {
385 name: u32::MAX,
386 ..*m
387 })
388 .collect();
389 (Vec::new(), cleared_mappings)
390 };
391
392 let mut result = SourceMap::from_parts(
393 sm.file.clone(),
394 sm.source_root.clone(),
395 sources,
396 sources_content,
397 names,
398 mappings,
399 sm.ignore_list.clone(),
400 sm.debug_id.clone(),
401 sm.scopes.clone(),
402 );
403
404 result.extensions = sm.extensions.clone();
406
407 result
408}
409
410pub enum DecodedMap {
418 Regular(SourceMap),
420}
421
422impl DecodedMap {
423 pub fn from_json(json: &str) -> Result<Self, ParseError> {
425 let sm = SourceMap::from_json(json)?;
426 Ok(Self::Regular(sm))
427 }
428
429 pub fn original_position_for(&self, line: u32, column: u32) -> Option<OriginalLocation> {
431 match self {
432 DecodedMap::Regular(sm) => sm.original_position_for(line, column),
433 }
434 }
435
436 pub fn generated_position_for(
438 &self,
439 source: &str,
440 line: u32,
441 column: u32,
442 ) -> Option<GeneratedLocation> {
443 match self {
444 DecodedMap::Regular(sm) => sm.generated_position_for(source, line, column),
445 }
446 }
447
448 pub fn sources(&self) -> &[String] {
450 match self {
451 DecodedMap::Regular(sm) => &sm.sources,
452 }
453 }
454
455 pub fn names(&self) -> &[String] {
457 match self {
458 DecodedMap::Regular(sm) => &sm.names,
459 }
460 }
461
462 pub fn source(&self, idx: u32) -> &str {
468 match self {
469 DecodedMap::Regular(sm) => sm.source(idx),
470 }
471 }
472
473 pub fn name(&self, idx: u32) -> &str {
479 match self {
480 DecodedMap::Regular(sm) => sm.name(idx),
481 }
482 }
483
484 pub fn debug_id(&self) -> Option<&str> {
486 match self {
487 DecodedMap::Regular(sm) => sm.debug_id.as_deref(),
488 }
489 }
490
491 pub fn set_debug_id(&mut self, id: impl Into<String>) {
493 match self {
494 DecodedMap::Regular(sm) => sm.debug_id = Some(id.into()),
495 }
496 }
497
498 pub fn to_json(&self) -> String {
500 match self {
501 DecodedMap::Regular(sm) => sm.to_json(),
502 }
503 }
504
505 pub fn into_source_map(self) -> Option<SourceMap> {
507 match self {
508 DecodedMap::Regular(sm) => Some(sm),
509 }
510 }
511}
512
513#[cfg(test)]
516mod tests {
517 use super::*;
518
519 #[test]
522 fn common_prefix_basic() {
523 let paths = vec!["/a/b/c/file1.js", "/a/b/c/file2.js", "/a/b/c/file3.js"];
524 let result = find_common_prefix(paths.into_iter());
525 assert_eq!(result, Some("/a/b/c/".to_string()));
526 }
527
528 #[test]
529 fn common_prefix_different_depths() {
530 let paths = vec!["/a/b/c/file1.js", "/a/b/d/file2.js"];
531 let result = find_common_prefix(paths.into_iter());
532 assert_eq!(result, Some("/a/b/".to_string()));
533 }
534
535 #[test]
536 fn common_prefix_only_root() {
537 let paths = vec!["/a/file1.js", "/b/file2.js"];
538 let result = find_common_prefix(paths.into_iter());
539 assert_eq!(result, None);
541 }
542
543 #[test]
544 fn common_prefix_single_path() {
545 let paths = vec!["/a/b/c.js"];
546 let result = find_common_prefix(paths.into_iter());
547 assert_eq!(result, None);
548 }
549
550 #[test]
551 fn common_prefix_no_absolute_paths() {
552 let paths = vec!["a/b/c.js", "a/b/d.js"];
553 let result = find_common_prefix(paths.into_iter());
554 assert_eq!(result, None);
555 }
556
557 #[test]
558 fn common_prefix_mixed_absolute_relative() {
559 let paths = vec!["/a/b/c.js", "a/b/d.js", "/a/b/e.js"];
560 let result = find_common_prefix(paths.into_iter());
561 assert_eq!(result, Some("/a/b/".to_string()));
563 }
564
565 #[test]
566 fn common_prefix_empty_iterator() {
567 let paths: Vec<&str> = vec![];
568 let result = find_common_prefix(paths.into_iter());
569 assert_eq!(result, None);
570 }
571
572 #[test]
573 fn common_prefix_identical_paths() {
574 let paths = vec!["/a/b/c.js", "/a/b/c.js"];
575 let result = find_common_prefix(paths.into_iter());
576 assert_eq!(result, Some("/a/b/".to_string()));
577 }
578
579 #[test]
582 fn relative_path_sibling_dirs() {
583 assert_eq!(make_relative_path("/a/b/c.js", "/a/d/e.js"), "../d/e.js");
584 }
585
586 #[test]
587 fn relative_path_same_dir() {
588 assert_eq!(make_relative_path("/a/b/c.js", "/a/b/d.js"), "d.js");
589 }
590
591 #[test]
592 fn relative_path_same_file() {
593 assert_eq!(make_relative_path("/a/b/c.js", "/a/b/c.js"), ".");
594 }
595
596 #[test]
597 fn relative_path_deeper_target() {
598 assert_eq!(make_relative_path("/a/b/c.js", "/a/b/d/e/f.js"), "d/e/f.js");
599 }
600
601 #[test]
602 fn relative_path_multiple_ups() {
603 assert_eq!(make_relative_path("/a/b/c/d.js", "/a/e.js"), "../../e.js");
604 }
605
606 #[test]
607 fn relative_path_completely_different() {
608 assert_eq!(
609 make_relative_path("/a/b/c.js", "/x/y/z.js"),
610 "../../x/y/z.js"
611 );
612 }
613
614 #[test]
617 fn is_sourcemap_regular() {
618 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
619 assert!(is_sourcemap(json));
620 }
621
622 #[test]
623 fn is_sourcemap_indexed() {
624 let json = r#"{"version":3,"sections":[{"offset":{"line":0,"column":0},"map":{"version":3,"sources":[],"names":[],"mappings":""}}]}"#;
625 assert!(is_sourcemap(json));
626 }
627
628 #[test]
629 fn is_sourcemap_with_source_root() {
630 let json = r#"{"version":3,"sourceRoot":"/src/","mappings":"AAAA"}"#;
631 assert!(is_sourcemap(json));
632 }
633
634 #[test]
635 fn is_sourcemap_with_sources_content() {
636 let json = r#"{"version":3,"sourcesContent":["var x;"],"mappings":"AAAA"}"#;
637 assert!(is_sourcemap(json));
638 }
639
640 #[test]
641 fn is_sourcemap_invalid_json() {
642 assert!(!is_sourcemap("not json"));
643 }
644
645 #[test]
646 fn is_sourcemap_missing_version() {
647 let json = r#"{"sources":["a.js"],"mappings":"AAAA"}"#;
648 assert!(!is_sourcemap(json));
649 }
650
651 #[test]
652 fn is_sourcemap_missing_mappings() {
653 let json = r#"{"version":3,"sources":["a.js"]}"#;
654 assert!(!is_sourcemap(json));
655 }
656
657 #[test]
658 fn is_sourcemap_empty_object() {
659 assert!(!is_sourcemap("{}"));
660 }
661
662 #[test]
663 fn is_sourcemap_array() {
664 assert!(!is_sourcemap("[]"));
665 }
666
667 #[test]
670 fn resolve_url_relative() {
671 let result = resolve_source_map_url("https://example.com/js/app.js", "app.js.map");
672 assert_eq!(
673 result,
674 Some("https://example.com/js/app.js.map".to_string())
675 );
676 }
677
678 #[test]
679 fn resolve_url_parent_traversal() {
680 let result = resolve_source_map_url("https://example.com/js/app.js", "../maps/app.js.map");
681 assert_eq!(
682 result,
683 Some("https://example.com/maps/app.js.map".to_string())
684 );
685 }
686
687 #[test]
688 fn resolve_url_absolute_http() {
689 let result = resolve_source_map_url(
690 "https://example.com/js/app.js",
691 "https://cdn.example.com/maps/app.js.map",
692 );
693 assert_eq!(
694 result,
695 Some("https://cdn.example.com/maps/app.js.map".to_string())
696 );
697 }
698
699 #[test]
700 fn resolve_url_absolute_slash() {
701 let result = resolve_source_map_url("https://example.com/js/app.js", "/maps/app.js.map");
702 assert_eq!(result, Some("/maps/app.js.map".to_string()));
703 }
704
705 #[test]
706 fn resolve_url_data_url() {
707 let result = resolve_source_map_url(
708 "https://example.com/js/app.js",
709 "data:application/json;base64,abc",
710 );
711 assert_eq!(result, None);
712 }
713
714 #[test]
715 fn resolve_url_filesystem_path() {
716 let result = resolve_source_map_url("/js/app.js", "app.js.map");
717 assert_eq!(result, Some("/js/app.js.map".to_string()));
718 }
719
720 #[test]
721 fn resolve_url_no_directory() {
722 let result = resolve_source_map_url("app.js", "app.js.map");
723 assert_eq!(result, Some("app.js.map".to_string()));
724 }
725
726 #[test]
727 fn resolve_url_excessive_traversal() {
728 let result =
730 resolve_source_map_url("https://example.com/js/app.js", "../../../maps/app.js.map");
731 assert_eq!(
732 result,
733 Some("https://example.com/maps/app.js.map".to_string())
734 );
735 }
736
737 #[test]
740 fn resolve_path_simple() {
741 let result = resolve_source_map_path(Path::new("/js/app.js"), "app.js.map");
742 assert_eq!(result, Some(PathBuf::from("/js/app.js.map")));
743 }
744
745 #[test]
746 fn resolve_path_parent_traversal() {
747 let result = resolve_source_map_path(Path::new("/js/app.js"), "../maps/app.js.map");
748 assert_eq!(result, Some(PathBuf::from("/maps/app.js.map")));
749 }
750
751 #[test]
752 fn resolve_path_subdirectory() {
753 let result = resolve_source_map_path(Path::new("/src/app.js"), "maps/app.js.map");
754 assert_eq!(result, Some(PathBuf::from("/src/maps/app.js.map")));
755 }
756
757 #[test]
760 fn data_url_prefix() {
761 let url = to_data_url(r#"{"version":3}"#);
762 assert!(url.starts_with("data:application/json;base64,"));
763 }
764
765 #[test]
766 fn data_url_roundtrip() {
767 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
768 let url = to_data_url(json);
769 let encoded = url.strip_prefix("data:application/json;base64,").unwrap();
770 let decoded = base64_decode(encoded);
771 assert_eq!(decoded, json);
772 }
773
774 #[test]
775 fn data_url_empty_json() {
776 let url = to_data_url("{}");
777 let encoded = url.strip_prefix("data:application/json;base64,").unwrap();
778 let decoded = base64_decode(encoded);
779 assert_eq!(decoded, "{}");
780 }
781
782 #[test]
783 fn base64_encode_padding_1() {
784 let encoded = base64_encode(b"A");
786 assert_eq!(encoded, "QQ==");
787 }
788
789 #[test]
790 fn base64_encode_padding_2() {
791 let encoded = base64_encode(b"AB");
793 assert_eq!(encoded, "QUI=");
794 }
795
796 #[test]
797 fn base64_encode_no_padding() {
798 let encoded = base64_encode(b"ABC");
800 assert_eq!(encoded, "QUJD");
801 }
802
803 #[test]
804 fn base64_encode_empty() {
805 assert_eq!(base64_encode(b""), "");
806 }
807
808 fn base64_decode(input: &str) -> String {
810 let mut lookup = [0u8; 128];
811 for (i, &c) in BASE64_CHARS.iter().enumerate() {
812 lookup[c as usize] = i as u8;
813 }
814
815 let bytes: Vec<u8> = input.bytes().filter(|&b| b != b'=').collect();
816 let mut result = Vec::with_capacity(bytes.len() * 3 / 4);
817
818 for chunk in bytes.chunks(4) {
819 let vals: Vec<u8> = chunk.iter().map(|&b| lookup[b as usize]).collect();
820 if vals.len() >= 2 {
821 result.push((vals[0] << 2) | (vals[1] >> 4));
822 }
823 if vals.len() >= 3 {
824 result.push((vals[1] << 4) | (vals[2] >> 2));
825 }
826 if vals.len() >= 4 {
827 result.push((vals[2] << 6) | vals[3]);
828 }
829 }
830
831 String::from_utf8(result).unwrap()
832 }
833
834 #[test]
837 fn rewrite_options_default() {
838 let opts = RewriteOptions::default();
839 assert!(opts.with_names);
840 assert!(opts.with_source_contents);
841 assert!(opts.strip_prefixes.is_empty());
842 }
843
844 fn make_test_sourcemap() -> SourceMap {
845 let json = r#"{
846 "version": 3,
847 "sources": ["/src/app/main.js", "/src/app/utils.js"],
848 "names": ["foo", "bar"],
849 "mappings": "AACA,SCCA",
850 "sourcesContent": ["var foo;", "var bar;"]
851 }"#;
852 SourceMap::from_json(json).unwrap()
853 }
854
855 #[test]
856 fn rewrite_strip_explicit_prefix() {
857 let sm = make_test_sourcemap();
858 let opts = RewriteOptions {
859 strip_prefixes: &["/src/app/"],
860 ..Default::default()
861 };
862 let rewritten = rewrite_sources(&sm, &opts);
863 assert_eq!(rewritten.sources, vec!["main.js", "utils.js"]);
864 }
865
866 #[test]
867 fn rewrite_strip_auto_prefix() {
868 let sm = make_test_sourcemap();
869 let opts = RewriteOptions {
870 strip_prefixes: &["~"],
871 ..Default::default()
872 };
873 let rewritten = rewrite_sources(&sm, &opts);
874 assert_eq!(rewritten.sources, vec!["main.js", "utils.js"]);
875 }
876
877 #[test]
878 fn rewrite_without_names() {
879 let sm = make_test_sourcemap();
880 let opts = RewriteOptions {
881 with_names: false,
882 ..Default::default()
883 };
884 let rewritten = rewrite_sources(&sm, &opts);
885 for m in rewritten.all_mappings() {
887 assert_eq!(m.name, u32::MAX);
888 }
889 }
890
891 #[test]
892 fn rewrite_without_sources_content() {
893 let sm = make_test_sourcemap();
894 let opts = RewriteOptions {
895 with_source_contents: false,
896 ..Default::default()
897 };
898 let rewritten = rewrite_sources(&sm, &opts);
899 for content in &rewritten.sources_content {
900 assert!(content.is_none());
901 }
902 }
903
904 #[test]
905 fn rewrite_preserves_mappings() {
906 let sm = make_test_sourcemap();
907 let opts = RewriteOptions::default();
908 let rewritten = rewrite_sources(&sm, &opts);
909 assert_eq!(rewritten.all_mappings().len(), sm.all_mappings().len());
910 let loc = rewritten.original_position_for(0, 0);
912 assert!(loc.is_some());
913 }
914
915 #[test]
916 fn rewrite_preserves_debug_id() {
917 let json = r#"{
918 "version": 3,
919 "sources": ["a.js"],
920 "names": [],
921 "mappings": "AAAA",
922 "debugId": "test-id-123"
923 }"#;
924 let sm = SourceMap::from_json(json).unwrap();
925 let opts = RewriteOptions::default();
926 let rewritten = rewrite_sources(&sm, &opts);
927 assert_eq!(rewritten.debug_id.as_deref(), Some("test-id-123"));
928 }
929
930 #[test]
931 fn rewrite_preserves_extensions() {
932 let json = r#"{
933 "version": 3,
934 "sources": ["a.js"],
935 "names": [],
936 "mappings": "AAAA",
937 "x_facebook_sources": [[{"names": ["<global>"], "mappings": "AAA"}]]
938 }"#;
939 let sm = SourceMap::from_json(json).unwrap();
940 assert!(sm.extensions.contains_key("x_facebook_sources"));
941
942 let opts = RewriteOptions::default();
943 let rewritten = rewrite_sources(&sm, &opts);
944 assert!(rewritten.extensions.contains_key("x_facebook_sources"));
945 assert_eq!(
946 sm.extensions["x_facebook_sources"],
947 rewritten.extensions["x_facebook_sources"]
948 );
949 }
950
951 #[test]
952 fn rewrite_without_names_clears_names_vec() {
953 let sm = make_test_sourcemap();
954 let opts = RewriteOptions {
955 with_names: false,
956 ..Default::default()
957 };
958 let rewritten = rewrite_sources(&sm, &opts);
959 assert!(rewritten.names.is_empty());
960 }
961
962 #[test]
963 fn rewrite_strip_no_match() {
964 let sm = make_test_sourcemap();
965 let opts = RewriteOptions {
966 strip_prefixes: &["/other/"],
967 ..Default::default()
968 };
969 let rewritten = rewrite_sources(&sm, &opts);
970 assert_eq!(rewritten.sources, sm.sources);
971 }
972
973 #[test]
976 fn decoded_map_from_json() {
977 let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AACAA"}"#;
978 let dm = DecodedMap::from_json(json).unwrap();
979 assert_eq!(dm.sources(), &["a.js"]);
980 assert_eq!(dm.names(), &["foo"]);
981 }
982
983 #[test]
984 fn decoded_map_original_position() {
985 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
986 let dm = DecodedMap::from_json(json).unwrap();
987 let loc = dm.original_position_for(0, 0).unwrap();
988 assert_eq!(dm.source(loc.source), "a.js");
989 assert_eq!(loc.line, 0);
990 assert_eq!(loc.column, 0);
991 }
992
993 #[test]
994 fn decoded_map_generated_position() {
995 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
996 let dm = DecodedMap::from_json(json).unwrap();
997 let pos = dm.generated_position_for("a.js", 0, 0).unwrap();
998 assert_eq!(pos.line, 0);
999 assert_eq!(pos.column, 0);
1000 }
1001
1002 #[test]
1003 fn decoded_map_source_and_name() {
1004 let json =
1005 r#"{"version":3,"sources":["a.js","b.js"],"names":["x","y"],"mappings":"AACAA,GCCA"}"#;
1006 let dm = DecodedMap::from_json(json).unwrap();
1007 assert_eq!(dm.source(0), "a.js");
1008 assert_eq!(dm.source(1), "b.js");
1009 assert_eq!(dm.name(0), "x");
1010 assert_eq!(dm.name(1), "y");
1011 }
1012
1013 #[test]
1014 fn decoded_map_debug_id() {
1015 let json = r#"{"version":3,"sources":[],"names":[],"mappings":"","debugId":"abc-123"}"#;
1016 let dm = DecodedMap::from_json(json).unwrap();
1017 assert_eq!(dm.debug_id(), Some("abc-123"));
1018 }
1019
1020 #[test]
1021 fn decoded_map_set_debug_id() {
1022 let json = r#"{"version":3,"sources":[],"names":[],"mappings":""}"#;
1023 let mut dm = DecodedMap::from_json(json).unwrap();
1024 assert_eq!(dm.debug_id(), None);
1025 dm.set_debug_id("new-id");
1026 assert_eq!(dm.debug_id(), Some("new-id"));
1027 }
1028
1029 #[test]
1030 fn decoded_map_to_json() {
1031 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1032 let dm = DecodedMap::from_json(json).unwrap();
1033 let output = dm.to_json();
1034 assert!(output.contains("\"version\":3"));
1036 assert!(output.contains("\"sources\":[\"a.js\"]"));
1037 }
1038
1039 #[test]
1040 fn decoded_map_into_source_map() {
1041 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1042 let dm = DecodedMap::from_json(json).unwrap();
1043 let sm = dm.into_source_map().unwrap();
1044 assert_eq!(sm.sources, vec!["a.js"]);
1045 }
1046
1047 #[test]
1048 fn decoded_map_invalid_json() {
1049 let result = DecodedMap::from_json("not json");
1050 assert!(result.is_err());
1051 }
1052
1053 #[test]
1054 fn decoded_map_roundtrip() {
1055 let json = r#"{"version":3,"sources":["a.js"],"names":["foo"],"mappings":"AACAA","sourcesContent":["var foo;"]}"#;
1056 let dm = DecodedMap::from_json(json).unwrap();
1057 let output = dm.to_json();
1058 let dm2 = DecodedMap::from_json(&output).unwrap();
1059 assert_eq!(dm2.sources(), &["a.js"]);
1060 assert_eq!(dm2.names(), &["foo"]);
1061 }
1062
1063 #[test]
1066 fn data_url_with_is_sourcemap() {
1067 let json = r#"{"version":3,"sources":["a.js"],"names":[],"mappings":"AAAA"}"#;
1068 assert!(is_sourcemap(json));
1069 let url = to_data_url(json);
1070 assert!(url.starts_with("data:application/json;base64,"));
1071 }
1072
1073 #[test]
1074 fn rewrite_then_serialize() {
1075 let sm = make_test_sourcemap();
1076 let opts = RewriteOptions {
1077 strip_prefixes: &["~"],
1078 with_source_contents: false,
1079 ..Default::default()
1080 };
1081 let rewritten = rewrite_sources(&sm, &opts);
1082 let json = rewritten.to_json();
1083 assert!(is_sourcemap(&json));
1084
1085 let parsed = SourceMap::from_json(&json).unwrap();
1087 assert_eq!(parsed.sources, vec!["main.js", "utils.js"]);
1088 }
1089
1090 #[test]
1091 fn decoded_map_rewrite_roundtrip() {
1092 let json = r#"{"version":3,"sources":["/src/a.js","/src/b.js"],"names":["x"],"mappings":"AACAA,GCAA","sourcesContent":["var x;","var y;"]}"#;
1093 let dm = DecodedMap::from_json(json).unwrap();
1094 let sm = dm.into_source_map().unwrap();
1095
1096 let opts = RewriteOptions {
1097 strip_prefixes: &["~"],
1098 with_source_contents: true,
1099 ..Default::default()
1100 };
1101 let rewritten = rewrite_sources(&sm, &opts);
1102 assert_eq!(rewritten.sources, vec!["a.js", "b.js"]);
1103
1104 let dm2 = DecodedMap::Regular(rewritten);
1105 let output = dm2.to_json();
1106 assert!(is_sourcemap(&output));
1107 }
1108}