Repository: vercel/next.js
Pull request: Turbopack: extract parse_source_map_comment

<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the PR.
- Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to understand the PR)
- When linking to a Slack thread, you might want to share details of the conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->


Closes PACK-5489

Changed files:
filename: turbopack/crates/turbopack-ecmascript/src/lib.rs
patch: @@ -23,6 +23,7 @@ mod path_visitor;
 pub mod references;
 pub mod runtime_functions;
 pub mod side_effect_optimization;
+pub mod source_map;
 pub(crate) mod special_cases;
 pub(crate) mod static_code;
 mod swc_comments;

filename: turbopack/crates/turbopack-ecmascript/src/references/mod.rs
patch: @@ -61,8 +61,8 @@ use swc_core::{
 use tracing::Instrument;
 use turbo_rcstr::{RcStr, rcstr};
 use turbo_tasks::{
-    FxIndexMap, FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt,
-    UpcastStrict, ValueToString, Vc, trace::TraceRawVcs,
+    FxIndexMap, FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, Upcast,
+    ValueToString, Vc, trace::TraceRawVcs,
 };
 use turbo_tasks_fs::FileSystemPath;
 use turbopack_core::{
@@ -74,7 +74,7 @@ use turbopack_core::{
     error::PrettyPrintError,
     issue::{IssueExt, IssueSeverity, IssueSource, StyledString, analyze::AnalyzeIssue},
     module::Module,
-    reference::{ModuleReference, ModuleReferences, SourceMapReference},
+    reference::{ModuleReference, ModuleReferences},
     reference_type::{CommonJsReferenceSubType, ReferenceType},
     resolve::{
         FindContextFileResult, ModulePart, find_context_file,
@@ -156,12 +156,12 @@ use crate::{
         node::PackageJsonReference,
         require_context::{RequireContextAssetReference, RequireContextMap},
         type_issue::SpecifiedModuleTypeIssue,
-        util::InlineSourceMap,
     },
     runtime_functions::{
         TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXPORTS, TURBOPACK_GLOBAL,
         TURBOPACK_REQUIRE_REAL, TURBOPACK_REQUIRE_STUB, TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS,
     },
+    source_map::parse_source_map_comment,
     tree_shake::{find_turbopack_part_id_in_asserts, part_of_module, split_module},
     utils::{AstPathRange, module_value_to_well_known_object},
 };
@@ -254,11 +254,8 @@ impl AnalyzeEcmascriptModuleResultBuilder {
     }
 
     /// Adds an asset reference to the analysis result.
-    pub fn add_reference(
-        &mut self,
-        reference: ResolvedVc<impl UpcastStrict<Box<dyn ModuleReference>>>,
-    ) {
-        let r = ResolvedVc::upcast(reference);
+    pub fn add_reference(&mut self, reference: ResolvedVc<impl Upcast<Box<dyn ModuleReference>>>) {
+        let r = ResolvedVc::upcast_non_strict(reference);
         self.references.insert(r);
     }
 
@@ -668,51 +665,18 @@ pub async fn analyse_ecmascript_module_internal(
     if options.extract_source_map {
         let span = tracing::info_span!("source map reference");
         async {
-            // Only use the last sourceMappingURL comment by spec
-            let mut paths_by_pos = Vec::new();
-            for (pos, comments) in comments.trailing.iter() {
-                for comment in comments.iter().rev() {
-                    static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
-                        LazyLock::new(|| Regex::new(r"# sourceMappingURL=(.*)$").unwrap());
-                    if let Some(m) = SOURCE_MAP_FILE_REFERENCE.captures(&comment.text) {
-                        let path = m.get(1).unwrap().as_str();
-                        paths_by_pos.push((pos, path));
-                        break;
-                    }
-                }
-            }
-
-            let mut source_map_from_comment = false;
-            if let Some((_, path)) = paths_by_pos.into_iter().max_by_key(|&(pos, _)| pos) {
-                static JSON_DATA_URL_BASE64: LazyLock<Regex> = LazyLock::new(|| {
-                    Regex::new(r"^data:application\/json;(?:charset=utf-8;)?base64").unwrap()
-                });
-                let origin_path = origin.origin_path().owned().await?;
-                if path.ends_with(".map") {
-                    let source_map_origin = origin_path.parent().join(path)?;
-                    let reference = SourceMapReference::new(origin_path, source_map_origin)
-                        .to_resolved()
-                        .await?;
+            if let Some((source_map, reference)) = parse_source_map_comment(
+                source,
+                Either::Left(comments),
+                &*origin.origin_path().await?,
+            )
+            .await?
+            {
+                analysis.set_source_map(source_map);
+                if let Some(reference) = reference {
                     analysis.add_reference(reference);
-                    analysis.set_source_map(ResolvedVc::upcast(reference));
-                    source_map_from_comment = true;
-                } else if JSON_DATA_URL_BASE64.is_match(path) {
-                    analysis.set_source_map(ResolvedVc::upcast(
-                        InlineSourceMap {
-                            origin_path,
-                            source_map: path.into(),
-                        }
-                        .resolved_cell(),
-                    ));
-                    source_map_from_comment = true;
                 }
             }
-            if !source_map_from_comment
-                && let Some(generate_source_map) =
-                    ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source)
-            {
-                analysis.set_source_map(generate_source_map);
-            }
             anyhow::Ok(())
         }
         .instrument(span)

filename: turbopack/crates/turbopack-ecmascript/src/references/util.rs
patch: @@ -2,13 +2,7 @@ use anyhow::Result;
 use swc_core::{ecma::ast::Expr, quote};
 use turbo_rcstr::{RcStr, rcstr};
 use turbo_tasks::Vc;
-use turbo_tasks_fs::{FileSystemPath, rope::Rope};
-use turbopack_core::{
-    resolve::parse::Request,
-    source_map::{
-        GenerateSourceMap, OptionStringifiedSourceMap, utils::resolve_source_map_sources,
-    },
-};
+use turbopack_core::{self, resolve::parse::Request};
 
 /// Creates a IIFE expression that throws a "Cannot find module" error for the
 /// given request string
@@ -42,36 +36,3 @@ pub async fn request_to_string(request: Vc<Request>) -> Result<Vc<RcStr>> {
             .unwrap_or(rcstr!("unknown")),
     ))
 }
-
-#[turbo_tasks::value(shared)]
-#[derive(Debug, Clone)]
-pub struct InlineSourceMap {
-    /// The file path of the module containing the sourcemap data URL
-    pub origin_path: FileSystemPath,
-    /// The Base64 encoded JSON sourcemap string
-    pub source_map: RcStr,
-}
-
-#[turbo_tasks::value_impl]
-impl GenerateSourceMap for InlineSourceMap {
-    #[turbo_tasks::function]
-    pub async fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
-        let source_map = maybe_decode_data_url(&self.source_map);
-        let source_map =
-            resolve_source_map_sources(source_map.as_ref(), self.origin_path.clone()).await?;
-        Ok(Vc::cell(source_map))
-    }
-}
-
-fn maybe_decode_data_url(url: &str) -> Option<Rope> {
-    const DATA_PREAMBLE: &str = "data:application/json;base64,";
-
-    if !url.starts_with(DATA_PREAMBLE) {
-        return None;
-    }
-    let data_b64 = &url[DATA_PREAMBLE.len()..];
-    data_encoding::BASE64
-        .decode(data_b64.as_bytes())
-        .ok()
-        .map(Rope::from)
-}

filename: turbopack/crates/turbopack-ecmascript/src/source_map.rs
patch: @@ -0,0 +1,136 @@
+use std::sync::LazyLock;
+
+use anyhow::Result;
+use either::Either;
+use regex::Regex;
+use turbo_rcstr::RcStr;
+use turbo_tasks::{ResolvedVc, Vc};
+use turbo_tasks_fs::{FileSystemPath, rope::Rope};
+use turbopack_core::{
+    reference::{ModuleReference, SourceMapReference},
+    source::Source,
+    source_map::{
+        GenerateSourceMap, OptionStringifiedSourceMap, utils::resolve_source_map_sources,
+    },
+};
+
+use crate::swc_comments::ImmutableComments;
+
+#[turbo_tasks::value(shared)]
+#[derive(Debug, Clone)]
+pub struct InlineSourceMap {
+    /// The file path of the module containing the sourcemap data URL
+    pub origin_path: FileSystemPath,
+    /// The Base64 encoded JSON sourcemap string
+    pub source_map: RcStr,
+}
+
+#[turbo_tasks::value_impl]
+impl GenerateSourceMap for InlineSourceMap {
+    #[turbo_tasks::function]
+    pub async fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
+        let source_map = maybe_decode_data_url(&self.source_map);
+        let source_map =
+            resolve_source_map_sources(source_map.as_ref(), self.origin_path.clone()).await?;
+        Ok(Vc::cell(source_map))
+    }
+}
+
+fn maybe_decode_data_url(url: &str) -> Option<Rope> {
+    const DATA_PREAMBLE: &str = "data:application/json;base64,";
+    const DATA_PREAMBLE_CHARSET: &str = "data:application/json;charset=utf-8;base64,";
+
+    let data_b64 = if let Some(data) = url.strip_prefix(DATA_PREAMBLE) {
+        data
+    } else if let Some(data) = url.strip_prefix(DATA_PREAMBLE_CHARSET) {
+        data
+    } else {
+        return None;
+    };
+
+    data_encoding::BASE64
+        .decode(data_b64.as_bytes())
+        .ok()
+        .map(Rope::from)
+}
+
+pub async fn parse_source_map_comment(
+    source: ResolvedVc<Box<dyn Source>>,
+    comments: Either<&ImmutableComments, &str>,
+    origin_path: &FileSystemPath,
+) -> Result<
+    Option<(
+        ResolvedVc<Box<dyn GenerateSourceMap>>,
+        Option<ResolvedVc<Box<dyn ModuleReference>>>,
+    )>,
+> {
+    // See https://tc39.es/ecma426/#sec-MatchSourceMapURL for the official regex.
+    let source_map_comment = match comments {
+        Either::Left(comments) => {
+            // Only use the last sourceMappingURL comment by spec
+            static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
+                LazyLock::new(|| Regex::new(r"[@#]\s*sourceMappingURL=(\S*)$").unwrap());
+            let mut paths_by_pos = Vec::new();
+            for (pos, comments) in comments.trailing.iter() {
+                for comment in comments.iter().rev() {
+                    if let Some(m) = SOURCE_MAP_FILE_REFERENCE.captures(&comment.text) {
+                        let path = m.get(1).unwrap().as_str();
+                        paths_by_pos.push((pos, path));
+                        break;
+                    }
+                }
+            }
+            paths_by_pos
+                .into_iter()
+                .max_by_key(|&(pos, _)| pos)
+                .map(|(_, path)| path)
+        }
+        Either::Right(file_content) => {
+            // Find a matching comment at the end of the file (only followed by whitespace)
+            static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
+                LazyLock::new(|| Regex::new(r"\n//[@#]\s*sourceMappingURL=(\S*)[\n\s]*$").unwrap());
+
+            file_content.rfind("\n//").and_then(|start| {
+                let line = &file_content[start..];
+                SOURCE_MAP_FILE_REFERENCE
+                    .captures(line)
+                    .map(|m| m.get(1).unwrap().as_str())
+            })
+        }
+    };
+
+    if let Some(path) = source_map_comment {
+        static JSON_DATA_URL_BASE64: LazyLock<Regex> = LazyLock::new(|| {
+            Regex::new(r"^data:application\/json;(?:charset=utf-8;)?base64").unwrap()
+        });
+        if path.ends_with(".map") {
+            let source_map_origin = origin_path.parent().join(path)?;
+            let reference = SourceMapReference::new(origin_path.clone(), source_map_origin)
+                .to_resolved()
+                .await?;
+            return Ok(Some((
+                ResolvedVc::upcast(reference),
+                Some(ResolvedVc::upcast(reference)),
+            )));
+        } else if JSON_DATA_URL_BASE64.is_match(path) {
+            return Ok(Some((
+                ResolvedVc::upcast(
+                    InlineSourceMap {
+                        origin_path: origin_path.clone(),
+                        source_map: path.into(),
+                    }
+                    .resolved_cell(),
+                ),
+                None,
+            )));
+        }
+    }
+
+    if let Some(generate_source_map) =
+        ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source)
+    {
+        return Ok(Some((generate_source_map, None)));
+    }
+
+    Ok(None)
+}