turbopack_ecmascript/references/esm/
url.rs

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