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