Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 83 additions & 10 deletions crates/oxc_minifier/src/ast_passes/replace_global_defines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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>> {
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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);
}
}
6 changes: 5 additions & 1 deletion crates/oxc_minifier/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
46 changes: 32 additions & 14 deletions crates/oxc_minifier/tests/oxc/replace_global_defines.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}