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