turbopack_ecmascript/references/
pattern_mapping.rs

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