turbopack_ecmascript/references/esm/
base.rs

1use anyhow::{Result, anyhow, bail};
2use strsim::jaro;
3use swc_core::{
4    common::{BytePos, DUMMY_SP, Span},
5    ecma::ast::{Decl, Expr, ExprStmt, Ident, Stmt},
6    quote,
7};
8use turbo_rcstr::RcStr;
9use turbo_tasks::{ResolvedVc, Value, ValueToString, Vc};
10use turbo_tasks_fs::FileSystemPath;
11use turbopack_core::{
12    chunk::{
13        ChunkableModuleReference, ChunkingContext, ChunkingType, ChunkingTypeOption,
14        ModuleChunkItemIdExt,
15    },
16    context::AssetContext,
17    issue::{
18        Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, OptionIssueSource,
19        OptionStyledString, StyledString,
20    },
21    module::Module,
22    reference::ModuleReference,
23    reference_type::{EcmaScriptModulesReferenceSubType, ImportWithType},
24    resolve::{
25        ExternalType, ModulePart, ModuleResolveResult, ModuleResolveResultItem, RequestKey,
26        origin::{ResolveOrigin, ResolveOriginExt},
27        parse::Request,
28    },
29};
30use turbopack_resolve::ecmascript::esm_resolve;
31
32use super::export::{all_known_export_names, is_export_missing};
33use crate::{
34    TreeShakingMode,
35    analyzer::imports::ImportAnnotations,
36    chunk::EcmascriptChunkPlaceable,
37    code_gen::CodeGeneration,
38    magic_identifier,
39    references::util::{request_to_string, throw_module_not_found_expr},
40    runtime_functions::{TURBOPACK_EXTERNAL_IMPORT, TURBOPACK_EXTERNAL_REQUIRE, TURBOPACK_IMPORT},
41    tree_shake::{TURBOPACK_PART_IMPORT_SOURCE, asset::EcmascriptModulePartAsset},
42    utils::module_id_to_lit,
43};
44
45#[turbo_tasks::value]
46pub enum ReferencedAsset {
47    Some(ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>),
48    External(RcStr, ExternalType),
49    None,
50    Unresolvable,
51}
52
53impl ReferencedAsset {
54    pub async fn get_ident(
55        &self,
56        chunking_context: Vc<Box<dyn ChunkingContext>>,
57    ) -> Result<Option<String>> {
58        Ok(match self {
59            ReferencedAsset::Some(asset) => {
60                Some(Self::get_ident_from_placeable(asset, chunking_context).await?)
61            }
62            ReferencedAsset::External(request, ty) => Some(magic_identifier::mangle(&format!(
63                "{ty} external {request}"
64            ))),
65            ReferencedAsset::None | ReferencedAsset::Unresolvable => None,
66        })
67    }
68
69    pub(crate) async fn get_ident_from_placeable(
70        asset: &Vc<Box<dyn EcmascriptChunkPlaceable>>,
71        chunking_context: Vc<Box<dyn ChunkingContext>>,
72    ) -> Result<String> {
73        let id = asset.chunk_item_id(Vc::upcast(chunking_context)).await?;
74        Ok(magic_identifier::mangle(&format!("imported module {id}")))
75    }
76}
77
78#[turbo_tasks::value_impl]
79impl ReferencedAsset {
80    #[turbo_tasks::function]
81    pub async fn from_resolve_result(resolve_result: Vc<ModuleResolveResult>) -> Result<Vc<Self>> {
82        // TODO handle multiple keyed results
83        let result = resolve_result.await?;
84        if result.is_unresolvable_ref() {
85            return Ok(ReferencedAsset::Unresolvable.cell());
86        }
87        for (_, result) in result.primary.iter() {
88            match result {
89                ModuleResolveResultItem::External {
90                    name: request, ty, ..
91                } => {
92                    return Ok(ReferencedAsset::External(request.clone(), *ty).cell());
93                }
94                &ModuleResolveResultItem::Module(module) => {
95                    if let Some(placeable) =
96                        ResolvedVc::try_downcast::<Box<dyn EcmascriptChunkPlaceable>>(module)
97                    {
98                        return Ok(ReferencedAsset::Some(placeable).cell());
99                    }
100                }
101                // TODO ignore should probably be handled differently
102                _ => {}
103            }
104        }
105        Ok(ReferencedAsset::None.cell())
106    }
107}
108
109#[turbo_tasks::value(transparent)]
110pub struct EsmAssetReferences(Vec<ResolvedVc<EsmAssetReference>>);
111
112#[turbo_tasks::value_impl]
113impl EsmAssetReferences {
114    #[turbo_tasks::function]
115    pub fn empty() -> Vc<Self> {
116        Vc::cell(Vec::new())
117    }
118}
119
120#[turbo_tasks::value(shared)]
121#[derive(Hash, Debug)]
122pub struct EsmAssetReference {
123    pub origin: ResolvedVc<Box<dyn ResolveOrigin>>,
124    pub request: ResolvedVc<Request>,
125    pub annotations: ImportAnnotations,
126    pub issue_source: IssueSource,
127    pub export_name: Option<ModulePart>,
128    pub import_externals: bool,
129}
130
131impl EsmAssetReference {
132    fn get_origin(&self) -> Vc<Box<dyn ResolveOrigin>> {
133        if let Some(transition) = self.annotations.transition() {
134            self.origin.with_transition(transition.into())
135        } else {
136            *self.origin
137        }
138    }
139}
140
141impl EsmAssetReference {
142    pub fn new(
143        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
144        request: ResolvedVc<Request>,
145        issue_source: IssueSource,
146        annotations: Value<ImportAnnotations>,
147        export_name: Option<ModulePart>,
148        import_externals: bool,
149    ) -> Self {
150        EsmAssetReference {
151            origin,
152            request,
153            issue_source,
154            annotations: annotations.into_value(),
155            export_name,
156            import_externals,
157        }
158    }
159}
160
161#[turbo_tasks::value_impl]
162impl EsmAssetReference {
163    #[turbo_tasks::function]
164    pub(crate) fn get_referenced_asset(self: Vc<Self>) -> Vc<ReferencedAsset> {
165        ReferencedAsset::from_resolve_result(self.resolve_reference())
166    }
167}
168
169#[turbo_tasks::value_impl]
170impl ModuleReference for EsmAssetReference {
171    #[turbo_tasks::function]
172    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
173        let ty = if matches!(self.annotations.module_type(), Some("json")) {
174            EcmaScriptModulesReferenceSubType::ImportWithType(ImportWithType::Json)
175        } else if let Some(part) = &self.export_name {
176            EcmaScriptModulesReferenceSubType::ImportPart(part.clone())
177        } else {
178            EcmaScriptModulesReferenceSubType::Import
179        };
180
181        if let Some(ModulePart::Evaluation) = &self.export_name {
182            let module: ResolvedVc<crate::EcmascriptModuleAsset> =
183                ResolvedVc::try_downcast_type(self.origin)
184                    .expect("EsmAssetReference origin should be a EcmascriptModuleAsset");
185
186            let tree_shaking_mode = module.options().await?.tree_shaking_mode;
187
188            if let Some(TreeShakingMode::ModuleFragments) = tree_shaking_mode {
189                let side_effect_free_packages = module.asset_context().side_effect_free_packages();
190
191                if *module
192                    .is_marked_as_side_effect_free(side_effect_free_packages)
193                    .await?
194                {
195                    return Ok(ModuleResolveResult {
196                        primary: Box::new([(
197                            RequestKey::default(),
198                            ModuleResolveResultItem::Ignore,
199                        )]),
200                        affecting_sources: Default::default(),
201                    }
202                    .cell());
203                }
204            }
205        }
206
207        if let Request::Module { module, .. } = &*self.request.await? {
208            if module == TURBOPACK_PART_IMPORT_SOURCE {
209                if let Some(part) = &self.export_name {
210                    let module: ResolvedVc<crate::EcmascriptModuleAsset> =
211                        ResolvedVc::try_downcast_type(self.origin)
212                            .expect("EsmAssetReference origin should be a EcmascriptModuleAsset");
213
214                    return Ok(*ModuleResolveResult::module(ResolvedVc::upcast(
215                        EcmascriptModulePartAsset::select_part(*module, part.clone())
216                            .to_resolved()
217                            .await?,
218                    )));
219                }
220
221                bail!("export_name is required for part import")
222            }
223        }
224
225        let result = esm_resolve(
226            self.get_origin().resolve().await?,
227            *self.request,
228            Value::new(ty),
229            false,
230            Some(self.issue_source.clone()),
231        )
232        .await?;
233
234        if let Some(ModulePart::Export(export_name)) = &self.export_name {
235            for &module in result.primary_modules().await? {
236                if let Some(module) = ResolvedVc::try_downcast(module) {
237                    if *is_export_missing(*module, export_name.clone()).await? {
238                        InvalidExport {
239                            export: export_name.clone(),
240                            module,
241                            source: self.issue_source.clone(),
242                        }
243                        .resolved_cell()
244                        .emit();
245                    }
246                }
247            }
248        }
249
250        Ok(result)
251    }
252}
253
254#[turbo_tasks::value_impl]
255impl ValueToString for EsmAssetReference {
256    #[turbo_tasks::function]
257    async fn to_string(&self) -> Result<Vc<RcStr>> {
258        Ok(Vc::cell(
259            format!(
260                "import {} with {}",
261                self.request.to_string().await?,
262                self.annotations
263            )
264            .into(),
265        ))
266    }
267}
268
269#[turbo_tasks::value_impl]
270impl ChunkableModuleReference for EsmAssetReference {
271    #[turbo_tasks::function]
272    fn chunking_type(&self) -> Result<Vc<ChunkingTypeOption>> {
273        Ok(Vc::cell(
274            if let Some(chunking_type) = self.annotations.chunking_type() {
275                match chunking_type {
276                    "parallel" => Some(ChunkingType::Parallel {
277                        inherit_async: true,
278                        hoisted: true,
279                    }),
280                    "none" => None,
281                    _ => return Err(anyhow!("unknown chunking_type: {}", chunking_type)),
282                }
283            } else {
284                Some(ChunkingType::Parallel {
285                    inherit_async: true,
286                    hoisted: true,
287                })
288            },
289        ))
290    }
291}
292
293impl EsmAssetReference {
294    pub async fn code_generation(
295        self: Vc<Self>,
296        chunking_context: Vc<Box<dyn ChunkingContext>>,
297    ) -> Result<CodeGeneration> {
298        let this = &*self.await?;
299
300        // only chunked references can be imported
301        let result = if this.annotations.chunking_type() != Some("none") {
302            let import_externals = this.import_externals;
303            let referenced_asset = self.get_referenced_asset().await?;
304            if let ReferencedAsset::Unresolvable = &*referenced_asset {
305                // Insert code that throws immediately at time of import if a request is
306                // unresolvable
307                let request = request_to_string(*this.request).await?.to_string();
308                let stmt = Stmt::Expr(ExprStmt {
309                    expr: Box::new(throw_module_not_found_expr(&request)),
310                    span: DUMMY_SP,
311                });
312                Some((format!("throw {request}").into(), stmt))
313            } else if let Some(ident) = referenced_asset.get_ident(chunking_context).await? {
314                let span = this
315                    .issue_source
316                    .to_swc_offsets()
317                    .await?
318                    .map_or(DUMMY_SP, |(start, end)| {
319                        Span::new(BytePos(start), BytePos(end))
320                    });
321                match &*referenced_asset {
322                    ReferencedAsset::Unresolvable => {
323                        unreachable!()
324                    }
325                    ReferencedAsset::Some(asset) => {
326                        let id = asset.chunk_item_id(Vc::upcast(chunking_context)).await?;
327                        let name = ident;
328                        Some((
329                            id.to_string().into(),
330                            var_decl_with_span(
331                                quote!(
332                                    "var $name = $turbopack_import($id);" as Stmt,
333                                    name = Ident::new(name.clone().into(), DUMMY_SP, Default::default()),
334                                    turbopack_import: Expr = TURBOPACK_IMPORT.into(),
335                                    id: Expr = module_id_to_lit(&id),
336                                ),
337                                span,
338                            ),
339                        ))
340                    }
341                    ReferencedAsset::External(request, ExternalType::EcmaScriptModule) => {
342                        if !*chunking_context
343                            .environment()
344                            .supports_esm_externals()
345                            .await?
346                        {
347                            bail!(
348                                "the chunking context ({}) does not support external modules (esm \
349                                 request: {})",
350                                chunking_context.name().await?,
351                                request
352                            );
353                        }
354                        Some((
355                            ident.clone().into(),
356                            var_decl_with_span(
357                                if import_externals {
358                                    quote!(
359                                        "var $name = $turbopack_external_import($id);" as Stmt,
360                                        name = Ident::new(ident.clone().into(), DUMMY_SP, Default::default()),
361                                        turbopack_external_import: Expr = TURBOPACK_EXTERNAL_IMPORT.into(),
362                                        id: Expr = Expr::Lit(request.clone().to_string().into())
363                                    )
364                                } else {
365                                    quote!(
366                                        "var $name = $turbopack_external_require($id, () => require($id), true);" as Stmt,
367                                        name = Ident::new(ident.clone().into(), DUMMY_SP, Default::default()),
368                                        turbopack_external_require: Expr = TURBOPACK_EXTERNAL_REQUIRE.into(),
369                                        id: Expr = Expr::Lit(request.clone().to_string().into())
370                                    )
371                                },
372                                span,
373                            ),
374                        ))
375                    }
376                    ReferencedAsset::External(
377                        request,
378                        ExternalType::CommonJs | ExternalType::Url,
379                    ) => {
380                        if !*chunking_context
381                            .environment()
382                            .supports_commonjs_externals()
383                            .await?
384                        {
385                            bail!(
386                                "the chunking context ({}) does not support external modules \
387                                 (request: {})",
388                                chunking_context.name().await?,
389                                request
390                            );
391                        }
392                        Some((
393                            ident.clone().into(),
394                            var_decl_with_span(
395                                quote!(
396                                    "var $name = $turbopack_external_require($id, () => require($id), true);" as Stmt,
397                                    name = Ident::new(ident.clone().into(), DUMMY_SP, Default::default()),
398                                    turbopack_external_require: Expr = TURBOPACK_EXTERNAL_REQUIRE.into(),
399                                    id: Expr = Expr::Lit(request.clone().to_string().into())
400                                ),
401                                span,
402                            ),
403                        ))
404                    }
405                    // fallback in case we introduce a new `ExternalType`
406                    #[allow(unreachable_patterns)]
407                    ReferencedAsset::External(request, ty) => {
408                        bail!(
409                            "Unsupported external type {:?} for ESM reference with request: {:?}",
410                            ty,
411                            request
412                        )
413                    }
414                    ReferencedAsset::None => None,
415                }
416            } else {
417                None
418            }
419        } else {
420            None
421        };
422
423        if let Some((key, stmt)) = result {
424            Ok(CodeGeneration::hoisted_stmt(key, stmt))
425        } else {
426            Ok(CodeGeneration::empty())
427        }
428    }
429}
430
431fn var_decl_with_span(mut decl: Stmt, span: Span) -> Stmt {
432    match &mut decl {
433        Stmt::Decl(Decl::Var(decl)) => decl.span = span,
434        _ => panic!("Expected Stmt::Decl::Var"),
435    };
436    decl
437}
438
439#[turbo_tasks::value(shared)]
440pub struct InvalidExport {
441    export: RcStr,
442    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
443    source: IssueSource,
444}
445
446#[turbo_tasks::value_impl]
447impl Issue for InvalidExport {
448    #[turbo_tasks::function]
449    fn severity(&self) -> Vc<IssueSeverity> {
450        IssueSeverity::Error.into()
451    }
452
453    #[turbo_tasks::function]
454    async fn title(&self) -> Result<Vc<StyledString>> {
455        Ok(StyledString::Line(vec![
456            StyledString::Text("Export ".into()),
457            StyledString::Code(self.export.clone()),
458            StyledString::Text(" doesn't exist in target module".into()),
459        ])
460        .cell())
461    }
462
463    #[turbo_tasks::function]
464    fn stage(&self) -> Vc<IssueStage> {
465        IssueStage::Bindings.into()
466    }
467
468    #[turbo_tasks::function]
469    fn file_path(&self) -> Vc<FileSystemPath> {
470        self.source.file_path()
471    }
472
473    #[turbo_tasks::function]
474    async fn description(&self) -> Result<Vc<OptionStyledString>> {
475        let export_names = all_known_export_names(*self.module).await?;
476        let did_you_mean = export_names
477            .iter()
478            .map(|s| (s, jaro(self.export.as_str(), s.as_str())))
479            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
480            .map(|(s, _)| s);
481        Ok(Vc::cell(Some(
482            StyledString::Stack(vec![
483                StyledString::Line(vec![
484                    StyledString::Text("The export ".into()),
485                    StyledString::Code(self.export.clone()),
486                    StyledString::Text(" was not found in module ".into()),
487                    StyledString::Strong(self.module.ident().to_string().owned().await?),
488                    StyledString::Text(".".into()),
489                ]),
490                if let Some(did_you_mean) = did_you_mean {
491                    StyledString::Line(vec![
492                        StyledString::Text("Did you mean to import ".into()),
493                        StyledString::Code(did_you_mean.clone()),
494                        StyledString::Text("?".into()),
495                    ])
496                } else {
497                    StyledString::Strong("The module has no exports at all.".into())
498                },
499                StyledString::Text(
500                    "All exports of the module are statically known (It doesn't have dynamic \
501                     exports). So it's known statically that the requested export doesn't exist."
502                        .into(),
503                ),
504            ])
505            .resolved_cell(),
506        )))
507    }
508
509    #[turbo_tasks::function]
510    async fn detail(&self) -> Result<Vc<OptionStyledString>> {
511        let export_names = all_known_export_names(*self.module).await?;
512        Ok(Vc::cell(Some(
513            StyledString::Line(vec![
514                StyledString::Text("These are the exports of the module:\n".into()),
515                StyledString::Code(
516                    export_names
517                        .iter()
518                        .map(|s| s.as_str())
519                        .intersperse(", ")
520                        .collect::<String>()
521                        .into(),
522                ),
523            ])
524            .resolved_cell(),
525        )))
526    }
527
528    #[turbo_tasks::function]
529    fn source(&self) -> Vc<OptionIssueSource> {
530        Vc::cell(Some(self.source.clone()))
531    }
532}