From 6fcb0d05c6c78043f57b9f5b3183519cd195832d Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sat, 20 Sep 2025 14:41:56 +0000 Subject: [PATCH] fix(minifier)!: receive supported engines instead of ecmascript versions (#13933) fixes https://github.com/oxc-project/oxc/issues/8905 --- Cargo.lock | 3 +- crates/oxc_minifier/Cargo.toml | 1 + crates/oxc_minifier/src/ctx.rs | 8 + crates/oxc_minifier/src/options.rs | 20 ++- .../minimize_conditional_expression.rs | 152 +++++++++--------- .../src/peephole/minimize_conditions.rs | 23 +-- .../peephole/minimize_logical_expression.rs | 4 +- .../src/peephole/remove_unused_expression.rs | 83 +++++----- .../src/peephole/replace_known_methods.rs | 41 ++--- .../peephole/substitute_alternate_syntax.rs | 11 +- crates/oxc_minifier/src/tester.rs | 18 +++ napi/minify/Cargo.toml | 2 +- napi/minify/index.d.ts | 10 +- napi/minify/src/options.rs | 28 ++-- 14 files changed, 202 insertions(+), 202 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f19468aea1c28..1062763c105d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2115,6 +2115,7 @@ dependencies = [ "oxc_ast", "oxc_ast_visit", "oxc_codegen", + "oxc_compat", "oxc_ecmascript", "oxc_mangler", "oxc_parser", @@ -2138,13 +2139,13 @@ dependencies = [ "napi-derive", "oxc_allocator", "oxc_codegen", + "oxc_compat", "oxc_diagnostics", "oxc_minifier", "oxc_napi", "oxc_parser", "oxc_sourcemap", "oxc_span", - "oxc_syntax", ] [[package]] diff --git a/crates/oxc_minifier/Cargo.toml b/crates/oxc_minifier/Cargo.toml index f009fdc079175..d86a55b74d5bb 100644 --- a/crates/oxc_minifier/Cargo.toml +++ b/crates/oxc_minifier/Cargo.toml @@ -25,6 +25,7 @@ oxc_allocator = { workspace = true } oxc_ast = { workspace = true } oxc_ast_visit = { workspace = true } oxc_codegen = { workspace = true } +oxc_compat = { workspace = true } oxc_ecmascript = { workspace = true } oxc_mangler = { workspace = true } oxc_parser = { workspace = true } diff --git a/crates/oxc_minifier/src/ctx.rs b/crates/oxc_minifier/src/ctx.rs index ee4040e1ee0a6..c31f9b2521530 100644 --- a/crates/oxc_minifier/src/ctx.rs +++ b/crates/oxc_minifier/src/ctx.rs @@ -16,6 +16,7 @@ use oxc_syntax::{ use oxc_traverse::Ancestor; use crate::{options::CompressOptions, state::MinifierState, symbol_value::SymbolValue}; +use oxc_compat::ESFeature; pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>; @@ -106,6 +107,13 @@ impl<'a> Ctx<'a, '_> { &self.0.state.options } + /// Check if the target engines supports a feature. + /// + /// Returns `true` if the feature is supported. + pub fn supports_feature(&self, feature: ESFeature) -> bool { + !self.options().target.has_feature(feature) + } + pub fn source_type(&self) -> SourceType { self.0.state.source_type } diff --git a/crates/oxc_minifier/src/options.rs b/crates/oxc_minifier/src/options.rs index fe8405e6a1c61..d5a3a47d78bfc 100644 --- a/crates/oxc_minifier/src/options.rs +++ b/crates/oxc_minifier/src/options.rs @@ -1,18 +1,16 @@ -use oxc_syntax::es_target::ESTarget; +use oxc_compat::EngineTargets; pub use oxc_ecmascript::side_effects::PropertyReadSideEffects; #[derive(Debug, Clone)] pub struct CompressOptions { - /// Set desired EcmaScript standard version for output. + /// Engine targets for feature detection. /// - /// e.g. + /// Used to determine which ES features are supported by the target engines + /// and whether transformations can be applied. /// - /// * catch optional binding when >= es2019 - /// * `??` operator >= es2020 - /// - /// Default `ESTarget::ESNext` - pub target: ESTarget, + /// Default: empty (supports all features) + pub target: EngineTargets, /// Remove `debugger;` statements. /// @@ -59,7 +57,7 @@ impl Default for CompressOptions { impl CompressOptions { pub fn smallest() -> Self { Self { - target: ESTarget::ESNext, + target: EngineTargets::default(), keep_names: CompressOptionsKeepNames::all_false(), drop_debugger: true, drop_console: false, @@ -73,7 +71,7 @@ impl CompressOptions { pub fn safest() -> Self { Self { - target: ESTarget::ESNext, + target: EngineTargets::default(), keep_names: CompressOptionsKeepNames::all_true(), drop_debugger: false, drop_console: false, @@ -87,7 +85,7 @@ impl CompressOptions { pub fn dce() -> Self { Self { - target: ESTarget::ESNext, + target: EngineTargets::default(), keep_names: CompressOptionsKeepNames::all_true(), drop_debugger: false, drop_console: false, diff --git a/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs index 812d5da42557b..ec990820fd734 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs @@ -1,13 +1,12 @@ +use crate::ctx::Ctx; use oxc_allocator::TakeIn; use oxc_ast::{NONE, ast::*}; +use oxc_compat::ESFeature; use oxc_ecmascript::{ constant_evaluation::{ConstantEvaluation, ConstantValue}, side_effects::MayHaveSideEffects, }; use oxc_span::{ContentEq, GetSpan}; -use oxc_syntax::es_target::ESTarget; - -use crate::ctx::Ctx; use super::PeepholeOptimizations; @@ -299,70 +298,73 @@ impl<'a> PeepholeOptimizations { } // Try using the "??" or "?." operators - if ctx.options().target >= ESTarget::ES2020 { - if let Expression::BinaryExpression(test_binary) = &mut expr.test { - if let Some(is_negate) = match test_binary.operator { - BinaryOperator::Inequality => Some(true), - BinaryOperator::Equality => Some(false), - _ => None, - } { - // a == null / a != null / (a = foo) == null / (a = foo) != null - let value_expr_with_id_name = if test_binary.left.is_null() { - if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.right) - .filter(|id| !ctx.is_global_reference(id)) - { - Some((id.name, &mut test_binary.right)) - } else { - None - } - } else if test_binary.right.is_null() { - if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.left) - .filter(|id| !ctx.is_global_reference(id)) - { - Some((id.name, &mut test_binary.left)) - } else { - None - } - } else { - None - }; - if let Some((target_id_name, value_expr)) = value_expr_with_id_name { - // `a == null ? b : a` -> `a ?? b` - // `a != null ? a : b` -> `a ?? b` - // `(a = foo) == null ? b : a` -> `(a = foo) ?? b` - // `(a = foo) != null ? a : b` -> `(a = foo) ?? b` - let maybe_same_id_expr = + if (ctx.supports_feature(ESFeature::ES2020NullishCoalescingOperator) + || ctx.supports_feature(ESFeature::ES2020OptionalChaining)) + && let Expression::BinaryExpression(test_binary) = &mut expr.test + && let Some(is_negate) = match test_binary.operator { + BinaryOperator::Inequality => Some(true), + BinaryOperator::Equality => Some(false), + _ => None, + } + { + // a == null / a != null / (a = foo) == null / (a = foo) != null + let value_expr_with_id_name = if test_binary.left.is_null() { + if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.right) + .filter(|id| !ctx.is_global_reference(id)) + { + Some((id.name, &mut test_binary.right)) + } else { + None + } + } else if test_binary.right.is_null() { + if let Some(id) = Self::extract_id_or_assign_to_id(&test_binary.left) + .filter(|id| !ctx.is_global_reference(id)) + { + Some((id.name, &mut test_binary.left)) + } else { + None + } + } else { + None + }; + if let Some((target_id_name, value_expr)) = value_expr_with_id_name { + if ctx.supports_feature(ESFeature::ES2020NullishCoalescingOperator) { + // `a == null ? b : a` -> `a ?? b` + // `a != null ? a : b` -> `a ?? b` + // `(a = foo) == null ? b : a` -> `(a = foo) ?? b` + // `(a = foo) != null ? a : b` -> `(a = foo) ?? b` + let maybe_same_id_expr = + if is_negate { &mut expr.consequent } else { &mut expr.alternate }; + if maybe_same_id_expr.is_specific_id(&target_id_name) { + return Some(ctx.ast.expression_logical( + expr.span, + value_expr.take_in(ctx.ast), + LogicalOperator::Coalesce, + if is_negate { + expr.alternate.take_in(ctx.ast) + } else { + expr.consequent.take_in(ctx.ast) + }, + )); + } + } + if ctx.supports_feature(ESFeature::ES2020OptionalChaining) { + // "a == null ? undefined : a.b.c[d](e)" => "a?.b.c[d](e)" + // "a != null ? a.b.c[d](e) : undefined" => "a?.b.c[d](e)" + // "(a = foo) == null ? undefined : a.b.c[d](e)" => "(a = foo)?.b.c[d](e)" + // "(a = foo) != null ? a.b.c[d](e) : undefined" => "(a = foo)?.b.c[d](e)" + let maybe_undefined_expr = + if is_negate { &expr.alternate } else { &expr.consequent }; + if ctx.is_expression_undefined(maybe_undefined_expr) { + let expr_to_inject_optional_chaining = if is_negate { &mut expr.consequent } else { &mut expr.alternate }; - if maybe_same_id_expr.is_specific_id(&target_id_name) { - return Some(ctx.ast.expression_logical( - expr.span, - value_expr.take_in(ctx.ast), - LogicalOperator::Coalesce, - if is_negate { - expr.alternate.take_in(ctx.ast) - } else { - expr.consequent.take_in(ctx.ast) - }, - )); - } - - // "a == null ? undefined : a.b.c[d](e)" => "a?.b.c[d](e)" - // "a != null ? a.b.c[d](e) : undefined" => "a?.b.c[d](e)" - // "(a = foo) == null ? undefined : a.b.c[d](e)" => "(a = foo)?.b.c[d](e)" - // "(a = foo) != null ? a.b.c[d](e) : undefined" => "(a = foo)?.b.c[d](e)" - let maybe_undefined_expr = - if is_negate { &expr.alternate } else { &expr.consequent }; - if ctx.is_expression_undefined(maybe_undefined_expr) { - let expr_to_inject_optional_chaining = - if is_negate { &mut expr.consequent } else { &mut expr.alternate }; - if Self::inject_optional_chaining_if_matched( - &target_id_name, - value_expr, - expr_to_inject_optional_chaining, - ctx, - ) { - return Some(expr_to_inject_optional_chaining.take_in(ctx.ast)); - } + if Self::inject_optional_chaining_if_matched( + &target_id_name, + value_expr, + expr_to_inject_optional_chaining, + ctx, + ) { + return Some(expr_to_inject_optional_chaining.take_in(ctx.ast)); } } } @@ -590,18 +592,7 @@ impl<'a> PeepholeOptimizations { #[cfg(test)] mod test { - use oxc_syntax::es_target::ESTarget; - - use crate::{ - CompressOptions, - tester::{test, test_options, test_same}, - }; - - fn test_es2019(source_text: &str, expected: &str) { - let target = ESTarget::ES2019; - let options = CompressOptions { target, ..CompressOptions::default() }; - test_options(source_text, expected, &options); - } + use crate::tester::{test, test_same, test_target}; #[test] fn test_minimize_expr_condition() { @@ -663,7 +654,7 @@ mod test { test("var a; a != null ? a : b", "var a; a ?? b"); test("var a; (a = _a) != null ? a : b", "var a; (a = _a) ?? b"); test("v = a != null ? a : b", "v = a == null ? b : a"); // accessing global `a` may have a getter with side effects - test_es2019("var a; v = a != null ? a : b", "var a; v = a == null ? b : a"); + test_target("var a; v = a != null ? a : b", "var a; v = a == null ? b : a", "chrome79"); test("var a; v = a != null ? a.b.c[d](e) : undefined", "var a; v = a?.b.c[d](e)"); test( "var a; v = (a = _a) != null ? a.b.c[d](e) : undefined", @@ -674,9 +665,10 @@ mod test { "var a, undefined = 1; v = a != null ? a.b.c[d](e) : undefined", "var a; v = a == null ? 1 : a.b.c[d](e)", ); - test_es2019( + test_target( "var a; v = a != null ? a.b.c[d](e) : undefined", "var a; v = a == null ? void 0 : a.b.c[d](e)", + "chrome79", ); test("v = cmp !== 0 ? cmp : (bar, cmp);", "v = (cmp === 0 && bar, cmp);"); test("v = cmp === 0 ? cmp : (bar, cmp);", "v = (cmp === 0 || bar, cmp);"); diff --git a/crates/oxc_minifier/src/peephole/minimize_conditions.rs b/crates/oxc_minifier/src/peephole/minimize_conditions.rs index a0e3f32db50f3..c239aee696ce0 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditions.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditions.rs @@ -1,12 +1,12 @@ use oxc_allocator::TakeIn; use oxc_ast::ast::*; +use oxc_compat::ESFeature; use oxc_ecmascript::{ constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType}, side_effects::MayHaveSideEffects, }; use oxc_semantic::ReferenceFlags; use oxc_span::GetSpan; -use oxc_syntax::es_target::ESTarget; use crate::ctx::Ctx; @@ -175,10 +175,9 @@ impl<'a> PeepholeOptimizations { expr: &mut AssignmentExpression<'a>, ctx: &mut Ctx<'a, '_>, ) { - if ctx.options().target < ESTarget::ES2020 { - return; - } - if !matches!(expr.operator, AssignmentOperator::Assign) { + if !ctx.supports_feature(ESFeature::ES2021LogicalAssignmentOperators) + || !matches!(expr.operator, AssignmentOperator::Assign) + { return; } @@ -263,11 +262,7 @@ impl<'a> PeepholeOptimizations { /// #[cfg(test)] mod test { - use crate::{ - CompressOptions, - tester::{test, test_options, test_same, test_same_options}, - }; - use oxc_syntax::es_target::ESTarget; + use crate::tester::{test, test_same, test_target, test_target_same}; /** Check that removing blocks with 1 child works */ #[test] @@ -1406,9 +1401,7 @@ mod test { // foo() might have a side effect test_same("foo().a || (foo().a = 3)"); - let target = ESTarget::ES2019; - let options = CompressOptions { target, ..CompressOptions::default() }; - test_same_options("x || (x = 3)", &options); + test_target_same("x || (x = 3)", "chrome84"); test("x || (a, x = 3)", "x ||= (a, 3)"); test("x && (a, x = 3)", "x &&= (a, 3)"); @@ -1445,9 +1438,7 @@ mod test { // Example case: `let f = false; f = f || (() => {}); console.log(f.name)` test("var x; x = x || (() => 'a')", "var x; x ||= (() => 'a')"); - let target = ESTarget::ES2019; - let options = CompressOptions { target, ..CompressOptions::default() }; - test_options("var x; x = x || 1", "var x = x || 1", &options); + test_target("var x; x = x || 1", "var x = x || 1", "chrome84"); } #[test] diff --git a/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs b/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs index 26483b0e6f017..4fae5decf6288 100644 --- a/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs +++ b/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs @@ -1,8 +1,8 @@ use oxc_allocator::TakeIn; use oxc_ast::ast::*; +use oxc_compat::ESFeature; use oxc_semantic::ReferenceFlags; use oxc_span::{ContentEq, GetSpan}; -use oxc_syntax::es_target::ESTarget; use crate::ctx::Ctx; @@ -209,7 +209,7 @@ impl<'a> PeepholeOptimizations { expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>, ) { - if ctx.options().target < ESTarget::ES2020 { + if !ctx.supports_feature(ESFeature::ES2021LogicalAssignmentOperators) { return; } let Expression::LogicalExpression(e) = expr else { return }; diff --git a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs index 773edfc01bde7..dec11ba914a4f 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs @@ -2,12 +2,12 @@ use std::iter; use oxc_allocator::{TakeIn, Vec}; use oxc_ast::ast::*; +use oxc_compat::ESFeature; use oxc_ecmascript::{ ToPrimitive, side_effects::{MayHaveSideEffects, MayHaveSideEffectsContext}, }; use oxc_span::GetSpan; -use oxc_syntax::es_target::ESTarget; use crate::{CompressOptionsUnused, ctx::Ctx}; @@ -80,7 +80,9 @@ impl<'a> PeepholeOptimizations { } // try optional chaining and nullish coalescing - if ctx.options().target >= ESTarget::ES2020 { + if ctx.supports_feature(ESFeature::ES2020OptionalChaining) + || ctx.supports_feature(ESFeature::ES2020NullishCoalescingOperator) + { let LogicalExpression { span: logical_span, left: logical_left, @@ -93,22 +95,25 @@ impl<'a> PeepholeOptimizations { // "a == null || a.b()" => "a?.b()" (LogicalOperator::And, BinaryOperator::Inequality) | (LogicalOperator::Or, BinaryOperator::Equality) => { - let name_and_id = if let Expression::Identifier(id) = &binary_expr.left { - (!ctx.is_global_reference(id) && binary_expr.right.is_null()) - .then_some((id.name, &mut binary_expr.left)) - } else if let Expression::Identifier(id) = &binary_expr.right { - (!ctx.is_global_reference(id) && binary_expr.left.is_null()) - .then_some((id.name, &mut binary_expr.right)) - } else { - None - }; - if let Some((name, id)) = name_and_id { - if Self::inject_optional_chaining_if_matched( - &name, - id, - logical_right, - ctx, - ) { + if ctx.supports_feature(ESFeature::ES2020OptionalChaining) { + let name_and_id = if let Expression::Identifier(id) = &binary_expr.left + { + (!ctx.is_global_reference(id) && binary_expr.right.is_null()) + .then_some((id.name, &mut binary_expr.left)) + } else if let Expression::Identifier(id) = &binary_expr.right { + (!ctx.is_global_reference(id) && binary_expr.left.is_null()) + .then_some((id.name, &mut binary_expr.right)) + } else { + None + }; + if let Some((name, id)) = name_and_id + && Self::inject_optional_chaining_if_matched( + &name, + id, + logical_right, + ctx, + ) + { *e = logical_right.take_in(ctx.ast); ctx.state.changed = true; return false; @@ -121,17 +126,19 @@ impl<'a> PeepholeOptimizations { // "a != null || (a = b)" => "a ??= b" (LogicalOperator::And, BinaryOperator::Equality) | (LogicalOperator::Or, BinaryOperator::Inequality) => { - let new_left_hand_expr = if binary_expr.right.is_null() { - Some(&mut binary_expr.left) - } else if binary_expr.left.is_null() { - Some(&mut binary_expr.right) - } else { - None - }; - if let Some(new_left_hand_expr) = new_left_hand_expr { - if let Expression::AssignmentExpression(assignment_expr) = logical_right - { - if assignment_expr.operator == AssignmentOperator::Assign + if ctx.supports_feature(ESFeature::ES2020NullishCoalescingOperator) { + let new_left_hand_expr = if binary_expr.right.is_null() { + Some(&mut binary_expr.left) + } else if binary_expr.left.is_null() { + Some(&mut binary_expr.right) + } else { + None + }; + if let Some(new_left_hand_expr) = new_left_hand_expr { + if ctx.supports_feature(ESFeature::ES2021LogicalAssignmentOperators) + && let Expression::AssignmentExpression(assignment_expr) = + logical_right + && assignment_expr.operator == AssignmentOperator::Assign && Self::has_no_side_effect_for_evaluation_same_target( &assignment_expr.left, new_left_hand_expr, @@ -144,16 +151,16 @@ impl<'a> PeepholeOptimizations { ctx.state.changed = true; return false; } - } - *e = ctx.ast.expression_logical( - *logical_span, - new_left_hand_expr.take_in(ctx.ast), - LogicalOperator::Coalesce, - logical_right.take_in(ctx.ast), - ); - ctx.state.changed = true; - return false; + *e = ctx.ast.expression_logical( + *logical_span, + new_left_hand_expr.take_in(ctx.ast), + LogicalOperator::Coalesce, + logical_right.take_in(ctx.ast), + ); + ctx.state.changed = true; + return false; + } } } _ => {} diff --git a/crates/oxc_minifier/src/peephole/replace_known_methods.rs b/crates/oxc_minifier/src/peephole/replace_known_methods.rs index dae2fd26acbc6..830a25fe9e1d4 100644 --- a/crates/oxc_minifier/src/peephole/replace_known_methods.rs +++ b/crates/oxc_minifier/src/peephole/replace_known_methods.rs @@ -4,6 +4,7 @@ use cow_utils::CowUtils; use oxc_allocator::{Box, TakeIn}; use oxc_ast::ast::*; +use oxc_compat::ESFeature; use oxc_ecmascript::{ StringCharAt, StringCharAtResult, ToBigInt, ToIntegerIndex, constant_evaluation::{ConstantEvaluation, DetermineValueType}, @@ -13,7 +14,6 @@ use oxc_regular_expression::{ RegexUnsupportedPatterns, has_unsupported_regular_expression_pattern, }; use oxc_span::SPAN; -use oxc_syntax::es_target::ESTarget; use oxc_traverse::Ancestor; use crate::ctx::Ctx; @@ -68,7 +68,7 @@ impl<'a> PeepholeOptimizations { object: &Expression<'a>, ctx: &Ctx<'a, '_>, ) -> Option> { - if ctx.options().target < ESTarget::ES2016 { + if !ctx.supports_feature(ESFeature::ES2016ExponentiationOperator) { return None; } if !Self::validate_global_reference(object, "Math", ctx) @@ -267,7 +267,7 @@ impl<'a> PeepholeOptimizations { } } Expression::StringLiteral(base_str) => { - if ctx.state.options.target < ESTarget::ES2015 + if !ctx.supports_feature(ESFeature::ES2015TemplateLiterals) || args.is_empty() || !args.iter().all(Argument::is_expression) { @@ -509,27 +509,27 @@ impl<'a> PeepholeOptimizations { "NEGATIVE_INFINITY" => num(span, f64::NEG_INFINITY), "NaN" => num(span, f64::NAN), "MAX_SAFE_INTEGER" => { - if ctx.options().target < ESTarget::ES2016 { - num(span, 2.0f64.powi(53) - 1.0) - } else { + if ctx.supports_feature(ESFeature::ES2016ExponentiationOperator) { // 2**53 - 1 pow_with_expr(span, 2.0, 53.0, BinaryOperator::Subtraction, 1.0) + } else { + num(span, 2.0f64.powi(53) - 1.0) } } "MIN_SAFE_INTEGER" => { - if ctx.options().target < ESTarget::ES2016 { - num(span, -(2.0f64.powi(53) - 1.0)) - } else { + if ctx.supports_feature(ESFeature::ES2016ExponentiationOperator) { // -(2**53 - 1) ctx.ast.expression_unary( span, UnaryOperator::UnaryNegation, pow_with_expr(SPAN, 2.0, 53.0, BinaryOperator::Subtraction, 1.0), ) + } else { + num(span, -(2.0f64.powi(53) - 1.0)) } } "EPSILON" => { - if ctx.options().target < ESTarget::ES2016 { + if !ctx.supports_feature(ESFeature::ES2016ExponentiationOperator) { return None; } // 2**-52 @@ -601,18 +601,7 @@ impl<'a> PeepholeOptimizations { /// Port from: #[cfg(test)] mod test { - use oxc_syntax::es_target::ESTarget; - - use crate::{ - CompressOptions, - tester::{test, test_options, test_same}, - }; - - #[track_caller] - fn test_es2015(code: &str, expected: &str) { - let options = CompressOptions { target: ESTarget::ES2015, ..CompressOptions::default() }; - test_options(code, expected, &options); - } + use crate::tester::{test, test_same, test_target}; #[track_caller] fn test_value(code: &str, expected: &str) { @@ -1600,7 +1589,7 @@ mod test { test_same("v = Math.pow(...a, 1)"); test_same("v = Math.pow(1, ...a)"); test_same("v = Math.pow(1, 2, 3)"); - test_es2015("v = Math.pow(2, 3)", "v = Math.pow(2, 3)"); + test_target("v = Math.pow(2, 3)", "v = Math.pow(2, 3)", "chrome51"); test_same("v = Unknown.pow(1, 2)"); } @@ -1643,9 +1632,9 @@ mod test { test_same("Number.MIN_SAFE_INTEGER = 1"); test_same("Number.EPSILON = 1"); - test_es2015("v = Number.MAX_SAFE_INTEGER", "v = 9007199254740991"); - test_es2015("v = Number.MIN_SAFE_INTEGER", "v = -9007199254740991"); - test_es2015("v = Number.EPSILON", "v = Number.EPSILON"); + test_target("v = Number.MAX_SAFE_INTEGER", "v = 9007199254740991", "chrome51"); + test_target("v = Number.MIN_SAFE_INTEGER", "v = -9007199254740991", "chrome51"); + test_target("v = Number.EPSILON", "v = Number.EPSILON", "chrome51"); } #[test] diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 8776d005d3eec..e0d53e49096e9 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -2,13 +2,13 @@ use std::iter::repeat_with; use oxc_allocator::{CloneIn, TakeIn, Vec}; use oxc_ast::{NONE, ast::*}; +use oxc_compat::ESFeature; use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType}; use oxc_ecmascript::{ToJsString, ToNumber, side_effects::MayHaveSideEffects}; use oxc_semantic::ReferenceFlags; use oxc_span::GetSpan; use oxc_span::SPAN; use oxc_syntax::{ - es_target::ESTarget, number::NumberBase, operator::{BinaryOperator, UnaryOperator}, }; @@ -1426,7 +1426,7 @@ impl<'a> PeepholeOptimizations { } pub fn substitute_catch_clause(catch: &mut CatchClause<'a>, ctx: &Ctx<'a, '_>) { - if ctx.options().target >= ESTarget::ES2019 { + if ctx.supports_feature(ESFeature::ES2019OptionalCatchBinding) { if let Some(param) = &catch.param { if let BindingPatternKind::BindingIdentifier(ident) = ¶m.pattern.kind { if catch.body.body.is_empty() @@ -1526,14 +1526,13 @@ where #[cfg(test)] mod test { use oxc_span::SourceType; - use oxc_syntax::es_target::ESTarget; use crate::{ CompressOptions, CompressOptionsUnused, options::CompressOptionsKeepNames, tester::{ default_options, test, test_options, test_same, test_same_options, - test_same_options_source_type, + test_same_options_source_type, test_target_same, }, }; @@ -2225,9 +2224,7 @@ mod test { // console.log(a);"#, // ); - let target = ESTarget::ES2018; - let options = CompressOptions { target, ..CompressOptions::default() }; - test_same_options("try { foo } catch(e) {}", &options); + test_target_same("try { foo } catch(e) {}", "chrome65"); } #[test] diff --git a/crates/oxc_minifier/src/tester.rs b/crates/oxc_minifier/src/tester.rs index 414190c10d1e5..9f76e1b62e8a6 100644 --- a/crates/oxc_minifier/src/tester.rs +++ b/crates/oxc_minifier/src/tester.rs @@ -1,6 +1,7 @@ #![expect(clippy::allow_attributes)] use oxc_allocator::Allocator; use oxc_codegen::{Codegen, CodegenOptions}; +use oxc_compat::EngineTargets; use oxc_parser::{ParseOptions, Parser}; use oxc_span::SourceType; @@ -14,12 +15,22 @@ pub fn default_options() -> CompressOptions { } } +pub fn get_targets(target_list: &str) -> EngineTargets { + EngineTargets::from_target(target_list).unwrap() +} + #[allow(dead_code)] #[track_caller] pub fn test_same(source_text: &str) { test(source_text, source_text); } +#[allow(dead_code)] +#[track_caller] +pub fn test_target_same(source_text: &str, target: &str) { + test_target(source_text, source_text, target); +} + #[allow(dead_code)] #[track_caller] pub fn test_same_options(source_text: &str, options: &CompressOptions) { @@ -41,6 +52,13 @@ pub fn test(source_text: &str, expected: &str) { test_options(source_text, expected, &default_options()); } +#[allow(dead_code)] +#[track_caller] +pub fn test_target(source_text: &str, expected: &str, target: &str) { + let options = CompressOptions { target: get_targets(target), ..default_options() }; + test_options(source_text, expected, &options); +} + #[track_caller] pub fn test_options(source_text: &str, expected: &str, options: &CompressOptions) { let source_type = SourceType::mjs(); diff --git a/napi/minify/Cargo.toml b/napi/minify/Cargo.toml index b0a1f9f42f2df..1fe9ba0f4420d 100644 --- a/napi/minify/Cargo.toml +++ b/napi/minify/Cargo.toml @@ -24,13 +24,13 @@ doctest = false [dependencies] oxc_allocator = { workspace = true } oxc_codegen = { workspace = true } +oxc_compat = { workspace = true } oxc_diagnostics = { workspace = true } oxc_minifier = { workspace = true } oxc_napi = { workspace = true } oxc_parser = { workspace = true } oxc_sourcemap = { workspace = true, features = ["napi"] } oxc_span = { workspace = true } -oxc_syntax = { workspace = true } napi = { workspace = true } napi-derive = { workspace = true } diff --git a/napi/minify/index.d.ts b/napi/minify/index.d.ts index a812655712c2e..80b0809a8ed3b 100644 --- a/napi/minify/index.d.ts +++ b/napi/minify/index.d.ts @@ -15,14 +15,16 @@ export interface CompressOptions { * * Set `esnext` to enable all target highering. * - * e.g. + * Example: * - * * catch optional binding when >= es2019 - * * `??` operator >= es2020 + * * `'es2015'` + * * `['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11']` * * @default 'esnext' + * + * @see [esbuild#target](https://esbuild.github.io/api/#target) */ - target?: 'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'es2023' | 'es2024' + target?: string | Array /** * Pass true to discard calls to `console.*`. * diff --git a/napi/minify/src/options.rs b/napi/minify/src/options.rs index af8a91672c87b..1f86ebb3d1694 100644 --- a/napi/minify/src/options.rs +++ b/napi/minify/src/options.rs @@ -1,10 +1,8 @@ -use std::str::FromStr; - use napi::Either; use napi_derive::napi; +use oxc_compat::EngineTargets; use oxc_minifier::TreeShakeOptions; -use oxc_syntax::es_target::ESTarget; #[napi(object)] pub struct CompressOptions { @@ -12,16 +10,15 @@ pub struct CompressOptions { /// /// Set `esnext` to enable all target highering. /// - /// e.g. + /// Example: /// - /// * catch optional binding when >= es2019 - /// * `??` operator >= es2020 + /// * `'es2015'` + /// * `['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11']` /// /// @default 'esnext' - #[napi( - ts_type = "'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'es2023' | 'es2024'" - )] - pub target: Option, + /// + /// @see [esbuild#target](https://esbuild.github.io/api/#target) + pub target: Option>>, /// Pass true to discard calls to `console.*`. /// @@ -48,12 +45,11 @@ impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions { fn try_from(o: &CompressOptions) -> Result { let default = oxc_minifier::CompressOptions::default(); Ok(oxc_minifier::CompressOptions { - target: o - .target - .as_ref() - .map(|s| ESTarget::from_str(s)) - .transpose()? - .unwrap_or(default.target), + target: match &o.target { + Some(Either::A(s)) => EngineTargets::from_target(s)?, + Some(Either::B(list)) => EngineTargets::from_target_list(list)?, + _ => default.target, + }, drop_console: o.drop_console.unwrap_or(default.drop_console), drop_debugger: o.drop_debugger.unwrap_or(default.drop_debugger), // TODO