Skip to content

Commit 0084361

Browse files
authored
[turbopack] Fix URL fragment and query handling in Turbopack (#79993)
Fix URL fragment and query handling in Turbopack ## What? This PR improves how URL fragments and query strings are handled in Turbopack by: 1. Standardizing the representation of fragments and queries in `AssetIdent` 2. Ensuring fragments and queries are either empty or include their prefix characters (`#` and `?`) 3. Fixing the URL parsing logic to correctly handle fragments that contain `?` character 4. Removing some ad-hoc workarounds for this issue in the `resolver` and `Assetident::to_string` ## Why? The previous implementation had inconsistencies in how fragments and queries were represented which lead to a variety of confusing issues e.g. does `AssetIdent::to_string` need to add a `?` ?, well sometimes depending on how it was constructed.
1 parent 1f39908 commit 0084361

File tree

9 files changed

+445
-348
lines changed

9 files changed

+445
-348
lines changed

crates/next-api/src/server_actions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ pub async fn to_rsc_context(
213213
// module.
214214
let source = FileSource::new_with_query(
215215
client_module.ident().path().root().join(entry_path.into()),
216-
Vc::cell(entry_query.into()),
216+
entry_query.into(),
217217
);
218218
let module = asset_context
219219
.process(

crates/next-core/src/next_client_reference/ecmascript_client_reference/ecmascript_client_reference_transition.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ impl Transition for NextEcmascriptClientReferenceTransition {
7171
.replace("next/dist/esm/", "next/dist/")
7272
.into(),
7373
);
74-
Vc::upcast(FileSource::new_with_query(path, *ident_ref.query))
74+
Vc::upcast(FileSource::new_with_query_and_fragment(
75+
path,
76+
(*ident_ref.query.await?).clone(),
77+
(*ident_ref.fragment.await?).clone(),
78+
))
7579
} else {
7680
source
7781
};

turbopack/crates/turbopack-core/src/file_source.rs

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,48 @@ use crate::{
1313
/// references to other [Source]s.
1414
#[turbo_tasks::value]
1515
pub struct FileSource {
16-
pub path: ResolvedVc<FileSystemPath>,
17-
pub query: ResolvedVc<RcStr>,
16+
path: ResolvedVc<FileSystemPath>,
17+
query: RcStr,
18+
fragment: RcStr,
19+
}
20+
21+
impl FileSource {
22+
pub fn new(path: Vc<FileSystemPath>) -> Vc<Self> {
23+
FileSource::new_with_query_and_fragment(path, RcStr::default(), RcStr::default())
24+
}
25+
pub fn new_with_query(path: Vc<FileSystemPath>, query: RcStr) -> Vc<Self> {
26+
FileSource::new_with_query_and_fragment(path, query, RcStr::default())
27+
}
1828
}
1929

2030
#[turbo_tasks::value_impl]
2131
impl FileSource {
2232
#[turbo_tasks::function]
23-
pub fn new(path: ResolvedVc<FileSystemPath>) -> Vc<Self> {
33+
pub fn new_with_query_and_fragment(
34+
path: ResolvedVc<FileSystemPath>,
35+
query: RcStr,
36+
fragment: RcStr,
37+
) -> Vc<Self> {
2438
Self::cell(FileSource {
2539
path,
26-
query: ResolvedVc::cell(RcStr::default()),
40+
query,
41+
fragment,
2742
})
2843
}
29-
30-
#[turbo_tasks::function]
31-
pub async fn new_with_query(
32-
path: ResolvedVc<FileSystemPath>,
33-
query: ResolvedVc<RcStr>,
34-
) -> Result<Vc<Self>> {
35-
if query.await?.is_empty() {
36-
Ok(Self::new(*path))
37-
} else {
38-
Ok(Self::cell(FileSource { path, query }))
39-
}
40-
}
4144
}
4245

4346
#[turbo_tasks::value_impl]
4447
impl Source for FileSource {
4548
#[turbo_tasks::function]
4649
fn ident(&self) -> Vc<AssetIdent> {
47-
AssetIdent::from_path(*self.path).with_query(*self.query)
50+
let mut ident = AssetIdent::from_path(*self.path);
51+
if !self.query.is_empty() {
52+
ident = ident.with_query(Vc::cell(self.query.clone()));
53+
}
54+
if !self.fragment.is_empty() {
55+
ident = ident.with_fragment(Vc::cell(self.fragment.clone()));
56+
}
57+
ident
4858
}
4959
}
5060

turbopack/crates/turbopack-core/src/ident.rs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ use crate::resolve::ModulePart;
1515
pub struct AssetIdent {
1616
/// The primary path of the asset
1717
pub path: ResolvedVc<FileSystemPath>,
18-
/// The query string of the asset (e.g. `?foo=bar`)
18+
/// The query string of the asset this is either the empty string or a query string that starts
19+
/// with a `?` (e.g. `?foo=bar`)
1920
pub query: ResolvedVc<RcStr>,
20-
/// The fragment of the asset (e.g. `#foo`)
21-
pub fragment: Option<ResolvedVc<RcStr>>,
21+
/// The fragment of the asset, this is either the empty string or a fragment string that starts
22+
/// with a `#` (e.g. `#foo`)
23+
pub fragment: ResolvedVc<RcStr>,
2224
/// The assets that are nested in this asset
2325
pub assets: Vec<(ResolvedVc<RcStr>, ResolvedVc<AssetIdent>)>,
2426
/// The modifiers of this asset (e.g. `client chunks`)
@@ -57,14 +59,10 @@ impl ValueToString for AssetIdent {
5759
async fn to_string(&self) -> Result<Vc<RcStr>> {
5860
let mut s = self.path.to_string().owned().await?.into_owned();
5961

60-
let query = self.query.await?;
61-
if !query.is_empty() {
62-
write!(s, "?{}", &*query)?;
63-
}
64-
65-
if let Some(fragment) = &self.fragment {
66-
write!(s, "#{}", fragment.await?)?;
67-
}
62+
// The query string is either empty or non-empty starting with `?` so we can just concat
63+
s.push_str(&self.query.await?);
64+
// ditto for fragment
65+
s.push_str(&self.fragment.await?);
6866

6967
if !self.assets.is_empty() {
7068
s.push_str(" {");
@@ -121,8 +119,19 @@ impl ValueToString for AssetIdent {
121119
#[turbo_tasks::value_impl]
122120
impl AssetIdent {
123121
#[turbo_tasks::function]
124-
pub fn new(ident: Value<AssetIdent>) -> Vc<Self> {
125-
ident.into_value().cell()
122+
pub async fn new(ident: Value<AssetIdent>) -> Result<Vc<Self>> {
123+
let query = &*ident.query.await?;
124+
// TODO(lukesandberg); downgrade to debug_assert
125+
assert!(
126+
query.is_empty() || query.starts_with("?"),
127+
"query should be empty or start with a `?`"
128+
);
129+
let fragment = &*ident.fragment.await?;
130+
assert!(
131+
fragment.is_empty() || fragment.starts_with("#"),
132+
"query should be empty or start with a `?`"
133+
);
134+
Ok(ident.into_value().cell())
126135
}
127136

128137
/// Creates an [AssetIdent] from a [Vc<FileSystemPath>]
@@ -131,7 +140,7 @@ impl AssetIdent {
131140
Self::new(Value::new(AssetIdent {
132141
path,
133142
query: ResolvedVc::cell(RcStr::default()),
134-
fragment: None,
143+
fragment: ResolvedVc::cell(RcStr::default()),
135144
assets: Vec::new(),
136145
modifiers: Vec::new(),
137146
parts: Vec::new(),
@@ -147,6 +156,13 @@ impl AssetIdent {
147156
Self::new(Value::new(this))
148157
}
149158

159+
#[turbo_tasks::function]
160+
pub fn with_fragment(&self, fragment: ResolvedVc<RcStr>) -> Vc<Self> {
161+
let mut this = self.clone();
162+
this.fragment = fragment;
163+
Self::new(Value::new(this))
164+
}
165+
150166
#[turbo_tasks::function]
151167
pub fn with_modifier(&self, modifier: ResolvedVc<RcStr>) -> Vc<Self> {
152168
let mut this = self.clone();
@@ -252,9 +268,10 @@ impl AssetIdent {
252268
query.deterministic_hash(&mut hasher);
253269
has_hash = true;
254270
}
255-
if let Some(fragment) = fragment {
271+
let fragment = fragment.await?;
272+
if !fragment.is_empty() {
256273
1_u8.deterministic_hash(&mut hasher);
257-
fragment.await?.deterministic_hash(&mut hasher);
274+
fragment.deterministic_hash(&mut hasher);
258275
has_hash = true;
259276
}
260277
for (key, ident) in assets.iter() {

turbopack/crates/turbopack-core/src/resolve/mod.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2203,18 +2203,17 @@ async fn resolve_relative_request(
22032203

22042204
let fragment_val = fragment.await?;
22052205
if !fragment_val.is_empty() {
2206-
new_path.push(Pattern::Alternatives(
2207-
once(Pattern::Constant("".into()))
2208-
.chain(once(Pattern::Constant(format!("#{fragment_val}").into())))
2209-
.collect(),
2210-
));
2206+
new_path.push(Pattern::Alternatives(vec![
2207+
Pattern::Constant(RcStr::default()),
2208+
Pattern::Constant((*fragment_val).clone()),
2209+
]));
22112210
}
22122211

22132212
if !options_value.fully_specified {
22142213
// Add the extensions as alternatives to the path
22152214
// read_matches keeps the order of alternatives intact
22162215
new_path.push(Pattern::Alternatives(
2217-
once(Pattern::Constant("".into()))
2216+
once(Pattern::Constant(RcStr::default()))
22182217
.chain(
22192218
options_value
22202219
.extensions
@@ -2328,10 +2327,7 @@ async fn resolve_relative_request(
23282327
}
23292328
if !fragment_val.is_empty() {
23302329
// If the fragment is not empty, we need to strip it from the matched pattern
2331-
if let Some(matched_pattern) = matched_pattern
2332-
.strip_suffix(&**fragment_val)
2333-
.and_then(|s| s.strip_suffix('#'))
2334-
{
2330+
if let Some(matched_pattern) = matched_pattern.strip_suffix(&**fragment_val) {
23352331
results.push(
23362332
resolved(
23372333
RequestKey::new(matched_pattern.into()),
@@ -2864,9 +2860,13 @@ async fn resolved(
28642860
Ok(*ResolveResult::source_with_affecting_sources(
28652861
request_key,
28662862
ResolvedVc::upcast(
2867-
FileSource::new_with_query(**path, query)
2868-
.to_resolved()
2869-
.await?,
2863+
FileSource::new_with_query_and_fragment(
2864+
**path,
2865+
(*query.await?).clone(),
2866+
(*fragment.await?).clone(),
2867+
)
2868+
.to_resolved()
2869+
.await?,
28702870
),
28712871
symlinks
28722872
.iter()

0 commit comments

Comments
 (0)