mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
refactor(transformer): deserialize engine target strings to specific keys (#7139)
This commit is contained in:
parent
9aa34b4896
commit
55e6989830
4 changed files with 122 additions and 119 deletions
|
|
@ -2,7 +2,7 @@ use std::sync::OnceLock;
|
|||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use super::{Targets, Version};
|
||||
use super::Targets;
|
||||
|
||||
/// Reference: <https://github.com/swc-project/swc/blob/ea14fc8e5996dcd736b8deb4cc99262d07dfff44/crates/swc_ecma_preset_env/src/transform_data.rs#L194-L218>
|
||||
pub fn features() -> &'static FxHashMap<String, Targets> {
|
||||
|
|
@ -20,19 +20,12 @@ pub fn features() -> &'static FxHashMap<String, Targets> {
|
|||
|
||||
map.into_iter()
|
||||
.map(|(feature, mut versions)| {
|
||||
(feature, {
|
||||
let version = versions.get("safari");
|
||||
if version.is_some_and(|v| v == "tp") {
|
||||
versions.remove("safari");
|
||||
}
|
||||
|
||||
Targets::new(
|
||||
versions
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.parse::<Version>().unwrap()))
|
||||
.collect::<FxHashMap<String, Version>>(),
|
||||
)
|
||||
})
|
||||
let version = versions.get("safari");
|
||||
if version.is_some_and(|v| v == "tp") {
|
||||
versions.remove("safari");
|
||||
}
|
||||
let versions = versions.into_iter().collect::<Vec<_>>();
|
||||
(feature, Targets::parse_versions(versions))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ mod query;
|
|||
mod targets;
|
||||
|
||||
pub use data::{bugfix_features, features};
|
||||
pub use targets::{Targets, Version};
|
||||
pub use targets::Targets;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
//! Module for `browserslist` queries.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
|
@ -16,15 +14,13 @@ pub enum Query {
|
|||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
type QueryResult = Result<Targets, Error>;
|
||||
|
||||
fn cache() -> &'static DashMap<Query, Targets> {
|
||||
static CACHE: OnceLock<DashMap<Query, Targets>> = OnceLock::new();
|
||||
CACHE.get_or_init(DashMap::new)
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub fn exec(&self) -> QueryResult {
|
||||
pub fn exec(&self) -> Result<Targets, Error> {
|
||||
if let Some(v) = cache().get(self) {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
|
|
@ -47,7 +43,13 @@ impl Query {
|
|||
};
|
||||
|
||||
let result = match result {
|
||||
Ok(distribs) => Targets::parse_versions(distribs),
|
||||
Ok(distribs) => {
|
||||
let versions = distribs
|
||||
.into_iter()
|
||||
.map(|d| (d.name().to_string(), d.version().to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
Targets::parse_versions(versions)
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(OxcDiagnostic::error(format!("failed to resolve query: {err}")).into())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
//! Base crate for `preset-env`-like crates.
|
||||
//!
|
||||
//! This crate provides an interface to convert `browserslist` query to
|
||||
//! something usable.
|
||||
//!
|
||||
//! This file is copied from <https://github.com/swc-project/swc/blob/ea14fc8e5996dcd736b8deb4cc99262d07dfff44/crates/preset_env_base/src/lib.rs>
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use oxc_diagnostics::Error;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
|
@ -19,87 +10,104 @@ use super::query::Query;
|
|||
///
|
||||
/// This type mainly stores `minimum version for each browsers with support for
|
||||
/// a feature`.
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
#[serde(try_from = "BabelTargets")] // https://github.com/serde-rs/serde/issues/642#issuecomment-683276351
|
||||
pub struct Targets(FxHashMap<String, Version>);
|
||||
|
||||
impl Deref for Targets {
|
||||
type Target = FxHashMap<String, Version>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(try_from = "BabelTargets")]
|
||||
pub struct Targets {
|
||||
chrome: Option<Version>,
|
||||
deno: Option<Version>,
|
||||
edge: Option<Version>,
|
||||
firefox: Option<Version>,
|
||||
hermes: Option<Version>,
|
||||
ie: Option<Version>,
|
||||
ios: Option<Version>,
|
||||
node: Option<Version>,
|
||||
opera: Option<Version>,
|
||||
rhino: Option<Version>,
|
||||
safari: Option<Version>,
|
||||
}
|
||||
|
||||
impl Targets {
|
||||
pub fn new(map: FxHashMap<String, Version>) -> Self {
|
||||
Self(map)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// * Query is invalid.
|
||||
pub fn try_from_query(query: &str) -> Result<Self, Error> {
|
||||
Query::Single(query.to_string()).exec().map(|v| v.0).map(Self)
|
||||
Query::Single(query.to_string()).exec()
|
||||
}
|
||||
|
||||
/// Returns true if all fields are [None].
|
||||
pub fn is_any_target(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
*self == Self::default()
|
||||
}
|
||||
|
||||
pub fn should_enable(&self, targets: &Targets) -> bool {
|
||||
if let (Some(v1), Some(v2)) = (&self.chrome, &targets.chrome) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.deno, &targets.deno) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.edge, &targets.edge) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.firefox, &targets.firefox) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.hermes, &targets.hermes) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.ie, &targets.ie) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.ios, &targets.ios) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.node, &targets.node) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.opera, &targets.opera) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.rhino, &targets.rhino) {
|
||||
return v1 < v2;
|
||||
}
|
||||
if let (Some(v1), Some(v2)) = (&self.safari, &targets.safari) {
|
||||
return v1 < v2;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Parses the value returned from `browserslist`.
|
||||
pub fn parse_versions(distribs: Vec<browserslist::Distrib>) -> Self {
|
||||
fn remap(key: &str) -> &str {
|
||||
match key {
|
||||
"and_chr" => "chrome",
|
||||
"and_ff" => "firefox",
|
||||
"ie_mob" => "ie",
|
||||
"ios_saf" => "ios",
|
||||
"op_mob" => "opera",
|
||||
_ => key,
|
||||
pub fn parse_versions(versions: Vec<(String, String)>) -> Self {
|
||||
let mut targets = Self::default();
|
||||
for (name, version) in versions {
|
||||
let Ok(browser) = targets.get_version_mut(&name) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(version) = version.parse::<Version>() else {
|
||||
continue;
|
||||
};
|
||||
if browser.is_none() || browser.is_some_and(|v| version < v) {
|
||||
browser.replace(version);
|
||||
}
|
||||
}
|
||||
|
||||
let mut data = FxHashMap::default();
|
||||
for dist in distribs {
|
||||
let browser = dist.name();
|
||||
let browser = remap(browser);
|
||||
let version = dist.version();
|
||||
match browser {
|
||||
"and_qq" | "and_uc" | "baidu" | "bb" | "kaios" | "op_mini" => continue,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let version =
|
||||
version.split_once('-').map_or(version, |(version, _)| version).parse::<Version>();
|
||||
|
||||
let Ok(version) = version else { continue };
|
||||
|
||||
// lowest version
|
||||
let is_lowest = data.get(browser).map_or(true, |v| v > &version);
|
||||
if is_lowest {
|
||||
data.insert(browser.to_string(), version);
|
||||
}
|
||||
}
|
||||
|
||||
Self(data)
|
||||
targets
|
||||
}
|
||||
|
||||
pub fn should_enable(&self, feature: &Targets) -> bool {
|
||||
self.0.iter().any(|(target_name, target_version)| {
|
||||
feature
|
||||
.get(target_name)
|
||||
.or_else(|| match target_name.as_str() {
|
||||
// Fall back to Chrome versions if Android browser data
|
||||
// is missing from the feature data. It appears the
|
||||
// Android browser has aligned its versioning with Chrome.
|
||||
"android" => feature.get("chrome"),
|
||||
_ => None,
|
||||
})
|
||||
.map_or(false, |feature_version| feature_version > target_version)
|
||||
})
|
||||
fn get_version_mut(&mut self, key: &str) -> Result<&mut Option<Version>, ()> {
|
||||
match key {
|
||||
"chrome" | "and_chr" => Ok(&mut self.chrome),
|
||||
"deno" => Ok(&mut self.deno),
|
||||
"edge" => Ok(&mut self.edge),
|
||||
"firefox" | "and_ff" => Ok(&mut self.firefox),
|
||||
"hermes" => Ok(&mut self.hermes),
|
||||
"ie" | "ie_mob" => Ok(&mut self.ie),
|
||||
"ios" | "ios_saf" => Ok(&mut self.ios),
|
||||
"node" => Ok(&mut self.node),
|
||||
"opera" | "op_mob" => Ok(&mut self.opera),
|
||||
"rhino" => Ok(&mut self.rhino),
|
||||
"safari" => Ok(&mut self.safari),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,49 +140,49 @@ impl TryFrom<BabelTargets> for Targets {
|
|||
type Error = Error;
|
||||
fn try_from(value: BabelTargets) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
BabelTargets::String(s) => Query::Single(s).exec().map(|v| v.0).map(Self),
|
||||
BabelTargets::Array(v) => Query::Multiple(v).exec().map(|v| v.0).map(Self),
|
||||
BabelTargets::String(s) => Query::Single(s).exec(),
|
||||
BabelTargets::Array(v) => Query::Multiple(v).exec(),
|
||||
BabelTargets::Map(map) => {
|
||||
let mut new_map = FxHashMap::default();
|
||||
for (k, v) in map {
|
||||
let mut targets = Self::default();
|
||||
for (key, value) in map {
|
||||
// TODO: Implement these targets.
|
||||
if matches!(k.as_str(), "esmodules" | "node" | "safari" | "browsers" | "deno") {
|
||||
if matches!(key.as_str(), "esmodules" | "browsers") {
|
||||
continue;
|
||||
}
|
||||
// TODO: Implement `Version::from_number`
|
||||
if matches!(v, BabelTargetsValue::Int(_) | BabelTargetsValue::Float(_)) {
|
||||
if matches!(value, BabelTargetsValue::Int(_) | BabelTargetsValue::Float(_)) {
|
||||
continue;
|
||||
};
|
||||
let BabelTargetsValue::String(v) = v else {
|
||||
return Err(Error::msg(format!("{v:?} is not a string for {k}.")));
|
||||
let BabelTargetsValue::String(v) = value else {
|
||||
return Err(Error::msg(format!("{value:?} is not a string for {key}.")));
|
||||
};
|
||||
// TODO: Implement this target.
|
||||
if key == "node" && v == "current" {
|
||||
continue;
|
||||
}
|
||||
// TODO: Implement this target.
|
||||
if key == "safari" && v == "tp" {
|
||||
continue;
|
||||
}
|
||||
// TODO: Some keys are not implemented yet.
|
||||
// <https://babel.dev/docs/options#targets>:
|
||||
// Supported environments: android, chrome, deno, edge, electron, firefox, ie, ios, node, opera, rhino, safari, samsung.
|
||||
let Ok(target) = targets.get_version_mut(&key) else {
|
||||
continue;
|
||||
};
|
||||
match Version::parse(&v) {
|
||||
Ok(v) => {
|
||||
new_map.insert(k, v);
|
||||
Ok(version) => {
|
||||
target.replace(version);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(oxc_diagnostics::Error::msg(format!(
|
||||
"Failed to parse `{v}` for `{k}`\n{err:?}"
|
||||
"Failed to parse `{v}` for `{key}`\n{err:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Self(new_map))
|
||||
Ok(targets)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Targets, Version};
|
||||
|
||||
#[test]
|
||||
fn should_enable_android_falls_back_to_chrome() {
|
||||
let mut targets = Targets::default();
|
||||
targets.0.insert("android".to_string(), "51.0.0".parse::<Version>().unwrap());
|
||||
let mut feature = Targets::default();
|
||||
feature.0.insert("chrome".to_string(), "51.0.0".parse::<Version>().unwrap());
|
||||
assert!(!targets.should_enable(&feature));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue