Skip to main content

turbopack_ecmascript/references/
pattern_mapping.rs

1use std::{borrow::Cow, collections::HashSet};
2
3use anyhow::Result;
4use bincode::{Decode, Encode};
5use swc_core::{
6    common::DUMMY_SP,
7    ecma::ast::{
8        CallExpr, Callee, Expr, ExprOrSpread, KeyValueProp, Lit, ObjectLit, Prop, PropName,
9        PropOrSpread,
10    },
11    quote, quote_expr,
12};
13use turbo_rcstr::{RcStr, rcstr};
14use turbo_tasks::{
15    FxIndexMap, NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, Vc, debug::ValueDebugFormat,
16    trace::TraceRawVcs,
17};
18use turbopack_core::{
19    chunk::{ChunkableModule, ChunkingContext, ModuleChunkItemIdExt, ModuleId},
20    issue::{
21        IssueExt, IssueSeverity, StyledString, code_gen::CodeGenerationIssue,
22        module::emit_unknown_module_type_error,
23    },
24    resolve::{
25        ExternalType, ModuleResolveResult, ModuleResolveResultItem, origin::ResolveOrigin,
26        parse::Request,
27    },
28};
29
30use crate::{
31    references::util::{
32        request_to_string, throw_module_not_found_error_expr, throw_module_not_found_expr,
33        throw_module_not_found_expr_async,
34    },
35    runtime_functions::{
36        TURBOPACK_ASYNC_LOADER, TURBOPACK_EXTERNAL_IMPORT, TURBOPACK_EXTERNAL_REQUIRE,
37        TURBOPACK_IMPORT, TURBOPACK_MODULE_CONTEXT, TURBOPACK_REQUIRE,
38    },
39    utils::module_id_to_lit,
40};
41
42#[derive(PartialEq, Eq, ValueDebugFormat, TraceRawVcs, NonLocalValue, Encode, Decode)]
43pub(crate) enum SinglePatternMapping {
44    /// Invalid request.
45    Invalid,
46    /// Unresolvable request.
47    Unresolvable(String),
48    /// Ignored request.
49    Ignored,
50    /// Constant request that always maps to the same module.
51    ///
52    /// ### Example
53    /// ```js
54    /// require("./module")
55    /// ```
56    Module(ModuleId),
57    /// Constant request that always maps to the same module.
58    /// This is used for dynamic imports.
59    /// Module id points to a loader module.
60    ///
61    /// ### Example
62    /// ```js
63    /// import("./module")
64    /// ```
65    ModuleLoader(ModuleId),
66    /// External reference with request and type
67    External(RcStr, ExternalType),
68}
69
70/// A mapping from a request pattern (e.g. "./module", `./images/${name}.png`)
71/// to corresponding module ids. The same pattern can map to multiple module ids
72/// at runtime when using variable interpolation.
73#[turbo_tasks::value]
74pub(crate) enum PatternMapping {
75    /// Constant request that always maps to the same module.
76    ///
77    /// ### Example
78    /// ```js
79    /// require("./module")
80    /// ```
81    Single(SinglePatternMapping),
82    /// Variable request that can map to different modules at runtime.
83    ///
84    /// ### Example
85    /// ```js
86    /// require(`./images/${name}.png`)
87    /// ```
88    Map(#[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<RcStr, SinglePatternMapping>),
89}
90
91#[derive(
92    Copy, Clone, Debug, Eq, PartialEq, Hash, TraceRawVcs, TaskInput, NonLocalValue, Encode, Decode,
93)]
94pub(crate) enum ResolveType {
95    AsyncChunkLoader,
96    ChunkItem,
97}
98
99impl SinglePatternMapping {
100    pub fn create_id(&self, key_expr: Cow<'_, Expr>) -> Expr {
101        match self {
102            Self::Invalid => {
103                quote!(
104                    "(() => {throw new Error('could not resolve \"' + $arg + '\" into a module');})()" as Expr,
105                    arg: Expr = key_expr.into_owned()
106                )
107            }
108            Self::Unresolvable(request) => throw_module_not_found_expr(request),
109            Self::Ignored => {
110                quote!("undefined" as Expr)
111            }
112            Self::Module(module_id) | Self::ModuleLoader(module_id) => module_id_to_lit(module_id),
113            Self::External(s, _) => Expr::Lit(Lit::Str(s.as_str().into())),
114        }
115    }
116
117    pub fn create_require(&self, key_expr: Cow<'_, Expr>) -> Expr {
118        match self {
119            Self::Invalid => self.create_id(key_expr),
120            Self::Unresolvable(request) => throw_module_not_found_expr(request),
121            Self::Ignored => quote!("{}" as Expr),
122            Self::Module(_) | Self::ModuleLoader(_) => quote!(
123                "$turbopack_require($arg)" as Expr,
124                turbopack_require: Expr = TURBOPACK_REQUIRE.into(),
125                arg: Expr = self.create_id(key_expr)
126            ),
127            Self::External(request, ExternalType::CommonJs) => quote!(
128                "$turbopack_external_require($arg, () => require($arg))" as Expr,
129                turbopack_external_require: Expr = TURBOPACK_EXTERNAL_REQUIRE.into(),
130                arg: Expr = request.as_str().into()
131            ),
132            Self::External(request, ty) => throw_module_not_found_error_expr(
133                request,
134                &format!("Unsupported external type {ty:?} for commonjs reference"),
135            ),
136        }
137    }
138
139    pub fn create_import(&self, key_expr: Cow<'_, Expr>, import_externals: bool) -> Expr {
140        match self {
141            Self::Invalid => {
142                let error = quote_expr!(
143                    "() => {throw new Error('could not resolve \"' + $arg + '\" into a module');}",
144                    arg: Expr = key_expr.into_owned()
145                );
146                Expr::Call(CallExpr {
147                    callee: Callee::Expr(quote_expr!("Promise.resolve().then")),
148                    args: vec![ExprOrSpread {
149                        spread: None,
150                        expr: error,
151                    }],
152                    span: DUMMY_SP,
153                    ..Default::default()
154                })
155            }
156            Self::Unresolvable(request) => throw_module_not_found_expr_async(request),
157            Self::External(_, ExternalType::EcmaScriptModule) => {
158                if import_externals {
159                    Expr::Call(CallExpr {
160                        callee: Callee::Expr(Box::new(TURBOPACK_EXTERNAL_IMPORT.into())),
161                        args: vec![ExprOrSpread {
162                            spread: None,
163                            expr: Box::new(key_expr.into_owned()),
164                        }],
165                        span: DUMMY_SP,
166                        ..Default::default()
167                    })
168                } else {
169                    Expr::Call(CallExpr {
170                        callee: Callee::Expr(quote_expr!("Promise.resolve().then")),
171                        args: vec![ExprOrSpread {
172                            spread: None,
173                            expr: quote_expr!(
174                                "() => $turbopack_external_require($arg, () => require($arg), true)",
175                                turbopack_external_require: Expr = TURBOPACK_EXTERNAL_REQUIRE.into(),
176                                arg: Expr = key_expr.into_owned()
177                            ),
178                        }],
179                        span: DUMMY_SP,
180                        ..Default::default()
181                    })
182                }
183            }
184            Self::External(_, ExternalType::CommonJs | ExternalType::Url) => Expr::Call(CallExpr {
185                callee: Callee::Expr(quote_expr!("Promise.resolve().then")),
186                args: vec![ExprOrSpread {
187                    spread: None,
188                    expr: quote_expr!(
189                        "() => $turbopack_external_require($arg, () => require($arg), true)",
190                        turbopack_external_require: Expr = TURBOPACK_EXTERNAL_REQUIRE.into(),
191                        arg: Expr = key_expr.into_owned()
192                    ),
193                }],
194                span: DUMMY_SP,
195                ..Default::default()
196            }),
197            #[allow(unreachable_patterns)]
198            Self::External(request, ty) => throw_module_not_found_error_expr(
199                request,
200                &format!("Unsupported external type {ty:?} for dynamic import reference"),
201            ),
202            Self::ModuleLoader(module_id) => {
203                quote!("$turbopack_async_loader($id)" as Expr,
204                    turbopack_async_loader: Expr = TURBOPACK_ASYNC_LOADER.into(),
205                    id: Expr = module_id_to_lit(module_id)
206                )
207            }
208            Self::Ignored => {
209                quote!("Promise.resolve({})" as Expr)
210            }
211            Self::Module(_) => Expr::Call(CallExpr {
212                callee: Callee::Expr(quote_expr!("Promise.resolve().then")),
213                args: vec![ExprOrSpread {
214                    spread: None,
215                    expr: quote_expr!(
216                        "() => $turbopack_import($arg)",
217                        turbopack_import: Expr = TURBOPACK_IMPORT.into(),
218                        arg: Expr = self.create_id(key_expr)
219                    ),
220                }],
221                span: DUMMY_SP,
222                ..Default::default()
223            }),
224        }
225    }
226}
227
228enum ImportMode {
229    Require,
230    Import { import_externals: bool },
231}
232
233fn create_context_map(
234    map: &FxIndexMap<RcStr, SinglePatternMapping>,
235    key_expr: &Expr,
236    import_mode: ImportMode,
237) -> Expr {
238    let props = map
239        .iter()
240        .map(|(k, v)| {PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {key: PropName::Str(k.as_str().into()),
241                value: quote_expr!(
242                        "{id: () => $id, module: () => $module}",
243                        id: Expr = v.create_id(Cow::Borrowed(key_expr)),
244                        module: Expr = match import_mode {ImportMode::Require => v.create_require(Cow::Borrowed(key_expr)),
245                            ImportMode::Import {import_externals} => v.create_import(Cow::Borrowed(key_expr), import_externals),},
246                    ),})))})
247        .collect();
248
249    Expr::Object(ObjectLit {
250        span: DUMMY_SP,
251        props,
252    })
253}
254
255impl PatternMapping {
256    pub fn create_id(&self, key_expr: Expr) -> Expr {
257        match self {
258            PatternMapping::Single(pm) => pm.create_id(Cow::Owned(key_expr)),
259            PatternMapping::Map(map) => {
260                let map = create_context_map(map, &key_expr, ImportMode::Require);
261
262                quote!("$turbopack_module_context($map).resolve($key)" as Expr,
263                    turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
264                    map: Expr = map,
265                    key: Expr = key_expr
266                )
267            }
268        }
269    }
270
271    pub fn create_require(&self, key_expr: Expr) -> Expr {
272        match self {
273            PatternMapping::Single(pm) => pm.create_require(Cow::Owned(key_expr)),
274            PatternMapping::Map(map) => {
275                let map = create_context_map(map, &key_expr, ImportMode::Require);
276
277                quote!("$turbopack_module_context($map)($key)" as Expr,
278                    turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
279                    map: Expr = map,
280                    key: Expr = key_expr
281                )
282            }
283        }
284    }
285
286    pub fn create_import(&self, key_expr: Expr, import_externals: bool) -> Expr {
287        match self {
288            PatternMapping::Single(pm) => pm.create_import(Cow::Owned(key_expr), import_externals),
289            PatternMapping::Map(map) => {
290                let map =
291                    create_context_map(map, &key_expr, ImportMode::Import { import_externals });
292
293                quote!("$turbopack_module_context($map).import($key)" as Expr,
294                    turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
295                    map: Expr = map,
296                    key: Expr = key_expr
297                )
298            }
299        }
300    }
301}
302
303async fn to_single_pattern_mapping(
304    origin: Vc<Box<dyn ResolveOrigin>>,
305    chunking_context: Vc<Box<dyn ChunkingContext>>,
306    resolve_item: &ModuleResolveResultItem,
307    primary: &[(turbopack_core::resolve::RequestKey, ModuleResolveResultItem)],
308    resolve_type: ResolveType,
309) -> Result<SinglePatternMapping> {
310    let module = match resolve_item {
311        ModuleResolveResultItem::Module(module) => *module,
312        ModuleResolveResultItem::External { name: s, ty, .. } => {
313            return Ok(SinglePatternMapping::External(s.clone(), *ty));
314        }
315        ModuleResolveResultItem::Ignore => return Ok(SinglePatternMapping::Ignored),
316        ModuleResolveResultItem::Unknown(source) => {
317            emit_unknown_module_type_error(**source).await?;
318            return Ok(SinglePatternMapping::Unresolvable(
319                "unknown module type".to_string(),
320            ));
321        }
322        ModuleResolveResultItem::Error(issue) => {
323            return Ok(SinglePatternMapping::Unresolvable(
324                issue
325                    .into_trait_ref()
326                    .await?
327                    .title()
328                    .await?
329                    .to_unstyled_string(),
330            ));
331        }
332        ModuleResolveResultItem::Duplicate(first) => {
333            return Box::pin(to_single_pattern_mapping(
334                origin,
335                chunking_context,
336                &primary[*first].1,
337                primary,
338                resolve_type,
339            ))
340            .await;
341        }
342        ModuleResolveResultItem::Empty | ModuleResolveResultItem::Custom(_) => {
343            // TODO implement mapping
344            CodeGenerationIssue {
345                severity: IssueSeverity::Bug,
346                title: StyledString::Text(rcstr!(
347                    "pattern mapping is not implemented for this result"
348                ))
349                .resolved_cell(),
350                message: StyledString::Text(
351                    format!(
352                        "the reference resolves to a non-trivial result, which is not supported \
353                         yet: {resolve_item:?}"
354                    )
355                    .into(),
356                )
357                .resolved_cell(),
358                path: origin.origin_path().owned().await?,
359                source: None,
360            }
361            .resolved_cell()
362            .emit();
363            return Ok(SinglePatternMapping::Invalid);
364        }
365    };
366    if let Some(chunkable) = ResolvedVc::try_downcast::<Box<dyn ChunkableModule>>(module) {
367        match resolve_type {
368            ResolveType::AsyncChunkLoader => {
369                let ident = chunking_context.async_loader_chunk_item_ident(*chunkable);
370                let loader_id = chunking_context
371                    .chunk_item_id_strategy()
372                    .await?
373                    .get_id_from_ident(ident)
374                    .await?;
375                return Ok(SinglePatternMapping::ModuleLoader(loader_id));
376            }
377            ResolveType::ChunkItem => {
378                let item_id = chunkable.chunk_item_id(chunking_context).await?;
379                return Ok(SinglePatternMapping::Module(item_id));
380            }
381        }
382    }
383    CodeGenerationIssue {
384        severity: IssueSeverity::Bug,
385        title: StyledString::Text(rcstr!("non-ecmascript placeable asset")).resolved_cell(),
386        message: StyledString::Text(rcstr!(
387            "asset is not placeable in ESM chunks, so it doesn't have a module id"
388        ))
389        .resolved_cell(),
390        path: origin.origin_path().owned().await?,
391        source: None,
392    }
393    .resolved_cell()
394    .emit();
395    Ok(SinglePatternMapping::Invalid)
396}
397
398#[turbo_tasks::value_impl]
399impl PatternMapping {
400    /// Resolves a request into a pattern mapping.
401    // NOTE(alexkirsz) I would rather have used `resolve` here but it's already reserved by the Vc
402    // impl.
403    #[turbo_tasks::function]
404    pub async fn resolve_request(
405        request: Vc<Request>,
406        origin: Vc<Box<dyn ResolveOrigin>>,
407        chunking_context: Vc<Box<dyn ChunkingContext>>,
408        resolve_result: Vc<ModuleResolveResult>,
409        resolve_type: ResolveType,
410    ) -> Result<Vc<PatternMapping>> {
411        let result = resolve_result.await?;
412        match result.primary.len() {
413            0 => Ok(PatternMapping::Single(SinglePatternMapping::Unresolvable(
414                request_to_string(request).await?.to_string(),
415            ))
416            .cell()),
417            1 if !request.request_pattern().await?.has_dynamic_parts() => {
418                let resolve_item = &result.primary.first().unwrap().1;
419                let single_pattern_mapping = to_single_pattern_mapping(
420                    origin,
421                    chunking_context,
422                    resolve_item,
423                    &result.primary,
424                    resolve_type,
425                )
426                .await?;
427                Ok(PatternMapping::Single(single_pattern_mapping).cell())
428            }
429            _ => {
430                let primary = &result.primary;
431                let mut set = HashSet::new();
432                let items: Vec<(RcStr, &ModuleResolveResultItem)> = primary
433                    .iter()
434                    .filter_map(|(k, v)| {
435                        let request = k.request.as_ref()?;
436                        set.insert(request).then(|| (request.clone(), v))
437                    })
438                    .collect();
439                let map = items
440                    .into_iter()
441                    .map(|(k, v)| async move {
442                        let single_pattern_mapping = to_single_pattern_mapping(
443                            origin,
444                            chunking_context,
445                            v,
446                            primary,
447                            resolve_type,
448                        )
449                        .await?;
450                        Ok((k, single_pattern_mapping))
451                    })
452                    .try_join()
453                    .await?
454                    .into_iter()
455                    .collect();
456                Ok(PatternMapping::Map(map).cell())
457            }
458        }
459    }
460}