1use std::{collections::BTreeMap, future::Future, pin::Pin};
2
3use anyhow::{Result, bail};
4use bincode::{Decode, Encode};
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{
7 FxIndexSet, NonLocalValue, ResolvedVc, TryJoinIterExt, ValueToString, Vc,
8 debug::ValueDebugFormat, trace::TraceRawVcs, turbofmt,
9};
10use turbo_tasks_fs::{FileSystemPath, glob::Glob};
11
12use crate::{
13 issue::Issue,
14 resolve::{
15 AliasPattern, ExternalTraced, ExternalType, ResolveResult, ResolveResultItem,
16 alias_map::{AliasMap, AliasTemplate},
17 parse::Request,
18 pattern::Pattern,
19 plugin::{AfterResolvePlugin, BeforeResolvePlugin},
20 },
21};
22
23#[turbo_tasks::value(transparent)]
24#[derive(Debug)]
25pub struct ExcludedExtensions(#[bincode(with = "turbo_bincode::indexset")] pub FxIndexSet<RcStr>);
26
27#[derive(
29 TraceRawVcs, Hash, PartialEq, Eq, Clone, Debug, ValueDebugFormat, NonLocalValue, Encode, Decode,
30)]
31pub enum ResolveModules {
32 Nested(FileSystemPath, Vec<RcStr>),
35 Path {
37 dir: FileSystemPath,
38 excluded_extensions: ResolvedVc<ExcludedExtensions>,
39 },
40}
41
42#[derive(
43 TraceRawVcs, Hash, PartialEq, Eq, Clone, Copy, Debug, NonLocalValue, Encode, Decode, Default,
44)]
45pub enum ConditionValue {
46 Set,
47 #[default]
48 Unset,
49 Unknown,
50}
51
52impl From<bool> for ConditionValue {
53 fn from(v: bool) -> Self {
54 if v {
55 ConditionValue::Set
56 } else {
57 ConditionValue::Unset
58 }
59 }
60}
61
62pub type ResolutionConditions = BTreeMap<RcStr, ConditionValue>;
63
64#[derive(TraceRawVcs, Hash, PartialEq, Eq, Clone, Debug, NonLocalValue, Encode, Decode)]
66pub enum ResolveIntoPackage {
67 ExportsField {
71 conditions: ResolutionConditions,
72 unspecified_conditions: ConditionValue,
73 },
74 MainField { field: RcStr },
80}
81
82#[derive(TraceRawVcs, Hash, PartialEq, Eq, Clone, Debug, NonLocalValue, Encode, Decode)]
84pub enum ResolveInPackage {
85 AliasField(RcStr),
87 ImportsField {
91 conditions: ResolutionConditions,
92 unspecified_conditions: ConditionValue,
93 },
94}
95
96#[turbo_tasks::value(shared)]
97#[derive(Clone)]
98pub enum ImportMapping {
99 External(Option<RcStr>, ExternalType, ExternalTraced),
101 PrimaryAlternativeExternal {
106 name: Option<RcStr>,
107 ty: ExternalType,
108 traced: ExternalTraced,
109 lookup_dir: FileSystemPath,
110 },
111 Direct(ResolvedVc<ResolveResult>),
113 PrimaryAlternative(RcStr, Option<FileSystemPath>),
117 Ignore,
119 Empty,
121 Error(ResolvedVc<Box<dyn Issue>>),
123 Alternatives(Vec<ResolvedVc<ImportMapping>>),
124 Dynamic(ResolvedVc<Box<dyn ImportMappingReplacement>>),
125}
126
127#[turbo_tasks::value(shared)]
130#[derive(Clone)]
131pub enum ReplacedImportMapping {
132 External {
133 name_override: Option<RcStr>,
134 ty: ExternalType,
135 traced: ExternalTraced,
136 target: Option<FileSystemPath>,
137 },
138 PrimaryAlternativeExternal {
139 name: Option<RcStr>,
140 ty: ExternalType,
141 traced: ExternalTraced,
142 lookup_dir: FileSystemPath,
143 },
144 Direct(ResolvedVc<ResolveResult>),
145 PrimaryAlternative(Pattern, Option<FileSystemPath>),
146 Ignore,
147 Empty,
148 Alternatives(Vec<ResolvedVc<ReplacedImportMapping>>),
149 Dynamic(ResolvedVc<Box<dyn ImportMappingReplacement>>),
150 Error(ResolvedVc<Box<dyn Issue>>),
151}
152
153impl ImportMapping {
154 pub fn primary_alternatives(
155 list: Vec<RcStr>,
156 lookup_path: Option<FileSystemPath>,
157 ) -> ImportMapping {
158 if list.is_empty() {
159 ImportMapping::Ignore
160 } else if list.len() == 1 {
161 ImportMapping::PrimaryAlternative(list.into_iter().next().unwrap(), lookup_path)
162 } else {
163 ImportMapping::Alternatives(
164 list.into_iter()
165 .map(|s| {
166 ImportMapping::PrimaryAlternative(s, lookup_path.clone()).resolved_cell()
167 })
168 .collect(),
169 )
170 }
171 }
172}
173
174impl AliasTemplate for ResolvedVc<ImportMapping> {
175 type Output<'a> = <Vc<ImportMapping> as AliasTemplate>::Output<'a>;
176
177 fn convert(&self) -> Self::Output<'_> {
178 (**self).convert()
179 }
180
181 fn replace<'a>(&'a self, capture: &Pattern) -> Self::Output<'a> {
182 (**self).replace(capture)
183 }
184}
185
186impl AliasTemplate for Vc<ImportMapping> {
187 type Output<'a> =
188 Pin<Box<dyn Future<Output = Result<ResolvedVc<ReplacedImportMapping>>> + Send + 'a>>;
189
190 fn convert(&self) -> Self::Output<'_> {
191 Box::pin(async move {
192 let this = &*self.await?;
193 Ok(match this {
194 ImportMapping::External(name, ty, traced) => ReplacedImportMapping::External {
195 name_override: name.clone(),
196 ty: *ty,
197 traced: *traced,
198 target: None,
200 },
201 ImportMapping::PrimaryAlternativeExternal {
202 name,
203 ty,
204 traced,
205 lookup_dir,
206 } => ReplacedImportMapping::PrimaryAlternativeExternal {
207 name: name.clone(),
208 ty: *ty,
209 traced: *traced,
210 lookup_dir: lookup_dir.clone(),
211 },
212 ImportMapping::PrimaryAlternative(name, lookup_dir) => {
213 ReplacedImportMapping::PrimaryAlternative(
214 (*name).clone().into(),
215 lookup_dir.clone(),
216 )
217 }
218 ImportMapping::Direct(v) => ReplacedImportMapping::Direct(*v),
219 ImportMapping::Ignore => ReplacedImportMapping::Ignore,
220 ImportMapping::Empty => ReplacedImportMapping::Empty,
221 ImportMapping::Alternatives(alternatives) => ReplacedImportMapping::Alternatives(
222 alternatives
223 .iter()
224 .map(|mapping| mapping.convert())
225 .try_join()
226 .await?,
227 ),
228 ImportMapping::Dynamic(replacement) => ReplacedImportMapping::Dynamic(*replacement),
229 ImportMapping::Error(issue) => ReplacedImportMapping::Error(*issue),
230 }
231 .resolved_cell())
232 })
233 }
234
235 fn replace<'a>(&'a self, capture: &Pattern) -> Self::Output<'a> {
236 let capture = capture.clone();
237 Box::pin(async move {
238 let this = &*self.await?;
239 Ok(match this {
240 ImportMapping::External(name, ty, traced) => {
241 if let Some(name) = name {
242 ReplacedImportMapping::External {
243 name_override: capture
244 .spread_into_star(name)
245 .as_constant_string()
246 .cloned(),
247 ty: *ty,
248 traced: *traced,
249 target: None,
250 }
251 } else {
252 ReplacedImportMapping::External {
253 name_override: None,
254 ty: *ty,
255 traced: *traced,
256 target: None,
257 }
258 }
259 }
260 ImportMapping::PrimaryAlternativeExternal {
261 name,
262 ty,
263 traced,
264 lookup_dir,
265 } => {
266 if let Some(name) = name {
267 ReplacedImportMapping::PrimaryAlternativeExternal {
268 name: capture.spread_into_star(name).as_constant_string().cloned(),
269 ty: *ty,
270 traced: *traced,
271 lookup_dir: lookup_dir.clone(),
272 }
273 } else {
274 ReplacedImportMapping::PrimaryAlternativeExternal {
275 name: None,
276 ty: *ty,
277 traced: *traced,
278 lookup_dir: lookup_dir.clone(),
279 }
280 }
281 }
282 ImportMapping::PrimaryAlternative(name, lookup_dir) => {
283 ReplacedImportMapping::PrimaryAlternative(
284 capture.spread_into_star(name),
285 lookup_dir.clone(),
286 )
287 }
288 ImportMapping::Direct(v) => ReplacedImportMapping::Direct(*v),
289 ImportMapping::Ignore => ReplacedImportMapping::Ignore,
290 ImportMapping::Empty => ReplacedImportMapping::Empty,
291 ImportMapping::Alternatives(alternatives) => ReplacedImportMapping::Alternatives(
292 alternatives
293 .iter()
294 .map(|mapping| mapping.replace(&capture))
295 .try_join()
296 .await?,
297 ),
298 ImportMapping::Dynamic(replacement) => {
299 replacement
300 .replace(Pattern::new(capture.clone()))
301 .owned()
302 .await?
303 }
304 ImportMapping::Error(issue) => ReplacedImportMapping::Error(*issue),
305 }
306 .resolved_cell())
307 })
308 }
309}
310
311#[turbo_tasks::value(shared)]
312#[derive(Clone, Default)]
313pub struct ImportMap {
314 map: AliasMap<ResolvedVc<ImportMapping>>,
315}
316
317impl ImportMap {
318 pub fn new(map: AliasMap<ResolvedVc<ImportMapping>>) -> ImportMap {
320 Self { map }
321 }
322
323 pub fn empty() -> Self {
325 Self::default()
326 }
327
328 pub fn extend_ref(&mut self, other: &ImportMap) {
330 let Self { map } = other.clone();
331 self.map.extend(map);
332 }
333
334 pub fn insert_alias(&mut self, alias: AliasPattern, mapping: ResolvedVc<ImportMapping>) {
336 self.map.insert(alias, mapping);
337 }
338
339 pub fn insert_exact_alias<'a>(
341 &mut self,
342 pattern: impl Into<RcStr> + 'a,
343 mapping: ResolvedVc<ImportMapping>,
344 ) {
345 self.map.insert(AliasPattern::exact(pattern), mapping);
346 }
347
348 pub fn insert_wildcard_alias<'a>(
350 &mut self,
351 prefix: impl Into<RcStr> + 'a,
352 mapping: ResolvedVc<ImportMapping>,
353 ) {
354 self.map
355 .insert(AliasPattern::wildcard(prefix, rcstr!("")), mapping);
356 }
357
358 pub fn insert_wildcard_alias_with_suffix<'p, 's>(
360 &mut self,
361 prefix: impl Into<RcStr> + 'p,
362 suffix: impl Into<RcStr> + 's,
363 mapping: ResolvedVc<ImportMapping>,
364 ) {
365 self.map
366 .insert(AliasPattern::wildcard(prefix, suffix), mapping);
367 }
368
369 pub fn insert_singleton_alias<'a>(
372 &mut self,
373 prefix: impl Into<RcStr> + 'a,
374 context_path: FileSystemPath,
375 ) {
376 let prefix: RcStr = prefix.into();
377 let wildcard_prefix: RcStr = (prefix.to_string() + "/").into();
378 let wildcard_alias: RcStr = (prefix.to_string() + "/*").into();
379 self.insert_exact_alias(
380 prefix.clone(),
381 ImportMapping::PrimaryAlternative(prefix.clone(), Some(context_path.clone()))
382 .resolved_cell(),
383 );
384 self.insert_wildcard_alias(
385 wildcard_prefix,
386 ImportMapping::PrimaryAlternative(wildcard_alias, Some(context_path)).resolved_cell(),
387 );
388 }
389}
390
391#[turbo_tasks::value_impl]
392impl ImportMap {
393 #[turbo_tasks::function]
395 pub async fn extend(self: Vc<Self>, other: ResolvedVc<ImportMap>) -> Result<Vc<Self>> {
396 let mut import_map = self.owned().await?;
397 import_map.extend_ref(&*other.await?);
398 Ok(import_map.cell())
399 }
400}
401
402#[turbo_tasks::value(shared)]
403#[derive(Clone, Default)]
404pub struct ResolvedMap {
405 pub by_glob: Vec<(FileSystemPath, ResolvedVc<Glob>, ResolvedVc<ImportMapping>)>,
406}
407
408#[turbo_tasks::value(shared)]
409#[derive(Clone)]
410pub enum ImportMapResult {
411 Result(ResolvedVc<ResolveResult>),
412 External {
413 name: RcStr,
414 ty: ExternalType,
415 traced: ExternalTraced,
416 target: Option<FileSystemPath>,
417 },
418 AliasExternal {
419 name: RcStr,
420 ty: ExternalType,
421 traced: ExternalTraced,
422 lookup_dir: FileSystemPath,
423 },
424 Alias(ResolvedVc<Request>, Option<FileSystemPath>),
425 Alternatives(Vec<ImportMapResult>),
426 NoEntry,
427 Error(ResolvedVc<Box<dyn Issue>>),
428}
429
430async fn import_mapping_to_result(
431 mapping: Vc<ReplacedImportMapping>,
432 lookup_path: FileSystemPath,
433 request: Vc<Request>,
434) -> Result<ImportMapResult> {
435 Ok(match &*mapping.await? {
436 ReplacedImportMapping::Direct(result) => ImportMapResult::Result(*result),
437 ReplacedImportMapping::External {
438 name_override,
439 ty,
440 traced,
441 target,
442 } => ImportMapResult::External {
443 name: if let Some(name) = name_override {
444 name.clone()
445 } else if let Some(request) = request.await?.request() {
446 request
447 } else {
448 bail!(
449 "Cannot resolve external reference with dynamic request {:?}",
450 request.request_pattern().await?.describe_as_string()
451 )
452 },
453 ty: *ty,
454 traced: *traced,
455 target: target.clone(),
456 },
457 ReplacedImportMapping::PrimaryAlternativeExternal {
458 name,
459 ty,
460 traced,
461 lookup_dir,
462 } => ImportMapResult::AliasExternal {
463 name: if let Some(name) = name {
464 name.clone()
465 } else if let Some(request) = request.await?.request() {
466 request
467 } else {
468 bail!(
469 "Cannot resolve external reference with dynamic request {:?}",
470 request.request_pattern().await?.describe_as_string()
471 )
472 },
473 ty: *ty,
474 traced: *traced,
475 lookup_dir: lookup_dir.clone(),
476 },
477 ReplacedImportMapping::Ignore => ImportMapResult::Result(
478 ResolveResult::primary(ResolveResultItem::Ignore).resolved_cell(),
479 ),
480 ReplacedImportMapping::Empty => ImportMapResult::Result(
481 ResolveResult::primary(ResolveResultItem::Empty).resolved_cell(),
482 ),
483 ReplacedImportMapping::PrimaryAlternative(name, context) => {
484 let request = Request::parse(name.clone()).to_resolved().await?;
485 ImportMapResult::Alias(request, context.clone())
486 }
487 ReplacedImportMapping::Alternatives(list) => ImportMapResult::Alternatives(
488 list.iter()
489 .map(|mapping| {
490 Box::pin(import_mapping_to_result(
491 **mapping,
492 lookup_path.clone(),
493 request,
494 ))
495 })
496 .try_join()
497 .await?,
498 ),
499 ReplacedImportMapping::Dynamic(replacement) => {
500 replacement.result(lookup_path, request).owned().await?
501 }
502 ReplacedImportMapping::Error(issue) => ImportMapResult::Error(*issue),
503 })
504}
505
506#[turbo_tasks::value_impl]
507impl ValueToString for ImportMapResult {
508 #[turbo_tasks::function]
509 async fn to_string(&self) -> Result<Vc<RcStr>> {
510 match self {
511 ImportMapResult::Result(_) => Ok(Vc::cell(rcstr!("Resolved by import map"))),
512 ImportMapResult::External { .. } => Ok(Vc::cell(rcstr!("TODO external"))),
513 ImportMapResult::AliasExternal { .. } => Ok(Vc::cell(rcstr!("TODO external"))),
514 ImportMapResult::Alias(request, context) => {
515 let s = if let Some(path) = context {
516 turbofmt!("aliased to {} inside of {path}", *request).await?
517 } else {
518 turbofmt!("aliased to {}", *request).await?
519 };
520 Ok(Vc::cell(s))
521 }
522 ImportMapResult::Alternatives(alternatives) => {
523 let strings = alternatives
527 .iter()
528 .map(|alternative| alternative.clone().cell().to_string())
529 .try_join()
530 .await?;
531 let strings = strings
532 .iter()
533 .map(|string| string.as_str())
534 .collect::<Vec<_>>();
535 Ok(Vc::cell(strings.join(" | ").into()))
536 }
537 ImportMapResult::NoEntry => Ok(Vc::cell(rcstr!("No import map entry"))),
538 ImportMapResult::Error(issue) => Ok(Vc::cell(
539 format!(
540 "error: {}",
541 issue
542 .into_trait_ref()
543 .await?
544 .title()
545 .await?
546 .to_unstyled_string()
547 )
548 .into(),
549 )),
550 }
551 }
552}
553
554impl ImportMap {
555 pub async fn lookup(
558 &self,
559 lookup_path: FileSystemPath,
560 request: Vc<Request>,
561 ) -> Result<ImportMapResult> {
562 let request_pattern = request.request_pattern().await?;
565 if matches!(*request_pattern, Pattern::Dynamic | Pattern::DynamicNoSlash) {
566 return Ok(ImportMapResult::NoEntry);
569 }
570
571 let (req_rel, rest) = request_pattern.split_could_match("./");
572 let (req_rel_parent, req_rest) =
573 rest.map(|r| r.split_could_match("../")).unwrap_or_default();
574
575 let lookup_rel = req_rel.as_ref().and_then(|req| {
576 self.map
577 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("./"))
578 .next()
579 });
580 let lookup_rel_parent = req_rel_parent.as_ref().and_then(|req| {
581 self.map
582 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("../"))
583 .next()
584 });
585 let lookup = req_rest
586 .as_ref()
587 .and_then(|req| self.map.lookup(req).next());
588
589 let results = lookup_rel
590 .into_iter()
591 .chain(lookup_rel_parent)
592 .chain(lookup)
593 .map(async |result| {
594 import_mapping_to_result(*result?.output.await?, lookup_path.clone(), request).await
595 })
596 .try_join()
597 .await?;
598
599 Ok(match results.len() {
600 0 => ImportMapResult::NoEntry,
601 1 => results.into_iter().next().unwrap(),
602 2.. => ImportMapResult::Alternatives(results),
603 })
604 }
605}
606
607#[turbo_tasks::value_impl]
608impl ResolvedMap {
609 #[turbo_tasks::function]
610 pub async fn lookup(
611 &self,
612 resolved: FileSystemPath,
613 lookup_path: FileSystemPath,
614 request: Vc<Request>,
615 ) -> Result<Vc<ImportMapResult>> {
616 for (root, glob, mapping) in self.by_glob.iter() {
617 if let Some(path) = root.get_path_to(&resolved)
618 && glob.await?.matches(path)
619 {
620 return Ok(import_mapping_to_result(
621 *mapping.convert().await?,
622 lookup_path,
623 request,
624 )
625 .await?
626 .cell());
627 }
628 }
629 Ok(ImportMapResult::NoEntry.cell())
630 }
631}
632
633#[turbo_tasks::value(shared)]
634#[derive(Clone, Debug, Default)]
635pub struct ResolveOptions {
636 pub fully_specified: bool,
639 pub prefer_relative: bool,
642 pub extensions: Vec<RcStr>,
644 pub modules: Vec<ResolveModules>,
646 pub into_package: Vec<ResolveIntoPackage>,
648 pub in_package: Vec<ResolveInPackage>,
650 pub default_files: Vec<RcStr>,
652 pub import_map: Option<ResolvedVc<ImportMap>>,
654 pub fallback_import_map: Option<ResolvedVc<ImportMap>>,
656 pub resolved_map: Option<ResolvedVc<ResolvedMap>>,
657 pub before_resolve_plugins: Vec<ResolvedVc<Box<dyn BeforeResolvePlugin>>>,
658 pub after_resolve_plugins: Vec<ResolvedVc<Box<dyn AfterResolvePlugin>>>,
659 pub enable_typescript_with_output_extension: bool,
661 pub loose_errors: bool,
663 pub collect_affecting_sources: bool,
665 pub parse_data_uris: bool,
667
668 pub placeholder_for_future_extensions: (),
669}
670
671#[turbo_tasks::value_impl]
672impl ResolveOptions {
673 #[turbo_tasks::function]
676 pub async fn with_extended_import_map(
677 self: Vc<Self>,
678 import_map: Vc<ImportMap>,
679 ) -> Result<Vc<Self>> {
680 let mut resolve_options = self.owned().await?;
681 resolve_options.import_map = Some(
682 resolve_options
683 .import_map
684 .map(|current_import_map| current_import_map.extend(import_map))
685 .unwrap_or(import_map)
686 .to_resolved()
687 .await?,
688 );
689 Ok(resolve_options.cell())
690 }
691
692 #[turbo_tasks::function]
695 pub async fn with_extended_fallback_import_map(
696 self: Vc<Self>,
697 extended_import_map: ResolvedVc<ImportMap>,
698 ) -> Result<Vc<Self>> {
699 let mut resolve_options = self.owned().await?;
700 resolve_options.fallback_import_map =
701 if let Some(current_fallback) = resolve_options.fallback_import_map {
702 Some(
703 current_fallback
704 .extend(*extended_import_map)
705 .to_resolved()
706 .await?,
707 )
708 } else {
709 Some(extended_import_map)
710 };
711 Ok(resolve_options.cell())
712 }
713
714 #[turbo_tasks::function]
716 pub async fn with_extensions(self: Vc<Self>, extensions: Vec<RcStr>) -> Result<Vc<Self>> {
717 let mut resolve_options = self.owned().await?;
718 resolve_options.extensions = extensions;
719 Ok(resolve_options.cell())
720 }
721
722 #[turbo_tasks::function]
724 pub async fn with_fully_specified(self: Vc<Self>, fully_specified: bool) -> Result<Vc<Self>> {
725 let mut resolve_options = self.owned().await?;
726 if resolve_options.fully_specified == fully_specified {
727 return Ok(self);
728 }
729 resolve_options.fully_specified = fully_specified;
730 Ok(resolve_options.cell())
731 }
732}
733
734#[turbo_tasks::value(shared)]
735#[derive(Hash, Clone, Debug)]
736pub struct ResolveModulesOptions {
737 pub modules: Vec<ResolveModules>,
738 pub extensions: Vec<RcStr>,
739}
740
741#[turbo_tasks::function]
742pub async fn resolve_modules_options(
743 options: Vc<ResolveOptions>,
744) -> Result<Vc<ResolveModulesOptions>> {
745 let options = options.await?;
746 Ok(ResolveModulesOptions {
747 modules: options.modules.clone(),
748 extensions: options.extensions.clone(),
749 }
750 .cell())
751}
752
753#[turbo_tasks::value_trait]
754pub trait ImportMappingReplacement {
755 #[turbo_tasks::function]
756 fn replace(self: Vc<Self>, capture: Vc<Pattern>) -> Vc<ReplacedImportMapping>;
757 #[turbo_tasks::function]
758 fn result(
759 self: Vc<Self>,
760 lookup_path: FileSystemPath,
761 request: Vc<Request>,
762 ) -> Vc<ImportMapResult>;
763}