refactor(preset-env): Extract common logic for browserslist support (#3674)

This commit is contained in:
Donny/강동윤 2022-02-22 18:03:40 +09:00 committed by GitHub
parent 8012056bd8
commit b1d24702e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 305 additions and 238 deletions

View File

@ -84,6 +84,8 @@ jobs:
# Use scripts/github/create-matrix.sh to create this.
- crate: ast_node
os: ubuntu-latest
- crate: better_scoped_tls
os: ubuntu-latest
- crate: enum_kind
os: ubuntu-latest
- crate: from_variant
@ -94,6 +96,8 @@ jobs:
os: ubuntu-latest
- crate: node_macro_deps
os: ubuntu-latest
- crate: preset_env_base
os: ubuntu-latest
- crate: string_enum
os: ubuntu-latest
- crate: swc

17
Cargo.lock generated
View File

@ -1852,6 +1852,21 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "preset_env_base"
version = "0.1.0"
dependencies = [
"ahash",
"anyhow",
"browserslist-rs",
"dashmap",
"from_variant",
"once_cell",
"semver 1.0.4",
"serde",
"st-map",
]
[[package]]
name = "pretty_assertions"
version = "0.7.2"
@ -3040,10 +3055,10 @@ version = "0.93.2"
dependencies = [
"ahash",
"anyhow",
"browserslist-rs",
"dashmap",
"indexmap",
"once_cell",
"preset_env_base",
"pretty_assertions",
"semver 1.0.4",
"serde",

View File

@ -0,0 +1,21 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Common logic for targetting vairous browsers"
documentation = "https://rustdoc.swc.rs/preset_env_base/"
edition = "2021"
license = "Apache-2.0"
name = "preset_env_base"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ahash = "0.7.4"
anyhow = "1"
browserslist-rs = "=0.8.0"
dashmap = "4.0.2"
from_variant = {version = "0.1.3", path = "../from_variant"}
once_cell = "1.9.0"
semver = {version = "1.0.4", features = ["serde"]}
serde = {version = "1", features = ["derive"]}
st-map = "0.1.2"

View File

@ -0,0 +1,99 @@
use anyhow::Error;
use serde::Deserialize;
use st_map::StaticMap;
use self::version::Version;
pub mod query;
pub mod version;
/// A map without allocation.
#[derive(Debug, Default, Deserialize, Clone, Copy, StaticMap)]
#[serde(deny_unknown_fields)]
pub struct BrowserData<T: Default> {
#[serde(default)]
pub chrome: T,
#[serde(default)]
pub and_chr: T,
#[serde(default)]
pub and_ff: T,
#[serde(default)]
pub op_mob: T,
#[serde(default)]
pub ie: T,
#[serde(default)]
pub edge: T,
#[serde(default)]
pub firefox: T,
#[serde(default)]
pub safari: T,
#[serde(default)]
pub node: T,
#[serde(default)]
pub ios: T,
#[serde(default)]
pub samsung: T,
#[serde(default)]
pub opera: T,
#[serde(default)]
pub android: T,
#[serde(default)]
pub electron: T,
#[serde(default)]
pub phantom: T,
#[serde(default)]
pub opera_mobile: T,
#[serde(default)]
pub rhino: T,
}
pub type Versions = BrowserData<Option<Version>>;
impl BrowserData<Option<Version>> {
pub fn is_any_target(&self) -> bool {
self.iter().all(|(_, v)| v.is_none())
}
pub fn parse_versions(distribs: Vec<browserslist::Distrib>) -> Result<Self, Error> {
fn remap(key: &str) -> &str {
match key {
"and_chr" => "chrome",
"and_ff" => "firefox",
"ie_mob" => "ie",
"ios_saf" => "ios",
"op_mob" => "opera",
_ => key,
}
}
let mut data: Versions = BrowserData::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(|(version, _)| version)
.unwrap_or(version)
.parse()
.unwrap();
// lowest version
if data[&browser].map(|v| v > version).unwrap_or(true) {
for (k, v) in data.iter_mut() {
if browser == k {
*v = Some(version);
}
}
}
}
Ok(data)
}
}

View File

@ -0,0 +1,140 @@
#![deny(clippy::all)]
use std::collections::HashMap;
use anyhow::{Context, Error};
use dashmap::DashMap;
use from_variant::FromVariant;
use once_cell::sync::Lazy;
use serde::Deserialize;
use crate::{version::Version, BrowserData, Versions};
#[derive(Debug, Clone, Deserialize, FromVariant)]
#[serde(untagged)]
pub enum Targets {
Query(Query),
EsModules(EsModules),
Versions(Versions),
/// This uses `ahash` directly to reduce build time.
///
/// This type is identical to `swc_common::collections::AHashMap`
HashMap(HashMap<String, QueryOrVersion, ahash::RandomState>),
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct EsModules {
#[allow(dead_code)]
esmodules: bool,
}
#[derive(Debug, Clone, Deserialize, FromVariant)]
#[serde(untagged)]
pub enum QueryOrVersion {
Query(Query),
Version(Version),
}
#[derive(Debug, Clone, Deserialize, FromVariant, Eq, PartialEq, PartialOrd, Ord, Hash)]
#[serde(untagged)]
pub enum Query {
Single(String),
Multiple(Vec<String>),
}
type QueryResult = Result<Versions, Error>;
impl Query {
fn exec(&self) -> QueryResult {
fn query<T>(s: &[T]) -> QueryResult
where
T: AsRef<str>,
{
let distribs = browserslist::resolve(
s,
browserslist::Opts::new()
.mobile_to_desktop(true)
.ignore_unknown_versions(true),
)
.with_context(|| {
format!(
"failed to resolve browserslist query: {:?}",
s.iter().map(|v| v.as_ref()).collect::<Vec<_>>()
)
})?;
let versions =
BrowserData::parse_versions(distribs).expect("failed to parse browser version");
Ok(versions)
}
static CACHE: Lazy<DashMap<Query, Versions, ahash::RandomState>> =
Lazy::new(Default::default);
if let Some(v) = CACHE.get(self) {
return Ok(*v);
}
let result = match *self {
Query::Single(ref s) => {
if s.is_empty() {
query(&["defaults"])
} else {
query(&[s])
}
}
Query::Multiple(ref s) => query(s),
}
.context("failed to execute query")?;
CACHE.insert(self.clone(), result);
Ok(result)
}
}
pub fn targets_to_versions(v: Option<Targets>) -> Result<Versions, Error> {
match v {
None => Ok(Default::default()),
Some(Targets::Versions(v)) => Ok(v),
Some(Targets::Query(q)) => q
.exec()
.context("failed to convert target query to version data"),
Some(Targets::HashMap(mut map)) => {
let q = map.remove("browsers").map(|q| match q {
QueryOrVersion::Query(q) => q.exec().expect("failed to run query"),
_ => unreachable!(),
});
let node = map.remove("node").map(|q| match q {
QueryOrVersion::Version(v) => v,
QueryOrVersion::Query(..) => unreachable!(),
});
if map.is_empty() {
if let Some(mut q) = q {
q.node = node;
return Ok(q);
}
}
unimplemented!("Targets: {:?}", map)
}
_ => unimplemented!("Option<Targets>: {:?}", v),
}
}
#[cfg(test)]
mod tests {
use super::Query;
#[test]
fn test_empty() {
let res = Query::Single("".into()).exec().unwrap();
assert!(
!res.is_any_target(),
"empty query should return non-empty result"
);
}
}

View File

@ -1,9 +1,15 @@
//! Module for browser versions
use std::{cmp, cmp::Ordering, fmt, str::FromStr};
use serde::{de, de::Visitor, Deserialize, Deserializer};
use crate::Versions;
/// A version of a browser.
///
/// This is similar to semver, but this assumes a production build. (No tag like
/// `alpha`)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Version {
pub major: u16,

View File

@ -12,10 +12,10 @@ version = "0.93.2"
[dependencies]
ahash = "0.7.4"
anyhow = "1"
browserslist-rs = "=0.8.0"
dashmap = "4.0.2"
indexmap = "1.6.2"
once_cell = "1.9.0"
preset_env_base = {version = "0.1.0", path = "../preset_env_base"}
semver = {version = "1.0.4", features = ["serde"]}
serde = {version = "1", features = ["derive"]}
serde_json = "1"

View File

@ -1,11 +1,11 @@
use indexmap::IndexSet;
use preset_env_base::{version::should_enable, Versions};
use swc_atoms::js_word;
use swc_common::{util::move_map::MoveMap, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_visit::{Fold, FoldWith};
use super::builtin::BUILTINS;
use crate::{version::should_enable, Versions};
#[derive(Debug)]
pub struct Entry {
@ -17,7 +17,7 @@ pub struct Entry {
impl Entry {
pub fn new(target: Versions, regenerator: bool) -> Self {
let is_any_target = target.is_any_target();
let is_web_target = target.iter().any(|(k, v)| {
let is_web_target = target.into_iter().any(|(k, v)| {
if k == "node" {
return false;
}

View File

@ -1,4 +1,5 @@
use indexmap::IndexSet;
use preset_env_base::{version::should_enable, Versions};
use swc_atoms::{js_word, JsWord};
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
@ -9,7 +10,7 @@ use self::{
builtin::BUILTINS,
data::{BUILTIN_TYPES, INSTANCE_PROPERTIES, STATIC_PROPERTIES},
};
use crate::{util::DataMapExt, version::should_enable, Versions};
use crate::util::DataMapExt;
mod builtin;
mod data;

View File

@ -1,12 +1,15 @@
use indexmap::IndexSet;
use once_cell::sync::Lazy;
use preset_env_base::{
version::{should_enable, Version},
Versions,
};
use swc_atoms::js_word;
use swc_common::{collections::AHashMap, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_visit::{Fold, FoldWith};
use super::compat::DATA as CORE_JS_COMPAT_DATA;
use crate::{version::should_enable, Version, Versions};
static ENTRIES: Lazy<AHashMap<String, Vec<&'static str>>> = Lazy::new(|| {
serde_json::from_str::<AHashMap<String, Vec<String>>>(include_str!("entries.json"))

View File

@ -1,4 +1,5 @@
use indexmap::IndexSet;
use preset_env_base::version::should_enable;
use swc_atoms::{js_word, JsWord};
use swc_common::DUMMY_SP;
use swc_ecma_ast::*;
@ -14,7 +15,6 @@ use crate::{
},
},
util::DataMapExt,
version::should_enable,
Versions,
};

View File

@ -4,18 +4,11 @@
use std::path::PathBuf;
use anyhow::{Context, Error};
use dashmap::DashMap;
use once_cell::sync::Lazy;
use preset_env_base::query::{targets_to_versions, Query};
pub use preset_env_base::{query::Targets, version::Version, BrowserData, Versions};
use serde::Deserialize;
use st_map::StaticMap;
use swc_atoms::{js_word, JsWord};
use swc_common::{
chain,
collections::{AHashMap, AHashSet},
comments::Comments,
FromVariant, Mark, DUMMY_SP,
};
use swc_common::{chain, collections::AHashSet, comments::Comments, FromVariant, Mark, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_transforms::{
compat::{bugfixes, es2015, es2016, es2017, es2018, es2019, es2020, es2021, es2022, es3},
@ -24,7 +17,7 @@ use swc_ecma_transforms::{
use swc_ecma_utils::prepend_stmts;
use swc_ecma_visit::{Fold, FoldWith, VisitWith};
pub use self::{transform_data::Feature, version::Version};
pub use self::transform_data::Feature;
#[macro_use]
mod util;
@ -32,7 +25,6 @@ mod corejs2;
mod corejs3;
mod regenerator;
mod transform_data;
mod version;
pub fn preset_env<C>(global_mark: Mark, comments: Option<C>, c: Config) -> impl Fold
where
@ -265,46 +257,6 @@ where
)
}
/// A map without allocation.
#[derive(Debug, Default, Deserialize, Clone, Copy, StaticMap)]
#[serde(deny_unknown_fields)]
pub struct BrowserData<T: Default> {
#[serde(default)]
pub chrome: T,
#[serde(default)]
pub and_chr: T,
#[serde(default)]
pub and_ff: T,
#[serde(default)]
pub op_mob: T,
#[serde(default)]
pub ie: T,
#[serde(default)]
pub edge: T,
#[serde(default)]
pub firefox: T,
#[serde(default)]
pub safari: T,
#[serde(default)]
pub node: T,
#[serde(default)]
pub ios: T,
#[serde(default)]
pub samsung: T,
#[serde(default)]
pub opera: T,
#[serde(default)]
pub android: T,
#[serde(default)]
pub electron: T,
#[serde(default)]
pub phantom: T,
#[serde(default)]
pub opera_mobile: T,
#[serde(default)]
pub rhino: T,
}
#[derive(Debug)]
struct Polyfills {
mode: Option<Mode>,
@ -451,57 +403,6 @@ pub enum Mode {
Entry,
}
pub type Versions = BrowserData<Option<Version>>;
impl BrowserData<Option<Version>> {
pub(crate) fn is_any_target(&self) -> bool {
self.iter().all(|(_, v)| v.is_none())
}
pub(crate) fn parse_versions(distribs: Vec<browserslist::Distrib>) -> Result<Self, Error> {
fn remap(key: &str) -> &str {
match key {
"and_chr" => "chrome",
"and_ff" => "firefox",
"ie_mob" => "ie",
"ios_saf" => "ios",
"op_mob" => "opera",
_ => key,
}
}
let mut data: Versions = BrowserData::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(|(version, _)| version)
.unwrap_or(version)
.parse()
.unwrap();
// lowest version
if data[&browser].map(|v| v > version).unwrap_or(true) {
for (k, v) in data.iter_mut() {
if browser == k {
*v = Some(version);
}
}
}
}
Ok(data)
}
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Config {
@ -586,128 +487,3 @@ impl FeatureOrModule {
(features, modules)
}
}
#[derive(Debug, Clone, Deserialize, FromVariant)]
#[serde(untagged)]
pub enum Targets {
Query(Query),
EsModules(EsModules),
Versions(Versions),
HashMap(AHashMap<String, QueryOrVersion>),
}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct EsModules {
esmodules: bool,
}
#[derive(Debug, Clone, Deserialize, FromVariant)]
#[serde(untagged)]
pub enum QueryOrVersion {
Query(Query),
Version(Version),
}
#[derive(Debug, Clone, Deserialize, FromVariant, Eq, PartialEq, PartialOrd, Ord, Hash)]
#[serde(untagged)]
pub enum Query {
Single(String),
Multiple(Vec<String>),
}
type QueryResult = Result<Versions, Error>;
impl Query {
fn exec(&self) -> QueryResult {
fn query<T>(s: &[T]) -> QueryResult
where
T: AsRef<str>,
{
let distribs = browserslist::resolve(
s,
browserslist::Opts::new()
.mobile_to_desktop(true)
.ignore_unknown_versions(true),
)
.with_context(|| {
format!(
"failed to resolve browserslist query: {:?}",
s.iter().map(|v| v.as_ref()).collect::<Vec<_>>()
)
})?;
let versions =
BrowserData::parse_versions(distribs).expect("failed to parse browser version");
Ok(versions)
}
static CACHE: Lazy<DashMap<Query, Versions, ahash::RandomState>> =
Lazy::new(Default::default);
if let Some(v) = CACHE.get(self) {
return Ok(*v);
}
let result = match *self {
Query::Single(ref s) => {
if s.is_empty() {
query(&["defaults"])
} else {
query(&[s])
}
}
Query::Multiple(ref s) => query(s),
}
.context("failed to execute query")?;
CACHE.insert(self.clone(), result);
Ok(result)
}
}
fn targets_to_versions(v: Option<Targets>) -> Result<Versions, Error> {
match v {
None => Ok(Default::default()),
Some(Targets::Versions(v)) => Ok(v),
Some(Targets::Query(q)) => q
.exec()
.context("failed to convert target query to version data"),
Some(Targets::HashMap(mut map)) => {
let q = map.remove("browsers").map(|q| match q {
QueryOrVersion::Query(q) => q.exec().expect("failed to run query"),
_ => unreachable!(),
});
let node = map.remove("node").map(|q| match q {
QueryOrVersion::Version(v) => v,
QueryOrVersion::Query(..) => unreachable!(),
});
if map.is_empty() {
if let Some(mut q) = q {
q.node = node;
return Ok(q);
}
}
unimplemented!("Targets: {:?}", map)
}
_ => unimplemented!("Option<Targets>: {:?}", v),
}
}
#[cfg(test)]
mod tests {
use super::Query;
#[test]
fn test_empty() {
let res = Query::Single("".into()).exec().unwrap();
assert!(
!res.is_any_target(),
"empty query should return non-empty result"
);
}
}

View File

@ -1,9 +1,11 @@
use once_cell::sync::Lazy;
use preset_env_base::{
version::{should_enable, Version},
BrowserData, Versions,
};
use string_enum::StringEnum;
use swc_common::collections::AHashMap;
use crate::{version::should_enable, BrowserData, Version, Versions};
impl Feature {
pub fn should_enable(self, target: Versions, bugfixes: bool, default: bool) -> bool {
let f = if bugfixes {

View File

@ -5,7 +5,7 @@
"homepage": "https://swc.rs",
"main": "./index.js",
"author": "강동윤 <kdy1997.dev@gmail.com>",
"license": "Apache-2.0 AND MIT",
"license": "Apache-2.0",
"keywords": [
"swc",
"swcpack",