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