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    },
34    runtime_functions::{
35        TURBOPACK_ASYNC_LOADER, TURBOPACK_EXTERNAL_IMPORT, TURBOPACK_EXTERNAL_REQUIRE,
36        TURBOPACK_IMPORT, TURBOPACK_MODULE_CONTEXT, TURBOPACK_REQUIRE,
37    },
38    utils::module_id_to_lit,
39};
40
41#[derive(PartialEq, Eq, ValueDebugFormat, TraceRawVcs, NonLocalValue, Encode, Decode)]
42pub(crate) enum SinglePatternMapping {
43    /// Invalid request.
44    Invalid,
45    /// Unresolvable request.
46    Unresolvable(String),
47    /// Ignored request.
48    Ignored,
49    /// Constant request that always maps to the same module.
50    ///
51    /// ### Example
52    /// ```js
53    /// require("./module")
54    /// ```
55    Module(ModuleId),
56    /// Constant request that always maps to the same module.
57    /// This is used for dynamic imports.
58    /// Module id points to a loader module.
59    ///
60    /// ### Example
61    /// ```js
62    /// import("./module")
63    /// ```
64    ModuleLoader(ModuleId),
65    /// External reference with request and type
66    External(RcStr, ExternalType),
67}
68
69/// A mapping from a request pattern (e.g. "./module", `./images/${name}.png`)
70/// to corresponding module ids. The same pattern can map to multiple module ids
71/// at runtime when using variable interpolation.
72#[turbo_tasks::value]
73pub(crate) enum PatternMapping {
74    /// Constant request that always maps to the same module.
75    ///
76    /// ### Example
77    /// ```js
78    /// require("./module")
79    /// ```
80    Single(SinglePatternMapping),
81    /// Variable request that can map to different modules at runtime.
82    ///
83    /// ### Example
84    /// ```js
85    /// require(`./images/${name}.png`)
86    /// ```
87    Map(#[bincode(with = "turbo_bincode::indexmap")] FxIndexMap<String, SinglePatternMapping>),
88}
89
90#[derive(
91    Copy, Clone, Debug, Eq, PartialEq, Hash, TraceRawVcs, TaskInput, NonLocalValue, Encode, Decode,
92)]
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(_) => self.create_id(key_expr),
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<String, SinglePatternMapping>,
234    key_expr: &Expr,
235    import_mode: ImportMode,
236) -> Expr {
237    let props = map
238        .iter()
239        .map(|(k, v)| {
240            PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
241                key: PropName::Str(k.as_str().into()),
242                value: quote_expr!(
243                        "{ id: () => $id, module: () => $module }",
244                        id: Expr = v.create_id(Cow::Borrowed(key_expr)),
245                        module: Expr = match import_mode {
246                            ImportMode::Require => v.create_require(Cow::Borrowed(key_expr)),
247                            ImportMode::Import { import_externals } => v.create_import(Cow::Borrowed(key_expr), import_externals),
248                        },
249                    ),
250            })))
251        })
252        .collect();
253
254    Expr::Object(ObjectLit {
255        span: DUMMY_SP,
256        props,
257    })
258}
259
260impl PatternMapping {
261    pub fn create_id(&self, key_expr: Expr) -> Expr {
262        match self {
263            PatternMapping::Single(pm) => pm.create_id(Cow::Owned(key_expr)),
264            PatternMapping::Map(map) => {
265                let map = create_context_map(map, &key_expr, ImportMode::Require);
266
267                quote!("$turbopack_module_context($map).resolve($key)" as Expr,
268                    turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
269                    map: Expr = map,
270                    key: Expr = key_expr
271                )
272            }
273        }
274    }
275
276    pub fn create_require(&self, key_expr: Expr) -> Expr {
277        match self {
278            PatternMapping::Single(pm) => pm.create_require(Cow::Owned(key_expr)),
279            PatternMapping::Map(map) => {
280                let map = create_context_map(map, &key_expr, ImportMode::Require);
281
282                quote!("$turbopack_module_context($map)($key)" as Expr,
283                    turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
284                    map: Expr = map,
285                    key: Expr = key_expr
286                )
287            }
288        }
289    }
290
291    pub fn create_import(&self, key_expr: Expr, import_externals: bool) -> Expr {
292        match self {
293            PatternMapping::Single(pm) => pm.create_import(Cow::Owned(key_expr), import_externals),
294            PatternMapping::Map(map) => {
295                let map =
296                    create_context_map(map, &key_expr, ImportMode::Import { import_externals });
297
298                quote!("$turbopack_module_context($map).import($key)" as Expr,
299                    turbopack_module_context: Expr = TURBOPACK_MODULE_CONTEXT.into(),
300                    map: Expr = map,
301                    key: Expr = key_expr
302                )
303            }
304        }
305    }
306}
307
308async fn to_single_pattern_mapping(
309    origin: Vc<Box<dyn ResolveOrigin>>,
310    chunking_context: Vc<Box<dyn ChunkingContext>>,
311    resolve_item: &ModuleResolveResultItem,
312    resolve_type: ResolveType,
313) -> Result<SinglePatternMapping> {
314    let module = match resolve_item {
315        ModuleResolveResultItem::Module(module) => *module,
316        ModuleResolveResultItem::External { name: s, ty, .. } => {
317            return Ok(SinglePatternMapping::External(s.clone(), *ty));
318        }
319        ModuleResolveResultItem::Ignore => return Ok(SinglePatternMapping::Ignored),
320        ModuleResolveResultItem::Unknown(source) => {
321            emit_unknown_module_type_error(**source).await?;
322            return Ok(SinglePatternMapping::Unresolvable(
323                "unknown module type".to_string(),
324            ));
325        }
326        ModuleResolveResultItem::Error(str) => {
327            return Ok(SinglePatternMapping::Unresolvable(str.await?.to_string()));
328        }
329        ModuleResolveResultItem::OutputAsset(_)
330        | ModuleResolveResultItem::Empty
331        | ModuleResolveResultItem::Custom(_) => {
332            // TODO implement mapping
333            CodeGenerationIssue {
334                severity: IssueSeverity::Bug,
335                title: StyledString::Text(rcstr!(
336                    "pattern mapping is not implemented for this result"
337                ))
338                .resolved_cell(),
339                message: StyledString::Text(
340                    format!(
341                        "the reference resolves to a non-trivial result, which is not supported \
342                         yet: {resolve_item:?}"
343                    )
344                    .into(),
345                )
346                .resolved_cell(),
347                path: origin.origin_path().owned().await?,
348            }
349            .resolved_cell()
350            .emit();
351            return Ok(SinglePatternMapping::Invalid);
352        }
353    };
354    if let Some(chunkable) = ResolvedVc::try_downcast::<Box<dyn ChunkableModule>>(module) {
355        match resolve_type {
356            ResolveType::AsyncChunkLoader => {
357                let loader_id = chunking_context.async_loader_chunk_item_id(*chunkable);
358                return Ok(SinglePatternMapping::ModuleLoader(loader_id.owned().await?));
359            }
360            ResolveType::ChunkItem => {
361                let item_id = chunkable.chunk_item_id(chunking_context);
362                return Ok(SinglePatternMapping::Module(item_id.owned().await?));
363            }
364        }
365    }
366    CodeGenerationIssue {
367        severity: IssueSeverity::Bug,
368        title: StyledString::Text(rcstr!("non-ecmascript placeable asset")).resolved_cell(),
369        message: StyledString::Text(rcstr!(
370            "asset is not placeable in ESM chunks, so it doesn't have a module id"
371        ))
372        .resolved_cell(),
373        path: origin.origin_path().owned().await?,
374    }
375    .resolved_cell()
376    .emit();
377    Ok(SinglePatternMapping::Invalid)
378}
379
380#[turbo_tasks::value_impl]
381impl PatternMapping {
382    /// Resolves a request into a pattern mapping.
383    // NOTE(alexkirsz) I would rather have used `resolve` here but it's already reserved by the Vc
384    // impl.
385    #[turbo_tasks::function]
386    pub async fn resolve_request(
387        request: Vc<Request>,
388        origin: Vc<Box<dyn ResolveOrigin>>,
389        chunking_context: Vc<Box<dyn ChunkingContext>>,
390        resolve_result: Vc<ModuleResolveResult>,
391        resolve_type: ResolveType,
392    ) -> Result<Vc<PatternMapping>> {
393        let result = resolve_result.await?;
394        match result.primary.len() {
395            0 => Ok(PatternMapping::Single(SinglePatternMapping::Unresolvable(
396                request_to_string(request).await?.to_string(),
397            ))
398            .cell()),
399            1 if !request.request_pattern().await?.has_dynamic_parts() => {
400                let resolve_item = &result.primary.first().unwrap().1;
401                let single_pattern_mapping =
402                    to_single_pattern_mapping(origin, chunking_context, resolve_item, resolve_type)
403                        .await?;
404                Ok(PatternMapping::Single(single_pattern_mapping).cell())
405            }
406            _ => {
407                let mut set = HashSet::new();
408                let map = result
409                    .primary
410                    .iter()
411                    .filter_map(|(k, v)| {
412                        let request = k.request.as_ref()?;
413                        set.insert(request).then(|| (request.to_string(), v))
414                    })
415                    .map(|(k, v)| async move {
416                        let single_pattern_mapping =
417                            to_single_pattern_mapping(origin, chunking_context, v, resolve_type)
418                                .await?;
419                        Ok((k, single_pattern_mapping))
420                    })
421                    .try_join()
422                    .await?
423                    .into_iter()
424                    .collect();
425                Ok(PatternMapping::Map(map).cell())
426            }
427        }
428    }
429}