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