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