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::DataUri { .. } => {
294                    result = Request::Dynamic;
295                }
296                Request::Uri { .. } => {
297                    result = Request::Dynamic;
298                }
299                Request::Unknown { path } => {
300                    path.push(item);
301                }
302                Request::Dynamic => {}
303                Request::Alternatives { .. } => unreachable!(),
304            };
305        }
306
307        result
308    }
309
310    pub fn parse_string(request: RcStr) -> Vc<Self> {
311        Self::parse(request.into())
312    }
313
314    pub fn parse(mut request: Pattern) -> Vc<Self> {
315        // Call normalize before parse_inner to improve cache hits.
316        request.normalize();
317        Self::parse_inner(request)
318    }
319}
320
321#[turbo_tasks::value_impl]
322impl Request {
323    #[turbo_tasks::function]
324    async fn parse_inner(request: Pattern) -> Result<Vc<Self>> {
325        // Because we are normalized, we should handle alternatives here
326        if let Pattern::Alternatives(alts) = request {
327            Ok(Self::cell(Self::Alternatives {
328                requests: alts
329                    .into_iter()
330                    // We can call parse_inner directly because these patterns are already
331                    // normalized.  We don't call `Self::parse_ref` so we can try to get a cache hit
332                    // on the sub-patterns
333                    .map(|p| Self::parse_inner(p).to_resolved())
334                    .try_join()
335                    .await?,
336            }))
337        } else {
338            Ok(Self::cell(Self::parse_ref(request)))
339        }
340    }
341
342    #[turbo_tasks::function]
343    pub fn raw(
344        request: Pattern,
345        query: RcStr,
346        fragment: RcStr,
347        force_in_lookup_dir: bool,
348    ) -> Vc<Self> {
349        Self::cell(Request::Raw {
350            path: request,
351            force_in_lookup_dir,
352            query,
353            fragment,
354        })
355    }
356
357    #[turbo_tasks::function]
358    pub fn relative(
359        request: Pattern,
360        query: RcStr,
361        fragment: RcStr,
362        force_in_lookup_dir: bool,
363    ) -> Vc<Self> {
364        Self::cell(Request::Relative {
365            path: request,
366            force_in_lookup_dir,
367            query,
368            fragment,
369        })
370    }
371
372    #[turbo_tasks::function]
373    pub fn module(module: Pattern, path: Pattern, query: RcStr, fragment: RcStr) -> Vc<Self> {
374        Self::cell(Request::Module {
375            module,
376            path,
377            query,
378            fragment,
379        })
380    }
381
382    #[turbo_tasks::function]
383    pub async fn as_relative(self: Vc<Self>) -> Result<Vc<Self>> {
384        let this = self.await?;
385        Ok(match &*this {
386            Request::Empty
387            | Request::Raw { .. }
388            | Request::ServerRelative { .. }
389            | Request::Windows { .. }
390            | Request::Relative { .. }
391            | Request::DataUri { .. }
392            | Request::Uri { .. }
393            | Request::Dynamic => self,
394            Request::Module {
395                module,
396                path,
397                query: _,
398                fragment: _,
399            } => {
400                let mut pat = module.clone();
401                pat.push_front(rcstr!("./").into());
402                pat.push(path.clone());
403                // TODO add query
404                Self::parse(pat)
405            }
406            Request::PackageInternal { path } => {
407                let mut pat = Pattern::Constant(rcstr!("./"));
408                pat.push(path.clone());
409                Self::parse(pat)
410            }
411            Request::Unknown { path } => {
412                let mut pat = Pattern::Constant(rcstr!("./"));
413                pat.push(path.clone());
414                Self::parse(pat)
415            }
416            Request::Alternatives { requests } => {
417                let requests = requests
418                    .iter()
419                    .copied()
420                    .map(|v| *v)
421                    .map(Request::as_relative)
422                    .map(|v| async move { v.to_resolved().await })
423                    .try_join()
424                    .await?;
425                Request::Alternatives { requests }.cell()
426            }
427        })
428    }
429
430    #[turbo_tasks::function]
431    pub async fn with_query(self: Vc<Self>, query: RcStr) -> Result<Vc<Self>> {
432        Ok(match &*self.await? {
433            Request::Raw {
434                path,
435                query: _,
436                force_in_lookup_dir,
437                fragment,
438            } => Request::raw(path.clone(), query, fragment.clone(), *force_in_lookup_dir),
439            Request::Relative {
440                path,
441                query: _,
442                force_in_lookup_dir,
443                fragment,
444            } => Request::relative(path.clone(), query, fragment.clone(), *force_in_lookup_dir),
445            Request::Module {
446                module,
447                path,
448                query: _,
449                fragment,
450            } => Request::module(module.clone(), path.clone(), query, fragment.clone()),
451            Request::ServerRelative {
452                path,
453                query: _,
454                fragment,
455            } => Request::ServerRelative {
456                path: path.clone(),
457                query,
458                fragment: fragment.clone(),
459            }
460            .cell(),
461            Request::Windows {
462                path,
463                query: _,
464                fragment,
465            } => Request::Windows {
466                path: path.clone(),
467                query,
468                fragment: fragment.clone(),
469            }
470            .cell(),
471            Request::Empty => self,
472            Request::PackageInternal { .. } => self,
473            Request::DataUri { .. } => self,
474            Request::Uri { .. } => self,
475            Request::Unknown { .. } => self,
476            Request::Dynamic => self,
477            Request::Alternatives { requests } => {
478                let requests = requests
479                    .iter()
480                    .copied()
481                    .map(|req| req.with_query(query.clone()))
482                    .map(|v| async move { v.to_resolved().await })
483                    .try_join()
484                    .await?;
485                Request::Alternatives { requests }.cell()
486            }
487        })
488    }
489
490    #[turbo_tasks::function]
491    pub async fn with_fragment(self: Vc<Self>, fragment: RcStr) -> Result<Vc<Self>> {
492        Ok(match &*self.await? {
493            Request::Raw {
494                path,
495                query,
496                force_in_lookup_dir,
497                fragment: _,
498            } => Request::Raw {
499                path: path.clone(),
500                query: query.clone(),
501                force_in_lookup_dir: *force_in_lookup_dir,
502                fragment,
503            }
504            .cell(),
505            Request::Relative {
506                path,
507                query,
508                force_in_lookup_dir,
509                fragment: _,
510            } => Request::Relative {
511                path: path.clone(),
512                query: query.clone(),
513                force_in_lookup_dir: *force_in_lookup_dir,
514                fragment,
515            }
516            .cell(),
517            Request::Module {
518                module,
519                path,
520                query,
521                fragment: _,
522            } => Request::Module {
523                module: module.clone(),
524                path: path.clone(),
525                query: query.clone(),
526                fragment,
527            }
528            .cell(),
529            Request::ServerRelative {
530                path,
531                query,
532                fragment: _,
533            } => Request::ServerRelative {
534                path: path.clone(),
535                query: query.clone(),
536                fragment,
537            }
538            .cell(),
539            Request::Windows {
540                path,
541                query,
542                fragment: _,
543            } => Request::Windows {
544                path: path.clone(),
545                query: query.clone(),
546                fragment,
547            }
548            .cell(),
549            Request::Empty => self,
550            Request::PackageInternal { .. } => self,
551            Request::DataUri { .. } => self,
552            Request::Uri { .. } => self,
553            Request::Unknown { .. } => self,
554            Request::Dynamic => self,
555            Request::Alternatives { requests } => {
556                let requests = requests
557                    .iter()
558                    .copied()
559                    .map(|req| req.with_fragment(fragment.clone()))
560                    .map(|v| async move { v.to_resolved().await })
561                    .try_join()
562                    .await?;
563                Request::Alternatives { requests }.cell()
564            }
565        })
566    }
567
568    #[turbo_tasks::function]
569    pub async fn append_path(self: Vc<Self>, suffix: RcStr) -> Result<Vc<Self>> {
570        Ok(match &*self.await? {
571            Request::Raw {
572                path,
573                query,
574                force_in_lookup_dir,
575                fragment,
576            } => {
577                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
578                pat.normalize();
579                Self::raw(pat, query.clone(), fragment.clone(), *force_in_lookup_dir)
580            }
581            Request::Relative {
582                path,
583                query,
584                force_in_lookup_dir,
585                fragment,
586            } => {
587                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
588                pat.normalize();
589                Self::relative(pat, query.clone(), fragment.clone(), *force_in_lookup_dir)
590            }
591            Request::Module {
592                module,
593                path,
594                query,
595                fragment,
596            } => {
597                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
598                pat.normalize();
599                Self::module(module.clone(), pat, query.clone(), fragment.clone())
600            }
601            Request::ServerRelative {
602                path,
603                query,
604                fragment,
605            } => {
606                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
607                pat.normalize();
608                Self::ServerRelative {
609                    path: pat,
610                    query: query.clone(),
611                    fragment: fragment.clone(),
612                }
613                .cell()
614            }
615            Request::Windows {
616                path,
617                query,
618                fragment,
619            } => {
620                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
621                pat.normalize();
622                Self::Windows {
623                    path: pat,
624                    query: query.clone(),
625                    fragment: fragment.clone(),
626                }
627                .cell()
628            }
629            Request::Empty => Self::parse(suffix.into()),
630            Request::PackageInternal { path } => {
631                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
632                pat.normalize();
633                Self::PackageInternal { path: pat }.cell()
634            }
635            Request::DataUri {
636                media_type,
637                encoding,
638                data,
639            } => {
640                let data = ResolvedVc::cell(format!("{}{}", data.await?, suffix).into());
641                Self::DataUri {
642                    media_type: media_type.clone(),
643                    encoding: encoding.clone(),
644                    data,
645                }
646                .cell()
647            }
648            Request::Uri {
649                protocol,
650                remainder,
651                query,
652                fragment,
653            } => {
654                let remainder = format!("{remainder}{suffix}").into();
655                Self::Uri {
656                    protocol: protocol.clone(),
657                    remainder,
658                    query: query.clone(),
659                    fragment: fragment.clone(),
660                }
661                .cell()
662            }
663            Request::Unknown { path } => {
664                let mut pat = Pattern::concat([path.clone(), suffix.into()]);
665                pat.normalize();
666                Self::Unknown { path: pat }.cell()
667            }
668            Request::Dynamic => self,
669            Request::Alternatives { requests } => {
670                let requests = requests
671                    .iter()
672                    .map(|req| async { req.append_path(suffix.clone()).to_resolved().await })
673                    .try_join()
674                    .await?;
675                Request::Alternatives { requests }.cell()
676            }
677        })
678    }
679
680    #[turbo_tasks::function]
681    pub fn query(&self) -> Vc<RcStr> {
682        Vc::cell(match self {
683            Request::Windows { query, .. }
684            | Request::ServerRelative { query, .. }
685            | Request::Module { query, .. }
686            | Request::Relative { query, .. }
687            | Request::Raw { query, .. } => query.clone(),
688            Request::Dynamic
689            | Request::Unknown { .. }
690            | Request::Uri { .. }
691            | Request::DataUri { .. }
692            | Request::PackageInternal { .. }
693            | Request::Empty => RcStr::default(),
694            // TODO: is this correct, should we return the first one instead?
695            Request::Alternatives { .. } => RcStr::default(),
696        })
697    }
698
699    /// Turns the request into a pattern, similar to [Request::request()] but
700    /// more complete.
701    #[turbo_tasks::function]
702    pub async fn request_pattern(self: Vc<Self>) -> Result<Vc<Pattern>> {
703        Ok(Pattern::new(match &*self.await? {
704            Request::Raw { path, .. } => path.clone(),
705            Request::Relative { path, .. } => path.clone(),
706            Request::Module { module, path, .. } => {
707                let mut path = path.clone();
708                path.push_front(module.clone());
709                path.normalize();
710                path
711            }
712            Request::ServerRelative { path, .. } => path.clone(),
713            Request::Windows { path, .. } => path.clone(),
714            Request::Empty => Pattern::Constant(rcstr!("")),
715            Request::PackageInternal { path } => path.clone(),
716            Request::DataUri {
717                media_type,
718                encoding,
719                data,
720            } => Pattern::Constant(
721                stringify_data_uri(media_type, encoding, *data)
722                    .await?
723                    .into(),
724            ),
725            Request::Uri {
726                protocol,
727                remainder,
728                ..
729            } => Pattern::Constant(format!("{protocol}{remainder}").into()),
730            Request::Unknown { path } => path.clone(),
731            Request::Dynamic => Pattern::Dynamic,
732            Request::Alternatives { requests } => Pattern::Alternatives(
733                requests
734                    .iter()
735                    .map(async |r: &ResolvedVc<Request>| -> Result<Pattern> {
736                        Ok(r.request_pattern().owned().await?)
737                    })
738                    .try_join()
739                    .await?,
740            ),
741        }))
742    }
743}
744
745#[turbo_tasks::value_impl]
746impl ValueToString for Request {
747    #[turbo_tasks::function]
748    async fn to_string(&self) -> Result<Vc<RcStr>> {
749        Ok(Vc::cell(match self {
750            Request::Raw {
751                path,
752                force_in_lookup_dir,
753                ..
754            } => {
755                if *force_in_lookup_dir {
756                    format!("in-lookup-dir {}", path.describe_as_string()).into()
757                } else {
758                    path.describe_as_string().into()
759                }
760            }
761            Request::Relative {
762                path,
763                force_in_lookup_dir,
764                ..
765            } => {
766                if *force_in_lookup_dir {
767                    format!("relative-in-lookup-dir {}", path.describe_as_string()).into()
768                } else {
769                    format!("relative {}", path.describe_as_string()).into()
770                }
771            }
772            Request::Module { module, path, .. } => {
773                if path.could_match_others("") {
774                    format!(
775                        "module {} with subpath {}",
776                        module.describe_as_string(),
777                        path.describe_as_string()
778                    )
779                    .into()
780                } else {
781                    format!("module \"{}\"", module.describe_as_string()).into()
782                }
783            }
784            Request::ServerRelative { path, .. } => {
785                format!("server relative {}", path.describe_as_string()).into()
786            }
787            Request::Windows { path, .. } => {
788                format!("windows {}", path.describe_as_string()).into()
789            }
790            Request::Empty => rcstr!("empty"),
791            Request::PackageInternal { path } => {
792                format!("package internal {}", path.describe_as_string()).into()
793            }
794            Request::DataUri {
795                media_type,
796                encoding,
797                data,
798            } => format!(
799                "data uri \"{media_type}\" \"{encoding}\" \"{}\"",
800                data.await?
801            )
802            .into(),
803            Request::Uri {
804                protocol,
805                remainder,
806                ..
807            } => format!("uri \"{protocol}\" \"{remainder}\"").into(),
808            Request::Unknown { path } => format!("unknown {}", path.describe_as_string()).into(),
809            Request::Dynamic => rcstr!("dynamic"),
810            Request::Alternatives { requests } => {
811                let vec = requests.iter().map(|i| i.to_string()).try_join().await?;
812                vec.iter()
813                    .map(|r| r.as_str())
814                    .collect::<Vec<_>>()
815                    .join(" or ")
816                    .into()
817            }
818        }))
819    }
820}
821
822pub async fn stringify_data_uri(
823    media_type: &RcStr,
824    encoding: &RcStr,
825    data: ResolvedVc<RcStr>,
826) -> Result<String> {
827    Ok(format!(
828        "data:{media_type}{}{encoding},{}",
829        if encoding.is_empty() { "" } else { ";" },
830        data.await?
831    ))
832}
833
834#[cfg(test)]
835mod tests {
836    use super::*;
837
838    #[test]
839    fn test_parse_module() {
840        assert_eq!(
841            Request::Module {
842                module: rcstr!("foo").into(),
843                path: rcstr!("").into(),
844                query: rcstr!(""),
845                fragment: rcstr!(""),
846            },
847            Request::parse_ref(rcstr!("foo").into())
848        );
849        assert_eq!(
850            Request::Module {
851                module: rcstr!("@org/foo").into(),
852                path: rcstr!("").into(),
853                query: rcstr!(""),
854                fragment: rcstr!(""),
855            },
856            Request::parse_ref(rcstr!("@org/foo").into())
857        );
858
859        assert_eq!(
860            Request::Module {
861                module: Pattern::Concatenation(vec![
862                    Pattern::Constant(rcstr!("foo-")),
863                    Pattern::DynamicNoSlash,
864                ]),
865                path: Pattern::Dynamic,
866                query: rcstr!(""),
867                fragment: rcstr!(""),
868            },
869            Request::parse_ref(Pattern::Concatenation(vec![
870                Pattern::Constant(rcstr!("foo-")),
871                Pattern::Dynamic,
872            ]))
873        );
874
875        assert_eq!(
876            Request::Module {
877                module: Pattern::Concatenation(vec![
878                    Pattern::Constant(rcstr!("foo-")),
879                    Pattern::DynamicNoSlash,
880                ]),
881                path: Pattern::Concatenation(vec![
882                    Pattern::Dynamic,
883                    Pattern::Constant(rcstr!("/file")),
884                ]),
885                query: rcstr!(""),
886                fragment: rcstr!(""),
887            },
888            Request::parse_ref(Pattern::Concatenation(vec![
889                Pattern::Constant(rcstr!("foo-")),
890                Pattern::Dynamic,
891                Pattern::Constant(rcstr!("/file")),
892            ]))
893        );
894        assert_eq!(
895            Request::Module {
896                module: Pattern::Concatenation(vec![
897                    Pattern::Constant(rcstr!("foo-")),
898                    Pattern::DynamicNoSlash,
899                ]),
900                path: Pattern::Concatenation(vec![
901                    Pattern::Dynamic,
902                    Pattern::Constant(rcstr!("/file")),
903                    Pattern::Dynamic,
904                    Pattern::Constant(rcstr!("sub")),
905                ]),
906                query: rcstr!(""),
907                fragment: rcstr!(""),
908            },
909            Request::parse_ref(Pattern::Concatenation(vec![
910                Pattern::Constant(rcstr!("foo-")),
911                Pattern::Dynamic,
912                Pattern::Constant(rcstr!("/file")),
913                Pattern::Dynamic,
914                Pattern::Constant(rcstr!("sub")),
915            ]))
916        );
917
918        // TODO see parse_concatenation_pattern
919        // assert_eq!(
920        //     Request::Alternatives {
921        //         requests: vec![
922        //             Request::Module {
923        //                 module: Pattern::Concatenation(vec![
924        //                     Pattern::Constant(rcstr!("prefix")),
925        //                     Pattern::Dynamic,
926        //                     Pattern::Constant(rcstr!("suffix")),
927        //                 ]),
928        //                 path: rcstr!("subpath").into(),
929        //                 query: rcstr!(""),
930        //                 fragment: rcstr!(""),
931        //             }
932        //             .resolved_cell(),
933        //             Request::Module {
934        //                 module: Pattern::Concatenation(vec![
935        //                     Pattern::Constant(rcstr!("prefix")),
936        //                     Pattern::Dynamic,
937        //                 ]),
938        //                 path: Pattern::Concatenation(vec![
939        //                     Pattern::Dynamic,
940        //                     Pattern::Constant(rcstr!("suffix/subpath")),
941        //                 ]),
942        //                 query: rcstr!(""),
943        //                 fragment: rcstr!(""),
944        //             }
945        //             .resolved_cell()
946        //         ]
947        //     },
948        //     Request::parse_ref(Pattern::Concatenation(vec![
949        //         Pattern::Constant(rcstr!("prefix")),
950        //         Pattern::Dynamic,
951        //         Pattern::Constant(rcstr!("suffix/subpath")),
952        //     ]))
953        // );
954    }
955
956    #[test]
957    fn test_split_query_fragment() {
958        assert_eq!(
959            (
960                Pattern::Constant(rcstr!("foo")),
961                RcStr::default(),
962                RcStr::default()
963            ),
964            split_off_query_fragment("foo")
965        );
966        // These two cases are a bit odd, but it is important to treat `import './foo?'` differently
967        // from `import './foo'`, ditto for fragments.
968        assert_eq!(
969            (
970                Pattern::Constant(rcstr!("foo")),
971                rcstr!("?"),
972                RcStr::default()
973            ),
974            split_off_query_fragment("foo?")
975        );
976        assert_eq!(
977            (
978                Pattern::Constant(rcstr!("foo")),
979                RcStr::default(),
980                rcstr!("#")
981            ),
982            split_off_query_fragment("foo#")
983        );
984        assert_eq!(
985            (
986                Pattern::Constant(rcstr!("foo")),
987                rcstr!("?bar=baz"),
988                RcStr::default()
989            ),
990            split_off_query_fragment("foo?bar=baz")
991        );
992        assert_eq!(
993            (
994                Pattern::Constant(rcstr!("foo")),
995                RcStr::default(),
996                rcstr!("#stuff?bar=baz")
997            ),
998            split_off_query_fragment("foo#stuff?bar=baz")
999        );
1000
1001        assert_eq!(
1002            (
1003                Pattern::Constant(rcstr!("foo")),
1004                rcstr!("?bar=baz"),
1005                rcstr!("#stuff")
1006            ),
1007            split_off_query_fragment("foo?bar=baz#stuff")
1008        );
1009    }
1010}