1#![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}