diff --git a/crates/oxc_minifier/src/ast_passes/replace_global_defines.rs b/crates/oxc_minifier/src/ast_passes/replace_global_defines.rs index b656969173054..bfe4118e520b2 100644 --- a/crates/oxc_minifier/src/ast_passes/replace_global_defines.rs +++ b/crates/oxc_minifier/src/ast_passes/replace_global_defines.rs @@ -19,7 +19,12 @@ pub struct ReplaceGlobalDefinesConfig(Arc); #[derive(Debug)] struct ReplaceGlobalDefinesConfigImpl { identifier_defines: Vec<(/* key */ String, /* value */ String)>, - // TODO: dot defines + dot_defines: Vec<(/* member expression parts */ Vec, /* value */ String)>, +} + +enum IdentifierType { + Identifier, + DotDefines(Vec), } impl ReplaceGlobalDefinesConfig { @@ -30,21 +35,45 @@ impl ReplaceGlobalDefinesConfig { pub fn new>(defines: &[(S, S)]) -> Result> { let allocator = Allocator::default(); let mut identifier_defines = vec![]; + let mut dot_defines = vec![]; for (key, value) in defines { let key = key.as_ref(); + let value = value.as_ref(); - Self::check_key(key)?; Self::check_value(&allocator, value)?; - identifier_defines.push((key.to_string(), value.to_string())); + + match Self::check_key(key)? { + IdentifierType::Identifier => { + identifier_defines.push((key.to_string(), value.to_string())); + } + IdentifierType::DotDefines(parts) => { + dot_defines.push((parts, value.to_string())); + } + } } - Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl { identifier_defines }))) + + Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl { identifier_defines, dot_defines }))) } - fn check_key(key: &str) -> Result<(), Vec> { - if !is_identifier_name(key) { - return Err(vec![OxcDiagnostic::error(format!("`{key}` is not an identifier."))]); + fn check_key(key: &str) -> Result> { + let parts: Vec<&str> = key.split('.').collect(); + + assert!(!parts.is_empty()); + + if parts.len() == 1 { + if !is_identifier_name(parts[0]) { + return Err(vec![OxcDiagnostic::error(format!("`{key}` is not an identifier."))]); + } + return Ok(IdentifierType::Identifier); } - Ok(()) + + for part in &parts { + if !is_identifier_name(part) { + return Err(vec![OxcDiagnostic::error(format!("`{key}` is not an identifier."))]); + } + } + + Ok(IdentifierType::DotDefines(parts.iter().map(ToString::to_string).collect())) } fn check_value(allocator: &Allocator, source_text: &str) -> Result<(), Vec> { @@ -59,6 +88,7 @@ impl ReplaceGlobalDefinesConfig { /// /// * /// * +/// * pub struct ReplaceGlobalDefines<'a> { ast: AstBuilder<'a>, config: ReplaceGlobalDefinesConfig, @@ -84,8 +114,8 @@ impl<'a> ReplaceGlobalDefines<'a> { } fn replace_identifier_defines(&self, expr: &mut Expression<'a>) { - for (key, value) in &self.config.0.identifier_defines { - if let Expression::Identifier(ident) = expr { + if let Expression::Identifier(ident) = expr { + for (key, value) in &self.config.0.identifier_defines { if ident.name.as_str() == key { let value = self.parse_value(value); *expr = value; @@ -94,11 +124,54 @@ impl<'a> ReplaceGlobalDefines<'a> { } } } + + fn replace_dot_defines(&self, expr: &mut Expression<'a>) { + if let Expression::StaticMemberExpression(member) = expr { + 'outer: for (parts, value) in &self.config.0.dot_defines { + assert!(parts.len() > 1); + + let mut current_part_member_expression = Some(&*member); + let mut cur_part_name = &member.property.name; + + for (i, part) in parts.iter().enumerate().rev() { + if cur_part_name.as_str() != part { + continue 'outer; + } + + if i == 0 { + break; + } + + current_part_member_expression = + if let Some(member) = current_part_member_expression { + match &member.object.without_parenthesized() { + Expression::StaticMemberExpression(member) => { + cur_part_name = &member.property.name; + Some(member) + } + Expression::Identifier(ident) => { + cur_part_name = &ident.name; + None + } + _ => None, + } + } else { + continue 'outer; + }; + } + + let value = self.parse_value(value); + *expr = value; + break; + } + } + } } impl<'a> VisitMut<'a> for ReplaceGlobalDefines<'a> { fn visit_expression(&mut self, expr: &mut Expression<'a>) { self.replace_identifier_defines(expr); + self.replace_dot_defines(expr); walk_mut::walk_expression(self, expr); } } diff --git a/crates/oxc_minifier/tests/mod.rs b/crates/oxc_minifier/tests/mod.rs index 644eae4a0d164..f4fdf273ab130 100644 --- a/crates/oxc_minifier/tests/mod.rs +++ b/crates/oxc_minifier/tests/mod.rs @@ -29,7 +29,11 @@ pub(crate) fn test_with_options(source_text: &str, expected: &str, options: Comp ); } -fn run(source_text: &str, source_type: SourceType, options: Option) -> String { +pub(crate) fn run( + source_text: &str, + source_type: SourceType, + options: Option, +) -> String { let allocator = Allocator::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); let program = allocator.alloc(ret.program); diff --git a/crates/oxc_minifier/tests/oxc/replace_global_defines.rs b/crates/oxc_minifier/tests/oxc/replace_global_defines.rs index 6e431583da375..e0ee3e8b5118f 100644 --- a/crates/oxc_minifier/tests/oxc/replace_global_defines.rs +++ b/crates/oxc_minifier/tests/oxc/replace_global_defines.rs @@ -1,26 +1,44 @@ use oxc_allocator::Allocator; -use oxc_codegen::{CodegenOptions, WhitespaceRemover}; +use oxc_codegen::{CodeGenerator, CodegenOptions}; use oxc_minifier::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig}; use oxc_parser::Parser; use oxc_span::SourceType; +use crate::run; + pub(crate) fn test(source_text: &str, expected: &str, config: ReplaceGlobalDefinesConfig) { - let minified = { - let source_type = SourceType::default(); - let allocator = Allocator::default(); - let ret = Parser::new(&allocator, source_text, source_type).parse(); - let program = allocator.alloc(ret.program); - ReplaceGlobalDefines::new(&allocator, config).build(program); - WhitespaceRemover::new() - .with_options(CodegenOptions { single_quote: true }) - .build(program) - .source_text - }; - assert_eq!(minified, expected, "for source {source_text}"); + let source_type = SourceType::default(); + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); + let program = allocator.alloc(ret.program); + ReplaceGlobalDefines::new(&allocator, config).build(program); + let result = CodeGenerator::new() + .with_options(CodegenOptions { single_quote: true }) + .build(program) + .source_text; + let expected = run(expected, source_type, None); + assert_eq!(result, expected, "for source {source_text}"); } #[test] fn replace_global_definitions() { let config = ReplaceGlobalDefinesConfig::new(&[("id", "text"), ("str", "'text'")]).unwrap(); - test("id, str", "text,'text';", config); + test("id, str", "text, 'text'", config); +} + +#[test] +fn replace_global_definitions_dot() { + { + let config = + ReplaceGlobalDefinesConfig::new(&[("process.env.NODE_ENV", "production")]).unwrap(); + test("process.env.NODE_ENV", "production", config.clone()); + test("process.env", "process.env", config.clone()); + test("process.env.foo.bar", "process.env.foo.bar", config.clone()); + test("process", "process", config); + } + + { + let config = ReplaceGlobalDefinesConfig::new(&[("process", "production")]).unwrap(); + test("foo.process.NODE_ENV", "foo.process.NODE_ENV", config); + } }