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