diff --git a/crates/oxc_minifier/src/plugins/replace_global_defines.rs b/crates/oxc_minifier/src/plugins/replace_global_defines.rs index ecdf31acd464f..2c17f55630abc 100644 --- a/crates/oxc_minifier/src/plugins/replace_global_defines.rs +++ b/crates/oxc_minifier/src/plugins/replace_global_defines.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::sync::Arc; use oxc_allocator::Allocator; @@ -18,8 +19,9 @@ pub struct ReplaceGlobalDefinesConfig(Arc); #[derive(Debug)] struct ReplaceGlobalDefinesConfigImpl { - identifier_defines: Vec<(/* key */ CompactStr, /* value */ CompactStr)>, - dot_defines: Vec, + identifier: Vec<(/* key */ CompactStr, /* value */ CompactStr)>, + dot: Vec, + meta_proeperty: Vec, } #[derive(Debug)] @@ -29,6 +31,20 @@ pub struct DotDefine { pub value: CompactStr, } +#[derive(Debug)] +pub struct MetaPropertyDefine { + /// only store parts after `import.meta` + pub parts: Vec, + pub value: CompactStr, + pub postfix_wildcard: bool, +} + +impl MetaPropertyDefine { + pub fn new(parts: Vec, value: CompactStr, postfix_wildcard: bool) -> Self { + Self { parts, value, postfix_wildcard } + } +} + impl DotDefine { fn new(parts: Vec, value: CompactStr) -> Self { Self { parts, value } @@ -37,7 +53,9 @@ impl DotDefine { enum IdentifierType { Identifier, - DotDefines(Vec), + DotDefines { parts: Vec }, + // import.meta.a + ImportMeta { parts: Vec, postfix_wildcard: bool }, } impl ReplaceGlobalDefinesConfig { @@ -49,6 +67,7 @@ impl ReplaceGlobalDefinesConfig { let allocator = Allocator::default(); let mut identifier_defines = vec![]; let mut dot_defines = vec![]; + let mut meta_proeperty_defines = vec![]; for (key, value) in defines { let key = key.as_ref(); @@ -59,13 +78,35 @@ impl ReplaceGlobalDefinesConfig { IdentifierType::Identifier => { identifier_defines.push((CompactStr::new(key), CompactStr::new(value))); } - IdentifierType::DotDefines(parts) => { + IdentifierType::DotDefines { parts } => { dot_defines.push(DotDefine::new(parts, CompactStr::new(value))); } + IdentifierType::ImportMeta { parts, postfix_wildcard } => { + meta_proeperty_defines.push(MetaPropertyDefine::new( + parts, + CompactStr::new(value), + postfix_wildcard, + )); + } } } - - Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl { identifier_defines, dot_defines }))) + // Always move specific meta define before wildcard dot define + // Keep other order unchanged + // see test case replace_global_definitions_dot_with_postfix_mixed as an example + meta_proeperty_defines.sort_by(|a, b| { + if !a.postfix_wildcard && b.postfix_wildcard { + Ordering::Less + } else if a.postfix_wildcard && b.postfix_wildcard { + Ordering::Greater + } else { + Ordering::Equal + } + }); + Ok(Self(Arc::new(ReplaceGlobalDefinesConfigImpl { + identifier: identifier_defines, + dot: dot_defines, + meta_proeperty: meta_proeperty_defines, + }))) } fn check_key(key: &str) -> Result> { @@ -79,14 +120,40 @@ impl ReplaceGlobalDefinesConfig { } return Ok(IdentifierType::Identifier); } + let normalized_parts_len = + if parts[parts.len() - 1] == "*" { parts.len() - 1 } else { parts.len() }; + // We can ensure now the parts.len() >= 2 + let is_import_meta = parts[0] == "import" && parts[1] == "meta"; - for part in &parts { + for part in &parts[0..normalized_parts_len] { if !is_identifier_name(part) { return Err(vec![OxcDiagnostic::error(format!("`{key}` is not an identifier."))]); } } - - Ok(IdentifierType::DotDefines(parts.iter().map(|s| CompactStr::new(s)).collect())) + if is_import_meta { + Ok(IdentifierType::ImportMeta { + parts: parts + .iter() + .skip(2) + .take(normalized_parts_len - 2) + .map(|s| CompactStr::new(s)) + .collect(), + postfix_wildcard: normalized_parts_len != parts.len(), + }) + // StaticMemberExpression with postfix wildcard + } else if normalized_parts_len != parts.len() { + Err(vec![OxcDiagnostic::error( + "postfix wildcard is only allowed for `import.meta`.".to_string(), + )]) + } else { + Ok(IdentifierType::DotDefines { + parts: parts + .iter() + .take(normalized_parts_len) + .map(|s| CompactStr::new(s)) + .collect(), + }) + } } fn check_value(allocator: &Allocator, source_text: &str) -> Result<(), Vec> { @@ -136,7 +203,7 @@ impl<'a> ReplaceGlobalDefines<'a> { fn replace_identifier_defines(&self, expr: &mut Expression<'a>) { if let Expression::Identifier(ident) = expr { - for (key, value) in &self.config.0.identifier_defines { + for (key, value) in &self.config.0.identifier { if ident.name.as_str() == key { let value = self.parse_value(value); *expr = value; @@ -147,15 +214,93 @@ impl<'a> ReplaceGlobalDefines<'a> { } fn replace_dot_defines(&mut self, expr: &mut Expression<'a>) { - if let Expression::StaticMemberExpression(member) = expr { - for dot_define in &self.config.0.dot_defines { - if Self::is_dot_define(dot_define, member) { - let value = self.parse_value(&dot_define.value); - *expr = value; - break; + let Expression::StaticMemberExpression(member) = expr else { + return; + }; + for dot_define in &self.config.0.dot { + if Self::is_dot_define(dot_define, member) { + let value = self.parse_value(&dot_define.value); + *expr = value; + return; + } + } + for meta_proeperty_define in &self.config.0.meta_proeperty { + let ret = Self::is_meta_property_define(meta_proeperty_define, member); + if ret { + let value = self.parse_value(&meta_proeperty_define.value); + *expr = value; + break; + } + } + } + + pub fn is_meta_property_define( + meta_define: &MetaPropertyDefine, + member: &StaticMemberExpression<'a>, + ) -> bool { + debug_assert!(!meta_define.parts.is_empty()); + + let mut current_part_member_expression = Some(member); + let mut cur_part_name = &member.property.name; + let mut is_full_match = true; + let mut i = meta_define.parts.len() - 1; + let mut has_matched_part = false; + loop { + let part = &meta_define.parts[i]; + let matched = cur_part_name.as_str() == part; + if matched { + has_matched_part = true; + } else { + is_full_match = false; + // Considering import.meta.env.* + // ```js + // import.meta.env.test // should matched + // import.res.meta.env // should not matched + // ``` + // So we use has_matched_part to track if any part has matched. + + if !meta_define.postfix_wildcard || has_matched_part { + return false; + } + } + + current_part_member_expression = if let Some(member) = current_part_member_expression { + match &member.object { + Expression::StaticMemberExpression(member) => { + cur_part_name = &member.property.name; + Some(member) + } + Expression::MetaProperty(_) => { + if meta_define.postfix_wildcard { + // `import.meta.env` should not match `import.meta.env.*` + return has_matched_part && !is_full_match; + } + return true; + } + Expression::Identifier(_) => { + return false; + } + _ => None, } + } else { + return false; + }; + + // Config `import.meta.env.* -> 'undefined'` + // Considering try replace `import.meta.env` to `undefined`, for the first loop the i is already + // 0, if it did not match part name and still reach here, that means + // current_part_member_expression is still something, and possible to match in the + // further loop + if i == 0 && matched { + break; + } + + if matched { + i -= 1; } } + + false } pub fn is_dot_define(dot_define: &DotDefine, member: &StaticMemberExpression<'a>) -> bool { diff --git a/crates/oxc_minifier/tests/plugins/replace_global_defines.rs b/crates/oxc_minifier/tests/plugins/replace_global_defines.rs index 51b4f47970563..e41509872987c 100644 --- a/crates/oxc_minifier/tests/plugins/replace_global_defines.rs +++ b/crates/oxc_minifier/tests/plugins/replace_global_defines.rs @@ -42,3 +42,29 @@ fn replace_global_definitions_dot() { test("foo.process.NODE_ENV", "foo.process.NODE_ENV", config); } } + +#[test] +fn replace_global_definitions_dot_with_postfix_wildcard() { + { + let config = + ReplaceGlobalDefinesConfig::new(&[("import.meta.env.*", "undefined")]).unwrap(); + test("import.meta.env.result", "undefined", config.clone()); + test("import.meta.env", "import.meta.env", config); + } +} + +#[test] +fn replace_global_definitions_dot_with_postfix_mixed() { + { + let config = ReplaceGlobalDefinesConfig::new(&[ + ("import.meta.env.*", "undefined"), + ("import.meta.env", "env"), + ]) + .unwrap(); + test("import.meta.env.result", "undefined", config.clone()); + test("import.meta.env.result.many.nested", "undefined", config.clone()); + test("import.meta.env", "env", config.clone()); + test("import.meta.somethingelse", "import.meta.somethingelse", config.clone()); + test("import.meta", "import.meta", config); + } +}