turbopack_core/
code_builder.rs

1use std::{
2    cmp::min,
3    io::{BufRead, Result as IoResult, Write},
4    ops,
5};
6
7use anyhow::Result;
8use turbo_tasks::Vc;
9use turbo_tasks_fs::rope::{Rope, RopeBuilder};
10use turbo_tasks_hash::hash_xxh3_hash64;
11
12use crate::{
13    source_map::{GenerateSourceMap, OptionStringifiedSourceMap, SourceMap},
14    source_pos::SourcePos,
15};
16
17/// A mapping of byte-offset in the code string to an associated source map.
18pub type Mapping = (usize, Option<Rope>);
19
20/// Code stores combined output code and the source map of that output code.
21#[turbo_tasks::value(shared)]
22#[derive(Debug, Clone)]
23pub struct Code {
24    code: Rope,
25    mappings: Vec<Mapping>,
26}
27
28impl Code {
29    pub fn source_code(&self) -> &Rope {
30        &self.code
31    }
32
33    /// Tests if any code in this Code contains an associated source map.
34    pub fn has_source_map(&self) -> bool {
35        !self.mappings.is_empty()
36    }
37}
38
39/// CodeBuilder provides a mutable container to append source code.
40pub struct CodeBuilder {
41    code: RopeBuilder,
42    mappings: Option<Vec<Mapping>>,
43}
44
45impl Default for CodeBuilder {
46    fn default() -> Self {
47        Self {
48            code: RopeBuilder::default(),
49            mappings: Some(Vec::new()),
50        }
51    }
52}
53
54impl CodeBuilder {
55    pub fn new(collect_mappings: bool) -> Self {
56        Self {
57            code: RopeBuilder::default(),
58            mappings: collect_mappings.then(Vec::new),
59        }
60    }
61
62    /// Pushes synthetic runtime code without an associated source map. This is
63    /// the default concatenation operation, but it's designed to be used
64    /// with the `+=` operator.
65    fn push_static_bytes(&mut self, code: &'static [u8]) {
66        self.push_map(None);
67        self.code.push_static_bytes(code);
68    }
69
70    /// Pushes original user code with an optional source map if one is
71    /// available. If it's not, this is no different than pushing Synthetic
72    /// code.
73    pub fn push_source(&mut self, code: &Rope, map: Option<Rope>) {
74        self.push_map(map);
75        self.code += code;
76    }
77
78    /// Copies the Synthetic/Original code of an already constructed Code into
79    /// this instance.
80    pub fn push_code(&mut self, prebuilt: &Code) {
81        if let Some((index, _)) = prebuilt.mappings.first() {
82            if *index > 0 {
83                // If the index is positive, then the code starts with a synthetic section. We
84                // may need to push an empty map in order to end the current
85                // section's mappings.
86                self.push_map(None);
87            }
88
89            let len = self.code.len();
90            if let Some(mappings) = self.mappings.as_mut() {
91                mappings.extend(
92                    prebuilt
93                        .mappings
94                        .iter()
95                        .map(|(index, map)| (index + len, map.clone())),
96                );
97            }
98        } else {
99            self.push_map(None);
100        }
101
102        self.code += &prebuilt.code;
103    }
104
105    /// Setting breakpoints on synthetic code can cause weird behaviors
106    /// because Chrome will treat the location as belonging to the previous
107    /// original code section. By inserting an empty source map when reaching a
108    /// synthetic section directly after an original section, we tell Chrome
109    /// that the previous map ended at this point.
110    fn push_map(&mut self, map: Option<Rope>) {
111        let Some(mappings) = self.mappings.as_mut() else {
112            return;
113        };
114        if map.is_none() && matches!(mappings.last(), None | Some((_, None))) {
115            // No reason to push an empty map directly after an empty map
116            return;
117        }
118
119        debug_assert!(
120            map.is_some() || !mappings.is_empty(),
121            "the first mapping is never a None"
122        );
123        mappings.push((self.code.len(), map));
124    }
125
126    /// Tests if any code in this CodeBuilder contains an associated source map.
127    pub fn has_source_map(&self) -> bool {
128        self.mappings
129            .as_ref()
130            .is_some_and(|mappings| !mappings.is_empty())
131    }
132
133    pub fn build(self) -> Code {
134        Code {
135            code: self.code.build(),
136            mappings: self.mappings.unwrap_or_default(),
137        }
138    }
139}
140
141impl ops::AddAssign<&'static str> for CodeBuilder {
142    fn add_assign(&mut self, rhs: &'static str) {
143        self.push_static_bytes(rhs.as_bytes());
144    }
145}
146
147impl ops::AddAssign<&'static str> for &mut CodeBuilder {
148    fn add_assign(&mut self, rhs: &'static str) {
149        self.push_static_bytes(rhs.as_bytes());
150    }
151}
152
153impl Write for CodeBuilder {
154    fn write(&mut self, bytes: &[u8]) -> IoResult<usize> {
155        self.push_map(None);
156        self.code.write(bytes)
157    }
158
159    fn flush(&mut self) -> IoResult<()> {
160        self.code.flush()
161    }
162}
163
164impl From<Code> for CodeBuilder {
165    fn from(code: Code) -> Self {
166        let mut builder = CodeBuilder::default();
167        builder.push_code(&code);
168        builder
169    }
170}
171
172#[turbo_tasks::value_impl]
173impl GenerateSourceMap for Code {
174    /// Generates the source map out of all the pushed Original code.
175    /// The SourceMap v3 spec has a "sectioned" source map specifically designed
176    /// for concatenation in post-processing steps. This format consists of
177    /// a `sections` array, with section item containing a `offset` object
178    /// and a `map` object. The section's map applies only after the
179    /// starting offset, and until the start of the next section. This is by
180    /// far the simplest way to concatenate the source maps of the multiple
181    /// chunk items into a single map file.
182    #[turbo_tasks::function]
183    pub async fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
184        Ok(Vc::cell(Some(self.generate_source_map_ref()?)))
185    }
186}
187
188#[turbo_tasks::value_impl]
189impl Code {
190    /// Returns the hash of the source code of this Code.
191    #[turbo_tasks::function]
192    pub fn source_code_hash(&self) -> Vc<u64> {
193        let code = self;
194        let hash = hash_xxh3_hash64(code.source_code());
195        Vc::cell(hash)
196    }
197}
198
199impl Code {
200    pub fn generate_source_map_ref(&self) -> Result<Rope> {
201        let mut pos = SourcePos::new();
202        let mut last_byte_pos = 0;
203
204        let mut sections = Vec::with_capacity(self.mappings.len());
205        let mut read = self.code.read();
206        for (byte_pos, map) in &self.mappings {
207            let mut want = byte_pos - last_byte_pos;
208            while want > 0 {
209                let buf = read.fill_buf()?;
210                debug_assert!(!buf.is_empty());
211
212                let end = min(want, buf.len());
213                pos.update(&buf[0..end]);
214
215                read.consume(end);
216                want -= end;
217            }
218            last_byte_pos = *byte_pos;
219
220            if let Some(map) = map {
221                sections.push((pos, map.clone()))
222            } else {
223                // We don't need an empty source map when column is 0 or the next char is a newline.
224                if pos.column != 0 && read.fill_buf()?.first().is_some_and(|&b| b != b'\n') {
225                    sections.push((pos, SourceMap::empty_rope()));
226                }
227            }
228        }
229
230        if sections.len() == 1 && sections[0].0.line == 0 && sections[0].0.column == 0 {
231            Ok(sections.into_iter().next().unwrap().1)
232        } else {
233            SourceMap::sections_to_rope(sections)
234        }
235    }
236}