Repository: swc-project/swc (rust)

Issue:
preset-env: Switch to `browserslist-rs`
Currently `swc_ecma_preset_env` executes a node js script to use `browserslist`, but now there's https://crates.io/crates/browserslist-rs

Pull request:
feat(es/preset-env): Use `browserslist-rs`
swc_ecma_preset_env:
 - Fix `default_path`.
 - Use `browserslist-rs` for query. (Closes #2781)

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 077586d2d0..a22cc4fb74 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -256,6 +256,24 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "browserslist-rs"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa428c5a7369965603314e5fbfa5ae1755159bce2c35880d80ea84a866213b20"
+dependencies = [
+ "chrono",
+ "itertools",
+ "js-sys",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "build_const"
 version = "0.2.2"
@@ -298,10 +316,12 @@ version = "0.4.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
 dependencies = [
+ "js-sys",
  "libc",
  "num-integer",
  "num-traits",
  "time",
+ "wasm-bindgen",
  "winapi",
 ]
 
@@ -944,6 +964,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "0.4.8"
@@ -2193,6 +2222,18 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "618365e8e586c22123d692b72a7d791d5ee697817b65a218cdf12a98870af0f7"
+dependencies = [
+ "fnv",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "serde_derive"
 version = "1.0.130"
@@ -2829,6 +2870,8 @@ name = "swc_ecma_preset_env"
 version = "0.66.0"
 dependencies = [
  "ahash",
+ "anyhow",
+ "browserslist-rs",
  "dashmap",
  "indexmap",
  "once_cell",
@@ -3390,6 +3433,26 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "thread_local"
 version = "1.1.3"
diff --git a/crates/swc_ecma_preset_env/Cargo.toml b/crates/swc_ecma_preset_env/Cargo.toml
index 8f74053361..381bfcdded 100644
--- a/crates/swc_ecma_preset_env/Cargo.toml
+++ b/crates/swc_ecma_preset_env/Cargo.toml
@@ -11,6 +11,8 @@ version = "0.66.0"
 
 [dependencies]
 ahash = "0.7.4"
+anyhow = "1"
+browserslist-rs = "=0.2.0"
 dashmap = "4.0.2"
 indexmap = "1.6.2"
 once_cell = "1.2.0"
diff --git a/crates/swc_ecma_preset_env/package.json b/crates/swc_ecma_preset_env/package.json
deleted file mode 100644
index 8b8657eb2b..0000000000
--- a/crates/swc_ecma_preset_env/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-    "devDependencies": {
-        "browserslist": ">=4.16.5"
-    },
-    "browserslist": [
-        "defaults"
-    ]
-}
diff --git a/crates/swc_ecma_preset_env/src/lib.rs b/crates/swc_ecma_preset_env/src/lib.rs
index 5509506abe..35ffec01de 100644
--- a/crates/swc_ecma_preset_env/src/lib.rs
+++ b/crates/swc_ecma_preset_env/src/lib.rs
@@ -2,14 +2,12 @@
 #![recursion_limit = "256"]
 
 pub use self::{transform_data::Feature, version::Version};
+use anyhow::{Context, Error};
 use dashmap::DashMap;
 use once_cell::sync::Lazy;
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
 use st_map::StaticMap;
-use std::{
-    path::{Path, PathBuf},
-    process::Command,
-};
+use std::path::PathBuf;
 use swc_atoms::{js_word, JsWord};
 use swc_common::{
     chain,
@@ -38,8 +36,7 @@ where
     C: Comments,
 {
     let loose = c.loose;
-    let targets: Versions =
-        targets_to_versions(c.targets, &c.path).expect("failed to parse targets");
+    let targets: Versions = targets_to_versions(c.targets).expect("failed to parse targets");
     let is_any_target = targets.is_any_target();
 
     let (include, included_modules) = FeatureOrModule::split(c.include);
@@ -425,12 +422,12 @@ pub enum Mode {
 pub type Versions = BrowserData<Option<Version>>;
 
 impl BrowserData<Option<Version>> {
-    pub fn is_any_target(&self) -> bool {
+    pub(crate) fn is_any_target(&self) -> bool {
         self.iter().all(|(_, v)| v.is_none())
     }
 
-    pub fn parse_versions<'a>(lines: impl Iterator<Item = &'a str>) -> Result<Self, &'a str> {
-        fn remap(key: &str) -> String {
+    pub(crate) fn parse_versions(distribs: Vec<browserslist::Distrib>) -> Result<Self, Error> {
+        fn remap(key: &str) -> &str {
             match key {
                 "and_chr" => "chrome".into(),
                 "and_ff" => "firefox".into(),
@@ -441,13 +438,11 @@ impl BrowserData<Option<Version>> {
             }
         }
 
-        let browsers = lines.map(|v| {
-            let mut v = v.split(' ');
-            (remap(v.next().unwrap()), v.next().unwrap().to_string())
-        });
-
         let mut data: Versions = BrowserData::default();
-        for (browser, version) in browsers {
+        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,
 
@@ -527,7 +522,11 @@ fn default_targets() -> Option<Targets> {
 }
 
 fn default_path() -> PathBuf {
-    std::env::current_dir().unwrap()
+    if cfg!(target_arch = "wasm32") {
+        Default::default()
+    } else {
+        std::env::current_dir().unwrap()
+    }
 }
 
 #[derive(Debug, Clone, Deserialize, FromVariant)]
@@ -583,72 +582,69 @@ pub enum Query {
     Multiple(Vec<String>),
 }
 
-type QueryResult = Result<Versions, ()>;
+type QueryResult = Result<Versions, Error>;
 
 impl Query {
-    fn exec(&self, path: &Path) -> QueryResult {
-        fn query<T>(s: &[T], path: &Path) -> QueryResult
+    fn exec(&self) -> QueryResult {
+        fn query<T>(s: &[T]) -> QueryResult
         where
-            T: AsRef<str> + Serialize,
+            T: AsRef<str>,
         {
-            let output = {
-                let output = Command::new("node")
-                    .current_dir(path)
-                    .arg("-e")
-                    .arg(include_str!("query.js"))
-                    .arg(serde_json::to_string(&s).expect("failed to serialize with serde"))
-                    .output()
-                    .expect("failed to collect output");
-                if !output.status.success() {
-                    println!("query.js: Status {:?}", output.status,);
-                    println!(
-                        "{}\n{}",
-                        String::from_utf8_lossy(&output.stdout),
-                        String::from_utf8_lossy(&output.stderr),
-                    );
-                    return Err(());
-                }
-
-                output.stdout
-            };
-
-            let browsers: Vec<String> =
-                serde_json::from_slice(&output).expect("failed to read browser data output");
-            let versions = BrowserData::parse_versions(browsers.iter().map(|s| &**s))
-                .expect("failed to parse browser version");
+            let distribs = browserslist::resolve(
+                s,
+                &browserslist::Opts {
+                    mobile_to_desktop: false,
+                    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, QueryResult, ahash::RandomState>> =
+        static CACHE: Lazy<DashMap<Query, Versions, ahash::RandomState>> =
             Lazy::new(Default::default);
 
         if let Some(v) = CACHE.get(self) {
-            return match &*v {
-                Ok(v) => Ok(*v),
-                Err(err) => Err(*err),
-            };
+            return Ok(v.clone());
         }
 
         let result = match *self {
-            Query::Single(ref s) => query(&[s], path),
-            Query::Multiple(ref s) => query(&s, path),
-        };
+            Query::Single(ref s) => {
+                if s == "" {
+                    query(&["defaults"])
+                } else {
+                    query(&[s])
+                }
+            }
+            Query::Multiple(ref s) => query(&s),
+        }
+        .context("failed to execute query")?;
 
         CACHE.insert(self.clone(), result);
 
-        result
+        Ok(result)
     }
 }
 
-fn targets_to_versions(v: Option<Targets>, path: &Path) -> Result<Versions, ()> {
+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(path),
+        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(path).expect("failed to run query"),
+                QueryOrVersion::Query(q) => q.exec().expect("failed to run query"),
                 _ => unreachable!(),
             });
 
@@ -676,8 +672,7 @@ mod tests {
 
     #[test]
     fn test_empty() {
-        let path = std::env::current_dir().unwrap();
-        let res = Query::Single("".into()).exec(&path).unwrap();
+        let res = Query::Single("".into()).exec().unwrap();
         assert!(
             !res.is_any_target(),
             "empty query should return non-empty result"
diff --git a/crates/swc_ecma_preset_env/src/query.js b/crates/swc_ecma_preset_env/src/query.js
deleted file mode 100644
index bdb9eb5fff..0000000000
--- a/crates/swc_ecma_preset_env/src/query.js
+++ /dev/null
@@ -1,27 +0,0 @@
-"use strict";
-
-var browserslist;
-try{
-    browserslist = require('browserslist');
-} catch (e){
-    console.error('swc: You have to install `browserslist` to use `env`');
-    process.exit(1);
-}
-
-
-var target = JSON.parse(process.argv[1]);
-target = target.browsers ? target.browsers : target;
-target = Array.isArray(target) ? target : typeof target === 'string' ? [target] : Object.keys(target).map(function (k) {
-    return k + " " + target[k];
-});
-target = target.filter(function (v) {
-    return !v.startsWith('esmodules') && !!v;
-}); // console.log('Target: ', target);
-
-var browsers = browserslist(target && target.length ? target : undefined, {
-    mobileToDesktop: true
-});
-browsers = browsers.filter(function (v) {
-    return !v.includes("TP");
-});
-console.log(JSON.stringify(browsers));
\ No newline at end of file
diff --git a/cspell.json b/cspell.json
index a3e25e5473..1cdfcb18cd 100644
--- a/cspell.json
+++ b/cspell.json
@@ -43,6 +43,7 @@
         "Deser",
         "dont",
         "DWORD",
+        "ecma",
         "ecmascript",
         "eddyb",
         "elems",
@@ -125,5 +126,7 @@
         "unimpl",
         "untrusted"
     ],
-    "flagWords": ["actally"]
-}
+    "flagWords": [
+        "actally"
+    ]
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 1a4d625cb3..45c04f3e49 100644
--- a/package.json
+++ b/package.json
@@ -49,9 +49,7 @@
     "dependencies": {
         "@node-rs/helper": "^1.0.0"
     },
-    "optionalDependencies": {
-        "browserslist": "^4.16.6"
-    },
+    "optionalDependencies": {},
     "types": "./index.d.ts",
     "scripts": {
         "prepare": "husky install && git config feature.manyFiles true",
@@ -78,7 +76,6 @@
         "@types/node": "^14.14.41",
         "axios": "^0.21.1",
         "babel-plugin-transform-node-env-inline": "^0.4.3",
-        "browserslist": "^4.16.6",
         "class-validator": "^0.13.1",
         "core-js": "^2.6.11",
         "cross-env": "^7.0.3",


Test patch:
diff --git a/crates/swc_ecma_preset_env/tests/fixtures/corejs2/entry-chromeandroid/output.mjs b/crates/swc_ecma_preset_env/tests/fixtures/corejs2/entry-chromeandroid/output.mjs
index 1e62a6e45d..1561fb9a48 100644
--- a/crates/swc_ecma_preset_env/tests/fixtures/corejs2/entry-chromeandroid/output.mjs
+++ b/crates/swc_ecma_preset_env/tests/fixtures/corejs2/entry-chromeandroid/output.mjs
@@ -1,13 +1,3 @@
-import "core-js/modules/es6.array.sort";
-import "core-js/modules/es7.array.flat-map";
-import "core-js/modules/es7.object.define-getter";
-import "core-js/modules/es7.object.define-setter";
-import "core-js/modules/es7.object.lookup-getter";
-import "core-js/modules/es7.object.lookup-setter";
-import "core-js/modules/es7.promise.finally";
-import "core-js/modules/es7.string.trim-left";
-import "core-js/modules/es7.string.trim-right";
-import "core-js/modules/es7.symbol.async-iterator";
 import "core-js/modules/web.dom.iterable";
 import "core-js/modules/web.immediate";
 import "core-js/modules/web.timers";
diff --git a/crates/swc_ecma_preset_env/tests/fixtures/corejs3/entry-chromeandroid/output.mjs b/crates/swc_ecma_preset_env/tests/fixtures/corejs3/entry-chromeandroid/output.mjs
index dcd445945e..65a4c905d8 100644
--- a/crates/swc_ecma_preset_env/tests/fixtures/corejs3/entry-chromeandroid/output.mjs
+++ b/crates/swc_ecma_preset_env/tests/fixtures/corejs3/entry-chromeandroid/output.mjs
@@ -1,28 +1,2 @@
-import "core-js/modules/es.array.flat";
-import "core-js/modules/es.array.flat-map";
-import "core-js/modules/es.array.iterator";
-import "core-js/modules/es.array.reduce";
-import "core-js/modules/es.array.reduce-right";
-import "core-js/modules/es.array.sort";
-import "core-js/modules/es.array.unscopables.flat";
-import "core-js/modules/es.array.unscopables.flat-map";
-import "core-js/modules/es.math.hypot";
-import "core-js/modules/es.object.define-getter";
-import "core-js/modules/es.object.define-setter";
-import "core-js/modules/es.object.from-entries";
-import "core-js/modules/es.object.lookup-getter";
-import "core-js/modules/es.object.lookup-setter";
-import "core-js/modules/es.promise";
-import "core-js/modules/es.promise.finally";
-import "core-js/modules/es.string.replace";
-import "core-js/modules/es.string.trim-end";
-import "core-js/modules/es.string.trim-start";
-import "core-js/modules/es.symbol.async-iterator";
-import "core-js/modules/es.symbol.description";
-import "core-js/modules/web.dom-collections.iterator";
 import "core-js/modules/web.immediate";
-import "core-js/modules/web.queue-microtask";
-import "core-js/modules/web.url";
-import "core-js/modules/web.url-search-params";
-import "core-js/modules/web.url.to-json";
 1 ** 2;