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}