Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/oxc_transformer/examples/transformer.rs
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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()
};
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_transformer/src/options/babel/env/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
101 changes: 101 additions & 0 deletions crates/oxc_transformer/src/options/engine.rs
Original file line number Diff line number Diff line change
@@ -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),
])
})
}
66 changes: 7 additions & 59 deletions crates/oxc_transformer/src/options/engine_targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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;
}
}
Expand Down
36 changes: 35 additions & 1 deletion crates/oxc_transformer/src/options/env.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::str::FromStr;

use oxc_diagnostics::Error;
use serde::Deserialize;

Expand All @@ -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")]
Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 34 additions & 5 deletions crates/oxc_transformer/src/options/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod babel;

mod browserslist_query;
mod engine;
mod engine_targets;
mod env;
mod es_features;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 30 additions & 4 deletions crates/oxc_transformer/tests/integrations/es_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,22 @@ 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 {} }"),
];

// 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
Expand All @@ -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);
}
}
Loading