turbopack_ecmascript/references/esm/url.rs
1use anyhow::{Result, bail};
2use bincode::{Decode, Encode};
3use swc_core::{
4 ecma::ast::{Expr, ExprOrSpread, NewExpr},
5 quote,
6};
7use turbo_tasks::{NonLocalValue, ResolvedVc, ValueToString, Vc, debug::ValueDebugFormat};
8use turbopack_core::{
9 chunk::{ChunkingContext, ChunkingType, ModuleChunkItemIdExt},
10 environment::Rendering,
11 issue::IssueSource,
12 reference::ModuleReference,
13 reference_type::{ReferenceType, UrlReferenceSubType},
14 resolve::{
15 ExternalType, ModuleResolveResult, ResolveErrorMode, origin::ResolveOrigin, parse::Request,
16 url_resolve,
17 },
18};
19
20use crate::{
21 ast_path_trie::{AstPathId, AstPathTrie, AstPathTrieBuilder},
22 code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
23 create_visitor,
24 references::esm::base::ReferencedAsset,
25 runtime_functions::{
26 TURBOPACK_RELATIVE_URL, TURBOPACK_REQUIRE, TURBOPACK_RESOLVE_MODULE_ID_PATH,
27 },
28 utils::module_id_to_lit,
29};
30
31/// Determines how to treat `new URL(...)` rewrites.
32/// This allows to construct url depends on the different building context,
33/// e.g. SSR, CSR, or Node.js.
34#[turbo_tasks::task_input]
35#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Encode, Decode)]
36pub enum UrlRewriteBehavior {
37 /// Omits base, resulting in a relative URL.
38 Relative,
39 /// Uses the full URL, including the base.
40 Full,
41 /// Do not attempt to rewrite the URL.
42 None,
43}
44
45/// URL Asset References are injected during code analysis when we find a
46/// (statically analyzable) `new URL("path", import.meta.url)`.
47///
48/// It's responsible rewriting the `URL` constructor's arguments to allow the
49/// referenced file to be imported/fetched/etc.
50#[turbo_tasks::value]
51#[derive(ValueToString)]
52#[value_to_string("new URL({request})")]
53pub struct UrlAssetReference {
54 origin: ResolvedVc<Box<dyn ResolveOrigin>>,
55 request: ResolvedVc<Request>,
56 rendering: Rendering,
57 issue_source: IssueSource,
58 error_mode: ResolveErrorMode,
59 url_rewrite_behavior: UrlRewriteBehavior,
60}
61
62impl UrlAssetReference {
63 pub fn new(
64 origin: ResolvedVc<Box<dyn ResolveOrigin>>,
65 request: ResolvedVc<Request>,
66 rendering: Rendering,
67 issue_source: IssueSource,
68 error_mode: ResolveErrorMode,
69 url_rewrite_behavior: UrlRewriteBehavior,
70 ) -> Self {
71 UrlAssetReference {
72 origin,
73 request,
74 rendering,
75 issue_source,
76 error_mode,
77 url_rewrite_behavior,
78 }
79 }
80
81 pub(crate) fn get_referenced_asset(
82 self: Vc<Self>,
83 ) -> impl Future<Output = Result<ReferencedAsset>> {
84 ReferencedAsset::from_resolve_result(self.resolve_reference())
85 }
86}
87
88#[turbo_tasks::value_impl]
89impl ModuleReference for UrlAssetReference {
90 #[turbo_tasks::function]
91 fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
92 url_resolve(
93 *self.origin,
94 *self.request,
95 ReferenceType::Url(UrlReferenceSubType::EcmaScriptNewUrl),
96 Some(self.issue_source),
97 self.error_mode,
98 )
99 }
100
101 fn chunking_type(&self) -> Option<ChunkingType> {
102 Some(ChunkingType::Parallel {
103 inherit_async: false,
104 hoisted: false,
105 })
106 }
107
108 fn source(&self) -> Option<IssueSource> {
109 Some(self.issue_source)
110 }
111}
112
113impl IntoCodeGenReference for UrlAssetReference {
114 fn into_reference(self) -> ResolvedVc<Box<dyn ModuleReference>> {
115 ResolvedVc::upcast(self.resolved_cell())
116 }
117
118 fn into_code_gen_reference(
119 self,
120 _trie: &AstPathTrieBuilder,
121 path: AstPathId,
122 ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
123 let reference = self.resolved_cell();
124 (
125 ResolvedVc::upcast(reference),
126 CodeGen::UrlAssetReferenceCodeGen(UrlAssetReferenceCodeGen { reference, path }),
127 )
128 }
129}
130
131#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
132pub struct UrlAssetReferenceCodeGen {
133 reference: ResolvedVc<UrlAssetReference>,
134 path: AstPathId,
135}
136
137impl UrlAssetReferenceCodeGen {
138 /// Rewrites call to the `new URL()` ctor depends on the current
139 /// conditions. Generated code will point to the output path of the asset,
140 /// as similar to the webpack's behavior. This is based on the
141 /// configuration (UrlRewriteBehavior), and the current context
142 /// (rendering), lastly the asset's condition (if it's referenced /
143 /// external). The following table shows the behavior:
144 /*
145 * original call: `new URL(url, base);`
146 ┌───────────────────────────────┬─────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────┬───────────────────────┐
147 │ UrlRewriteBehavior\RefAsset │ ReferencedAsset::Some() │ ReferencedAsset::External │ ReferencedAsset::None │
148 ├───────────────────────────────┼─────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┼───────────────────────┤
149 │ Relative │ __turbopack_relative_url__(__turbopack_require__(urlId)) │ __turbopack_relative_url__(url) │ new URL(url, base) │
150 │ Full(RenderingClient::Client) │ new URL(__turbopack_require__(urlId), location.origin) │ new URL(url, location.origin) │ new URL(url, base) │
151 │ Full(RenderingClient::..) │ new URL(__turbopack_resolve_module_id_path__(urlId)) │ new URL(url, base) │ new URL(url, base) │
152 │ None │ new URL(url, base) │ new URL(url, base) │ new URL(url, base) │
153 └───────────────────────────────┴─────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────┴───────────────────────┘
154 */
155 pub async fn code_generation(
156 &self,
157 trie: &AstPathTrie,
158 chunking_context: Vc<Box<dyn ChunkingContext>>,
159 ) -> Result<CodeGeneration> {
160 let mut visitors = vec![];
161
162 let reference = self.reference.await?;
163
164 match reference.url_rewrite_behavior {
165 UrlRewriteBehavior::Relative => {
166 let referenced_asset = self.reference.get_referenced_asset().await?;
167
168 // if the referenced url is in the module graph of turbopack, replace it into
169 // the chunk item will be emitted into output path to point the
170 // static asset path. for the `new URL()` call, replace it into
171 // pseudo url object `__turbopack_relative_url__`
172 // which is injected by turbopack's runtime to resolve into the relative path
173 // omitting the base.
174 match &referenced_asset {
175 ReferencedAsset::Some(asset) => {
176 // We rewrite the first `new URL()` arguments to be a require() of the chunk
177 // item, which exports the static asset path to the linked file.
178 let id = asset.chunk_item_id(chunking_context).await?;
179
180 visitors.push(create_visitor!(trie, self.path, visit_mut_expr, |new_expr: &mut Expr| {
181 let should_rewrite_to_relative = if let Expr::New(NewExpr { args: Some(args), .. }) = new_expr {
182 matches!(args.first(), Some(ExprOrSpread { .. }))
183 } else {
184 false
185 };
186
187 if should_rewrite_to_relative {
188 *new_expr = quote!(
189 "new $turbopack_relative_url($turbopack_require($id))" as Expr,
190 turbopack_relative_url: Expr = TURBOPACK_RELATIVE_URL.into(),
191 turbopack_require: Expr = TURBOPACK_REQUIRE.into(),
192 id: Expr = module_id_to_lit(&id),
193 );
194 }
195 }));
196 }
197 ReferencedAsset::External(request, ExternalType::Url) => {
198 let request = request.to_string();
199 visitors.push(create_visitor!(trie, self.path, visit_mut_expr, |new_expr: &mut Expr| {
200 let should_rewrite_to_relative = if let Expr::New(NewExpr { args: Some(args), .. }) = new_expr {
201 matches!(args.first(), Some(ExprOrSpread { .. }))
202 } else {
203 false
204 };
205
206 if should_rewrite_to_relative {
207 *new_expr = quote!(
208 "new $turbopack_relative_url($id)" as Expr,
209 turbopack_relative_url: Expr = TURBOPACK_RELATIVE_URL.into(),
210 id: Expr = request.as_str().into(),
211 );
212 }
213 }));
214 }
215 ReferencedAsset::External(request, ty) => {
216 bail!(
217 "Unsupported external type {:?} for URL reference with request: {:?}",
218 ty,
219 request
220 )
221 }
222 ReferencedAsset::NonPlaceable(_)
223 | ReferencedAsset::None
224 | ReferencedAsset::Empty
225 | ReferencedAsset::Unresolvable => {}
226 }
227 }
228 UrlRewriteBehavior::Full => {
229 let referenced_asset = self.reference.get_referenced_asset().await?;
230
231 // For rendering environments (CSR), we rewrite the `import.meta.url` to
232 // be a location.origin because it allows us to access files from the root of
233 // the dev server.
234 //
235 // By default for the remaining environments, turbopack's runtime have overridden
236 // `import.meta.url`.
237 let rewrite_url_base = match reference.rendering {
238 Rendering::Client => Some(quote!("location.origin" as Expr)),
239 Rendering::None | Rendering::Server => None,
240 };
241
242 match &referenced_asset {
243 ReferencedAsset::Some(asset) => {
244 // We rewrite the first `new URL()` arguments to be a require() of the
245 // chunk item, which returns the asset path as its exports.
246 let id = asset.chunk_item_id(chunking_context).await?;
247
248 // If there's a rewrite to the base url, then the current rendering
249 // environment should able to resolve the asset path
250 // (asset_url) from the base. Wrap the module id
251 // with __turbopack_require__ which returns the asset_url.
252 //
253 // Otherwise, the environment should provide an absolute path to the actual
254 // output asset; delegate those calculation to the
255 // runtime fn __turbopack_resolve_module_id_path__.
256 let url_segment_resolver = if rewrite_url_base.is_some() {
257 quote!(
258 "$turbopack_require($id)" as Expr,
259 turbopack_require: Expr = TURBOPACK_REQUIRE.into(),
260 id: Expr = module_id_to_lit(&id),
261 )
262 } else {
263 quote!(
264 "$turbopack_resolve_module_id_path($id)" as Expr,
265 turbopack_resolve_module_id_path: Expr = TURBOPACK_RESOLVE_MODULE_ID_PATH.into(),
266 id: Expr = module_id_to_lit(&id),
267 )
268 };
269
270 visitors.push(create_visitor!(
271 trie,
272 self.path,
273 visit_mut_expr,
274 |new_expr: &mut Expr| {
275 if let Expr::New(NewExpr {
276 args: Some(args), ..
277 }) = new_expr
278 {
279 if let Some(ExprOrSpread { expr, spread: None }) =
280 args.get_mut(0)
281 {
282 **expr = url_segment_resolver.clone();
283 }
284
285 if let Some(ExprOrSpread { expr, spread: None }) =
286 args.get_mut(1)
287 {
288 if let Some(rewrite) = &rewrite_url_base {
289 **expr = rewrite.clone();
290 } else {
291 // If rewrite for the base doesn't exists, means
292 // __turbopack_resolve_module_id_path__
293 // should resolve the full path correctly and there
294 // shouldn't be a base.
295 args.remove(1);
296 }
297 }
298 }
299 }
300 ));
301 }
302 ReferencedAsset::External(request, ExternalType::Url) => {
303 let request = request.to_string();
304 visitors.push(create_visitor!(
305 trie,
306 self.path,
307 visit_mut_expr,
308 |new_expr: &mut Expr| {
309 if let Expr::New(NewExpr {
310 args: Some(args), ..
311 }) = new_expr
312 {
313 if let Some(ExprOrSpread { expr, spread: None }) =
314 args.get_mut(0)
315 {
316 *expr = request.as_str().into()
317 }
318
319 if let Some(rewrite) = &rewrite_url_base
320 && let Some(ExprOrSpread { expr, spread: None }) =
321 args.get_mut(1)
322 {
323 **expr = rewrite.clone();
324 }
325 }
326 }
327 ));
328 }
329 ReferencedAsset::External(request, ty) => {
330 bail!(
331 "Unsupported external type {:?} for URL reference with request: {:?}",
332 ty,
333 request
334 )
335 }
336 ReferencedAsset::NonPlaceable(_)
337 | ReferencedAsset::None
338 | ReferencedAsset::Empty
339 | ReferencedAsset::Unresolvable => {}
340 }
341 }
342 UrlRewriteBehavior::None => {
343 // Asked to not rewrite the URL, so we don't do anything.
344 }
345 };
346
347 Ok(CodeGeneration::visitors(visitors))
348 }
349}