1use std::{collections::BTreeMap, future::Future, pin::Pin};
2
3use anyhow::{Result, bail};
4use serde::{Deserialize, Serialize};
5use turbo_rcstr::RcStr;
6use turbo_tasks::{
7 FxIndexSet, NonLocalValue, ResolvedVc, TryJoinIterExt, Value, 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(ResolvedVc<FileSystemPath>, Vec<RcStr>),
45 Path {
47 dir: ResolvedVc<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: ResolvedVc<FileSystemPath>,
119 },
120 Direct(ResolvedVc<ResolveResult>),
122 PrimaryAlternative(RcStr, Option<ResolvedVc<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: ResolvedVc<FileSystemPath>,
143 },
144 Direct(ResolvedVc<ResolveResult>),
145 PrimaryAlternative(Pattern, Option<ResolvedVc<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<ResolvedVc<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| ImportMapping::PrimaryAlternative(s, lookup_path).resolved_cell())
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) => {
192 ReplacedImportMapping::External(name.clone(), *ty, *traced)
193 }
194 ImportMapping::PrimaryAlternativeExternal {
195 name,
196 ty,
197 traced,
198 lookup_dir,
199 } => ReplacedImportMapping::PrimaryAlternativeExternal {
200 name: name.clone(),
201 ty: *ty,
202 traced: *traced,
203 lookup_dir: *lookup_dir,
204 },
205 ImportMapping::PrimaryAlternative(name, lookup_dir) => {
206 ReplacedImportMapping::PrimaryAlternative((*name).clone().into(), *lookup_dir)
207 }
208 ImportMapping::Direct(v) => ReplacedImportMapping::Direct(*v),
209 ImportMapping::Ignore => ReplacedImportMapping::Ignore,
210 ImportMapping::Empty => ReplacedImportMapping::Empty,
211 ImportMapping::Alternatives(alternatives) => ReplacedImportMapping::Alternatives(
212 alternatives
213 .iter()
214 .map(|mapping| mapping.convert())
215 .try_join()
216 .await?,
217 ),
218 ImportMapping::Dynamic(replacement) => ReplacedImportMapping::Dynamic(*replacement),
219 }
220 .resolved_cell())
221 })
222 }
223
224 fn replace<'a>(&'a self, capture: &Pattern) -> Self::Output<'a> {
225 let capture = capture.clone();
226 Box::pin(async move {
227 let this = &*self.await?;
228 Ok(match this {
229 ImportMapping::External(name, ty, traced) => {
230 if let Some(name) = name {
231 ReplacedImportMapping::External(
232 capture.spread_into_star(name).as_string().map(|s| s.into()),
233 *ty,
234 *traced,
235 )
236 } else {
237 ReplacedImportMapping::External(None, *ty, *traced)
238 }
239 }
240 ImportMapping::PrimaryAlternativeExternal {
241 name,
242 ty,
243 traced,
244 lookup_dir,
245 } => {
246 if let Some(name) = name {
247 ReplacedImportMapping::PrimaryAlternativeExternal {
248 name: capture.spread_into_star(name).as_string().map(|s| s.into()),
249 ty: *ty,
250 traced: *traced,
251 lookup_dir: *lookup_dir,
252 }
253 } else {
254 ReplacedImportMapping::PrimaryAlternativeExternal {
255 name: None,
256 ty: *ty,
257 traced: *traced,
258 lookup_dir: *lookup_dir,
259 }
260 }
261 }
262 ImportMapping::PrimaryAlternative(name, lookup_dir) => {
263 ReplacedImportMapping::PrimaryAlternative(
264 capture.spread_into_star(name),
265 *lookup_dir,
266 )
267 }
268 ImportMapping::Direct(v) => ReplacedImportMapping::Direct(*v),
269 ImportMapping::Ignore => ReplacedImportMapping::Ignore,
270 ImportMapping::Empty => ReplacedImportMapping::Empty,
271 ImportMapping::Alternatives(alternatives) => ReplacedImportMapping::Alternatives(
272 alternatives
273 .iter()
274 .map(|mapping| mapping.replace(&capture))
275 .try_join()
276 .await?,
277 ),
278 ImportMapping::Dynamic(replacement) => {
279 (*replacement.replace(capture.clone().cell()).await?).clone()
280 }
281 }
282 .resolved_cell())
283 })
284 }
285}
286
287#[turbo_tasks::value(shared)]
288#[derive(Clone, Default)]
289pub struct ImportMap {
290 map: AliasMap<ResolvedVc<ImportMapping>>,
291}
292
293impl ImportMap {
294 pub fn new(map: AliasMap<ResolvedVc<ImportMapping>>) -> ImportMap {
296 Self { map }
297 }
298
299 pub fn empty() -> Self {
301 Self::default()
302 }
303
304 pub fn extend_ref(&mut self, other: &ImportMap) {
306 let Self { map } = other.clone();
307 self.map.extend(map);
308 }
309
310 pub fn insert_alias(&mut self, alias: AliasPattern, mapping: ResolvedVc<ImportMapping>) {
312 self.map.insert(alias, mapping);
313 }
314
315 pub fn insert_exact_alias<'a>(
317 &mut self,
318 pattern: impl Into<RcStr> + 'a,
319 mapping: ResolvedVc<ImportMapping>,
320 ) {
321 self.map.insert(AliasPattern::exact(pattern), mapping);
322 }
323
324 pub fn insert_wildcard_alias<'a>(
326 &mut self,
327 prefix: impl Into<RcStr> + 'a,
328 mapping: ResolvedVc<ImportMapping>,
329 ) {
330 self.map.insert(AliasPattern::wildcard(prefix, ""), mapping);
331 }
332
333 pub fn insert_wildcard_alias_with_suffix<'p, 's>(
335 &mut self,
336 prefix: impl Into<RcStr> + 'p,
337 suffix: impl Into<RcStr> + 's,
338 mapping: ResolvedVc<ImportMapping>,
339 ) {
340 self.map
341 .insert(AliasPattern::wildcard(prefix, suffix), mapping);
342 }
343
344 pub fn insert_singleton_alias<'a>(
347 &mut self,
348 prefix: impl Into<RcStr> + 'a,
349 context_path: ResolvedVc<FileSystemPath>,
350 ) {
351 let prefix: RcStr = prefix.into();
352 let wildcard_prefix: RcStr = (prefix.to_string() + "/").into();
353 let wildcard_alias: RcStr = (prefix.to_string() + "/*").into();
354 self.insert_exact_alias(
355 prefix.clone(),
356 ImportMapping::PrimaryAlternative(prefix.clone(), Some(context_path)).resolved_cell(),
357 );
358 self.insert_wildcard_alias(
359 wildcard_prefix,
360 ImportMapping::PrimaryAlternative(wildcard_alias, Some(context_path)).resolved_cell(),
361 );
362 }
363}
364
365#[turbo_tasks::value_impl]
366impl ImportMap {
367 #[turbo_tasks::function]
369 pub async fn extend(self: Vc<Self>, other: ResolvedVc<ImportMap>) -> Result<Vc<Self>> {
370 let mut import_map = self.owned().await?;
371 import_map.extend_ref(&*other.await?);
372 Ok(import_map.cell())
373 }
374}
375
376#[turbo_tasks::value(shared)]
377#[derive(Clone, Default)]
378pub struct ResolvedMap {
379 pub by_glob: Vec<(
380 ResolvedVc<FileSystemPath>,
381 ResolvedVc<Glob>,
382 ResolvedVc<ImportMapping>,
383 )>,
384}
385
386#[turbo_tasks::value(shared)]
387#[derive(Clone)]
388pub enum ImportMapResult {
389 Result(ResolvedVc<ResolveResult>),
390 External(RcStr, ExternalType, ExternalTraced),
391 AliasExternal {
392 name: RcStr,
393 ty: ExternalType,
394 traced: ExternalTraced,
395 lookup_dir: ResolvedVc<FileSystemPath>,
396 },
397 Alias(ResolvedVc<Request>, Option<ResolvedVc<FileSystemPath>>),
398 Alternatives(Vec<ImportMapResult>),
399 NoEntry,
400}
401
402async fn import_mapping_to_result(
403 mapping: Vc<ReplacedImportMapping>,
404 lookup_path: Vc<FileSystemPath>,
405 request: Vc<Request>,
406) -> Result<ImportMapResult> {
407 Ok(match &*mapping.await? {
408 ReplacedImportMapping::Direct(result) => ImportMapResult::Result(*result),
409 ReplacedImportMapping::External(name, ty, traced) => ImportMapResult::External(
410 if let Some(name) = name {
411 name.clone()
412 } else if let Some(request) = request.await?.request() {
413 request
414 } else {
415 bail!("Cannot resolve external reference without request")
416 },
417 *ty,
418 *traced,
419 ),
420 ReplacedImportMapping::PrimaryAlternativeExternal {
421 name,
422 ty,
423 traced,
424 lookup_dir,
425 } => ImportMapResult::AliasExternal {
426 name: if let Some(name) = name {
427 name.clone()
428 } else if let Some(request) = request.await?.request() {
429 request
430 } else {
431 bail!("Cannot resolve external reference without request")
432 },
433 ty: *ty,
434 traced: *traced,
435 lookup_dir: *lookup_dir,
436 },
437 ReplacedImportMapping::Ignore => {
438 ImportMapResult::Result(ResolveResult::primary(ResolveResultItem::Ignore))
439 }
440 ReplacedImportMapping::Empty => {
441 ImportMapResult::Result(ResolveResult::primary(ResolveResultItem::Empty))
442 }
443 ReplacedImportMapping::PrimaryAlternative(name, context) => {
444 let request = Request::parse(Value::new(name.clone()))
445 .to_resolved()
446 .await?;
447 ImportMapResult::Alias(request, *context)
448 }
449 ReplacedImportMapping::Alternatives(list) => ImportMapResult::Alternatives(
450 list.iter()
451 .map(|mapping| Box::pin(import_mapping_to_result(**mapping, lookup_path, request)))
452 .try_join()
453 .await?,
454 ),
455 ReplacedImportMapping::Dynamic(replacement) => {
456 replacement.result(lookup_path, request).owned().await?
457 }
458 })
459}
460
461#[turbo_tasks::value_impl]
462impl ValueToString for ImportMapResult {
463 #[turbo_tasks::function]
464 async fn to_string(&self) -> Result<Vc<RcStr>> {
465 match self {
466 ImportMapResult::Result(_) => Ok(Vc::cell("Resolved by import map".into())),
467 ImportMapResult::External(_, _, _) => Ok(Vc::cell("TODO external".into())),
468 ImportMapResult::AliasExternal { .. } => Ok(Vc::cell("TODO external".into())),
469 ImportMapResult::Alias(request, context) => {
470 let s = if let Some(path) = context {
471 let path = path.to_string().await?;
472 format!(
473 "aliased to {} inside of {}",
474 request.to_string().await?,
475 path
476 )
477 } else {
478 format!("aliased to {}", request.to_string().await?)
479 };
480 Ok(Vc::cell(s.into()))
481 }
482 ImportMapResult::Alternatives(alternatives) => {
483 let strings = alternatives
484 .iter()
485 .map(|alternative| alternative.clone().cell().to_string())
486 .try_join()
487 .await?;
488 let strings = strings
489 .iter()
490 .map(|string| string.as_str())
491 .collect::<Vec<_>>();
492 Ok(Vc::cell(strings.join(" | ").into()))
493 }
494 ImportMapResult::NoEntry => Ok(Vc::cell("No import map entry".into())),
495 }
496 }
497}
498
499impl ImportMap {
500 pub async fn lookup(
503 &self,
504 lookup_path: Vc<FileSystemPath>,
505 request: Vc<Request>,
506 ) -> Result<ImportMapResult> {
507 let request_pattern = request.request_pattern().await?;
510 let (req_rel, rest) = request_pattern.split_could_match("./");
511 let (req_rel_parent, req_rest) =
512 rest.map(|r| r.split_could_match("../")).unwrap_or_default();
513
514 let lookup_rel = req_rel.as_ref().and_then(|req| {
515 self.map
516 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("./"))
517 .next()
518 });
519 let lookup_rel_parent = req_rel_parent.as_ref().and_then(|req| {
520 self.map
521 .lookup_with_prefix_predicate(req, |prefix| prefix.starts_with("../"))
522 .next()
523 });
524 let lookup = req_rest
525 .as_ref()
526 .and_then(|req| self.map.lookup(req).next());
527
528 let results = lookup_rel
529 .into_iter()
530 .chain(lookup_rel_parent.into_iter())
531 .chain(lookup.into_iter())
532 .map(async |result| {
533 import_mapping_to_result(*result.try_join_into_self().await?, lookup_path, request)
534 .await
535 })
536 .try_join()
537 .await?;
538
539 Ok(match results.len() {
540 0 => ImportMapResult::NoEntry,
541 1 => results.into_iter().next().unwrap(),
542 2.. => ImportMapResult::Alternatives(results),
543 })
544 }
545}
546
547#[turbo_tasks::value_impl]
548impl ResolvedMap {
549 #[turbo_tasks::function]
550 pub async fn lookup(
551 &self,
552 resolved: Vc<FileSystemPath>,
553 lookup_path: Vc<FileSystemPath>,
554 request: Vc<Request>,
555 ) -> Result<Vc<ImportMapResult>> {
556 let resolved = resolved.await?;
557 for (root, glob, mapping) in self.by_glob.iter() {
558 let root = root.await?;
559 if let Some(path) = root.get_path_to(&resolved) {
560 if glob.await?.matches(path) {
561 return Ok(import_mapping_to_result(
562 *mapping.convert().await?,
563 lookup_path,
564 request,
565 )
566 .await?
567 .into());
568 }
569 }
570 }
571 Ok(ImportMapResult::NoEntry.into())
572 }
573}
574
575#[turbo_tasks::value(shared)]
576#[derive(Clone, Debug, Default)]
577pub struct ResolveOptions {
578 pub fully_specified: bool,
581 pub prefer_relative: bool,
584 pub extensions: Vec<RcStr>,
586 pub modules: Vec<ResolveModules>,
588 pub into_package: Vec<ResolveIntoPackage>,
590 pub in_package: Vec<ResolveInPackage>,
592 pub default_files: Vec<RcStr>,
594 pub import_map: Option<ResolvedVc<ImportMap>>,
596 pub fallback_import_map: Option<ResolvedVc<ImportMap>>,
598 pub resolved_map: Option<ResolvedVc<ResolvedMap>>,
599 pub before_resolve_plugins: Vec<ResolvedVc<Box<dyn BeforeResolvePlugin>>>,
600 pub after_resolve_plugins: Vec<ResolvedVc<Box<dyn AfterResolvePlugin>>>,
601 pub enable_typescript_with_output_extension: bool,
603 pub loose_errors: bool,
605 pub parse_data_uris: bool,
607
608 pub placeholder_for_future_extensions: (),
609}
610
611#[turbo_tasks::value_impl]
612impl ResolveOptions {
613 #[turbo_tasks::function]
616 pub async fn with_extended_import_map(
617 self: Vc<Self>,
618 import_map: Vc<ImportMap>,
619 ) -> Result<Vc<Self>> {
620 let mut resolve_options = self.owned().await?;
621 resolve_options.import_map = Some(
622 resolve_options
623 .import_map
624 .map(|current_import_map| current_import_map.extend(import_map))
625 .unwrap_or(import_map)
626 .to_resolved()
627 .await?,
628 );
629 Ok(resolve_options.into())
630 }
631
632 #[turbo_tasks::function]
635 pub async fn with_extended_fallback_import_map(
636 self: Vc<Self>,
637 extended_import_map: ResolvedVc<ImportMap>,
638 ) -> Result<Vc<Self>> {
639 let mut resolve_options = self.owned().await?;
640 resolve_options.fallback_import_map =
641 if let Some(current_fallback) = resolve_options.fallback_import_map {
642 Some(
643 current_fallback
644 .extend(*extended_import_map)
645 .to_resolved()
646 .await?,
647 )
648 } else {
649 Some(extended_import_map)
650 };
651 Ok(resolve_options.into())
652 }
653
654 #[turbo_tasks::function]
656 pub async fn with_extensions(self: Vc<Self>, extensions: Vec<RcStr>) -> Result<Vc<Self>> {
657 let mut resolve_options = self.owned().await?;
658 resolve_options.extensions = extensions;
659 Ok(resolve_options.into())
660 }
661
662 #[turbo_tasks::function]
664 pub async fn with_fully_specified(self: Vc<Self>, fully_specified: bool) -> Result<Vc<Self>> {
665 let mut resolve_options = self.owned().await?;
666 if resolve_options.fully_specified == fully_specified {
667 return Ok(self);
668 }
669 resolve_options.fully_specified = fully_specified;
670 Ok(resolve_options.cell())
671 }
672}
673
674#[turbo_tasks::value(shared)]
675#[derive(Hash, Clone, Debug)]
676pub struct ResolveModulesOptions {
677 pub modules: Vec<ResolveModules>,
678 pub extensions: Vec<RcStr>,
679}
680
681#[turbo_tasks::function]
682pub async fn resolve_modules_options(
683 options: Vc<ResolveOptions>,
684) -> Result<Vc<ResolveModulesOptions>> {
685 let options = options.await?;
686 Ok(ResolveModulesOptions {
687 modules: options.modules.clone(),
688 extensions: options.extensions.clone(),
689 }
690 .into())
691}
692
693#[turbo_tasks::value_trait]
694pub trait ImportMappingReplacement {
695 fn replace(self: Vc<Self>, capture: Vc<Pattern>) -> Vc<ReplacedImportMapping>;
696 fn result(
697 self: Vc<Self>,
698 lookup_path: Vc<FileSystemPath>,
699 request: Vc<Request>,
700 ) -> Vc<ImportMapResult>;
701}