Skip to content

Commit 8b0d684

Browse files
authored
Turbopack: extract parse_source_map_comment (#83938)
<!-- 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 # -->
1 parent cef3dd0 commit 8b0d684

File tree

4 files changed

+153
-91
lines changed

4 files changed

+153
-91
lines changed

turbopack/crates/turbopack-ecmascript/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ mod path_visitor;
2323
pub mod references;
2424
pub mod runtime_functions;
2525
pub mod side_effect_optimization;
26+
pub mod source_map;
2627
pub(crate) mod special_cases;
2728
pub(crate) mod static_code;
2829
mod swc_comments;

turbopack/crates/turbopack-ecmascript/src/references/mod.rs

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ use swc_core::{
6161
use tracing::Instrument;
6262
use turbo_rcstr::{RcStr, rcstr};
6363
use turbo_tasks::{
64-
FxIndexMap, FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt,
65-
UpcastStrict, ValueToString, Vc, trace::TraceRawVcs,
64+
FxIndexMap, FxIndexSet, NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, Upcast,
65+
ValueToString, Vc, trace::TraceRawVcs,
6666
};
6767
use turbo_tasks_fs::FileSystemPath;
6868
use turbopack_core::{
@@ -74,7 +74,7 @@ use turbopack_core::{
7474
error::PrettyPrintError,
7575
issue::{IssueExt, IssueSeverity, IssueSource, StyledString, analyze::AnalyzeIssue},
7676
module::Module,
77-
reference::{ModuleReference, ModuleReferences, SourceMapReference},
77+
reference::{ModuleReference, ModuleReferences},
7878
reference_type::{CommonJsReferenceSubType, ReferenceType},
7979
resolve::{
8080
FindContextFileResult, ModulePart, find_context_file,
@@ -156,12 +156,12 @@ use crate::{
156156
node::PackageJsonReference,
157157
require_context::{RequireContextAssetReference, RequireContextMap},
158158
type_issue::SpecifiedModuleTypeIssue,
159-
util::InlineSourceMap,
160159
},
161160
runtime_functions::{
162161
TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE, TURBOPACK_EXPORTS, TURBOPACK_GLOBAL,
163162
TURBOPACK_REQUIRE_REAL, TURBOPACK_REQUIRE_STUB, TURBOPACK_RUNTIME_FUNCTION_SHORTCUTS,
164163
},
164+
source_map::parse_source_map_comment,
165165
tree_shake::{find_turbopack_part_id_in_asserts, part_of_module, split_module},
166166
utils::{AstPathRange, module_value_to_well_known_object},
167167
};
@@ -254,11 +254,8 @@ impl AnalyzeEcmascriptModuleResultBuilder {
254254
}
255255

256256
/// Adds an asset reference to the analysis result.
257-
pub fn add_reference(
258-
&mut self,
259-
reference: ResolvedVc<impl UpcastStrict<Box<dyn ModuleReference>>>,
260-
) {
261-
let r = ResolvedVc::upcast(reference);
257+
pub fn add_reference(&mut self, reference: ResolvedVc<impl Upcast<Box<dyn ModuleReference>>>) {
258+
let r = ResolvedVc::upcast_non_strict(reference);
262259
self.references.insert(r);
263260
}
264261

@@ -668,51 +665,18 @@ pub async fn analyse_ecmascript_module_internal(
668665
if options.extract_source_map {
669666
let span = tracing::info_span!("source map reference");
670667
async {
671-
// Only use the last sourceMappingURL comment by spec
672-
let mut paths_by_pos = Vec::new();
673-
for (pos, comments) in comments.trailing.iter() {
674-
for comment in comments.iter().rev() {
675-
static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
676-
LazyLock::new(|| Regex::new(r"# sourceMappingURL=(.*)$").unwrap());
677-
if let Some(m) = SOURCE_MAP_FILE_REFERENCE.captures(&comment.text) {
678-
let path = m.get(1).unwrap().as_str();
679-
paths_by_pos.push((pos, path));
680-
break;
681-
}
682-
}
683-
}
684-
685-
let mut source_map_from_comment = false;
686-
if let Some((_, path)) = paths_by_pos.into_iter().max_by_key(|&(pos, _)| pos) {
687-
static JSON_DATA_URL_BASE64: LazyLock<Regex> = LazyLock::new(|| {
688-
Regex::new(r"^data:application\/json;(?:charset=utf-8;)?base64").unwrap()
689-
});
690-
let origin_path = origin.origin_path().owned().await?;
691-
if path.ends_with(".map") {
692-
let source_map_origin = origin_path.parent().join(path)?;
693-
let reference = SourceMapReference::new(origin_path, source_map_origin)
694-
.to_resolved()
695-
.await?;
668+
if let Some((source_map, reference)) = parse_source_map_comment(
669+
source,
670+
Either::Left(comments),
671+
&*origin.origin_path().await?,
672+
)
673+
.await?
674+
{
675+
analysis.set_source_map(source_map);
676+
if let Some(reference) = reference {
696677
analysis.add_reference(reference);
697-
analysis.set_source_map(ResolvedVc::upcast(reference));
698-
source_map_from_comment = true;
699-
} else if JSON_DATA_URL_BASE64.is_match(path) {
700-
analysis.set_source_map(ResolvedVc::upcast(
701-
InlineSourceMap {
702-
origin_path,
703-
source_map: path.into(),
704-
}
705-
.resolved_cell(),
706-
));
707-
source_map_from_comment = true;
708678
}
709679
}
710-
if !source_map_from_comment
711-
&& let Some(generate_source_map) =
712-
ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source)
713-
{
714-
analysis.set_source_map(generate_source_map);
715-
}
716680
anyhow::Ok(())
717681
}
718682
.instrument(span)

turbopack/crates/turbopack-ecmascript/src/references/util.rs

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@ use anyhow::Result;
22
use swc_core::{ecma::ast::Expr, quote};
33
use turbo_rcstr::{RcStr, rcstr};
44
use turbo_tasks::Vc;
5-
use turbo_tasks_fs::{FileSystemPath, rope::Rope};
6-
use turbopack_core::{
7-
resolve::parse::Request,
8-
source_map::{
9-
GenerateSourceMap, OptionStringifiedSourceMap, utils::resolve_source_map_sources,
10-
},
11-
};
5+
use turbopack_core::{self, resolve::parse::Request};
126

137
/// Creates a IIFE expression that throws a "Cannot find module" error for the
148
/// given request string
@@ -42,36 +36,3 @@ pub async fn request_to_string(request: Vc<Request>) -> Result<Vc<RcStr>> {
4236
.unwrap_or(rcstr!("unknown")),
4337
))
4438
}
45-
46-
#[turbo_tasks::value(shared)]
47-
#[derive(Debug, Clone)]
48-
pub struct InlineSourceMap {
49-
/// The file path of the module containing the sourcemap data URL
50-
pub origin_path: FileSystemPath,
51-
/// The Base64 encoded JSON sourcemap string
52-
pub source_map: RcStr,
53-
}
54-
55-
#[turbo_tasks::value_impl]
56-
impl GenerateSourceMap for InlineSourceMap {
57-
#[turbo_tasks::function]
58-
pub async fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
59-
let source_map = maybe_decode_data_url(&self.source_map);
60-
let source_map =
61-
resolve_source_map_sources(source_map.as_ref(), self.origin_path.clone()).await?;
62-
Ok(Vc::cell(source_map))
63-
}
64-
}
65-
66-
fn maybe_decode_data_url(url: &str) -> Option<Rope> {
67-
const DATA_PREAMBLE: &str = "data:application/json;base64,";
68-
69-
if !url.starts_with(DATA_PREAMBLE) {
70-
return None;
71-
}
72-
let data_b64 = &url[DATA_PREAMBLE.len()..];
73-
data_encoding::BASE64
74-
.decode(data_b64.as_bytes())
75-
.ok()
76-
.map(Rope::from)
77-
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use std::sync::LazyLock;
2+
3+
use anyhow::Result;
4+
use either::Either;
5+
use regex::Regex;
6+
use turbo_rcstr::RcStr;
7+
use turbo_tasks::{ResolvedVc, Vc};
8+
use turbo_tasks_fs::{FileSystemPath, rope::Rope};
9+
use turbopack_core::{
10+
reference::{ModuleReference, SourceMapReference},
11+
source::Source,
12+
source_map::{
13+
GenerateSourceMap, OptionStringifiedSourceMap, utils::resolve_source_map_sources,
14+
},
15+
};
16+
17+
use crate::swc_comments::ImmutableComments;
18+
19+
#[turbo_tasks::value(shared)]
20+
#[derive(Debug, Clone)]
21+
pub struct InlineSourceMap {
22+
/// The file path of the module containing the sourcemap data URL
23+
pub origin_path: FileSystemPath,
24+
/// The Base64 encoded JSON sourcemap string
25+
pub source_map: RcStr,
26+
}
27+
28+
#[turbo_tasks::value_impl]
29+
impl GenerateSourceMap for InlineSourceMap {
30+
#[turbo_tasks::function]
31+
pub async fn generate_source_map(&self) -> Result<Vc<OptionStringifiedSourceMap>> {
32+
let source_map = maybe_decode_data_url(&self.source_map);
33+
let source_map =
34+
resolve_source_map_sources(source_map.as_ref(), self.origin_path.clone()).await?;
35+
Ok(Vc::cell(source_map))
36+
}
37+
}
38+
39+
fn maybe_decode_data_url(url: &str) -> Option<Rope> {
40+
const DATA_PREAMBLE: &str = "data:application/json;base64,";
41+
const DATA_PREAMBLE_CHARSET: &str = "data:application/json;charset=utf-8;base64,";
42+
43+
let data_b64 = if let Some(data) = url.strip_prefix(DATA_PREAMBLE) {
44+
data
45+
} else if let Some(data) = url.strip_prefix(DATA_PREAMBLE_CHARSET) {
46+
data
47+
} else {
48+
return None;
49+
};
50+
51+
data_encoding::BASE64
52+
.decode(data_b64.as_bytes())
53+
.ok()
54+
.map(Rope::from)
55+
}
56+
57+
pub async fn parse_source_map_comment(
58+
source: ResolvedVc<Box<dyn Source>>,
59+
comments: Either<&ImmutableComments, &str>,
60+
origin_path: &FileSystemPath,
61+
) -> Result<
62+
Option<(
63+
ResolvedVc<Box<dyn GenerateSourceMap>>,
64+
Option<ResolvedVc<Box<dyn ModuleReference>>>,
65+
)>,
66+
> {
67+
// See https://tc39.es/ecma426/#sec-MatchSourceMapURL for the official regex.
68+
let source_map_comment = match comments {
69+
Either::Left(comments) => {
70+
// Only use the last sourceMappingURL comment by spec
71+
static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
72+
LazyLock::new(|| Regex::new(r"[@#]\s*sourceMappingURL=(\S*)$").unwrap());
73+
let mut paths_by_pos = Vec::new();
74+
for (pos, comments) in comments.trailing.iter() {
75+
for comment in comments.iter().rev() {
76+
if let Some(m) = SOURCE_MAP_FILE_REFERENCE.captures(&comment.text) {
77+
let path = m.get(1).unwrap().as_str();
78+
paths_by_pos.push((pos, path));
79+
break;
80+
}
81+
}
82+
}
83+
paths_by_pos
84+
.into_iter()
85+
.max_by_key(|&(pos, _)| pos)
86+
.map(|(_, path)| path)
87+
}
88+
Either::Right(file_content) => {
89+
// Find a matching comment at the end of the file (only followed by whitespace)
90+
static SOURCE_MAP_FILE_REFERENCE: LazyLock<Regex> =
91+
LazyLock::new(|| Regex::new(r"\n//[@#]\s*sourceMappingURL=(\S*)[\n\s]*$").unwrap());
92+
93+
file_content.rfind("\n//").and_then(|start| {
94+
let line = &file_content[start..];
95+
SOURCE_MAP_FILE_REFERENCE
96+
.captures(line)
97+
.map(|m| m.get(1).unwrap().as_str())
98+
})
99+
}
100+
};
101+
102+
if let Some(path) = source_map_comment {
103+
static JSON_DATA_URL_BASE64: LazyLock<Regex> = LazyLock::new(|| {
104+
Regex::new(r"^data:application\/json;(?:charset=utf-8;)?base64").unwrap()
105+
});
106+
if path.ends_with(".map") {
107+
let source_map_origin = origin_path.parent().join(path)?;
108+
let reference = SourceMapReference::new(origin_path.clone(), source_map_origin)
109+
.to_resolved()
110+
.await?;
111+
return Ok(Some((
112+
ResolvedVc::upcast(reference),
113+
Some(ResolvedVc::upcast(reference)),
114+
)));
115+
} else if JSON_DATA_URL_BASE64.is_match(path) {
116+
return Ok(Some((
117+
ResolvedVc::upcast(
118+
InlineSourceMap {
119+
origin_path: origin_path.clone(),
120+
source_map: path.into(),
121+
}
122+
.resolved_cell(),
123+
),
124+
None,
125+
)));
126+
}
127+
}
128+
129+
if let Some(generate_source_map) =
130+
ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source)
131+
{
132+
return Ok(Some((generate_source_map, None)));
133+
}
134+
135+
Ok(None)
136+
}

0 commit comments

Comments
 (0)