Skip to main content

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