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 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 _ => {}
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 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 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 #[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}