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}