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;
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_EXTERNAL_IMPORT, TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_IMPORT,
35        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_require($id))($turbopack_import)" as Expr,
212                    turbopack_require: Expr = TURBOPACK_REQUIRE.into(),
213                    turbopack_import: Expr = TURBOPACK_IMPORT.into(),
214                    id: Expr = module_id_to_lit(module_id)
215                )
216            }
217            Self::Ignored => {
218                quote!("Promise.resolve({})" as Expr)
219            }
220            Self::Module(_) => Expr::Call(CallExpr {
221                callee: Callee::Expr(quote_expr!("Promise.resolve().then")),
222                args: vec![ExprOrSpread {
223                    spread: None,
224                    expr: quote_expr!(
225                        "() => $turbopack_import($arg)",
226                        turbopack_import: Expr = TURBOPACK_IMPORT.into(),
227                        arg: Expr = self.create_id(key_expr)
228                    ),
229                }],
230                span: DUMMY_SP,
231                ..Default::default()
232            }),
233        }
234    }
235}
236
237enum ImportMode {
238    Require,
239    Import { import_externals: bool },
240}
241
242fn create_context_map(
243    map: &FxIndexMap<String, SinglePatternMapping>,
244    key_expr: &Expr,
245    import_mode: ImportMode,
246) -> Expr {
247    let props = map
248        .iter()
249        .map(|(k, v)| {
250            PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
251                key: PropName::Str(k.as_str().into()),
252                value: quote_expr!(
253                        "{ id: () => $id, module: () => $module }",
254                        id: Expr = v.create_id(Cow::Borrowed(key_expr)),
255                        module: Expr = match import_mode {
256                            ImportMode::Require => v.create_require(Cow::Borrowed(key_expr)),
257                            ImportMode::Import { import_externals } => v.create_import(Cow::Borrowed(key_expr), import_externals),
258                        },
259                    ),
260            })))
261        })
262        .collect();
263
264    Expr::Object(ObjectLit {
265        span: DUMMY_SP,
266        props,
267    })
268}
269
270impl PatternMapping {
271    pub fn create_id(&self, key_expr: Expr) -> Expr {
272        match self {
273            PatternMapping::Single(pm) => pm.create_id(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).resolve($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_require(&self, key_expr: Expr) -> Expr {
287        match self {
288            PatternMapping::Single(pm) => pm.create_require(Cow::Owned(key_expr)),
289            PatternMapping::Map(map) => {
290                let map = create_context_map(map, &key_expr, ImportMode::Require);
291
292                quote!("$turbopack_module_context($map)($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    pub fn create_import(&self, key_expr: Expr, import_externals: bool) -> Expr {
302        match self {
303            PatternMapping::Single(pm) => pm.create_import(Cow::Owned(key_expr), import_externals),
304            PatternMapping::Map(map) => {
305                let map =
306                    create_context_map(map, &key_expr, ImportMode::Import { import_externals });
307
308                quote!("$turbopack_module_context($map).import($key)" as Expr,
309                    turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
310                    map: Expr = map,
311                    key: Expr = key_expr
312                )
313            }
314        }
315    }
316}
317
318async fn to_single_pattern_mapping(
319    origin: Vc<Box<dyn ResolveOrigin>>,
320    chunking_context: Vc<Box<dyn ChunkingContext>>,
321    resolve_item: &ModuleResolveResultItem,
322    resolve_type: ResolveType,
323) -> Result<SinglePatternMapping> {
324    let module = match resolve_item {
325        ModuleResolveResultItem::Module(module) => *module,
326        ModuleResolveResultItem::External { name: s, ty, .. } => {
327            return Ok(SinglePatternMapping::External(s.clone(), *ty));
328        }
329        ModuleResolveResultItem::Ignore => return Ok(SinglePatternMapping::Ignored),
330        ModuleResolveResultItem::Unknown(source) => {
331            emit_unknown_module_type_error(**source).await?;
332            return Ok(SinglePatternMapping::Unresolvable(
333                "unknown module type".to_string(),
334            ));
335        }
336        ModuleResolveResultItem::Error(str) => {
337            return Ok(SinglePatternMapping::Unresolvable(str.await?.to_string()));
338        }
339        ModuleResolveResultItem::OutputAsset(_)
340        | ModuleResolveResultItem::Empty
341        | ModuleResolveResultItem::Custom(_) => {
342            // TODO implement mapping
343            CodeGenerationIssue {
344                severity: IssueSeverity::Bug.resolved_cell(),
345                title: StyledString::Text(
346                    "pattern mapping is not implemented for this result".into(),
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.origin_path().to_resolved().await?,
358            }
359            .resolved_cell()
360            .emit();
361            return Ok(SinglePatternMapping::Invalid);
362        }
363    };
364    if let Some(chunkable) = ResolvedVc::try_downcast::<Box<dyn ChunkableModule>>(module) {
365        match resolve_type {
366            ResolveType::AsyncChunkLoader => {
367                let loader_id = chunking_context.async_loader_chunk_item_id(*chunkable);
368                return Ok(SinglePatternMapping::ModuleLoader(loader_id.owned().await?));
369            }
370            ResolveType::ChunkItem => {
371                let item_id = chunkable.chunk_item_id(chunking_context);
372                return Ok(SinglePatternMapping::Module(item_id.owned().await?));
373            }
374        }
375    }
376    CodeGenerationIssue {
377        severity: IssueSeverity::Bug.resolved_cell(),
378        title: StyledString::Text("non-ecmascript placeable asset".into()).resolved_cell(),
379        message: StyledString::Text(
380            "asset is not placeable in ESM chunks, so it doesn't have a module id".into(),
381        )
382        .resolved_cell(),
383        path: origin.origin_path().to_resolved().await?,
384    }
385    .resolved_cell()
386    .emit();
387    Ok(SinglePatternMapping::Invalid)
388}
389
390#[turbo_tasks::value_impl]
391impl PatternMapping {
392    /// Resolves a request into a pattern mapping.
393    // NOTE(alexkirsz) I would rather have used `resolve` here but it's already reserved by the Vc
394    // impl.
395    #[turbo_tasks::function]
396    pub async fn resolve_request(
397        request: Vc<Request>,
398        origin: Vc<Box<dyn ResolveOrigin>>,
399        chunking_context: Vc<Box<dyn ChunkingContext>>,
400        resolve_result: Vc<ModuleResolveResult>,
401        resolve_type: ResolveType,
402    ) -> Result<Vc<PatternMapping>> {
403        let result = resolve_result.await?;
404        match result.primary.len() {
405            0 => Ok(PatternMapping::Single(SinglePatternMapping::Unresolvable(
406                request_to_string(request).await?.to_string(),
407            ))
408            .cell()),
409            1 if !request.request_pattern().await?.has_dynamic_parts() => {
410                let resolve_item = &result.primary.first().unwrap().1;
411                let single_pattern_mapping =
412                    to_single_pattern_mapping(origin, chunking_context, resolve_item, resolve_type)
413                        .await?;
414                Ok(PatternMapping::Single(single_pattern_mapping).cell())
415            }
416            _ => {
417                let mut set = HashSet::new();
418                let map = result
419                    .primary
420                    .iter()
421                    .filter_map(|(k, v)| {
422                        let request = k.request.as_ref()?;
423                        set.insert(request).then(|| (request.to_string(), v))
424                    })
425                    .map(|(k, v)| async move {
426                        let single_pattern_mapping =
427                            to_single_pattern_mapping(origin, chunking_context, v, resolve_type)
428                                .await?;
429                        Ok((k, single_pattern_mapping))
430                    })
431                    .try_join()
432                    .await?
433                    .into_iter()
434                    .collect();
435                Ok(PatternMapping::Map(map).cell())
436            }
437        }
438    }
439}