Skip to main content

turbopack_core/
code_builder.rs

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