feat(transformer): add esbuild comma separated target API --target=es2020,chrome58 (#7210)

This commit is contained in:
Boshen 2024-11-08 14:09:20 +00:00
parent b74686c598
commit a166a4abd7
8 changed files with 213 additions and 75 deletions

View file

@ -1,12 +1,12 @@
#![allow(clippy::print_stdout)]
use std::{path::Path, str::FromStr};
use std::path::Path;
use oxc_allocator::Allocator;
use oxc_codegen::CodeGenerator;
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{ESTarget, EnvOptions, TransformOptions, Transformer};
use oxc_transformer::{EnvOptions, TransformOptions, Transformer};
use pico_args::Arguments;
// Instruction:
@ -61,7 +61,7 @@ fn main() {
..TransformOptions::default()
}
} else if let Some(target) = &target {
TransformOptions::from(ESTarget::from_str(target).unwrap())
TransformOptions::from_target(target).unwrap()
} else {
TransformOptions::enable_all()
};

View file

@ -7,7 +7,7 @@ use oxc_diagnostics::Error;
pub use browserslist::Version;
use crate::options::{engine_targets::Engine, BrowserslistQuery, EngineTargets};
use crate::options::{BrowserslistQuery, Engine, EngineTargets};
/// <https://babel.dev/docs/babel-preset-env#targets>
#[derive(Debug, Deserialize)]

View file

@ -0,0 +1,101 @@
use std::{str::FromStr, sync::OnceLock};
use browserslist::Version;
use cow_utils::CowUtils;
use oxc_diagnostics::Error;
use rustc_hash::FxHashMap;
use serde::Deserialize;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Engine {
Chrome,
Deno,
Edge,
Firefox,
Hermes,
Ie,
Ios,
Node,
Opera,
Rhino,
Safari,
Samsung,
// TODO: electron to chromium
Electron,
// TODO: how to handle? There is a `op_mob` key below.
OperaMobile,
// TODO:
Android,
// Special Value for ESXXXX target.
Es,
}
impl Engine {
/// Parse format `chrome42`.
///
/// # Errors
///
/// * No matching target
/// * Invalid version
pub fn parse_name_and_version(s: &str) -> Result<(Engine, Version), Error> {
let s = s.cow_to_ascii_lowercase();
for (name, engine) in engines() {
if let Some(v) = s.strip_prefix(name) {
return Version::from_str(v).map(|version| (*engine,version))
.map_err(|_| Error::msg(
r#"All version numbers must be in the format "X", "X.Y", or "X.Y.Z" where X, Y, and Z are non-negative integers."#,
));
}
}
Err(Error::msg(format!("Invalid target '{s}'.")))
}
}
impl FromStr for Engine {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"chrome" | "and_chr" => Ok(Self::Chrome),
"deno" => Ok(Self::Deno),
"edge" => Ok(Self::Edge),
"firefox" | "and_ff" => Ok(Self::Firefox),
"hermes" => Ok(Self::Hermes),
"ie" | "ie_mob" => Ok(Self::Ie),
"ios" | "ios_saf" => Ok(Self::Ios),
"node" => Ok(Self::Node),
"opera" | "op_mob" => Ok(Self::Opera),
"rhino" => Ok(Self::Rhino),
"safari" => Ok(Self::Safari),
"samsung" => Ok(Self::Samsung),
"electron" => Ok(Self::Electron),
"opera_mobile" => Ok(Self::OperaMobile),
"android" => Ok(Self::Android),
_ => Err(()),
}
}
}
fn engines() -> &'static FxHashMap<&'static str, Engine> {
static ENGINES: OnceLock<FxHashMap<&'static str, Engine>> = OnceLock::new();
ENGINES.get_or_init(|| {
FxHashMap::from_iter([
("chrome", Engine::Chrome),
("deno", Engine::Deno),
("edge", Engine::Edge),
("firefox", Engine::Firefox),
("hermes", Engine::Hermes),
("ie", Engine::Ie),
("ios", Engine::Ios),
("node", Engine::Node),
("opera", Engine::Opera),
("rhino", Engine::Rhino),
("safari", Engine::Safari),
("samsung", Engine::Samsung),
("electron", Engine::Electron),
("opera_mobile", Engine::OperaMobile),
("android", Engine::Android),
])
})
}

View file

@ -12,60 +12,11 @@ use oxc_diagnostics::Error;
use super::{
babel::BabelTargets,
engine::Engine,
es_features::{features, ESFeature},
BrowserslistQuery,
};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Engine {
Chrome,
Deno,
Edge,
Firefox,
Hermes,
Ie,
Ios,
Node,
Opera,
Rhino,
Safari,
Samsung,
// TODO: electron to chromium
Electron,
// TODO: how to handle? There is a `op_mob` key below.
OperaMobile,
// TODO:
Android,
// Special Value for ESXXXX target.
Es,
}
impl FromStr for Engine {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"chrome" | "and_chr" => Ok(Self::Chrome),
"deno" => Ok(Self::Deno),
"edge" => Ok(Self::Edge),
"firefox" | "and_ff" => Ok(Self::Firefox),
"hermes" => Ok(Self::Hermes),
"ie" | "ie_mob" => Ok(Self::Ie),
"ios" | "ios_saf" => Ok(Self::Ios),
"node" => Ok(Self::Node),
"opera" | "op_mob" => Ok(Self::Opera),
"rhino" => Ok(Self::Rhino),
"safari" => Ok(Self::Safari),
"samsung" => Ok(Self::Samsung),
"electron" => Ok(Self::Electron),
"opera_mobile" => Ok(Self::OperaMobile),
"android" => Ok(Self::Android),
_ => Err(()),
}
}
}
/// A map of engine names to minimum supported versions.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(try_from = "BabelTargets")]
@ -101,16 +52,13 @@ impl EngineTargets {
}
pub fn has_feature(&self, feature: ESFeature) -> bool {
self.should_enable(&features()[&feature])
}
pub fn should_enable(&self, engine_targets: &EngineTargets) -> bool {
for (engine, version) in &engine_targets.0 {
if let Some(v) = self.0.get(engine) {
if *engine == Engine::Es && v <= version {
return true;
let feature_engine_targets = &features()[&feature];
for (engine, feature_version) in feature_engine_targets.iter() {
if let Some(target_version) = self.get(engine) {
if *engine == Engine::Es {
return target_version.0 < feature_version.0;
}
if v < version {
if target_version < feature_version {
return true;
}
}

View file

@ -1,3 +1,5 @@
use std::str::FromStr;
use oxc_diagnostics::Error;
use serde::Deserialize;
@ -14,7 +16,7 @@ use crate::{
EngineTargets,
};
use super::{babel::BabelEnvOptions, ESFeature};
use super::{babel::BabelEnvOptions, ESFeature, ESTarget, Engine};
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(try_from = "BabelEnvOptions")]
@ -102,6 +104,38 @@ impl EnvOptions {
pub fn from_browserslist_query(query: &str) -> Result<Self, Error> {
EngineTargets::try_from_query(query).map(Self::from)
}
pub(crate) fn from_target(s: &str) -> Result<Self, Error> {
if s.contains(',') {
Self::from_target_list(&s.split(',').collect::<Vec<_>>())
} else {
Self::from_target_list(&[s])
}
}
pub(crate) fn from_target_list<S: AsRef<str>>(list: &[S]) -> Result<Self, Error> {
let mut es_target = None;
let mut engine_targets = EngineTargets::default();
for s in list {
let s = s.as_ref();
// Parse `esXXXX`.
if let Ok(target) = ESTarget::from_str(s) {
if let Some(target) = es_target {
return Err(Error::msg(format!("'{target}' is already specified.")));
}
es_target = Some(target);
} else {
// Parse `chromeXX`, `edgeXX` etc.
let (engine, version) = Engine::parse_name_and_version(s)?;
if engine_targets.insert(engine, version).is_some() {
return Err(Error::msg(format!("'{s}' is already specified.")));
}
}
}
engine_targets.insert(Engine::Es, es_target.unwrap_or(ESTarget::default()).version());
Ok(EnvOptions::from(engine_targets))
}
}
impl From<BabelEnvOptions> for EnvOptions {

View file

@ -1,6 +1,7 @@
pub mod babel;
mod browserslist_query;
mod engine;
mod engine_targets;
mod env;
mod es_features;
@ -28,11 +29,8 @@ use crate::{
};
pub use self::{
browserslist_query::BrowserslistQuery,
engine_targets::{Engine, EngineTargets},
env::EnvOptions,
es_features::ESFeature,
es_target::ESTarget,
browserslist_query::BrowserslistQuery, engine::Engine, engine_targets::EngineTargets,
env::EnvOptions, es_features::ESFeature, es_target::ESTarget,
};
use self::babel::BabelOptions;
@ -88,6 +86,37 @@ impl TransformOptions {
},
}
}
/// Initialize from a comma separated list of `target`s and `environmens`s.
///
/// e.g. `es2022,chrome58,edge16`.
///
/// # Errors
///
/// * Same targets specified multiple times.
/// * No matching target.
/// * Invalid version.
pub fn from_target(s: &str) -> Result<Self, Error> {
EnvOptions::from_target(s).map(|env| Self { env, ..Self::default() })
}
/// Initialize from a list of `target`s and `environmens`s.
///
/// e.g. `["es2020", "chrome58", "edge16", "firefox57", "node12", "safari11"]`.
///
/// `target`: `es5`, `es2015` ... `es2024`, `esnext`.
/// `environment`: `chrome`, `deno`, `edge`, `firefox`, `hermes`, `ie`, `ios`, `node`, `opera`, `rhino`, `safari`
///
/// <https://esbuild.github.io/api/#target>
///
/// # Errors
///
/// * Same targets specified multiple times.
/// * No matching target.
/// * Invalid version.
pub fn from_target_list<S: AsRef<str>>(list: &[S]) -> Result<Self, Error> {
EnvOptions::from_target_list(list).map(|env| Self { env, ..Self::default() })
}
}
impl From<ESTarget> for TransformOptions {

View file

@ -13,9 +13,9 @@ fn es_target() {
("es2015", "a ** b"),
("es2016", "async function foo() {}"),
("es2017", "({ ...x })"),
("es2018", "try {} catch {}"),
("es2017", "try {} catch {}"),
("es2019", "a ?? b"),
("es2020", "a ||= b"),
("es2019", "a ||= b"),
("es2019", "1n ** 2n"), // test target error
("es2021", "class foo { static {} }"),
];
@ -23,12 +23,12 @@ fn es_target() {
// Test no transformation for esnext.
for (_, case) in cases {
let options = TransformOptions::from(ESTarget::from_str("esnext").unwrap());
assert_eq!(Ok(codegen(case, SourceType::mjs())), test(case, options));
assert_eq!(test(case, options), Ok(codegen(case, SourceType::mjs())));
}
let snapshot =
cases.into_iter().enumerate().fold(String::new(), |mut w, (i, (target, case))| {
let options = TransformOptions::from(ESTarget::from_str(target).unwrap());
let options = TransformOptions::from_target(target).unwrap();
let result = match test(case, options) {
Ok(code) => code,
Err(errors) => errors
@ -48,3 +48,29 @@ fn es_target() {
});
}
}
#[test]
fn target_list_pass() {
// https://vite.dev/config/build-options.html#build-target
let target = "es2020,edge88,firefox78,chrome87,safari14";
let result = TransformOptions::from_target(target).unwrap();
assert!(!result.env.es2019.optional_catch_binding);
assert!(!result.env.es2020.nullish_coalescing_operator);
assert!(!result.env.es2021.logical_assignment_operators);
assert!(result.env.es2022.class_static_block);
}
#[test]
fn target_list_fail() {
let targets = [
("asdf", "Invalid target 'asdf'."),
("es2020,es2020", "'es2020' is already specified."),
("chrome1,chrome1", "'chrome1' is already specified."),
("chromeXXX", "All version numbers must be in the format \"X\", \"X.Y\", or \"X.Y.Z\" where X, Y, and Z are non-negative integers."),
];
for (target, expected) in targets {
let result = TransformOptions::from_target(target);
assert_eq!(result.unwrap_err().to_string(), expected);
}
}

View file

@ -30,7 +30,7 @@ function _foo() {
import _objectSpread from '@babel/runtime/helpers/objectSpread2';
_objectSpread({}, x);
########## 4 es2018
########## 4 es2017
try {} catch {}
----------
try {} catch (_unused) {}
@ -41,7 +41,7 @@ a ?? b
var _a;
(_a = a) !== null && _a !== void 0 ? _a : b;
########## 6 es2020
########## 6 es2019
a ||= b
----------
a || (a = b);