preset_env_base/
query.rs

1//! Module for `browserslist` queries.
2#![deny(clippy::all)]
3
4use std::{collections::HashMap, path::PathBuf, sync::Arc};
5
6use anyhow::{anyhow, Context, Error};
7use dashmap::DashMap;
8use from_variant::FromVariant;
9use once_cell::sync::Lazy;
10use rustc_hash::FxBuildHasher;
11use serde::Deserialize;
12
13use crate::{version::Version, BrowserData, Versions};
14
15#[derive(Debug, Clone, Deserialize, FromVariant)]
16#[serde(untagged)]
17#[allow(clippy::large_enum_variant)]
18pub enum Targets {
19    Query(Query),
20    EsModules(EsModules),
21    Versions(Versions),
22    HashMap(HashMap<String, QueryOrVersion, FxBuildHasher>),
23}
24
25#[derive(Debug, Clone, Copy, Deserialize)]
26pub struct EsModules {
27    #[allow(dead_code)]
28    esmodules: bool,
29}
30
31#[derive(Debug, Clone, Deserialize, FromVariant)]
32#[serde(untagged)]
33pub enum QueryOrVersion {
34    Query(Query),
35    Version(Version),
36}
37
38#[derive(Debug, Clone, Deserialize, FromVariant, Eq, PartialEq, PartialOrd, Ord, Hash)]
39#[serde(untagged)]
40pub enum Query {
41    Single(String),
42    Multiple(Vec<String>),
43}
44
45type QueryResult = Result<Arc<Versions>, Error>;
46
47impl Query {
48    fn exec(&self) -> QueryResult {
49        fn query<T>(s: &[T]) -> QueryResult
50        where
51            T: AsRef<str>,
52        {
53            let distribs = browserslist::resolve(
54                s,
55                &browserslist::Opts {
56                    mobile_to_desktop: true,
57                    ignore_unknown_versions: true,
58                    ..Default::default()
59                },
60            )
61            .with_context(|| {
62                format!(
63                    "failed to resolve browserslist query: {:?}",
64                    s.iter().map(|v| v.as_ref()).collect::<Vec<_>>()
65                )
66            })?;
67
68            let versions =
69                BrowserData::parse_versions(distribs).expect("failed to parse browser version");
70
71            Ok(Arc::new(versions))
72        }
73
74        static CACHE: Lazy<DashMap<Query, Arc<Versions>, FxBuildHasher>> =
75            Lazy::new(Default::default);
76
77        if let Some(v) = CACHE.get(self) {
78            return Ok(v.clone());
79        }
80
81        let result = match *self {
82            Query::Single(ref s) => {
83                if s.is_empty() {
84                    query(&["defaults"])
85                } else {
86                    query(&[s])
87                }
88            }
89            Query::Multiple(ref s) => query(s),
90        }
91        .context("failed to execute query")?;
92
93        CACHE.insert(self.clone(), result.clone());
94
95        Ok(result)
96    }
97}
98
99pub fn targets_to_versions(
100    v: Option<Targets>,
101    path: Option<PathBuf>,
102) -> Result<Arc<Versions>, Error> {
103    match v {
104        #[cfg(not(target_arch = "wasm32"))]
105        None => {
106            let mut browserslist_opts = browserslist::Opts {
107                mobile_to_desktop: true,
108                ignore_unknown_versions: true,
109                ..Default::default()
110            };
111            if let Some(path) = path {
112                browserslist_opts.path = path
113                    .clone()
114                    .into_os_string()
115                    .into_string()
116                    .map_err(|_| anyhow!("Invalid path \"{:?}\"", path))?
117                    .into();
118            }
119            let distribs = browserslist::execute(&browserslist_opts)
120                .with_context(|| "failed to resolve browserslist query from browserslist config")?;
121
122            let versions = BrowserData::parse_versions(distribs)
123                .with_context(|| "failed to parse browser version")?;
124
125            Ok(Arc::new(versions))
126        }
127        #[cfg(target_arch = "wasm32")]
128        None => Ok(Default::default()),
129        Some(Targets::Versions(v)) => Ok(Arc::new(v)),
130        Some(Targets::Query(q)) => q
131            .exec()
132            .context("failed to convert target query to version data"),
133        Some(Targets::HashMap(mut map)) => {
134            let q = map.remove("browsers").map(|q| match q {
135                QueryOrVersion::Query(q) => q.exec().expect("failed to run query"),
136                _ => unreachable!(),
137            });
138
139            let node = map.remove("node").map(|q| match q {
140                QueryOrVersion::Version(v) => v,
141                QueryOrVersion::Query(..) => unreachable!(),
142            });
143
144            if map.is_empty() {
145                if let Some(q) = q {
146                    let mut q = *q;
147                    q.node = node;
148                    return Ok(Arc::new(q));
149                }
150            }
151
152            let mut result = Versions::default();
153            for (k, v) in map.iter() {
154                match v {
155                    QueryOrVersion::Query(q) => {
156                        let v = q.exec().context("failed to run query")?;
157
158                        for (k, v) in v.iter() {
159                            result.insert(k, *v);
160                        }
161                    }
162                    QueryOrVersion::Version(v) => {
163                        result.insert(k, Some(*v));
164                    }
165                }
166            }
167
168            unimplemented!("Targets: {:?}", map)
169        }
170        _ => unimplemented!("Option<Targets>: {:?}", v),
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::Query;
177
178    #[test]
179    fn test_empty() {
180        let res = Query::Single("".into()).exec().unwrap();
181        assert!(
182            !res.is_any_target(),
183            "empty query should return non-empty result"
184        );
185    }
186}