Skip to content

Commit 8701dcc

Browse files
authored
feat: ensure source maps are cached (#628)
1 parent decafab commit 8701dcc

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-0
lines changed

src/analysis.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ pub struct ModuleInfo {
276276
/// or `@import { SomeType } from "npm:some-module"`.
277277
#[serde(skip_serializing_if = "Vec::is_empty", default)]
278278
pub jsdoc_imports: Vec<JsDocImportInfo>,
279+
/// Source map URL extracted from sourceMappingURL comment
280+
#[serde(skip_serializing_if = "Option::is_none", default)]
281+
pub source_map_url: Option<SpecifierWithRange>,
279282
}
280283

281284
fn is_false(v: &bool) -> bool {
@@ -442,6 +445,14 @@ pub fn find_jsx_import_source_types(text: &str) -> Option<regex::Match<'_>> {
442445
.and_then(|c| c.get(1))
443446
}
444447

448+
/// Matches the `sourceMappingURL` comment.
449+
pub fn find_source_mapping_url(text: &str) -> Option<regex::Match<'_>> {
450+
static SOURCE_MAPPING_URL_RE: Lazy<Regex> = Lazy::new(|| {
451+
Regex::new(r"(?i)^[#@]\s*sourceMappingURL\s*=\s*(\S+)").unwrap()
452+
});
453+
SOURCE_MAPPING_URL_RE.captures(text).and_then(|c| c.get(1))
454+
}
455+
445456
/// Matches the `@ts-self-types` pragma.
446457
pub fn find_ts_self_types(text: &str) -> Option<regex::Match<'_>> {
447458
static TS_SELF_TYPES_RE: Lazy<Regex> = Lazy::new(|| {
@@ -505,6 +516,7 @@ mod test {
505516
jsx_import_source: None,
506517
jsx_import_source_types: None,
507518
jsdoc_imports: Vec::new(),
519+
source_map_url: None,
508520
};
509521
run_serialization_test(&module_info, json!({}));
510522
}
@@ -574,6 +586,7 @@ mod test {
574586
jsx_import_source: None,
575587
jsx_import_source_types: None,
576588
jsdoc_imports: Vec::new(),
589+
source_map_url: None,
577590
};
578591
run_serialization_test(
579592
&module_info,
@@ -659,6 +672,7 @@ mod test {
659672
jsx_import_source: None,
660673
jsx_import_source_types: None,
661674
jsdoc_imports: Vec::new(),
675+
source_map_url: None,
662676
};
663677
run_serialization_test(
664678
&module_info,
@@ -704,6 +718,7 @@ mod test {
704718
jsx_import_source: None,
705719
jsx_import_source_types: None,
706720
jsdoc_imports: Vec::new(),
721+
source_map_url: None,
707722
};
708723
run_serialization_test(
709724
&module_info,
@@ -734,6 +749,7 @@ mod test {
734749
}),
735750
jsx_import_source_types: None,
736751
jsdoc_imports: Vec::new(),
752+
source_map_url: None,
737753
};
738754
run_serialization_test(
739755
&module_info,
@@ -764,6 +780,7 @@ mod test {
764780
},
765781
}),
766782
jsdoc_imports: Vec::new(),
783+
source_map_url: None,
767784
};
768785
run_serialization_test(
769786
&module_info,
@@ -819,6 +836,7 @@ mod test {
819836
resolution_mode: Some(TypeScriptTypesResolutionMode::Require),
820837
},
821838
]),
839+
source_map_url: None,
822840
};
823841
run_serialization_test(
824842
&module_info,
@@ -1053,6 +1071,7 @@ mod test {
10531071
jsx_import_source: None,
10541072
jsx_import_source_types: None,
10551073
jsdoc_imports: Vec::new(),
1074+
source_map_url: None,
10561075
};
10571076
let json = json!({
10581077
"dependencies": [{
@@ -1097,6 +1116,7 @@ mod test {
10971116
jsx_import_source: None,
10981117
jsx_import_source_types: None,
10991118
jsdoc_imports: Vec::new(),
1119+
source_map_url: None,
11001120
};
11011121
let json = json!({
11021122
"dependencies": [{
@@ -1109,6 +1129,36 @@ mod test {
11091129
run_v1_deserialization_test(json, &expected);
11101130
}
11111131

1132+
#[test]
1133+
fn module_info_serialization_source_map_url() {
1134+
let module_info = ModuleInfo {
1135+
is_script: false,
1136+
dependencies: Vec::new(),
1137+
ts_references: Vec::new(),
1138+
self_types_specifier: None,
1139+
jsx_import_source: None,
1140+
jsx_import_source_types: None,
1141+
jsdoc_imports: Vec::new(),
1142+
source_map_url: Some(SpecifierWithRange {
1143+
text: "file.js.map".to_string(),
1144+
range: PositionRange {
1145+
start: Position::zeroed(),
1146+
end: Position::zeroed(),
1147+
},
1148+
}),
1149+
};
1150+
1151+
run_serialization_test(
1152+
&module_info,
1153+
json!({
1154+
"sourceMapUrl": {
1155+
"text": "file.js.map",
1156+
"range": [[0, 0], [0, 0]],
1157+
}
1158+
}),
1159+
);
1160+
}
1161+
11121162
#[track_caller]
11131163
fn run_serialization_test<
11141164
T: DeserializeOwned + Serialize + std::fmt::Debug + PartialEq + Eq,
@@ -1133,4 +1183,59 @@ mod test {
11331183
let deserialized_value = serde_json::from_value::<T>(json).unwrap();
11341184
assert_eq!(deserialized_value, *value);
11351185
}
1186+
1187+
#[test]
1188+
fn test_find_source_mapping_url() {
1189+
// Test with # prefix
1190+
let m = find_source_mapping_url("# sourceMappingURL=file.js.map");
1191+
assert!(m.is_some());
1192+
assert_eq!(m.unwrap().as_str(), "file.js.map");
1193+
1194+
// Test with @ prefix
1195+
let m = find_source_mapping_url("@ sourceMappingURL=file.js.map");
1196+
assert!(m.is_some());
1197+
assert_eq!(m.unwrap().as_str(), "file.js.map");
1198+
1199+
// Test case insensitivity
1200+
let m = find_source_mapping_url("# SOURCEMAPPINGURL=file.js.map");
1201+
assert!(m.is_some());
1202+
assert_eq!(m.unwrap().as_str(), "file.js.map");
1203+
1204+
// Test with no spaces
1205+
let m = find_source_mapping_url("#sourceMappingURL=file.js.map");
1206+
assert!(m.is_some());
1207+
assert_eq!(m.unwrap().as_str(), "file.js.map");
1208+
1209+
// Test with multiple spaces
1210+
let m = find_source_mapping_url("# sourceMappingURL = file.js.map");
1211+
assert!(m.is_some());
1212+
assert_eq!(m.unwrap().as_str(), "file.js.map");
1213+
1214+
// Test with URL containing query params
1215+
let m = find_source_mapping_url("# sourceMappingURL=file.js.map?v=123");
1216+
assert!(m.is_some());
1217+
assert_eq!(m.unwrap().as_str(), "file.js.map?v=123");
1218+
1219+
// Test with data URL
1220+
let m = find_source_mapping_url(
1221+
"# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==",
1222+
);
1223+
assert!(m.is_some());
1224+
assert_eq!(
1225+
m.unwrap().as_str(),
1226+
"data:application/json;base64,eyJ2ZXJzaW9uIjozfQ=="
1227+
);
1228+
1229+
// Test with relative path
1230+
let m = find_source_mapping_url("# sourceMappingURL=../maps/file.js.map");
1231+
assert!(m.is_some());
1232+
assert_eq!(m.unwrap().as_str(), "../maps/file.js.map");
1233+
1234+
// Test invalid formats
1235+
assert!(find_source_mapping_url("sourceMappingURL=file.js.map").is_none());
1236+
assert!(
1237+
find_source_mapping_url("// sourceMappingURL=file.js.map").is_none()
1238+
);
1239+
assert!(find_source_mapping_url("# sourceMappingURL").is_none());
1240+
}
11361241
}

src/ast/mod.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::analysis::find_jsx_import_source;
1616
use crate::analysis::find_jsx_import_source_types;
1717
use crate::analysis::find_path_reference;
1818
use crate::analysis::find_resolution_mode;
19+
use crate::analysis::find_source_mapping_url;
1920
use crate::analysis::find_ts_self_types;
2021
use crate::analysis::find_ts_types;
2122
use crate::analysis::find_types_reference;
@@ -268,6 +269,9 @@ impl<'a> ParserModuleAnalyzer<'a> {
268269
None => comments.get_leading(program.start()),
269270
},
270271
};
272+
// Get trailing comments from the program end to extract sourceMappingURL
273+
// which is typically at the very end of the file
274+
let trailing_comments = comments.get_trailing(program.end());
271275
ModuleInfo {
272276
is_script: program.compute_is_script(),
273277
dependencies: analyze_dependencies(program, text_info, comments),
@@ -288,6 +292,7 @@ impl<'a> ParserModuleAnalyzer<'a> {
288292
leading_comments,
289293
),
290294
jsdoc_imports: analyze_jsdoc_imports(media_type, text_info, comments),
295+
source_map_url: analyze_source_map_url(text_info, trailing_comments),
291296
}
292297
}
293298

@@ -608,6 +613,27 @@ fn analyze_ts_self_types(
608613
})
609614
}
610615

616+
// Search source map URL from trailing comments
617+
fn analyze_source_map_url(
618+
text_info: &SourceTextInfo,
619+
trailing_comments: Option<&Vec<deno_ast::swc::common::comments::Comment>>,
620+
) -> Option<SpecifierWithRange> {
621+
trailing_comments.and_then(|c| {
622+
c.iter().rev().find_map(|comment| {
623+
let source_mapping_url = find_source_mapping_url(&comment.text)?;
624+
Some(SpecifierWithRange {
625+
text: source_mapping_url.as_str().to_string(),
626+
range: comment_source_to_position_range(
627+
comment.start(),
628+
source_mapping_url.range(),
629+
text_info,
630+
true,
631+
),
632+
})
633+
})
634+
})
635+
}
636+
611637
/// Searches comments for any `@ts-types` or `@deno-types` compiler hints.
612638
pub fn analyze_ts_or_deno_types(
613639
text_info: &SourceTextInfo,
@@ -1459,6 +1485,7 @@ export {};
14591485
}),
14601486
jsx_import_source_types: None,
14611487
jsdoc_imports: vec![],
1488+
source_map_url: None,
14621489
},
14631490
);
14641491
}
@@ -1539,4 +1566,105 @@ export {};
15391566
Some(TypeScriptTypesResolutionMode::Import)
15401567
);
15411568
}
1569+
1570+
#[tokio::test]
1571+
async fn test_source_mapping_url_extraction() {
1572+
let specifier =
1573+
ModuleSpecifier::parse("file:///test.js").expect("bad specifier");
1574+
let source = r#"
1575+
export function test() {
1576+
return "hello";
1577+
}
1578+
//# sourceMappingURL=test.js.map
1579+
1580+
// comment after
1581+
1582+
"#;
1583+
let module_info = DefaultModuleAnalyzer
1584+
.analyze(&specifier, source.into(), MediaType::JavaScript)
1585+
.await
1586+
.unwrap();
1587+
assert!(module_info.source_map_url.is_some());
1588+
let source_map = module_info.source_map_url.unwrap();
1589+
assert_eq!(source_map.text, "test.js.map");
1590+
// Verify the range points to the correct location
1591+
assert_eq!(source_map.range.start.line, 4);
1592+
assert_eq!(source_map.range.start.character, 21);
1593+
assert_eq!(source_map.range.end.line, 4);
1594+
assert_eq!(source_map.range.end.character, 32);
1595+
}
1596+
1597+
#[tokio::test]
1598+
async fn test_source_mapping_url_with_at_prefix() {
1599+
let specifier =
1600+
ModuleSpecifier::parse("file:///test.js").expect("bad specifier");
1601+
let source = r#"
1602+
const x = 1;
1603+
//@ sourceMappingURL=bundle.js.map
1604+
"#;
1605+
let module_info = DefaultModuleAnalyzer
1606+
.analyze(&specifier, source.into(), MediaType::JavaScript)
1607+
.await
1608+
.unwrap();
1609+
assert!(module_info.source_map_url.is_some());
1610+
assert_eq!(module_info.source_map_url.unwrap().text, "bundle.js.map");
1611+
}
1612+
1613+
#[tokio::test]
1614+
async fn test_source_mapping_url_data_uri() {
1615+
let specifier =
1616+
ModuleSpecifier::parse("file:///test.js").expect("bad specifier");
1617+
let source = r#"
1618+
console.log("minified");
1619+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozfQ==
1620+
"#;
1621+
let module_info = DefaultModuleAnalyzer
1622+
.analyze(&specifier, source.into(), MediaType::JavaScript)
1623+
.await
1624+
.unwrap();
1625+
assert!(module_info.source_map_url.is_some());
1626+
assert_eq!(
1627+
module_info.source_map_url.unwrap().text,
1628+
"data:application/json;base64,eyJ2ZXJzaW9uIjozfQ=="
1629+
);
1630+
}
1631+
1632+
#[tokio::test]
1633+
async fn test_no_source_mapping_url() {
1634+
let specifier =
1635+
ModuleSpecifier::parse("file:///test.js").expect("bad specifier");
1636+
let source = r#"
1637+
export function test() {
1638+
return "hello";
1639+
}
1640+
"#;
1641+
let module_info = DefaultModuleAnalyzer
1642+
.analyze(&specifier, source.into(), MediaType::JavaScript)
1643+
.await
1644+
.unwrap();
1645+
assert!(module_info.source_map_url.is_none());
1646+
}
1647+
1648+
#[tokio::test]
1649+
async fn test_source_mapping_url_only_at_end() {
1650+
let specifier =
1651+
ModuleSpecifier::parse("file:///test.js").expect("bad specifier");
1652+
// sourceMappingURL in the middle of file should NOT be extracted
1653+
// since we only look for trailing comments at program.end()
1654+
let source = r#"
1655+
export function test() {
1656+
// This is not a sourceMappingURL comment
1657+
return "hello";
1658+
}
1659+
//# sourceMappingURL=test.js.map
1660+
console.log("more code");
1661+
"#;
1662+
let module_info = DefaultModuleAnalyzer
1663+
.analyze(&specifier, source.into(), MediaType::JavaScript)
1664+
.await
1665+
.unwrap();
1666+
// Should NOT extract the sourceMappingURL if there's code after it
1667+
// because it's not at the end of the program
1668+
assert!(module_info.source_map_url.is_none());
1669+
}
15421670
}

0 commit comments

Comments
 (0)