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