1use std::{collections::BTreeMap, future::Future, pin::Pin};
2
3use anyhow::{Result, bail};
4use serde::{Deserialize, Serialize};
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{
7 FxIndexSet, NonLocalValue, ResolvedVc, TryJoinIterExt, ValueToString, Vc,
8 debug::ValueDebugFormat, trace::TraceRawVcs,
9};
10use turbo_tasks_fs::{FileSystemPath, glob::Glob};
11
12use super::{
13 AliasPattern, ExternalType, ResolveResult, ResolveResultItem,
14 alias_map::{AliasMap, AliasTemplate},
15 pattern::Pattern,
16 plugin::BeforeResolvePlugin,
17};
18use crate::resolve::{ExternalTraced, parse::Request, plugin::AfterResolvePlugin};
19
20#[turbo_tasks::value(shared)]
21#[derive(Hash, Debug)]
22pub struct LockedVersions {}
23
24#[turbo_tasks::value(transparent)]
25#[derive(Debug)]
26pub struct ExcludedExtensions(pub FxIndexSet<RcStr>);
27
28#[derive(
30 TraceRawVcs,
31 Hash,
32 PartialEq,
33 Eq,
34 Clone,
35 Debug,
36 Serialize,
37 Deserialize,
38 ValueDebugFormat,
39 NonLocalValue,
40)]
41pub enum ResolveModules {
42 Nested(FileSystemPath, Vec<RcStr>),
45 Path {
47 dir: FileSystemPath,
48 excluded_extensions: ResolvedVc<ExcludedExtensions>,
49 },
50}
51
52#[derive(
53 TraceRawVcs, Hash, PartialEq, Eq, Clone, Copy, Debug, Serialize, Deserialize, NonLocalValue,
54)]
55pub enum ConditionValue {
56 Set,
57 Unset,
58 Unknown,
59}
60
61impl From<bool> for ConditionValue {
62 fn from(v: bool) -> Self {
63 if v {
64 ConditionValue::Set
65 } else {
66 ConditionValue::Unset
67 }
68 }
69}
70
71pub type ResolutionConditions = BTreeMap<RcStr, ConditionValue>;
72
73#[derive(TraceRawVcs, Hash, PartialEq, Eq, Clone, Debug, Serialize, Deserialize, NonLocalValue)]
75pub enum ResolveIntoPackage {
76 ExportsField {
80 conditions: ResolutionConditions,
81 unspecified_conditions: ConditionValue,
82 },
83 MainField { field: RcStr },
89}
90
91#[derive(TraceRawVcs, Hash, PartialEq, Eq, Clone, Debug, Serialize, Deserialize, NonLocalValue)]
93pub enum ResolveInPackage {
94 AliasField(RcStr),
96 ImportsField {
100 conditions: ResolutionConditions,
101 unspecified_conditions: ConditionValue,
102 },
103}
104
105#[turbo_tasks::value(shared)]
106#[derive(Clone)]
107pub enum ImportMapping {
108 External(Option<RcStr>, ExternalType, ExternalTraced),
110 PrimaryAlternativeExternal {
115 name: Option<RcStr>,
116 ty: ExternalType,
117 traced: ExternalTraced,
118 lookup_dir: FileSystemPath,
119 },
120 Direct(ResolvedVc<ResolveResult>),
122 PrimaryAlternative(RcStr, Option<FileSystemPath>),
126 Ignore,
127 Empty,
128 Alternatives(Vec<ResolvedVc<ImportMapping>>),
129 Dynamic(ResolvedVc<Box<dyn ImportMappingReplacement>>),
130}
131
132#[turbo_tasks::value(shared)]
135#[derive(Clone)]
136pub enum ReplacedImportMapping {
137 External(Option<RcStr>, ExternalType, ExternalTraced),
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}
151
152impl ImportMapping {
153 pub fn primary_alternatives(
154 list: Vec<RcStr>,
155 lookup_path: Option<FileSystemPath>,
156 ) -> ImportMapping {
157 if list.is_empty() {
158 ImportMapping::Ignore
159 } else if list.len() == 1 {
160 ImportMapping::PrimaryAlternative(list.into_iter().next().unwrap(), lookup_path)
161 } else {
162 ImportMapping::Alternatives(
163 list.into_iter()
164 .map(|s| {
165 ImportMapping::PrimaryAlternative(s, lookup_path.clone()).resolved_cell()
166 })
167 .collect(),
168 )
169 }
170 }
171}
172
173impl AliasTemplate for ResolvedVc<ImportMapping> {
174 type Output<'a> = <Vc<ImportMapping> as AliasTemplate>::Output<'a>;
175
176 fn convert(&self) -> Self::Output<'_> {
177 (**self).convert()
178 }
179
180 fn replace<'a>(&'a self, capture: &Pattern) -> Self::Output<'a> {
181 (**self).replace(capture)
182 }
183}
184
185impl AliasTemplate for Vc<ImportMapping> {
186 type Output<'a> =
187 Pin<Box<dyn Future<Output = Result<ResolvedVc<ReplacedImportMapping>>> + Send + 'a>>;
188
189 fn convert(&self) -> Self::Output<'_> {
190 Box::pin(async move {
191 let this = &*self.await?;
192 Ok(match this {
193 ImportMapping::External(name, ty, traced) => {
194 ReplacedImportMapping::External(name.clone(), *ty, *traced)
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 }
225 .resolved_cell())
226 })
227 }
228
229 fn replace<'a>(&'a self, capture: &Pattern) -> Self::Output<'a> {
230 let capture = capture.clone();
231 Box::pin(async move {
232 let this = &*self.await?;
233 Ok(match this {
234 ImportMapping::External(name, ty, traced) => {
235 if let Some(name) = name {
236 ReplacedImportMapping::External(
237 capture.spread_into_star(name).as_constant_string().cloned(),
238 *ty,
239 *traced,
240 )
241 } else {
242 ReplacedImportMapping::External(None, *ty, *traced)
243 }
244 }
245 ImportMapping::PrimaryAlternativeExternal {
246 name,
247 ty,
248 traced,
249 lookup_dir,
250 } => {
251 if let Some(name) = name {
252 ReplacedImportMapping::PrimaryAlternativeExternal {
253 name: capture.spread_into_star(name).as_constant_string().cloned(),
254 ty: *ty,
255 traced: *traced,
256 lookup_dir: lookup_dir.clone(),
257 }
258 } else {
259 ReplacedImportMapping::PrimaryAlternativeExternal {
260 name: None,
261 ty: *ty,
262 traced: *traced,
263 lookup_dir: lookup_dir.clone(),
264 }
265 }
266 }
267 ImportMapping::PrimaryAlternative(name, lookup_dir) => {
268 ReplacedImportMapping::PrimaryAlternative(
269 capture.spread_into_star(name),
270 lookup_dir.clone(),
271 )
272 }
273 ImportMapping::Direct(v) => ReplacedImportMapping::Direct(*v),
274 ImportMapping::Ignore => ReplacedImportMapping::Ignore,
275 ImportMapping::Empty => ReplacedImportMapping::Empty,
276 ImportMapping::Alternatives(alternatives) => ReplacedImportMapping::Alternatives(
277 alternatives
278 .iter()
279 .map(|mapping| mapping.replace(&capture))
280 .try_join()
281 .await?,
282 ),
283 ImportMapping::Dynamic(replacement) => {
284 replacement
285 .replace(Pattern::new(capture.clone()))
286 .owned()
287 .await?
288 }
289 }
290 .resolved_cell())
291 })
292 }
293}
294
295#[turbo_tasks::value(shared)]
296#[derive(Clone, Default)]
297pub struct ImportMap {
298 map: AliasMap<ResolvedVc<ImportMapping>>,
299}
300
301impl ImportMap {
302 pub fn new(map: AliasMap<ResolvedVc<ImportMapping>>) -> ImportMap {
304 Self { map }
305 }
306
307 pub fn empty() -> Self {
309 Self::default()
310 }
311
312 pub fn extend_ref(&mut self, other: &ImportMap) {
314 let Self { map } = other.clone();
315 self.map.extend(map);
316 }
317
318 pub fn insert_alias(&mut self, alias: AliasPattern, mapping: ResolvedVc<ImportMapping>) {
320 self.map.insert(alias, mapping);
321 }
322
323 pub fn insert_exact_alias<'a>(
325 &mut self,
326 pattern: impl Into<RcStr> + 'a,
327 mapping: ResolvedVc<ImportMapping>,
328 ) {
329 self.map.insert(AliasPattern::exact(pattern), mapping);
330 }
331
332 pub fn insert_wildcard_alias<'a>(
334 &mut self,
335 prefix: impl Into<RcStr> + 'a,
336 mapping: ResolvedVc<ImportMapping>,
337 ) {
338 self.map
339 .insert(AliasPattern::wildcard(prefix, rcstr!("")), mapping);
340 }
341
342 pub fn insert_wildcard_alias_with_suffix<'p, 's>(
344 &mut self,
345 prefix: impl Into<RcStr> + 'p,
346 suffix: impl Into<RcStr> + 's,
347 mapping: ResolvedVc<ImportMapping>,
348 ) {
349 self.map
350 .insert(AliasPattern::wildcard(prefix, suffix), mapping);
351 }
352
353 pub fn insert_singleton_alias<'a>(
356 &mut self,
357 prefix: impl Into<RcStr> + 'a,
358 context_path: FileSystemPath,
359 ) {
360 let prefix: RcStr = prefix.into();
361 let wildcard_prefix: RcStr = (prefix.to_string() + "/").into();
362 let wildcard_alias: RcStr = (prefix.to_string() + "/*").into();
363 self.insert_exact_alias(
364 prefix.clone(),
365 ImportMapping::PrimaryAlternative(prefix.clone(), Some(context_path.clone()))
366 .resolved_cell(),
367 );
368 self.insert_wildcard_alias(
369 wildcard_prefix,
370 ImportMapping::PrimaryAlternative(wildcard_alias, Some(context_path)).resolved_cell(),
371 );
372 }
373}
374
375#[turbo_tasks::value_impl]
376impl ImportMap {
377 #[turbo_tasks::function]
379 pub async fn extend(self: Vc<Self>, other: ResolvedVc<ImportMap>) -> Result<Vc<Self>> {
380 let mut import_map = self.owned().await?;
381 import_map.extend_ref(&*other.await?);
382 Ok(import_map.cell())
383 }
384}
385
386#[turbo_tasks::value(shared)]
387#[derive(Clone, Default)]
388pub struct ResolvedMap {
389 pub by_glob: Vec<(FileSystemPath, ResolvedVc<Glob>, ResolvedVc<ImportMapping>)>,
390}
391
392#[turbo_tasks::value(shared)]
393#[derive(Clone)]
394pub enum ImportMapResult {
395 Result(ResolvedVc<ResolveResult>),
396 External(RcStr, ExternalType, ExternalTraced),
397 AliasExternal {
398 name: RcStr,
399 ty: ExternalType,
400 traced: ExternalTraced,
401 lookup_dir: FileSystemPath,
402 },
403 Alias(ResolvedVc<Request>, Option<FileSystemPath>),
404 Alternatives(Vec<ImportMapResult>),
405 NoEntry,
406}
407
408async fn import_mapping_to_result(
409 mapping: Vc<ReplacedImportMapping>,
410 lookup_path: FileSystemPath,
411 request: Vc<Request>,
412) -> Result<ImportMapResult> {
413 Ok(match &*mapping.await? {
414 ReplacedImportMapping::Direct(result) => ImportMapResult::Result(*result),
415 ReplacedImportMapping::External(name, ty, traced) => ImportMapResult::External(
416 if let Some(name) = name {
417 name.clone()
418 } else if let Some(request) = request.await?.request() {
419 request
420 } else {
421 bail!(
422 "Cannot resolve external reference with dynamic request {:?}",
423 request.request_pattern().await?.describe_as_string()
424 )
425 },
426 *ty,
427 *traced,
428 ),
429 ReplacedImportMapping::PrimaryAlternativeExternal {
430 name,
431 ty,
432 traced,
433 lookup_dir,
434 } => ImportMapResult::AliasExternal {
435 name: if let Some(name) = name {
436 name.clone()
437 } else if let Some(request) = request.await?.request() {
438 request
439 } else {
440 bail!(
441 "Cannot resolve external reference with dynamic request {:?}",
442 request.request_pattern().await?.describe_as_string()
443 )
444 },
445 ty: *ty,
446 traced: *traced,
447 lookup_dir: lookup_dir.clone(),
448 },
449 ReplacedImportMapping::Ignore => {
450 ImportMapResult::Result(ResolveResult::primary(ResolveResultItem::Ignore))
451 }
452 ReplacedImportMapping::Empty => {
453 ImportMapResult::Result(ResolveResult::primary(ResolveResultItem::Empty))
454 }
455 ReplacedImportMapping::PrimaryAlternative(name, context) => {
456 let request = Request::parse(name.clone()).to_resolved().await?;
457 ImportMapResult::Alias(request, context.clone())
458 }
459 ReplacedImportMapping::Alternatives(list) => ImportMapResult::Alternatives(
460 list.iter()
461 .map(|mapping| {
462 Box::pin(import_mapping_to_result(
463 **mapping,
464 lookup_path.clone(),
465 request,
466 ))
467 })
468 .try_join()
469 .await?,
470 ),
471 ReplacedImportMapping::Dynamic(replacement) => {
472 replacement.result(lookup_path, request).owned().await?
473 }
474 })
475}
476
477#[turbo_tasks::value_impl]
478impl ValueToString for ImportMapResult {
479 #[turbo_tasks::function]
480 async fn to_string(&self) -> Result<Vc<RcStr>> {
481 match self {
482 ImportMapResult::Result(_) => Ok(Vc::cell(rcstr!("Resolved by import map"))),
483 ImportMapResult::External(_, _, _) => Ok(Vc::cell(rcstr!("TODO external"))),
484 ImportMapResult::AliasExternal { .. } => Ok(Vc::cell(rcstr!("TODO external"))),
485 ImportMapResult::Alias(request, context) => {
486 let s = if let Some(path) = context {
487 let path = path.value_to_string().await?;
488 format!(
489 "aliased to {} inside of {}",
490 request.to_string().await?,
491 path
492 )
493 } else {
494 format!("aliased to {}", request.to_string().await?)
495 };
496 Ok(Vc::cell(s.into()))
497 }
498 ImportMapResult::Alternatives(alternatives) => {
499 let strings = alternatives
500 .iter()
501 .map(|alternative| alternative.clone().cell().to_string())
502 .try_join()
503 .await?;
504 let strings = strings
505 .iter()
506 .map(|string| string.as_str())
507 .collect::<Vec<_>>();
508 Ok(Vc::cell(strings.join(" | ").into()))
509 }
510 ImportMapResult::NoEntry => Ok(Vc::cell(rcstr!("No import map entry"))),
511 }
512 }
513}
514
515impl ImportMap {
516 pub async fn lookup(
519 &self,
520 lookup_path: FileSystemPath,
521 request: Vc<Request>,
522 ) -> Result<ImportMapResult> {
523 let request_pattern = request.request_pattern().await?;
526 if matches!(*request_pattern, Pattern::Dynamic | Pattern::DynamicNoSlash) {
527 return Ok(ImportMapResult::NoEntry);
530 }
531
532 let (req_rel, rest) = request_pattern.split_could_match("./");
533 let (req_rel_parent, req_rest) =
534 rest.map(|r| r.split_could_match("../")).unwrap_or_default();
535
536 let lookup_rel = req_rel.as_ref().and_then(|req| {
537 self.map
538 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("./"))
539 .next()
540 });
541 let lookup_rel_parent = req_rel_parent.as_ref().and_then(|req| {
542 self.map
543 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("../"))
544 .next()
545 });
546 let lookup = req_rest
547 .as_ref()
548 .and_then(|req| self.map.lookup(req).next());
549
550 let results = lookup_rel
551 .into_iter()
552 .chain(lookup_rel_parent.into_iter())
553 .chain(lookup.into_iter())
554 .map(async |result| {
555 import_mapping_to_result(*result?.output.await?, lookup_path.clone(), request).await
556 })
557 .try_join()
558 .await?;
559
560 Ok(match results.len() {
561 0 => ImportMapResult::NoEntry,
562 1 => results.into_iter().next().unwrap(),
563 2.. => ImportMapResult::Alternatives(results),
564 })
565 }
566}
567
568#[turbo_tasks::value_impl]
569impl ResolvedMap {
570 #[turbo_tasks::function]
571 pub async fn lookup(
572 &self,
573 resolved: FileSystemPath,
574 lookup_path: FileSystemPath,
575 request: Vc<Request>,
576 ) -> Result<Vc<ImportMapResult>> {
577 for (root, glob, mapping) in self.by_glob.iter() {
578 if let Some(path) = root.get_path_to(&resolved)
579 && glob.await?.matches(path)
580 {
581 return Ok(import_mapping_to_result(
582 *mapping.convert().await?,
583 lookup_path,
584 request,
585 )
586 .await?
587 .into());
588 }
589 }
590 Ok(ImportMapResult::NoEntry.into())
591 }
592}
593
594#[turbo_tasks::value(shared)]
595#[derive(Clone, Debug, Default)]
596pub struct ResolveOptions {
597 pub fully_specified: bool,
600 pub prefer_relative: bool,
603 pub extensions: Vec<RcStr>,
605 pub modules: Vec<ResolveModules>,
607 pub into_package: Vec<ResolveIntoPackage>,
609 pub in_package: Vec<ResolveInPackage>,
611 pub default_files: Vec<RcStr>,
613 pub import_map: Option<ResolvedVc<ImportMap>>,
615 pub fallback_import_map: Option<ResolvedVc<ImportMap>>,
617 pub resolved_map: Option<ResolvedVc<ResolvedMap>>,
618 pub before_resolve_plugins: Vec<ResolvedVc<Box<dyn BeforeResolvePlugin>>>,
619 pub after_resolve_plugins: Vec<ResolvedVc<Box<dyn AfterResolvePlugin>>>,
620 pub enable_typescript_with_output_extension: bool,
622 pub loose_errors: bool,
624 pub parse_data_uris: bool,
626
627 pub placeholder_for_future_extensions: (),
628}
629
630#[turbo_tasks::value_impl]
631impl ResolveOptions {
632 #[turbo_tasks::function]
635 pub async fn with_extended_import_map(
636 self: Vc<Self>,
637 import_map: Vc<ImportMap>,
638 ) -> Result<Vc<Self>> {
639 let mut resolve_options = self.owned().await?;
640 resolve_options.import_map = Some(
641 resolve_options
642 .import_map
643 .map(|current_import_map| current_import_map.extend(import_map))
644 .unwrap_or(import_map)
645 .to_resolved()
646 .await?,
647 );
648 Ok(resolve_options.into())
649 }
650
651 #[turbo_tasks::function]
654 pub async fn with_extended_fallback_import_map(
655 self: Vc<Self>,
656 extended_import_map: ResolvedVc<ImportMap>,
657 ) -> Result<Vc<Self>> {
658 let mut resolve_options = self.owned().await?;
659 resolve_options.fallback_import_map =
660 if let Some(current_fallback) = resolve_options.fallback_import_map {
661 Some(
662 current_fallback
663 .extend(*extended_import_map)
664 .to_resolved()
665 .await?,
666 )
667 } else {
668 Some(extended_import_map)
669 };
670 Ok(resolve_options.into())
671 }
672
673 #[turbo_tasks::function]
675 pub async fn with_extensions(self: Vc<Self>, extensions: Vec<RcStr>) -> Result<Vc<Self>> {
676 let mut resolve_options = self.owned().await?;
677 resolve_options.extensions = extensions;
678 Ok(resolve_options.into())
679 }
680
681 #[turbo_tasks::function]
683 pub async fn with_fully_specified(self: Vc<Self>, fully_specified: bool) -> Result<Vc<Self>> {
684 let mut resolve_options = self.owned().await?;
685 if resolve_options.fully_specified == fully_specified {
686 return Ok(self);
687 }
688 resolve_options.fully_specified = fully_specified;
689 Ok(resolve_options.cell())
690 }
691}
692
693#[turbo_tasks::value(shared)]
694#[derive(Hash, Clone, Debug)]
695pub struct ResolveModulesOptions {
696 pub modules: Vec<ResolveModules>,
697 pub extensions: Vec<RcStr>,
698}
699
700#[turbo_tasks::function]
701pub async fn resolve_modules_options(
702 options: Vc<ResolveOptions>,
703) -> Result<Vc<ResolveModulesOptions>> {
704 let options = options.await?;
705 Ok(ResolveModulesOptions {
706 modules: options.modules.clone(),
707 extensions: options.extensions.clone(),
708 }
709 .into())
710}
711
712#[turbo_tasks::value_trait]
713pub trait ImportMappingReplacement {
714 #[turbo_tasks::function]
715 fn replace(self: Vc<Self>, capture: Vc<Pattern>) -> Vc<ReplacedImportMapping>;
716 #[turbo_tasks::function]
717 fn result(
718 self: Vc<Self>,
719 lookup_path: FileSystemPath,
720 request: Vc<Request>,
721 ) -> Vc<ImportMapResult>;
722}