feat(minifier): implement dot define for member expressions (#3959)

adds the ability to transform

`process.env.NODE_ENV`

into

`"development"`

when that config is passed in
This commit is contained in:
camc314 2024-08-05 12:04:13 +00:00
parent 0236e947bd
commit 229a0e9c45
3 changed files with 120 additions and 25 deletions

View file

@ -19,7 +19,12 @@ pub struct ReplaceGlobalDefinesConfig(Arc<ReplaceGlobalDefinesConfigImpl>);
#[derive(Debug)]
struct ReplaceGlobalDefinesConfigImpl {
identifier_defines: Vec<(/* key */ String, /* value */ String)>,
// TODO: dot defines
dot_defines: Vec<(/* member expression parts */ Vec<String>, /* value */ String)>,
}
enum IdentifierType {
Identifier,
DotDefines(Vec<String>),
}
impl ReplaceGlobalDefinesConfig {
@ -30,21 +35,45 @@ impl ReplaceGlobalDefinesConfig {
pub fn new<S: AsRef<str>>(defines: &[(S, S)]) -> Result<Self, Vec<OxcDiagnostic>> {
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<OxcDiagnostic>> {
if !is_identifier_name(key) {
return Err(vec![OxcDiagnostic::error(format!("`{key}` is not an identifier."))]);
fn check_key(key: &str) -> Result<IdentifierType, Vec<OxcDiagnostic>> {
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<OxcDiagnostic>> {
@ -59,6 +88,7 @@ impl ReplaceGlobalDefinesConfig {
///
/// * <https://esbuild.github.io/api/#define>
/// * <https://github.com/terser/terser?tab=readme-ov-file#conditional-compilation>
/// * <https://github.com/evanw/esbuild/blob/9c13ae1f06dfa909eb4a53882e3b7e4216a503fe/internal/config/globals.go#L852-L1014>
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);
}
}

View file

@ -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<CompressOptions>) -> String {
pub(crate) fn run(
source_text: &str,
source_type: SourceType,
options: Option<CompressOptions>,
) -> String {
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, source_type).parse();
let program = allocator.alloc(ret.program);

View file

@ -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);
}
}