turbopack_core/resolve/
parse.rs

1use std::sync::LazyLock;
2
3use anyhow::{Ok, Result};
4use regex::Regex;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, TryJoinIterExt, ValueToString, Vc};
7
8use super::pattern::Pattern;
9
10#[turbo_tasks::value(shared)]
11#[derive(Hash, Clone, Debug)]
12pub enum Request {
13    Raw {
14        path: Pattern,
15        query: RcStr,
16        force_in_lookup_dir: bool,
17        fragment: RcStr,
18    },
19    Relative {
20        path: Pattern,
21        query: RcStr,
22        force_in_lookup_dir: bool,
23        fragment: RcStr,
24    },
25    Module {
26        module: Pattern,
27        path: Pattern,
28        query: RcStr,
29        fragment: RcStr,
30    },
31    ServerRelative {
32        path: Pattern,
33        query: RcStr,
34        fragment: RcStr,
35    },
36    Windows {
37        path: Pattern,
38        query: RcStr,
39        fragment: RcStr,
40    },
41    Empty,
42    PackageInternal {
43        path: Pattern,
44    },
45    Uri {
46        protocol: RcStr,
47        remainder: RcStr,
48        query: RcStr,
49        fragment: RcStr,
50    },
51    DataUri {
52        media_type: RcStr,
53        encoding: RcStr,
54        data: ResolvedVc<RcStr>,
55    },
56    Unknown {
57        path: Pattern,
58    },
59    Dynamic,
60    Alternatives {
61        requests: Vec<ResolvedVc<Request>>,
62    },
63}
64
65/// Splits a string like `foo?bar#baz` into `(Pattern::Constant('foo'), '?bar', '#baz')`
66///
67/// If the hash or query portion are missing they will be empty strings otherwise they will be
68/// non-empty along with their prepender characters
69fn split_off_query_fragment(mut raw: &str) -> (Pattern, RcStr, RcStr) {
70    // Per the URI spec fragments can contain `?` characters, so we should trim it off first
71    // https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
72
73    let hash = match raw.as_bytes().iter().position(|&b| b == b'#') {
74        Some(pos) => {
75            let (prefix, hash) = raw.split_at(pos);
76            raw = prefix;
77            RcStr::from(hash)
78        }
79        None => RcStr::default(),
80    };
81
82    let query = match raw.as_bytes().iter().position(|&b| b == b'?') {
83        Some(pos) => {
84            let (prefix, query) = raw.split_at(pos);
85            raw = prefix;
86            RcStr::from(query)
87        }
88        None => RcStr::default(),
89    };
90    (Pattern::Constant(RcStr::from(raw)), query, hash)
91}
92
93static WINDOWS_PATH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[A-Za-z]:\\|\\\\").unwrap());
94static URI_PATH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([^/\\:]+:)(.+)$").unwrap());
95static DATA_URI_REMAINDER: LazyLock<Regex> =
96    LazyLock::new(|| Regex::new(r"^([^;,]*)(?:;([^,]+))?,(.*)$").unwrap());
97static MODULE_PATH: LazyLock<Regex> =
98    LazyLock::new(|| Regex::new(r"^((?:@[^/]+/)?[^/]+)(.*)$").unwrap());
99
100impl Request {
101    /// Turns the request into a string.
102    ///
103    /// This is not only used for printing the request to the user, but also for matching inner
104    /// assets.
105    ///
106    /// Note that this is only returns something for the most basic and fully constant patterns.
107    pub fn request(&self) -> Option<RcStr> {
108        Some(match self {
109            Request::Raw {
110                path: Pattern::Constant(path),
111                ..
112            } => path.clone(),
113            Request::Relative {
114                path: Pattern::Constant(path),
115                ..
116            } => path.clone(),
117            Request::Module {
118                module: Pattern::Constant(module),
119                path: Pattern::Constant(path),
120                ..
121            } => format!("{module}{path}").into(),
122            Request::ServerRelative {
123                path: Pattern::Constant(path),
124                ..
125            } => path.clone(),
126            Request::Windows {
127                path: Pattern::Constant(path),
128                ..
129            } => path.clone(),
130            Request::Empty => rcstr!(""),
131            Request::PackageInternal {
132                path: Pattern::Constant(path),
133                ..
134            } => path.clone(),
135            Request::Uri {
136                protocol,
137                remainder,
138                ..
139            } => format!("{protocol}{remainder}").into(),
140            Request::Unknown {
141                path: Pattern::Constant(path),
142            } => path.clone(),
143            _ => return None,
144        })
145    }
146
147    /// Internal construction function.  Should only be called with normalized patterns, or
148    /// recursively. Most users should call [Self::parse] instead.
149    fn parse_ref(request: Pattern) -> Self {
150        match request {
151            Pattern::Dynamic | Pattern::DynamicNoSlash => Request::Dynamic,
152            Pattern::Constant(r) => Request::parse_constant_pattern(r),
153            Pattern::Concatenation(list) => Request::parse_concatenation_pattern(list),
154            Pattern::Alternatives(_) => panic!(
155                "request should be normalized and alternatives should have already been handled.",
156            ),
157        }
158    }
159
160    fn parse_constant_pattern(r: RcStr) -> Self {
161        if r.is_empty() {
162            return Request::Empty;
163        }
164
165        if let Some(remainder) = r.strip_prefix("//") {
166            return Request::Uri {
167                protocol: rcstr!("//"),
168                remainder: remainder.into(),
169                query: RcStr::default(),
170                fragment: RcStr::default(),
171            };
172        }
173
174        if r.starts_with('/') {
175            let (path, query, fragment) = split_off_query_fragment(&r);
176
177            return Request::ServerRelative {
178                path,
179                query,
180                fragment,
181            };
182        }
183
184        if r.starts_with('#') {
185            return Request::PackageInternal {
186                path: Pattern::Constant(r),
187            };
188        }
189
190        if r.starts_with("./") || r.starts_with("../") || r == "." || r == ".." {
191            let (path, query, fragment) = split_off_query_fragment(&r);
192
193            return Request::Relative {
194                path,
195                force_in_lookup_dir: false,
196                query,
197                fragment,
198            };
199        }
200
201        if WINDOWS_PATH.is_match(&r) {
202            let (path, query, fragment) = split_off_query_fragment(&r);
203
204            return Request::Windows {
205                path,
206                query,
207                fragment,
208            };
209        }
210
211        if let Some(caps) = URI_PATH.captures(&r)
212            && let (Some(protocol), Some(remainder)) = (caps.get(1), caps.get(2))
213        {
214            if let Some(caps) = DATA_URI_REMAINDER.captures(remainder.as_str()) {
215                let media_type = caps.get(1).map_or(RcStr::default(), |m| m.as_str().into());
216                let encoding = caps.get(2).map_or(RcStr::default(), |e| e.as_str().into());
217                let data = caps.get(3).map_or(RcStr::default(), |d| d.as_str().into());
218
219                return Request::DataUri {
220                    media_type,
221                    encoding,
222                    data: ResolvedVc::cell(data),
223                };
224            }
225
226            return Request::Uri {
227                protocol: protocol.as_str().into(),
228                remainder: remainder.as_str().into(),
229                query: RcStr::default(),
230                fragment: RcStr::default(),
231            };
232        }
233
234        if let Some((module, path)) = MODULE_PATH
235            .captures(&r)
236            .and_then(|caps| caps.get(1).zip(caps.get(2)))
237        {
238            let (path, query, fragment) = split_off_query_fragment(path.as_str());
239
240            return Request::Module {
241                module: RcStr::from(module.as_str()).into(),
242                path,
243                query,
244                fragment,
245            };
246        }
247
248        Request::Unknown {
249            path: Pattern::Constant(r),
250        }
251    }
252
253    fn parse_concatenation_pattern(list: Vec<Pattern>) -> Self {
254        if list.is_empty() {
255            return Request::Empty;
256        }
257
258        let mut result = Self::parse_ref(list[0].clone());
259
260        for item in list.into_iter().skip(1) {
261            match &mut result {
262                Request::Raw { path, .. } => {
263                    path.push(item);
264                }
265                Request::Relative { path, .. } => {
266                    path.push(item);
267                }
268                Request::Module { module, path, .. } => {
269                    if path.is_empty() && matches!(item, Pattern::Dynamic) {
270                        // TODO ideally this would be more general (i.e. support also
271                        // `module-part<dynamic>more-module/subpath`) and not just handle
272                        // Pattern::Dynamic, but this covers the common case of
273                        // `require('@img/sharp-' + arch + '/sharp.node')`
274
275                        // Insert dynamic between module and path (by adding it to both of them,
276                        // because both could happen).
277                        module.push(Pattern::DynamicNoSlash);
278                    }
279                    path.push(item);
280                }
281                Request::ServerRelative { path, .. } => {
282                    path.push(item);
283                }
284                Request::Windows { path, .. } => {
285                    path.push(item);
286                }
287                Request::Empty => {
288                    result = Self::parse_ref(item);
289                }
290                Request::PackageInternal { path } => {
291                    path.push(item);
292                }
293                Request::Unknown { path } => {
294                    path.push(item);
295                }
296                Request::DataUri { .. } | Request::Uri { .. } | Request::Dynamic => {
297                    return Request::Dynamic;
298                }
299                Request::Alternatives { .. } => unreachable!(),
300            };
301        }
302        if let Request::Relative {
303            path,
304            fragment,
305            query,
306            ..
307        } = &mut result
308            && fragment.is_empty()
309            && query.is_empty()
310            && let Pattern::Concatenation(parts) = path
311            && let Pattern::Constant(last_part) = parts.last().unwrap()
312        {
313            let (prefix, new_query, new_fragment) = split_off_query_fragment(last_part);
314
315            *parts.last_mut().unwrap() = prefix;
316            *query = new_query;
317            *fragment = new_fragment;
318            // now that we have composed the request try to find the query and fragment
319            path.normalize();
320        }
321
322        result
323    }
324
325    pub fn parse_string(request: RcStr) -> Vc<Self> {
326        Self::parse(request.into())
327    }
328
329    pub fn parse(mut request: Pattern) -> Vc<Self> {
330        // Call normalize before parse_inner to improve cache hits.
331        request.normalize();
332        Self::parse_inner(request)
333    }
334}
335
336#[turbo_tasks::value_impl]
337impl Request {
338    #[turbo_tasks::function]
339    async fn parse_inner(request: Pattern) -> Result<Vc<Self>> {
340        // Because we are normalized, we should handle alternatives here
341        if let Pattern::Alternatives(alts) = request {
342            Ok(Self::cell(Self::Alternatives {
343                requests: alts
344                    .into_iter()
345                    // We can call parse_inner directly because these patterns are already
346                    // normalized.  We don't call `Self::parse_ref` so we can try to get a cache hit
347                    // on the sub-patterns
348                    .map(|p| Self::parse_inner(p).to_resolved())
349                    .try_join()
350                    .await?,
351            }))
352        } else {
353            Ok(Self::cell(Self::parse_ref(request)))
354        }
355    }
356
357    #[turbo_tasks::function]
358    pub fn raw(
359        request: Pattern,
360        query: RcStr,
361        fragment: RcStr,
362        force_in_lookup_dir: bool,
363    ) -> Vc<Self> {
364        Self::cell(Request::Raw {
365            path: request,
366            force_in_lookup_dir,
367            query,
368            fragment,
369        })
370    }
371
372    #[turbo_tasks::function]
373    pub fn relative(
374        request: Pattern,
375        query: RcStr,
376        fragment: RcStr,
377        force_in_lookup_dir: bool,
378    ) -> Vc<Self> {
379        Self::cell(Request::Relative {
380            path: request,
381            force_in_lookup_dir,
382            query,
383            fragment,
384        })
385    }
386
387    #[turbo_tasks::function]
388    pub fn module(module: Pattern, path: Pattern, query: RcStr, fragment: RcStr) -> Vc<Self> {
389        Self::cell(Request::Module {
390            module,
391            path,
392            query,
393            fragment,
394        })
395    }
396
397    #[turbo_tasks::function]
398    pub async fn as_relative(self: Vc<Self>) -> Result<Vc<Self>> {
399        let this = self.await?;
400        Ok(match &*this {
401            Request::Empty
402            | Request::Raw { .. }
403            | Request::ServerRelative { .. }
404            | Request::Windows { .. }
405            | Request::Relative { .. }
406            | Request::DataUri { .. }
407            | Request::Uri { .. }
408            | Request::Dynamic => self,
409            Request::Module {
410                module,
411                path,
412                query: _,
413                fragment: _,
414            } => {
415                let mut pat = module.clone();
416                pat.push_front(rcstr!("./").into());
417                pat.push(path.clone());
418                // TODO add query
419                Self::parse(pat)
420            }
421            Request::PackageInternal { path } => {
422                let mut pat = Pattern::Constant(rcstr!("./"));
423                pat.push(path.clone());
424                Self::parse(pat)
425            }
426            Request::Unknown { path } => {
427                let mut pat = Pattern::Constant(rcstr!("./"));
428                pat.push(path.clone());
429                Self::parse(pat)
430            }
431            Request::Alternatives { requests } => {
432                let requests = requests
433                    .iter()
434                    .copied()
435                    .map(|v| *v)
436                    .map(Request::as_relative)
437                    .map(|v| async move { v.to_resolved().await })
438                    .try_join()
439                    .await?;
440                Request::Alternatives { requests }.cell()
441            }
442        })
443    }
444
445    #[turbo_tasks::function]
446    pub async fn with_query(self: Vc<Self>, query: RcStr) -> Result<Vc<Self>> {
447        Ok(match &*self.await? {
448            Request::Raw {
449                path,
450                query: _,
451                force_in_lookup_dir,
452                fragment,
453            } => Request::raw(path.clone(), query, fragment.clone(), *force_in_lookup_dir),
454            Request::Relative {
455                path,
456                query: _,
457                force_in_lookup_dir,
458                fragment,
459            } => Request::relative(path.clone(), query, fragment.clone(), *force_in_lookup_dir),
460            Request::Module {
461                module,
462                path,
463                query: _,
464                fragment,
465            } => Request::module(module.clone(), path.clone(), query, fragment.clone()),
466            Request::ServerRelative {
467                path,
468                query: _,
469                fragment,
470            } => Request::ServerRelative {
471                path: path.clone(),
472                query,
473                fragment: fragment.clone(),
474            }
475            .cell(),
476            Request::Windows {
477                path,
478                query: _,
479                fragment,
480            } => Request::Windows {
481                path: path.clone(),
482                query,
483                fragment: fragment.clone(),
484            }
485            .cell(),
486            Request::Empty => self,
487            Request::PackageInternal { .. } => self,
488            Request::DataUri { .. } => self,
489            Request::Uri { .. } => self,
490            Request::Unknown { .. } => self,
491            Request::Dynamic => self,
492            Request::Alternatives { requests } => {
493                let requests = requests
494                    .iter()
495                    .copied()
496                    .map(|req| req.with_query(query.clone()))
497                    .map(|v| async move { v.to_resolved().await })
498                    .try_join()
499                    .await?;
500                Request::Alternatives { requests }.cell()
501            }
502        })
503    }
504
505    #[turbo_tasks::function]
506    pub async fn with_fragment(self: Vc<Self>, fragment: RcStr) -> Result<Vc<Self>> {
507        Ok(match &*self.await? {
508            Request::Raw {
509                path,
510                query,
511                force_in_lookup_dir,
512                fragment: _,
513            } => Request::Raw {
514                path: path.clone(),
515                query: query.clone(),
516                force_in_lookup_dir: *force_in_lookup_dir,
517                fragment,
518            }
519            .cell(),
520            Request::Relative {
521                path,
522                query,
523                force_in_lookup_dir,
524                fragment: _,
525            } => Request::Relative {
526                path: path.clone(),
527                query: query.clone(),
528                force_in_lookup_dir: *force_in_lookup_dir,
529                fragment,
530            }
531            .cell(),
532            Request::Module {
533                module,
534                path,
535                query,
536                fragment: _,
537            } => Request::Module {
538                module: module.clone(),
539                path: path.clone(),
540                query: query.clone(),
541                fragment,
542            }
543            .cell(),
544            Request::ServerRelative {
545                path,
546                query,
547                fragment: _,
548            } => Request::ServerRelative {
549                path: path.clone(),
550                query: query.clone(),
551                fragment,
552            }
553            .cell(),
554            Request::Windows {
555                path,
556                query,
557                fragment: _,
558            } => Request::Windows {
559                path: path.clone(),
560                query: query.clone(),
561                fragment,
562            }
563            .cell(),
564            Request::Empty => self,
565            Request::PackageInternal { .. } => self,
566            Request::DataUri { .. } => self,
567            Request::Uri { .. } => self,
568            Request::Unknown { .. } => self,
569            Request::Dynamic => self,
570            Request::Alternatives { requests } => {
571                let requests = requests
572                    .iter()
573                    .copied()
574                    .map(|req| req.with_fragment(fragment.clone()))
575                    .map(|v| async move { v.to_resolved().await })
576                    .try_join()
577                    .await?;
578                Request::Alternatives { requests }.cell()
579            }
580        })
581    }
582
583    #[turbo_tasks::function]
584    pub async fn append_path(self: Vc<Self>, suffix: RcStr) -> Result<Vc<Self>> {
585        Ok(match &*self.await? {
586            Request::Raw {
587                path,
588                query,
589                force_in_lookup_dir,
590                fragment,
591            } => {
592                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
593                pat.normalize();
594                Self::raw(pat, query.clone(), fragment.clone(), *force_in_lookup_dir)
595            }
596            Request::Relative {
597                path,
598                query,
599                force_in_lookup_dir,
600                fragment,
601            } => {
602                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
603                pat.normalize();
604                Self::relative(pat, query.clone(), fragment.clone(), *force_in_lookup_dir)
605            }
606            Request::Module {
607                module,
608                path,
609                query,
610                fragment,
611            } => {
612                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
613                pat.normalize();
614                Self::module(module.clone(), pat, query.clone(), fragment.clone())
615            }
616            Request::ServerRelative {
617                path,
618                query,
619                fragment,
620            } => {
621                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
622                pat.normalize();
623                Self::ServerRelative {
624                    path: pat,
625                    query: query.clone(),
626                    fragment: fragment.clone(),
627                }
628                .cell()
629            }
630            Request::Windows {
631                path,
632                query,
633                fragment,
634            } => {
635                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
636                pat.normalize();
637                Self::Windows {
638                    path: pat,
639                    query: query.clone(),
640                    fragment: fragment.clone(),
641                }
642                .cell()
643            }
644            Request::Empty => Self::parse(suffix.into()),
645            Request::PackageInternal { path } => {
646                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
647                pat.normalize();
648                Self::PackageInternal { path: pat }.cell()
649            }
650            Request::DataUri {
651                media_type,
652                encoding,
653                data,
654            } => {
655                let data = ResolvedVc::cell(format!("{}{}", data.await?, suffix).into());
656                Self::DataUri {
657                    media_type: media_type.clone(),
658                    encoding: encoding.clone(),
659                    data,
660                }
661                .cell()
662            }
663            Request::Uri {
664                protocol,
665                remainder,
666                query,
667                fragment,
668            } => {
669                let remainder = format!("{remainder}{suffix}").into();
670                Self::Uri {
671                    protocol: protocol.clone(),
672                    remainder,
673                    query: query.clone(),
674                    fragment: fragment.clone(),
675                }
676                .cell()
677            }
678            Request::Unknown { path } => {
679                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
680                pat.normalize();
681                Self::Unknown { path: pat }.cell()
682            }
683            Request::Dynamic => self,
684            Request::Alternatives { requests } => {
685                let requests = requests
686                    .iter()
687                    .map(|req| async { req.append_path(suffix.clone()).to_resolved().await })
688                    .try_join()
689                    .await?;
690                Request::Alternatives { requests }.cell()
691            }
692        })
693    }
694
695    #[turbo_tasks::function]
696    pub fn query(&self) -> Vc<RcStr> {
697        Vc::cell(match self {
698            Request::Windows { query, .. }
699            | Request::ServerRelative { query, .. }
700            | Request::Module { query, .. }
701            | Request::Relative { query, .. }
702            | Request::Raw { query, .. } => query.clone(),
703            Request::Dynamic
704            | Request::Unknown { .. }
705            | Request::Uri { .. }
706            | Request::DataUri { .. }
707            | Request::PackageInternal { .. }
708            | Request::Empty => RcStr::default(),
709            // TODO: is this correct, should we return the first one instead?
710            Request::Alternatives { .. } => RcStr::default(),
711        })
712    }
713
714    /// Turns the request into a pattern, similar to [Request::request()] but
715    /// more complete.
716    #[turbo_tasks::function]
717    pub async fn request_pattern(self: Vc<Self>) -> Result<Vc<Pattern>> {
718        Ok(Pattern::new(match &*self.await? {
719            Request::Raw { path, .. } => path.clone(),
720            Request::Relative { path, .. } => path.clone(),
721            Request::Module { module, path, .. } => {
722                let mut path = path.clone();
723                path.push_front(module.clone());
724                path.normalize();
725                path
726            }
727            Request::ServerRelative { path, .. } => path.clone(),
728            Request::Windows { path, .. } => path.clone(),
729            Request::Empty => Pattern::Constant(rcstr!("")),
730            Request::PackageInternal { path } => path.clone(),
731            Request::DataUri {
732                media_type,
733                encoding,
734                data,
735            } => Pattern::Constant(
736                stringify_data_uri(media_type, encoding, *data)
737                    .await?
738                    .into(),
739            ),
740            Request::Uri {
741                protocol,
742                remainder,
743                ..
744            } => Pattern::Constant(format!("{protocol}{remainder}").into()),
745            Request::Unknown { path } => path.clone(),
746            Request::Dynamic => Pattern::Dynamic,
747            Request::Alternatives { requests } => Pattern::Alternatives(
748                requests
749                    .iter()
750                    .map(async |r: &ResolvedVc<Request>| -> Result<Pattern> {
751                        Ok(r.request_pattern().owned().await?)
752                    })
753                    .try_join()
754                    .await?,
755            ),
756        }))
757    }
758}
759
760#[turbo_tasks::value_impl]
761impl ValueToString for Request {
762    #[turbo_tasks::function]
763    async fn to_string(&self) -> Result<Vc<RcStr>> {
764        Ok(Vc::cell(match self {
765            Request::Raw {
766                path,
767                force_in_lookup_dir,
768                ..
769            } => {
770                if *force_in_lookup_dir {
771                    format!("in-lookup-dir {}", path.describe_as_string()).into()
772                } else {
773                    path.describe_as_string().into()
774                }
775            }
776            Request::Relative {
777                path,
778                force_in_lookup_dir,
779                ..
780            } => {
781                if *force_in_lookup_dir {
782                    format!("relative-in-lookup-dir {}", path.describe_as_string()).into()
783                } else {
784                    format!("relative {}", path.describe_as_string()).into()
785                }
786            }
787            Request::Module { module, path, .. } => {
788                if path.could_match_others("") {
789                    format!(
790                        "module {} with subpath {}",
791                        module.describe_as_string(),
792                        path.describe_as_string()
793                    )
794                    .into()
795                } else {
796                    format!("module \"{}\"", module.describe_as_string()).into()
797                }
798            }
799            Request::ServerRelative { path, .. } => {
800                format!("server relative {}", path.describe_as_string()).into()
801            }
802            Request::Windows { path, .. } => {
803                format!("windows {}", path.describe_as_string()).into()
804            }
805            Request::Empty => rcstr!("empty"),
806            Request::PackageInternal { path } => {
807                format!("package internal {}", path.describe_as_string()).into()
808            }
809            Request::DataUri {
810                media_type,
811                encoding,
812                data,
813            } => format!(
814                "data uri \"{media_type}\" \"{encoding}\" \"{}\"",
815                data.await?
816            )
817            .into(),
818            Request::Uri {
819                protocol,
820                remainder,
821                ..
822            } => format!("uri \"{protocol}\" \"{remainder}\"").into(),
823            Request::Unknown { path } => format!("unknown {}", path.describe_as_string()).into(),
824            Request::Dynamic => rcstr!("dynamic"),
825            Request::Alternatives { requests } => {
826                let vec = requests.iter().map(|i| i.to_string()).try_join().await?;
827                vec.iter()
828                    .map(|r| r.as_str())
829                    .collect::<Vec<_>>()
830                    .join(" or ")
831                    .into()
832            }
833        }))
834    }
835}
836
837pub async fn stringify_data_uri(
838    media_type: &RcStr,
839    encoding: &RcStr,
840    data: ResolvedVc<RcStr>,
841) -> Result<String> {
842    Ok(format!(
843        "data:{media_type}{}{encoding},{}",
844        if encoding.is_empty() { "" } else { ";" },
845        data.await?
846    ))
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852
853    #[test]
854    fn test_parse_module() {
855        assert_eq!(
856            Request::Module {
857                module: rcstr!("foo").into(),
858                path: rcstr!("").into(),
859                query: rcstr!(""),
860                fragment: rcstr!(""),
861            },
862            Request::parse_ref(rcstr!("foo").into())
863        );
864        assert_eq!(
865            Request::Module {
866                module: rcstr!("@org/foo").into(),
867                path: rcstr!("").into(),
868                query: rcstr!(""),
869                fragment: rcstr!(""),
870            },
871            Request::parse_ref(rcstr!("@org/foo").into())
872        );
873
874        assert_eq!(
875            Request::Module {
876                module: Pattern::Concatenation(vec![
877                    Pattern::Constant(rcstr!("foo-")),
878                    Pattern::DynamicNoSlash,
879                ]),
880                path: Pattern::Dynamic,
881                query: rcstr!(""),
882                fragment: rcstr!(""),
883            },
884            Request::parse_ref(Pattern::Concatenation(vec![
885                Pattern::Constant(rcstr!("foo-")),
886                Pattern::Dynamic,
887            ]))
888        );
889
890        assert_eq!(
891            Request::Module {
892                module: Pattern::Concatenation(vec![
893                    Pattern::Constant(rcstr!("foo-")),
894                    Pattern::DynamicNoSlash,
895                ]),
896                path: Pattern::Concatenation(vec![
897                    Pattern::Dynamic,
898                    Pattern::Constant(rcstr!("/file")),
899                ]),
900                query: rcstr!(""),
901                fragment: rcstr!(""),
902            },
903            Request::parse_ref(Pattern::Concatenation(vec![
904                Pattern::Constant(rcstr!("foo-")),
905                Pattern::Dynamic,
906                Pattern::Constant(rcstr!("/file")),
907            ]))
908        );
909        assert_eq!(
910            Request::Module {
911                module: Pattern::Concatenation(vec![
912                    Pattern::Constant(rcstr!("foo-")),
913                    Pattern::DynamicNoSlash,
914                ]),
915                path: Pattern::Concatenation(vec![
916                    Pattern::Dynamic,
917                    Pattern::Constant(rcstr!("/file")),
918                    Pattern::Dynamic,
919                    Pattern::Constant(rcstr!("sub")),
920                ]),
921                query: rcstr!(""),
922                fragment: rcstr!(""),
923            },
924            Request::parse_ref(Pattern::Concatenation(vec![
925                Pattern::Constant(rcstr!("foo-")),
926                Pattern::Dynamic,
927                Pattern::Constant(rcstr!("/file")),
928                Pattern::Dynamic,
929                Pattern::Constant(rcstr!("sub")),
930            ]))
931        );
932
933        // TODO see parse_concatenation_pattern
934        // assert_eq!(
935        //     Request::Alternatives {
936        //         requests: vec![
937        //             Request::Module {
938        //                 module: Pattern::Concatenation(vec![
939        //                     Pattern::Constant(rcstr!("prefix")),
940        //                     Pattern::Dynamic,
941        //                     Pattern::Constant(rcstr!("suffix")),
942        //                 ]),
943        //                 path: rcstr!("subpath").into(),
944        //                 query: rcstr!(""),
945        //                 fragment: rcstr!(""),
946        //             }
947        //             .resolved_cell(),
948        //             Request::Module {
949        //                 module: Pattern::Concatenation(vec![
950        //                     Pattern::Constant(rcstr!("prefix")),
951        //                     Pattern::Dynamic,
952        //                 ]),
953        //                 path: Pattern::Concatenation(vec![
954        //                     Pattern::Dynamic,
955        //                     Pattern::Constant(rcstr!("suffix/subpath")),
956        //                 ]),
957        //                 query: rcstr!(""),
958        //                 fragment: rcstr!(""),
959        //             }
960        //             .resolved_cell()
961        //         ]
962        //     },
963        //     Request::parse_ref(Pattern::Concatenation(vec![
964        //         Pattern::Constant(rcstr!("prefix")),
965        //         Pattern::Dynamic,
966        //         Pattern::Constant(rcstr!("suffix/subpath")),
967        //     ]))
968        // );
969    }
970
971    #[test]
972    fn test_split_query_fragment() {
973        assert_eq!(
974            (
975                Pattern::Constant(rcstr!("foo")),
976                RcStr::default(),
977                RcStr::default()
978            ),
979            split_off_query_fragment("foo")
980        );
981        // These two cases are a bit odd, but it is important to treat `import './foo?'` differently
982        // from `import './foo'`, ditto for fragments.
983        assert_eq!(
984            (
985                Pattern::Constant(rcstr!("foo")),
986                rcstr!("?"),
987                RcStr::default()
988            ),
989            split_off_query_fragment("foo?")
990        );
991        assert_eq!(
992            (
993                Pattern::Constant(rcstr!("foo")),
994                RcStr::default(),
995                rcstr!("#")
996            ),
997            split_off_query_fragment("foo#")
998        );
999        assert_eq!(
1000            (
1001                Pattern::Constant(rcstr!("foo")),
1002                rcstr!("?bar=baz"),
1003                RcStr::default()
1004            ),
1005            split_off_query_fragment("foo?bar=baz")
1006        );
1007        assert_eq!(
1008            (
1009                Pattern::Constant(rcstr!("foo")),
1010                RcStr::default(),
1011                rcstr!("#stuff?bar=baz")
1012            ),
1013            split_off_query_fragment("foo#stuff?bar=baz")
1014        );
1015
1016        assert_eq!(
1017            (
1018                Pattern::Constant(rcstr!("foo")),
1019                rcstr!("?bar=baz"),
1020                rcstr!("#stuff")
1021            ),
1022            split_off_query_fragment("foo?bar=baz#stuff")
1023        );
1024    }
1025}