diff --git a/Cargo.lock b/Cargo.lock index ed43c998f8239..a9109c13e0dc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2022,6 +2022,7 @@ dependencies = [ "cow-utils", "dashmap 6.1.0", "indexmap", + "insta", "itoa", "oxc-browserslist", "oxc_allocator", diff --git a/crates/oxc_transformer/Cargo.toml b/crates/oxc_transformer/Cargo.toml index 6c87144acfb3f..6adc1ffe7bb1a 100644 --- a/crates/oxc_transformer/Cargo.toml +++ b/crates/oxc_transformer/Cargo.toml @@ -46,6 +46,7 @@ serde_json = { workspace = true } sha1 = { workspace = true } [dev-dependencies] +insta = { workspace = true } oxc_codegen = { workspace = true } oxc_parser = { workspace = true } pico-args = { workspace = true } diff --git a/crates/oxc_transformer/examples/transformer.rs b/crates/oxc_transformer/examples/transformer.rs index 5e8f491f62219..9495a428adca6 100644 --- a/crates/oxc_transformer/examples/transformer.rs +++ b/crates/oxc_transformer/examples/transformer.rs @@ -1,26 +1,27 @@ #![allow(clippy::print_stdout)] -use std::{env, path::Path}; +use std::{path::Path, str::FromStr}; use oxc_allocator::Allocator; use oxc_codegen::CodeGenerator; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; -use oxc_transformer::{TransformOptions, Transformer}; +use oxc_transformer::{ESTarget, TransformOptions, Transformer}; use pico_args::Arguments; // Instruction: -// create a `test.tsx`, -// run `cargo run -p oxc_transformer --example transformer` -// or `just watch "run -p oxc_transformer --example transformer"` +// create a `test.js`, +// run `just example transformer` or `just watch-example transformer` fn main() { let mut args = Arguments::from_env(); - let name = env::args().nth(1).unwrap_or_else(|| "test.js".to_string()); let targets: Option = args.opt_value_from_str("--targets").unwrap_or(None); + let target: Option = args.opt_value_from_str("--target").unwrap_or(None); + let name = args.free_from_str().unwrap_or_else(|_| "test.js".to_string()); let path = Path::new(&name); - let source_text = std::fs::read_to_string(path).expect("{name} not found"); + let source_text = + std::fs::read_to_string(path).unwrap_or_else(|err| panic!("{name} not found.\n{err}")); let allocator = Allocator::default(); let source_type = SourceType::from_path(path).unwrap(); @@ -62,6 +63,8 @@ fn main() { // ..BabelEnvOptions::default() // }) // .unwrap() + } else if let Some(target) = &target { + TransformOptions::from(ESTarget::from_str(target).unwrap()) } else { TransformOptions::enable_all() }; diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index b30ffa2edb68f..f1fd94df72c7c 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -56,7 +56,7 @@ pub use crate::{ jsx::{JsxOptions, JsxRuntime, ReactRefreshOptions}, options::{ babel::{BabelEnvOptions, BabelOptions, Targets}, - EnvOptions, TransformOptions, + ESTarget, EnvOptions, TransformOptions, }, plugins::*, typescript::{RewriteExtensionsMode, TypeScriptOptions}, diff --git a/crates/oxc_transformer/src/options/env.rs b/crates/oxc_transformer/src/options/env.rs index 423f788983822..c8d3aa5c905c8 100644 --- a/crates/oxc_transformer/src/options/env.rs +++ b/crates/oxc_transformer/src/options/env.rs @@ -1,3 +1,6 @@ +use std::str::FromStr; + +use cow_utils::CowUtils; use serde::Deserialize; use crate::{ @@ -14,6 +17,45 @@ use crate::{ use super::babel::BabelEnvOptions; +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +pub enum ESTarget { + ES5, + ES2015, + ES2016, + ES2017, + ES2018, + ES2019, + ES2020, + ES2021, + ES2022, + ES2023, + ES2024, + #[default] + ESNext, +} + +impl FromStr for ESTarget { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.cow_to_lowercase().as_ref() { + "es5" => Ok(Self::ES5), + "es2015" => Ok(Self::ES2015), + "es2016" => Ok(Self::ES2016), + "es2017" => Ok(Self::ES2017), + "es2018" => Ok(Self::ES2018), + "es2019" => Ok(Self::ES2019), + "es2020" => Ok(Self::ES2020), + "es2021" => Ok(Self::ES2021), + "es2022" => Ok(Self::ES2022), + "es2023" => Ok(Self::ES2023), + "es2024" => Ok(Self::ES2024), + "esnext" => Ok(Self::ESNext), + _ => Err(format!("Invalid target \"{s}\".")), + } + } +} + #[derive(Debug, Default, Clone, Deserialize)] #[serde(try_from = "BabelEnvOptions")] pub struct EnvOptions { @@ -46,10 +88,10 @@ impl EnvOptions { regexp: RegExpOptions { sticky_flag: true, unicode_flag: true, + unicode_property_escapes: true, dot_all_flag: true, - look_behind_assertions: true, named_capture_groups: true, - unicode_property_escapes: true, + look_behind_assertions: true, match_indices: true, set_notation: true, }, @@ -91,6 +133,40 @@ impl EnvOptions { } } +impl From for EnvOptions { + fn from(target: ESTarget) -> Self { + Self { + regexp: RegExpOptions { + sticky_flag: target < ESTarget::ES2015, + unicode_flag: target < ESTarget::ES2015, + unicode_property_escapes: target < ESTarget::ES2018, + dot_all_flag: target < ESTarget::ES2015, + named_capture_groups: target < ESTarget::ES2018, + look_behind_assertions: target < ESTarget::ES2018, + match_indices: target < ESTarget::ES2022, + set_notation: target < ESTarget::ES2024, + }, + es2015: ES2015Options { + arrow_function: (target < ESTarget::ES2015).then(ArrowFunctionsOptions::default), + }, + es2016: ES2016Options { exponentiation_operator: target < ESTarget::ES2016 }, + es2017: ES2017Options { async_to_generator: target < ESTarget::ES2017 }, + es2018: ES2018Options { + object_rest_spread: (target < ESTarget::ES2018) + .then(ObjectRestSpreadOptions::default), + async_generator_functions: target < ESTarget::ES2018, + }, + es2019: ES2019Options { optional_catch_binding: target < ESTarget::ES2019 }, + es2020: ES2020Options { nullish_coalescing_operator: target < ESTarget::ES2020 }, + es2021: ES2021Options { logical_assignment_operators: target < ESTarget::ES2021 }, + es2022: ES2022Options { + class_static_block: target < ESTarget::ES2022, + class_properties: (target < ESTarget::ES2022).then(ClassPropertiesOptions::default), + }, + } + } +} + impl TryFrom for EnvOptions { type Error = String; @@ -100,10 +176,10 @@ impl TryFrom for EnvOptions { regexp: RegExpOptions { sticky_flag: o.can_enable_plugin("transform-sticky-regex"), unicode_flag: o.can_enable_plugin("transform-unicode-regex"), + unicode_property_escapes: o.can_enable_plugin("transform-unicode-property-regex"), dot_all_flag: o.can_enable_plugin("transform-dotall-regex"), - look_behind_assertions: o.can_enable_plugin("esbuild-regexp-lookbehind-assertions"), named_capture_groups: o.can_enable_plugin("transform-named-capturing-groups-regex"), - unicode_property_escapes: o.can_enable_plugin("transform-unicode-property-regex"), + look_behind_assertions: o.can_enable_plugin("esbuild-regexp-lookbehind-assertions"), match_indices: o.can_enable_plugin("esbuild-regexp-match-indices"), set_notation: o.can_enable_plugin("transform-unicode-sets-regex"), }, diff --git a/crates/oxc_transformer/src/options/mod.rs b/crates/oxc_transformer/src/options/mod.rs index 1c56a464c92ad..9b21e04274fc7 100644 --- a/crates/oxc_transformer/src/options/mod.rs +++ b/crates/oxc_transformer/src/options/mod.rs @@ -5,8 +5,6 @@ use std::path::PathBuf; use oxc_diagnostics::Error; -pub use env::EnvOptions; - use crate::{ common::helper_loader::{HelperLoaderMode, HelperLoaderOptions}, compiler_assumptions::CompilerAssumptions, @@ -24,6 +22,8 @@ use crate::{ ReactRefreshOptions, }; +pub use env::{ESTarget, EnvOptions}; + use babel::BabelOptions; /// @@ -79,6 +79,12 @@ impl TransformOptions { } } +impl From for TransformOptions { + fn from(target: ESTarget) -> Self { + Self { env: EnvOptions::from(target), ..Self::default() } + } +} + impl TryFrom<&BabelOptions> for TransformOptions { type Error = Vec; diff --git a/crates/oxc_transformer/src/regexp/options.rs b/crates/oxc_transformer/src/regexp/options.rs index 70f8c2826afee..987e1fb97936e 100644 --- a/crates/oxc_transformer/src/regexp/options.rs +++ b/crates/oxc_transformer/src/regexp/options.rs @@ -1,26 +1,34 @@ #[derive(Default, Debug, Clone, Copy)] pub struct RegExpOptions { - /// Enables plugin to transform the RegExp literal has `y` flag + /// Enables plugin to transform the RegExp literal with a `y` flag + /// ES2015 pub sticky_flag: bool, - /// Enables plugin to transform the RegExp literal has `u` flag + /// Enables plugin to transform the RegExp literal with a `u` flag + /// ES2015 pub unicode_flag: bool, - /// Enables plugin to transform the RegExp literal has `s` flag - pub dot_all_flag: bool, + /// Enables plugin to transform the RegExp literal that has `\p{}` and `\P{}` unicode property escapes + /// ES2018 + pub unicode_property_escapes: bool, - /// Enables plugin to transform the RegExp literal has `(?<=)` or `(? + pub dot_all_flag: bool, - /// Enables plugin to transform the RegExp literal has `(?x)` named capture groups + /// Enables plugin to transform the RegExp literal that has `(?x)` named capture groups + /// ES2018 pub named_capture_groups: bool, - /// Enables plugin to transform the RegExp literal has `\p{}` and `\P{}` unicode property escapes - pub unicode_property_escapes: bool, + /// Enables plugin to transform the RegExp literal that has `(?<=)` or `(? + pub look_behind_assertions: bool, - /// Enables plugin to transform `d` flag + /// Enables plugin to transform the `d` flag + /// ES2022 pub match_indices: bool, - /// Enables plugin to transform the RegExp literal has `v` flag + /// Enables plugin to transform the RegExp literal that has `v` flag + /// ES2024 pub set_notation: bool, } diff --git a/crates/oxc_transformer/tests/es_target/mod.rs b/crates/oxc_transformer/tests/es_target/mod.rs new file mode 100644 index 0000000000000..87859972b658e --- /dev/null +++ b/crates/oxc_transformer/tests/es_target/mod.rs @@ -0,0 +1,60 @@ +use std::{path::Path, str::FromStr}; + +use oxc_allocator::Allocator; +use oxc_codegen::{CodeGenerator, CodegenOptions}; +use oxc_parser::Parser; +use oxc_semantic::SemanticBuilder; +use oxc_span::SourceType; +use oxc_transformer::{ESTarget, TransformOptions, Transformer}; + +use crate::run; + +pub(crate) fn test(source_text: &str, target: &str) -> String { + let source_type = SourceType::default(); + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); + let mut program = ret.program; + let (symbols, scopes) = + SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree(); + let options = TransformOptions::from(ESTarget::from_str(target).unwrap()); + Transformer::new(&allocator, Path::new(""), options).build_with_symbols_and_scopes( + symbols, + scopes, + &mut program, + ); + CodeGenerator::new() + .with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() }) + .build(&program) + .code +} + +#[test] +fn es2015() { + use std::fmt::Write; + + let cases = [ + ("es5", "() => {}"), + ("es2015", "a ** b"), + ("es2016", "async function foo() {}"), + ("es2017", "({ ...x })"), + ("es2018", "try {} catch {}"), + ("es2019", "a ?? b"), + ("es2020", "a ||= b"), + ("es2021", "class foo { static {} }"), + ]; + + // Test no transformation for esnext. + for (_, case) in cases { + assert_eq!(run(case, SourceType::mjs()), test(case, "esnext")); + } + + let snapshot = cases.iter().enumerate().fold(String::new(), |mut w, (i, (target, case))| { + let result = test(case, target); + write!(w, "########## {i} {target}\n{case}\n----------\n{result}\n").unwrap(); + w + }); + + insta::with_settings!({ prepend_module_to_snapshot => false, snapshot_suffix => "", omit_expression => true }, { + insta::assert_snapshot!("es_target", snapshot); + }); +} diff --git a/crates/oxc_transformer/tests/es_target/snapshots/es_target.snap b/crates/oxc_transformer/tests/es_target/snapshots/es_target.snap new file mode 100644 index 0000000000000..081b40f46b4fb --- /dev/null +++ b/crates/oxc_transformer/tests/es_target/snapshots/es_target.snap @@ -0,0 +1,54 @@ +--- +source: crates/oxc_transformer/tests/es_target/mod.rs +snapshot_kind: text +--- +########## 0 es5 +() => {} +---------- +(function() {}); + +########## 1 es2015 +a ** b +---------- +Math.pow(a, b); + +########## 2 es2016 +async function foo() {} +---------- +import _asyncToGenerator from '@babel/runtime/helpers/asyncToGenerator'; +function foo() { + return _foo.apply(this, arguments); +} +function _foo() { + _foo = _asyncToGenerator(function* () {}); + return _foo.apply(this, arguments); +} + +########## 3 es2017 +({ ...x }) +---------- +import _objectSpread from '@babel/runtime/helpers/objectSpread2'; +_objectSpread({}, x); + +########## 4 es2018 +try {} catch {} +---------- +try {} catch (_unused) {} + +########## 5 es2019 +a ?? b +---------- +var _a; +(_a = a) !== null && _a !== void 0 ? _a : b; + +########## 6 es2020 +a ||= b +---------- +a || (a = b); + +########## 7 es2021 +class foo { static {} } +---------- +class foo { + static #_ = (() => {})(); +} diff --git a/crates/oxc_transformer/tests/mod.rs b/crates/oxc_transformer/tests/mod.rs index 85cf1a563a20d..1c1df7721adfb 100644 --- a/crates/oxc_transformer/tests/mod.rs +++ b/crates/oxc_transformer/tests/mod.rs @@ -1 +1,16 @@ +mod es_target; mod plugins; + +use oxc_allocator::Allocator; +use oxc_codegen::{CodeGenerator, CodegenOptions}; +use oxc_parser::Parser; +use oxc_span::SourceType; + +pub fn run(source_text: &str, source_type: SourceType) -> String { + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); + CodeGenerator::new() + .with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() }) + .build(&ret.program) + .code +} diff --git a/crates/oxc_transformer/tests/plugins/inject_global_variables.rs b/crates/oxc_transformer/tests/plugins/inject_global_variables.rs index 0e33319849c8f..daa310abf6fcd 100644 --- a/crates/oxc_transformer/tests/plugins/inject_global_variables.rs +++ b/crates/oxc_transformer/tests/plugins/inject_global_variables.rs @@ -9,7 +9,7 @@ use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; use oxc_transformer::{InjectGlobalVariables, InjectGlobalVariablesConfig, InjectImport}; -use super::run; +use crate::run; pub(crate) fn test(source_text: &str, expected: &str, config: InjectGlobalVariablesConfig) { let source_type = SourceType::default(); diff --git a/crates/oxc_transformer/tests/plugins/mod.rs b/crates/oxc_transformer/tests/plugins/mod.rs index 83b1e17463969..f43e90253926e 100644 --- a/crates/oxc_transformer/tests/plugins/mod.rs +++ b/crates/oxc_transformer/tests/plugins/mod.rs @@ -1,16 +1,2 @@ mod inject_global_variables; mod replace_global_defines; - -use oxc_allocator::Allocator; -use oxc_codegen::{CodeGenerator, CodegenOptions}; -use oxc_parser::Parser; -use oxc_span::SourceType; - -fn run(source_text: &str, source_type: SourceType) -> String { - let allocator = Allocator::default(); - let ret = Parser::new(&allocator, source_text, source_type).parse(); - CodeGenerator::new() - .with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() }) - .build(&ret.program) - .code -} diff --git a/crates/oxc_transformer/tests/plugins/replace_global_defines.rs b/crates/oxc_transformer/tests/plugins/replace_global_defines.rs index b2d8d07e7cde1..783a453e8cb1c 100644 --- a/crates/oxc_transformer/tests/plugins/replace_global_defines.rs +++ b/crates/oxc_transformer/tests/plugins/replace_global_defines.rs @@ -5,7 +5,7 @@ use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; use oxc_transformer::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig}; -use super::run; +use crate::run; pub(crate) fn test(source_text: &str, expected: &str, config: ReplaceGlobalDefinesConfig) { let source_type = SourceType::default();