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!(
537 "error: {}",
538 issue
539 .into_trait_ref()
540 .await?
541 .title()
542 .await?
543 .to_unstyled_string()
544 )
545 .into(),
546 )),
547 }
548 }
549}
550
551impl ImportMap {
552 pub async fn lookup(
555 &self,
556 lookup_path: FileSystemPath,
557 request: Vc<Request>,
558 ) -> Result<ImportMapResult> {
559 let request_pattern = request.request_pattern().await?;
562 if matches!(*request_pattern, Pattern::Dynamic | Pattern::DynamicNoSlash) {
563 return Ok(ImportMapResult::NoEntry);
566 }
567
568 let (req_rel, rest) = request_pattern.split_could_match("./");
569 let (req_rel_parent, req_rest) =
570 rest.map(|r| r.split_could_match("../")).unwrap_or_default();
571
572 let lookup_rel = req_rel.as_ref().and_then(|req| {
573 self.map
574 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("./"))
575 .next()
576 });
577 let lookup_rel_parent = req_rel_parent.as_ref().and_then(|req| {
578 self.map
579 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("../"))
580 .next()
581 });
582 let lookup = req_rest
583 .as_ref()
584 .and_then(|req| self.map.lookup(req).next());
585
586 let results = lookup_rel
587 .into_iter()
588 .chain(lookup_rel_parent)
589 .chain(lookup)
590 .map(async |result| {
591 import_mapping_to_result(*result?.output.await?, lookup_path.clone(), request).await
592 })
593 .try_join()
594 .await?;
595
596 Ok(match results.len() {
597 0 => ImportMapResult::NoEntry,
598 1 => results.into_iter().next().unwrap(),
599 2.. => ImportMapResult::Alternatives(results),
600 })
601 }
602}
603
604#[turbo_tasks::value_impl]
605impl ResolvedMap {
606 #[turbo_tasks::function]
607 pub async fn lookup(
608 &self,
609 resolved: FileSystemPath,
610 lookup_path: FileSystemPath,
611 request: Vc<Request>,
612 ) -> Result<Vc<ImportMapResult>> {
613 for (root, glob, mapping) in self.by_glob.iter() {
614 if let Some(path) = root.get_path_to(&resolved)
615 && glob.await?.matches(path)
616 {
617 return Ok(import_mapping_to_result(
618 *mapping.convert().await?,
619 lookup_path,
620 request,
621 )
622 .await?
623 .cell());
624 }
625 }
626 Ok(ImportMapResult::NoEntry.cell())
627 }
628}
629
630#[turbo_tasks::value(shared)]
631#[derive(Clone, Debug, Default)]
632pub struct ResolveOptions {
633 pub fully_specified: bool,
636 pub prefer_relative: bool,
639 pub extensions: Vec<RcStr>,
641 pub modules: Vec<ResolveModules>,
643 pub into_package: Vec<ResolveIntoPackage>,
645 pub in_package: Vec<ResolveInPackage>,
647 pub default_files: Vec<RcStr>,
649 pub import_map: Option<ResolvedVc<ImportMap>>,
651 pub fallback_import_map: Option<ResolvedVc<ImportMap>>,
653 pub resolved_map: Option<ResolvedVc<ResolvedMap>>,
654 pub before_resolve_plugins: Vec<ResolvedVc<Box<dyn BeforeResolvePlugin>>>,
655 pub after_resolve_plugins: Vec<ResolvedVc<Box<dyn AfterResolvePlugin>>>,
656 pub enable_typescript_with_output_extension: bool,
658 pub loose_errors: bool,
660 pub collect_affecting_sources: bool,
662 pub parse_data_uris: bool,
664
665 pub placeholder_for_future_extensions: (),
666}
667
668#[turbo_tasks::value_impl]
669impl ResolveOptions {
670 #[turbo_tasks::function]
673 pub async fn with_extended_import_map(
674 self: Vc<Self>,
675 import_map: Vc<ImportMap>,
676 ) -> Result<Vc<Self>> {
677 let mut resolve_options = self.owned().await?;
678 resolve_options.import_map = Some(
679 resolve_options
680 .import_map
681 .map(|current_import_map| current_import_map.extend(import_map))
682 .unwrap_or(import_map)
683 .to_resolved()
684 .await?,
685 );
686 Ok(resolve_options.cell())
687 }
688
689 #[turbo_tasks::function]
692 pub async fn with_extended_fallback_import_map(
693 self: Vc<Self>,
694 extended_import_map: ResolvedVc<ImportMap>,
695 ) -> Result<Vc<Self>> {
696 let mut resolve_options = self.owned().await?;
697 resolve_options.fallback_import_map =
698 if let Some(current_fallback) = resolve_options.fallback_import_map {
699 Some(
700 current_fallback
701 .extend(*extended_import_map)
702 .to_resolved()
703 .await?,
704 )
705 } else {
706 Some(extended_import_map)
707 };
708 Ok(resolve_options.cell())
709 }
710
711 #[turbo_tasks::function]
713 pub async fn with_extensions(self: Vc<Self>, extensions: Vec<RcStr>) -> Result<Vc<Self>> {
714 let mut resolve_options = self.owned().await?;
715 resolve_options.extensions = extensions;
716 Ok(resolve_options.cell())
717 }
718
719 #[turbo_tasks::function]
721 pub async fn with_fully_specified(self: Vc<Self>, fully_specified: bool) -> Result<Vc<Self>> {
722 let mut resolve_options = self.owned().await?;
723 if resolve_options.fully_specified == fully_specified {
724 return Ok(self);
725 }
726 resolve_options.fully_specified = fully_specified;
727 Ok(resolve_options.cell())
728 }
729}
730
731#[turbo_tasks::value(shared)]
732#[derive(Hash, Clone, Debug)]
733pub struct ResolveModulesOptions {
734 pub modules: Vec<ResolveModules>,
735 pub extensions: Vec<RcStr>,
736}
737
738#[turbo_tasks::function]
739pub async fn resolve_modules_options(
740 options: Vc<ResolveOptions>,
741) -> Result<Vc<ResolveModulesOptions>> {
742 let options = options.await?;
743 Ok(ResolveModulesOptions {
744 modules: options.modules.clone(),
745 extensions: options.extensions.clone(),
746 }
747 .cell())
748}
749
750#[turbo_tasks::value_trait]
751pub trait ImportMappingReplacement {
752 #[turbo_tasks::function]
753 fn replace(self: Vc<Self>, capture: Vc<Pattern>) -> Vc<ReplacedImportMapping>;
754 #[turbo_tasks::function]
755 fn result(
756 self: Vc<Self>,
757 lookup_path: FileSystemPath,
758 request: Vc<Request>,
759 ) -> Vc<ImportMapResult>;
760}