mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(transformer): add esbuild comma separated target API --target=es2020,chrome58 (#7210)
This commit is contained in:
parent
b74686c598
commit
a166a4abd7
8 changed files with 213 additions and 75 deletions
|
|
@ -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()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
101
crates/oxc_transformer/src/options/engine.rs
Normal file
101
crates/oxc_transformer/src/options/engine.rs
Normal 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),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue