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