Skip to main content

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