diff --git a/crates/oxc_transformer/examples/transformer.rs b/crates/oxc_transformer/examples/transformer.rs index f2ad010b92b76..27c9880b109d0 100644 --- a/crates/oxc_transformer/examples/transformer.rs +++ b/crates/oxc_transformer/examples/transformer.rs @@ -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() }; diff --git a/crates/oxc_transformer/src/options/babel/env/targets.rs b/crates/oxc_transformer/src/options/babel/env/targets.rs index fc51b4a7b928c..554074dbee389 100644 --- a/crates/oxc_transformer/src/options/babel/env/targets.rs +++ b/crates/oxc_transformer/src/options/babel/env/targets.rs @@ -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}; /// #[derive(Debug, Deserialize)] diff --git a/crates/oxc_transformer/src/options/engine.rs b/crates/oxc_transformer/src/options/engine.rs new file mode 100644 index 0000000000000..d3217970faa57 --- /dev/null +++ b/crates/oxc_transformer/src/options/engine.rs @@ -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 { + 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> = 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), + ]) + }) +} diff --git a/crates/oxc_transformer/src/options/engine_targets.rs b/crates/oxc_transformer/src/options/engine_targets.rs index f5a29cc826b75..ddbc9eaf0e431 100644 --- a/crates/oxc_transformer/src/options/engine_targets.rs +++ b/crates/oxc_transformer/src/options/engine_targets.rs @@ -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 { - 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; } } diff --git a/crates/oxc_transformer/src/options/env.rs b/crates/oxc_transformer/src/options/env.rs index 77ca0843c1fc5..814038c5f2ab3 100644 --- a/crates/oxc_transformer/src/options/env.rs +++ b/crates/oxc_transformer/src/options/env.rs @@ -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 { EngineTargets::try_from_query(query).map(Self::from) } + + pub(crate) fn from_target(s: &str) -> Result { + if s.contains(',') { + Self::from_target_list(&s.split(',').collect::>()) + } else { + Self::from_target_list(&[s]) + } + } + + pub(crate) fn from_target_list>(list: &[S]) -> Result { + 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 for EnvOptions { diff --git a/crates/oxc_transformer/src/options/mod.rs b/crates/oxc_transformer/src/options/mod.rs index 7937c8f099f87..cdbfc38320671 100644 --- a/crates/oxc_transformer/src/options/mod.rs +++ b/crates/oxc_transformer/src/options/mod.rs @@ -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 { + 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` + /// + /// + /// + /// # Errors + /// + /// * Same targets specified multiple times. + /// * No matching target. + /// * Invalid version. + pub fn from_target_list>(list: &[S]) -> Result { + EnvOptions::from_target_list(list).map(|env| Self { env, ..Self::default() }) + } } impl From for TransformOptions { diff --git a/crates/oxc_transformer/tests/integrations/es_target.rs b/crates/oxc_transformer/tests/integrations/es_target.rs index 2c014a6b18688..ad61efef67456 100644 --- a/crates/oxc_transformer/tests/integrations/es_target.rs +++ b/crates/oxc_transformer/tests/integrations/es_target.rs @@ -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); + } +} diff --git a/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap b/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap index 5d8328015dd29..6bfe409aec951 100644 --- a/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap +++ b/crates/oxc_transformer/tests/integrations/snapshots/es_target.snap @@ -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);