1mod source;
2mod source_joiner;
3
4use std::borrow::Cow;
5
6use oxc_sourcemap::Token;
7
8pub use oxc_sourcemap::{JSONSourceMap, OwnedSourceMap, SourceMapBuilder, SourcemapVisualizer};
9pub use source_joiner::SourceJoiner;
10
11pub type SourceMap = oxc_sourcemap::SourceMap<'static>;
14
15pub use crate::source::{Source, SourceMapSource};
16
17pub fn adjust_sourcemap_dst_lines(sourcemap: SourceMap, lines: u32) -> SourceMap {
21 if lines == 0 {
22 return sourcemap;
23 }
24
25 let tokens: Box<[Token]> = sourcemap
26 .get_tokens()
27 .filter(|t| t.get_dst_line() >= lines)
28 .map(|token| {
29 Token::new(
30 token.get_dst_line() - lines,
31 token.get_dst_col(),
32 token.get_src_line(),
33 token.get_src_col(),
34 token.get_source_id(),
35 token.get_name_id(),
36 )
37 })
38 .collect();
39
40 SourceMap::new(
41 sourcemap.get_file().map(|f| Cow::Owned(f.to_owned())),
42 sourcemap.get_names().map(|n| Cow::Owned(n.to_owned())).collect(),
43 sourcemap.get_source_root().map(|s| Cow::Owned(s.to_owned())),
44 sourcemap.get_sources().map(|s| Cow::Owned(s.to_owned())).collect(),
45 sourcemap.get_source_contents().map(|c| c.map(|s| Cow::Owned(s.to_owned()))).collect(),
46 tokens,
47 None,
48 )
49}
50
51pub fn empty_sourcemap() -> SourceMap {
53 SourceMap::new(None, vec![], None, vec![], vec![], Box::new([]), None)
54}
55
56pub fn collapse_sourcemaps(sourcemap_chain: &[&SourceMap]) -> SourceMap {
58 debug_assert!(sourcemap_chain.len() > 1);
59 if sourcemap_chain.len() == 1 {
60 return sourcemap_chain[0].clone();
62 }
63
64 let last_map = sourcemap_chain.last().expect("sourcemap_chain should not be empty");
65 let first_map = sourcemap_chain.first().expect("sourcemap_chain should not be empty");
66 let chain_without_last = &sourcemap_chain[..sourcemap_chain.len() - 1];
67
68 let sourcemap_and_lookup_table: Vec<_> = chain_without_last
70 .iter()
71 .rev()
72 .map(|sourcemap| (*sourcemap, sourcemap.generate_lookup_table()))
73 .collect();
74
75 let tokens: Box<[Token]> = last_map
76 .get_source_view_tokens()
77 .filter_map(|token| {
78 let original_token =
79 sourcemap_and_lookup_table.iter().try_fold(token, |token, (sourcemap, lookup_table)| {
80 sourcemap.lookup_source_view_token(
81 lookup_table,
82 token.get_src_line(),
83 token.get_src_col(),
84 )
85 });
86 original_token.map(|original_token| {
87 Token::new(
88 token.get_dst_line(),
89 token.get_dst_col(),
90 original_token.get_src_line(),
91 original_token.get_src_col(),
92 original_token.get_source_id(),
93 original_token.get_name_id(),
94 )
95 })
96 })
97 .collect();
98
99 SourceMap::new(
100 None,
101 first_map.get_names().map(|n| Cow::Owned(n.to_owned())).collect(),
102 None,
103 first_map.get_sources().map(|s| Cow::Owned(s.to_owned())).collect(),
104 first_map.get_source_contents().map(|x| x.map(|s| Cow::Owned(s.to_owned()))).collect(),
105 tokens,
106 None,
107 )
108}
109
110#[test]
111fn test_collapse_sourcemaps() {
112 use crate::{SourceJoiner, SourceMapSource, collapse_sourcemaps};
113 use oxc::{
114 allocator::Allocator,
115 codegen::{Codegen, CodegenOptions, CodegenReturn, CommentOptions},
116 parser::Parser,
117 span::SourceType,
118 };
119 use oxc_sourcemap::SourcemapVisualizer;
120
121 let allocator = Allocator::default();
122
123 let mut source_joiner = SourceJoiner::default();
124
125 let filename = "foo.js".to_string();
126 let source_text = "const foo = 1; console.log(foo);\n".to_string();
127 let source_type = SourceType::from_path(&filename).unwrap();
128 let ret1 = Parser::new(&allocator, &source_text, source_type).parse();
129 let CodegenReturn { map, code, .. } = Codegen::new()
130 .with_options(CodegenOptions {
131 comments: CommentOptions { normal: false, ..CommentOptions::default() },
132 source_map_path: Some(filename.into()),
133 ..CodegenOptions::default()
134 })
135 .build(&ret1.program);
136 source_joiner.append_source(SourceMapSource::new(code, map.unwrap().into_owned()));
137
138 let filename = "bar.js".to_string();
139 let source_text = "const bar = 2; console.log(bar);\n".to_string();
140 let ret2: oxc::parser::ParserReturn = Parser::new(&allocator, &source_text, source_type).parse();
141 let CodegenReturn { map, code, .. } = Codegen::new()
142 .with_options(CodegenOptions {
143 source_map_path: Some(filename.into()),
144 ..CodegenOptions::default()
145 })
146 .build(&ret2.program);
147 source_joiner.append_source(SourceMapSource::new(code, map.unwrap().into_owned()));
148
149 let (source_text, source_map) = source_joiner.join();
150
151 let mut sourcemap_chain = vec![];
152
153 sourcemap_chain.push(source_map.as_ref().unwrap());
154
155 let filename = "chunk.js".to_string();
156 let ret3 = Parser::new(&allocator, &source_text, source_type).parse();
157 let CodegenReturn { map, code, .. } = Codegen::new()
158 .with_options(CodegenOptions {
159 comments: CommentOptions { normal: false, ..CommentOptions::default() },
160 source_map_path: Some(filename.into()),
161 ..CodegenOptions::default()
162 })
163 .build(&ret3.program);
164 let map = map.unwrap().into_owned();
165 sourcemap_chain.push(&map);
166
167 let map = collapse_sourcemaps(&sourcemap_chain);
168 assert_eq!(
169 SourcemapVisualizer::new(&code, &map).get_text(),
170 r#"- foo.js
171(0:0) "const " --> (0:0) "const "
172(0:6) "foo = " --> (0:6) "foo = "
173(0:12) "1; " --> (0:12) "1;\n"
174(0:15) "console." --> (1:0) "console."
175(0:23) "log(" --> (1:8) "log("
176(0:27) "foo" --> (1:12) "foo"
177(0:30) ");\n" --> (1:15) ");\n"
178- bar.js
179(0:0) "const " --> (2:0) "const "
180(0:6) "bar = " --> (2:6) "bar = "
181(0:12) "2; " --> (2:12) "2;\n"
182(0:15) "console." --> (3:0) "console."
183(0:23) "log(" --> (3:8) "log("
184(0:27) "bar" --> (3:12) "bar"
185(0:30) ");\n" --> (3:15) ");\n"
186"#
187 );
188}
189
190#[test]
192fn test_collapse_sourcemaps_with_coarse_segments() {
193 use oxc_sourcemap::SourceMap;
194
195 fn get_loc(mut pos: usize, code: &str) -> (u32, u32) {
196 for (line_idx, line) in code.lines().enumerate() {
197 if pos <= line.len() {
198 #[expect(clippy::cast_possible_truncation)]
199 return (line_idx as u32, pos as u32);
200 }
201 pos -= line.len() + 1; }
203 panic!("position out of bounds");
204 }
205
206 let original_code = "import { useEffect } from 'react';
207
208export function App() {
209 useEffect(() => {
210 console.log('ReplayAnalyze');
211 }, []);
212
213 return <div>{'.'}</div>;
214}
215";
216 let transformed_code = r#"import{jsx}from"react/jsx-runtime";import{useEffect}from"react";export function App(){return useEffect((()=>{console.log("ReplayAnalyze")}),[]),jsx("div",{children:"."})}"#;
217
218 let esbuild_map_json = r#"{
220 "version": 3,
221 "sources": ["<stdin>"],
222 "sourcesContent": ["import { useEffect } from 'react';\n\nexport function App() {\n useEffect(() => {\n console.log('ReplayAnalyze');\n }, []);\n\n return <div>{'.'}</div>;\n}\n"],
223 "mappings": "AAOS;AAPT,SAAS,iBAAiB;AAEnB,gBAAS,MAAM;AACpB,YAAU,MAAM;AACd,YAAQ,IAAI,eAAe;AAAA,EAC7B,GAAG,CAAC,CAAC;AAEL,SAAO,oBAAC,SAAK,eAAI;AACnB;",
224 "names": []
225 }"#;
226 let esbuild_map = SourceMap::from_json_string(esbuild_map_json).unwrap();
228
229 let terser_map_json = r#"{
231 "version": 3,
232 "names": ["jsx", "useEffect", "App", "console", "log", "children"],
233 "sources": ["0"],
234 "sourcesContent": ["import { jsx } from \"react/jsx-runtime\";\nimport { useEffect } from \"react\";\nexport function App() {\n useEffect(() => {\n console.log(\"ReplayAnalyze\");\n }, []);\n return /* @__PURE__ */ jsx(\"div\", { children: \".\" });\n}\n"],
235 "mappings": "OAASA,QAAW,2BACXC,cAAiB,eACnB,SAASC,MAId,OAHAD,WAAU,KACRE,QAAQC,IAAI,gBAAgB,GAC3B,IACoBJ,IAAI,MAAO,CAAEK,SAAU,KAChD"
236 }"#;
237 let terser_map = SourceMap::from_json_string(terser_map_json).unwrap();
239
240 let collapsed = collapse_sourcemaps(&[&esbuild_map, &terser_map]);
241 let collapsed_lookup_table = collapsed.generate_lookup_table();
242
243 let generated_loc = get_loc(transformed_code.find("return").unwrap(), transformed_code);
244 let original_loc = collapsed
245 .lookup_source_view_token(&collapsed_lookup_table, generated_loc.0, generated_loc.1)
246 .map(|token| (token.get_src_line(), token.get_src_col()));
247 assert_eq!(
248 original_loc,
249 Some(get_loc(original_code.find("return").unwrap(), original_code)),
250 "collapsed sourcemap should map 'return' in transformed code back to original source"
251 );
252}