Skip to main content

oxc_compat/
engine_targets.rs

1use std::{
2    fmt::{Debug, Display},
3    ops::{Deref, DerefMut},
4    str::FromStr,
5};
6
7pub use browserslist::Version;
8use oxc_syntax::es_target::ESTarget;
9use rustc_hash::FxHashMap;
10use serde::Deserialize;
11
12use crate::browserslist_query::BrowserslistQuery;
13use crate::{babel_targets::BabelTargets, es_target::ESVersion};
14
15use super::{
16    Engine,
17    es_features::{ESFeature, features},
18};
19
20/// A map of engine names to minimum supported versions.
21#[derive(Debug, Default, Clone, Deserialize)]
22#[serde(try_from = "BabelTargets")]
23pub struct EngineTargets(FxHashMap<Engine, Version>);
24
25impl Deref for EngineTargets {
26    type Target = FxHashMap<Engine, Version>;
27
28    fn deref(&self) -> &Self::Target {
29        &self.0
30    }
31}
32
33impl DerefMut for EngineTargets {
34    fn deref_mut(&mut self) -> &mut Self::Target {
35        &mut self.0
36    }
37}
38
39impl Display for EngineTargets {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        for (idx, (engine, version)) in self.iter().enumerate() {
42            if idx > 0 {
43                f.write_str(",")?;
44            }
45            f.write_str(&engine.to_string())?;
46            if *engine == Engine::Es {
47                f.write_str(&version.0.to_string())?;
48            } else {
49                f.write_str(&version.to_string())?;
50            }
51        }
52        Ok(())
53    }
54}
55
56impl EngineTargets {
57    pub fn new(map: FxHashMap<Engine, Version>) -> Self {
58        Self(map)
59    }
60
61    /// # Errors
62    ///
63    /// * Query is invalid.
64    pub fn try_from_query(query: &str) -> Result<Self, String> {
65        BrowserslistQuery::Single(query.to_string()).exec()
66    }
67
68    /// Returns true if all fields are empty.
69    pub fn is_any_target(&self) -> bool {
70        self.0.is_empty()
71    }
72
73    /// Check if the target engines support the given ES feature.
74    ///
75    /// Returns `true` if the feature is NOT supported (needs transformation),
76    /// `false` if the feature IS supported (can be used natively).
77    pub fn has_feature(&self, feature: ESFeature) -> bool {
78        let feature_engine_targets = &features()[&feature];
79        for (engine, feature_version) in feature_engine_targets.iter() {
80            if let Some(target_version) = self.get(engine) {
81                if *engine == Engine::Es {
82                    return target_version.0 < feature_version.0;
83                }
84                if target_version < feature_version {
85                    return true;
86                }
87            }
88        }
89        false
90    }
91
92    /// Parses the value returned from `browserslist`.
93    pub fn parse_versions(versions: Vec<(String, String)>) -> Self {
94        let mut engine_targets = Self::default();
95        for (engine, version) in versions {
96            let Ok(engine) = Engine::from_str(&engine) else {
97                continue;
98            };
99            let Ok(version) = Version::from_str(&version) else {
100                continue;
101            };
102            engine_targets
103                .0
104                .entry(engine)
105                .and_modify(|v| {
106                    if version < *v {
107                        *v = version;
108                    }
109                })
110                .or_insert(version);
111        }
112        engine_targets
113    }
114
115    /// # Errors
116    ///
117    /// * When the query failed to parse.
118    pub fn from_target(s: &str) -> Result<Self, String> {
119        if s.contains(',') {
120            Self::from_target_list(&s.split(',').collect::<Vec<_>>())
121        } else {
122            Self::from_target_list(&[s])
123        }
124    }
125
126    /// # Errors
127    ///
128    /// * When the query failed to parse.
129    pub fn from_target_list<S: AsRef<str>>(list: &[S]) -> Result<Self, String> {
130        let mut es_target = None;
131        let mut engine_targets = EngineTargets::default();
132
133        for s in list {
134            let s = s.as_ref();
135            // Parse `esXXXX`.
136            if let Ok(target) = ESTarget::from_str(s) {
137                if let Some(target) = es_target {
138                    return Err(format!("'{target}' is already specified."));
139                }
140                es_target = Some(target);
141            } else {
142                // Parse `chromeXX`, `edgeXX` etc.
143                let (engine, version) = Engine::parse_name_and_version(s)?;
144                if engine_targets.insert(engine, version).is_some() {
145                    return Err(format!("'{s}' is already specified."));
146                }
147            }
148        }
149        engine_targets.insert(Engine::Es, es_target.unwrap_or(ESTarget::default()).version());
150        Ok(engine_targets)
151    }
152}
153
154#[test]
155fn test_displayed_value_is_parsable() {
156    let target = EngineTargets::new(FxHashMap::from_iter([
157        (Engine::Chrome, Version(139, 0, 0)),
158        (Engine::Deno, Version(2, 5, 1)),
159        (Engine::Es, Version(2024, 0, 0)),
160    ]));
161    let s = target.to_string();
162    let parsed = EngineTargets::from_target(&s).unwrap();
163    assert_eq!(target.0, parsed.0);
164}