turbopack_core/source_map/
utils.rs

1use std::collections::HashSet;
2
3use anyhow::{Context, Result};
4use const_format::concatcp;
5use once_cell::sync::Lazy;
6use regex::Regex;
7use serde::{Deserialize, Serialize};
8use turbo_tasks::{ValueToString, Vc};
9use turbo_tasks_fs::{
10    DiskFileSystem, FileContent, FileSystemPath, rope::Rope, util::uri_from_file,
11};
12
13use crate::SOURCE_URL_PROTOCOL;
14
15pub fn add_default_ignore_list(map: &mut sourcemap::SourceMap) {
16    let mut ignored_ids = HashSet::new();
17
18    for (source_id, source) in map.sources().enumerate() {
19        if source.starts_with(concatcp!(SOURCE_URL_PROTOCOL, "///[next]"))
20            || source.starts_with(concatcp!(SOURCE_URL_PROTOCOL, "///[turbopack]"))
21            || source.contains("/node_modules/")
22        {
23            ignored_ids.insert(source_id);
24        }
25    }
26
27    for ignored_id in ignored_ids {
28        map.add_to_ignore_list(ignored_id as _);
29    }
30}
31
32#[derive(Serialize, Deserialize)]
33struct SourceMapSectionOffsetJson {
34    line: u32,
35    offset: u32,
36}
37
38#[derive(Serialize, Deserialize)]
39struct SourceMapSectionItemJson {
40    offset: SourceMapSectionOffsetJson,
41    map: SourceMapJson,
42}
43
44// TODO this could be made (much) more efficient by not even de- and serializing other fields
45// (apart from `sources`) and just keep storing them as strings.
46#[derive(Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48struct SourceMapJson {
49    version: u32,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    file: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    source_root: Option<String>,
54    // Technically a required field, but we don't want to error here.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    sources: Option<Vec<Option<String>>>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    sources_content: Option<Vec<Option<String>>>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    names: Option<Vec<String>>,
61    mappings: String,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    ignore_list: Option<Vec<u32>>,
64
65    // A somewhat widespread non-standard extension
66    debug_id: Option<String>,
67
68    #[serde(skip_serializing_if = "Option::is_none")]
69    sections: Option<Vec<SourceMapSectionItemJson>>,
70}
71
72/// Replace the origin prefix in the `sources` with `turbopack:///` and read the the
73/// `sourceContent`s from disk.
74pub async fn resolve_source_map_sources(
75    map: Option<&Rope>,
76    origin: Vc<FileSystemPath>,
77) -> Result<Option<Rope>> {
78    async fn resolve_source(
79        original_source: &mut String,
80        original_content: &mut Option<String>,
81        origin: Vc<FileSystemPath>,
82    ) -> Result<()> {
83        if let Some(path) = *origin
84            .parent()
85            .try_join((&**original_source).into())
86            .await?
87        {
88            let path_str = path.to_string().await?;
89            let source = format!("{SOURCE_URL_PROTOCOL}///{path_str}");
90            *original_source = source;
91
92            if original_content.is_none() {
93                if let FileContent::Content(file) = &*path.read().await? {
94                    let text = file.content().to_str()?;
95                    *original_content = Some(text.to_string())
96                } else {
97                    *original_content = Some(format!("unable to read source {path_str}"));
98                }
99            }
100        } else {
101            let origin_str = origin.to_string().await?;
102            static INVALID_REGEX: Lazy<Regex> =
103                Lazy::new(|| Regex::new(r#"(?:^|/)(?:\.\.?(?:/|$))+"#).unwrap());
104            let source = INVALID_REGEX.replace_all(original_source, |s: &regex::Captures<'_>| {
105                s[0].replace('.', "_")
106            });
107            *original_source = format!("{SOURCE_URL_PROTOCOL}///{origin_str}/{source}");
108            if original_content.is_none() {
109                *original_content = Some(format!(
110                    "unable to access {original_source} in {origin_str} (it's leaving the \
111                     filesystem root)"
112                ));
113            }
114        }
115        anyhow::Ok(())
116    }
117
118    async fn resolve_map(map: &mut SourceMapJson, origin: Vc<FileSystemPath>) -> Result<()> {
119        if let Some(sources) = &mut map.sources {
120            let mut contents = if let Some(mut contents) = map.sources_content.take() {
121                contents.resize(sources.len(), None);
122                contents
123            } else {
124                Vec::with_capacity(sources.len())
125            };
126
127            for (source, content) in sources.iter_mut().zip(contents.iter_mut()) {
128                if let Some(source) = source {
129                    resolve_source(source, content, origin).await?;
130                }
131            }
132
133            map.sources_content = Some(contents);
134        }
135        Ok(())
136    }
137
138    let Some(map) = map else {
139        return Ok(None);
140    };
141
142    let Ok(mut map): serde_json::Result<SourceMapJson> = serde_json::from_reader(map.read()) else {
143        // Silently ignore invalid sourcemaps
144        return Ok(None);
145    };
146
147    resolve_map(&mut map, origin).await?;
148    for section in map.sections.iter_mut().flatten() {
149        resolve_map(&mut section.map, origin).await?;
150    }
151
152    let map = Rope::from(serde_json::to_vec(&map)?);
153    Ok(Some(map))
154}
155
156/// Turns `turbopack:///[project]` references in sourcemap sources into absolute `file://` uris. This
157/// is useful for debugging environments.
158pub async fn fileify_source_map(
159    map: Option<&Rope>,
160    context_path: Vc<FileSystemPath>,
161) -> Result<Option<Rope>> {
162    let Some(map) = map else {
163        return Ok(None);
164    };
165
166    let Ok(mut map): serde_json::Result<SourceMapJson> = serde_json::from_reader(map.read()) else {
167        // Silently ignore invalid sourcemaps
168        return Ok(None);
169    };
170
171    let context_fs = context_path.fs();
172    let context_fs = &*Vc::try_resolve_downcast_type::<DiskFileSystem>(context_fs)
173        .await?
174        .context("Expected the chunking context to have a DiskFileSystem")?
175        .await?;
176    let prefix = format!("{}///[{}]/", SOURCE_URL_PROTOCOL, context_fs.name());
177
178    let transform_source = async |src: &mut Option<String>| {
179        if let Some(src) = src {
180            if let Some(src_rest) = src.strip_prefix(&prefix) {
181                *src = uri_from_file(context_path, Some(src_rest)).await?;
182            }
183        }
184        anyhow::Ok(())
185    };
186
187    for src in map.sources.iter_mut().flatten() {
188        transform_source(src).await?;
189    }
190    for section in map.sections.iter_mut().flatten() {
191        for src in section.map.sources.iter_mut().flatten() {
192            transform_source(src).await?;
193        }
194    }
195
196    let map = Rope::from(serde_json::to_vec(&map)?);
197
198    Ok(Some(map))
199}