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