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::instrument;
9use turbo_rcstr::RcStr;
10use turbo_tasks::{ResolvedVc, Vc};
11use turbo_tasks_fs::{
12    File, FileContent,
13    rope::{Rope, RopeBuilder},
14};
15use turbo_tasks_hash::hash_xxh3_hash64;
16
17use crate::{
18    debug_id::generate_debug_id,
19    output::OutputAsset,
20    source_map::{GenerateSourceMap, SourceMap, SourceMapAsset},
21    source_pos::SourcePos,
22};
23
24/// A mapping of byte-offset in the code string to an associated source map.
25pub type Mapping = (usize, Option<Rope>);
26
27/// Code stores combined output code and the source map of that output code.
28#[turbo_tasks::value(shared)]
29#[derive(Debug, Clone)]
30pub struct Code {
31    code: Rope,
32    mappings: Vec<Mapping>,
33    should_generate_debug_id: bool,
34}
35
36impl Code {
37    pub fn source_code(&self) -> &Rope {
38        &self.code
39    }
40
41    /// Tests if any code in this Code contains an associated source map.
42    pub fn has_source_map(&self) -> bool {
43        !self.mappings.is_empty()
44    }
45    // Whether this code should have a debug id generated for it
46    pub fn should_generate_debug_id(&self) -> bool {
47        self.should_generate_debug_id
48    }
49
50    /// Take the source code out of the Code.
51    pub fn into_source_code(self) -> Rope {
52        self.code
53    }
54
55    // Formats the code with the source map and debug id comments as
56    pub async fn to_rope_with_magic_comments(
57        self: Vc<Self>,
58        source_map_path_fn: impl FnOnce() -> Vc<SourceMapAsset>,
59    ) -> Result<Rope> {
60        let code = self.await?;
61        Ok(
62            if code.has_source_map() || code.should_generate_debug_id() {
63                let mut rope_builder = RopeBuilder::default();
64                let debug_id = self.debug_id().await?;
65                // hand minified version of
66                // ```javascript
67                //  !() => {
68                //    (globalThis ??= {})[new g.Error().stack] = <debug_id>;
69                // }()
70                // ```
71                // But we need to be compatible with older runtimes since this code isn't transpiled
72                // according to a browser list. So we use `var`, `function` and
73                // try-caatch since we cannot rely on `Error.stack` being available.
74                // And finally to ensure it is on one line since that is what the source map
75                // expects.
76                // So like Thanos we have to do it ourselves.
77                if let Some(debug_id) = &*debug_id {
78                    // Test for `globalThis` first since it is available on all platforms released
79                    // since 2018! so it will mostly work
80                    const GLOBALTHIS_EXPR: &str = r#""undefined"!=typeof globalThis?globalThis:"undefined"!=typeof global?global:"undefined"!=typeof window?window:"undefined"!=typeof self?self:{}"#;
81                    const GLOBAL_VAR_NAME: &str = "_debugIds";
82                    writeln!(
83                        rope_builder,
84                        r#";!function(){{try {{ var e={GLOBALTHIS_EXPR},n=(new e.Error).stack;n&&((e.{GLOBAL_VAR_NAME}|| (e.{GLOBAL_VAR_NAME}={{}}))[n]="{debug_id}")}}catch(e){{}}}}();"#,
85                    )?;
86                }
87
88                rope_builder.concat(&code.code);
89                rope_builder.push_static_bytes(b"\n");
90                // Add debug ID comment if enabled
91                if let Some(debug_id) = &*debug_id {
92                    write!(rope_builder, "\n//# debugId={}", debug_id)?;
93                }
94
95                if code.has_source_map() {
96                    let source_map_path = source_map_path_fn().path().await?;
97                    write!(
98                        rope_builder,
99                        "\n//# sourceMappingURL={}",
100                        urlencoding::encode(source_map_path.file_name())
101                    )?;
102                }
103                rope_builder.build()
104            } else {
105                code.code.clone()
106            },
107        )
108    }
109}
110
111/// CodeBuilder provides a mutable container to append source code.
112pub struct CodeBuilder {
113    code: RopeBuilder,
114    mappings: Option<Vec<Mapping>>,
115    should_generate_debug_id: bool,
116}
117
118impl Default for CodeBuilder {
119    fn default() -> Self {
120        Self {
121            code: RopeBuilder::default(),
122            mappings: Some(Vec::new()),
123            should_generate_debug_id: false,
124        }
125    }
126}
127
128impl CodeBuilder {
129    pub fn new(collect_mappings: bool, should_generate_debug_id: bool) -> Self {
130        Self {
131            code: RopeBuilder::default(),
132            mappings: collect_mappings.then(Vec::new),
133            should_generate_debug_id,
134        }
135    }
136
137    /// Pushes synthetic runtime code without an associated source map. This is
138    /// the default concatenation operation, but it's designed to be used
139    /// with the `+=` operator.
140    fn push_static_bytes(&mut self, code: &'static [u8]) {
141        self.push_map(None);
142        self.code.push_static_bytes(code);
143    }
144
145    /// Pushes original user code with an optional source map if one is
146    /// available. If it's not, this is no different than pushing Synthetic
147    /// code.
148    pub fn push_source(&mut self, code: &Rope, map: Option<Rope>) {
149        self.push_map(map);
150        self.code += code;
151    }
152
153    /// Copies the Synthetic/Original code of an already constructed Code into
154    /// this instance.
155    ///
156    /// This adjusts the source map to be relative to the new code object
157    pub fn push_code(&mut self, prebuilt: &Code) {
158        if let Some((index, _)) = prebuilt.mappings.first() {
159            if *index > 0 {
160                // If the index is positive, then the code starts with a synthetic section. We
161                // may need to push an empty map in order to end the current
162                // section's mappings.
163                self.push_map(None);
164            }
165
166            let len = self.code.len();
167            if let Some(mappings) = self.mappings.as_mut() {
168                mappings.extend(
169                    prebuilt
170                        .mappings
171                        .iter()
172                        .map(|(index, map)| (index + len, map.clone())),
173                );
174            }
175        } else {
176            self.push_map(None);
177        }
178
179        self.code += &prebuilt.code;
180    }
181
182    /// Setting breakpoints on synthetic code can cause weird behaviors
183    /// because Chrome will treat the location as belonging to the previous
184    /// original code section. By inserting an empty source map when reaching a
185    /// synthetic section directly after an original section, we tell Chrome
186    /// that the previous map ended at this point.
187    fn push_map(&mut self, map: Option<Rope>) {
188        let Some(mappings) = self.mappings.as_mut() else {
189            return;
190        };
191        if map.is_none() && matches!(mappings.last(), None | Some((_, None))) {
192            // No reason to push an empty map directly after an empty map
193            return;
194        }
195
196        debug_assert!(
197            map.is_some() || !mappings.is_empty(),
198            "the first mapping is never a None"
199        );
200        mappings.push((self.code.len(), map));
201    }
202
203    /// Tests if any code in this CodeBuilder contains an associated source map.
204    pub fn has_source_map(&self) -> bool {
205        self.mappings
206            .as_ref()
207            .is_some_and(|mappings| !mappings.is_empty())
208    }
209
210    pub fn build(self) -> Code {
211        Code {
212            code: self.code.build(),
213            mappings: self.mappings.unwrap_or_default(),
214            should_generate_debug_id: self.should_generate_debug_id,
215        }
216    }
217}
218
219impl ops::AddAssign<&'static str> for CodeBuilder {
220    fn add_assign(&mut self, rhs: &'static str) {
221        self.push_static_bytes(rhs.as_bytes());
222    }
223}
224
225impl ops::AddAssign<&'static str> for &mut CodeBuilder {
226    fn add_assign(&mut self, rhs: &'static str) {
227        self.push_static_bytes(rhs.as_bytes());
228    }
229}
230
231impl Write for CodeBuilder {
232    fn write(&mut self, bytes: &[u8]) -> IoResult<usize> {
233        self.push_map(None);
234        self.code.write(bytes)
235    }
236
237    fn flush(&mut self) -> IoResult<()> {
238        self.code.flush()
239    }
240}
241
242impl From<Code> for CodeBuilder {
243    fn from(code: Code) -> Self {
244        let mut builder = CodeBuilder::default();
245        builder.push_code(&code);
246        builder
247    }
248}
249
250#[turbo_tasks::value_impl]
251impl GenerateSourceMap for Code {
252    /// Generates the source map out of all the pushed Original code.
253    /// The SourceMap v3 spec has a "sectioned" source map specifically designed
254    /// for concatenation in post-processing steps. This format consists of
255    /// a `sections` array, with section item containing a `offset` object
256    /// and a `map` object. The section's map applies only after the
257    /// starting offset, and until the start of the next section. This is by
258    /// far the simplest way to concatenate the source maps of the multiple
259    /// chunk items into a single map file.
260    #[turbo_tasks::function]
261    pub async fn generate_source_map(self: ResolvedVc<Self>) -> Result<Vc<FileContent>> {
262        let debug_id = self.debug_id().owned().await?;
263        Ok(FileContent::Content(File::from(self.await?.generate_source_map_ref(debug_id))).cell())
264    }
265}
266
267#[turbo_tasks::value(transparent)]
268pub struct OptionDebugId(Option<RcStr>);
269
270#[turbo_tasks::value_impl]
271impl Code {
272    /// Returns the hash of the source code of this Code.
273    #[turbo_tasks::function]
274    pub fn source_code_hash(&self) -> Vc<u64> {
275        let code = self;
276        let hash = hash_xxh3_hash64(code.source_code());
277        Vc::cell(hash)
278    }
279
280    #[turbo_tasks::function]
281    pub fn debug_id(&self) -> Vc<OptionDebugId> {
282        Vc::cell(if self.should_generate_debug_id {
283            Some(generate_debug_id(self.source_code()))
284        } else {
285            None
286        })
287    }
288}
289
290impl Code {
291    /// Generates a source map from the code's mappings.
292    #[instrument(level = "trace", name = "Code::generate_source_map", skip_all)]
293    pub fn generate_source_map_ref(&self, debug_id: Option<RcStr>) -> Rope {
294        // A debug id should be passed only if the code should generate a debug id, it is however
295        // allowed to turn it off to access intermediate states of the code (e.g. for minification)
296        debug_assert!(debug_id.is_none() || self.should_generate_debug_id);
297        // If there is a debug id the first line will be modifying the global object. see
298        // `[to_rope_with_magic_comments]` for more details.
299        let mut pos = SourcePos::new(if debug_id.is_some() { 1 } else { 0 });
300
301        let mut last_byte_pos = 0;
302
303        let mut sections = Vec::with_capacity(self.mappings.len());
304        let mut read = self.code.read();
305        for (byte_pos, map) in &self.mappings {
306            let mut want = byte_pos - last_byte_pos;
307            while want > 0 {
308                // `fill_buf` never returns an error.
309                let buf = read.fill_buf().unwrap();
310                debug_assert!(!buf.is_empty());
311
312                let end = min(want, buf.len());
313                pos.update(&buf[0..end]);
314
315                read.consume(end);
316                want -= end;
317            }
318            last_byte_pos = *byte_pos;
319
320            if let Some(map) = map {
321                sections.push((pos, map.clone()))
322            } else {
323                // We don't need an empty source map when column is 0 or the next char is a newline.
324                if pos.column != 0
325                    && read
326                        .fill_buf()
327                        .unwrap()
328                        .first()
329                        .is_some_and(|&b| b != b'\n')
330                {
331                    sections.push((pos, SourceMap::empty_rope()));
332                }
333            }
334        }
335
336        if sections.len() == 1
337            && sections[0].0.line == 0
338            && sections[0].0.column == 0
339            && debug_id.is_none()
340        {
341            sections.into_iter().next().unwrap().1
342        } else {
343            SourceMap::sections_to_rope(sections, debug_id)
344        }
345    }
346}