mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(oxc_minifier): define plugin with postfix wildcard (#4979)
1. support `import.meta.env.*` in `ReplaceGlobalDefine`
This commit is contained in:
parent
ae94b9a890
commit
2b21be31ff
2 changed files with 188 additions and 17 deletions
|
|
@ -1,3 +1,4 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
|
|
@ -18,8 +19,9 @@ pub struct ReplaceGlobalDefinesConfig(Arc<ReplaceGlobalDefinesConfigImpl>);
|
|||
|
||||
#[derive(Debug)]
|
||||
struct ReplaceGlobalDefinesConfigImpl {
|
||||
identifier_defines: Vec<(/* key */ CompactStr, /* value */ CompactStr)>,
|
||||
dot_defines: Vec<DotDefine>,
|
||||
identifier: Vec<(/* key */ CompactStr, /* value */ CompactStr)>,
|
||||
dot: Vec<DotDefine>,
|
||||
meta_proeperty: Vec<MetaPropertyDefine>,
|
||||
}
|
||||
|
||||
#[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<CompactStr>,
|
||||
pub value: CompactStr,
|
||||
pub postfix_wildcard: bool,
|
||||
}
|
||||
|
||||
impl MetaPropertyDefine {
|
||||
pub fn new(parts: Vec<CompactStr>, value: CompactStr, postfix_wildcard: bool) -> Self {
|
||||
Self { parts, value, postfix_wildcard }
|
||||
}
|
||||
}
|
||||
|
||||
impl DotDefine {
|
||||
fn new(parts: Vec<CompactStr>, value: CompactStr) -> Self {
|
||||
Self { parts, value }
|
||||
|
|
@ -37,7 +53,9 @@ impl DotDefine {
|
|||
|
||||
enum IdentifierType {
|
||||
Identifier,
|
||||
DotDefines(Vec<CompactStr>),
|
||||
DotDefines { parts: Vec<CompactStr> },
|
||||
// import.meta.a
|
||||
ImportMeta { parts: Vec<CompactStr>, 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<IdentifierType, Vec<OxcDiagnostic>> {
|
||||
|
|
@ -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<OxcDiagnostic>> {
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue