Skip to content

Core Concepts ​

This guide covers the fundamental concepts you need to understand to write effective ReluxScript plugins.

The Visitor Pattern ​

ReluxScript uses the visitor pattern to traverse and transform Abstract Syntax Trees (ASTs).

How It Works ​

  1. The compiler parses JavaScript/TypeScript into an AST
  2. Your visitor methods are called for each matching node type
  3. You can inspect, modify, or replace nodes
  4. The modified AST is converted back to code
reluxscript
plugin MyPlugin {
    // This method is called for EVERY function call in the code
    fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
        // Inspect the node
        if matches!(node.callee, "console.log") {
            // Transform it
            *node = Statement::empty();
        }
    }
}

Visitor Methods ​

Each visitor method corresponds to an AST node type:

MethodNode TypeExample
visit_identifierVariable namesfoo, bar
visit_call_expressionFunction callsfn(), obj.method()
visit_binary_expressionOperationsa + b, x * y
visit_function_declarationFunctionsfunction foo() {}
visit_jsx_elementJSX<Component />

See Visitor Methods API for the complete list.

Mutations Must Be Explicit ​

One of ReluxScript's core principles: all changes must be explicit.

Node Replacement ​

reluxscript
// ✅ Explicit: replacing the entire node
*node = Statement::empty();

// ✅ Explicit: creating a new node
*node = CallExpression {
    callee: Identifier::new("newFn"),
    arguments: vec![]
};

Value Extraction ​

reluxscript
// ✅ Explicit: cloning the value
let name = node.name.clone();

// ❌ Error: implicit borrow not allowed
let name = node.name;

Why? This ensures the generated code works correctly in both JavaScript (garbage collected) and Rust (borrow checked).

The matches! Macro ​

Pattern matching on AST nodes:

reluxscript
// Simple pattern
if matches!(node.callee, "console.log") {
    // This is console.log()
}

// Complex pattern
if matches!(node.callee, MemberExpression {
    object: Identifier { name: "console" },
    property: Identifier { name: "log" }
}) {
    // Also matches console.log()
}

Type System ​

Primitive Types ​

reluxscript
let name: Str = "hello";      // String
let count: i32 = 42;           // Integer
let ratio: f64 = 3.14;         // Float
let flag: bool = true;         // Boolean

Container Types ​

reluxscript
let items: Vec<Str> = vec!["a", "b", "c"];     // Array/Vector
let maybe: Option<Str> = Some("value");         // Optional value
let result: Result<Str, Str> = Ok("success");  // Result type

References ​

reluxscript
fn process(node: &CallExpression) {     // Immutable reference
    // Can read, cannot modify
}

fn transform(node: &mut CallExpression) {  // Mutable reference
    // Can read and modify
}

Pattern Matching ​

Match Expression ​

reluxscript
match node.operator {
    "+" => handle_addition(),
    "-" => handle_subtraction(),
    "*" | "/" => handle_multiplication_division(),
    _ => handle_other(),
}

If-Let Pattern ​

reluxscript
if let Some(name) = get_identifier_name(node) {
    // name is available here
    println!("Found identifier: {}", name);
}

Destructuring ​

reluxscript
match &node.callee {
    Expression::MemberExpression(member) => {
        // Access member.object and member.property
        let obj = &member.object;
        let prop = &member.property;
    }
    _ => {}
}

Context Object ​

The Context provides information about the current traversal:

reluxscript
fn visit_identifier(node: &mut Identifier, ctx: &Context) {
    // Check if this identifier is bound in the current scope
    if ctx.scope.has_binding(&node.name) {
        // It's a local variable
    }

    // Generate a unique identifier
    let unique_name = ctx.generate_uid("temp");

    // Get the filename
    println!("Processing: {}", ctx.filename);
}

Warning: ctx.scope operations are cheap in Babel but expensive in SWC (requires pre-pass analysis).

Error Handling ​

ReluxScript uses Result<T, E> for error handling:

reluxscript
fn parse_config(path: &Str) -> Result<Config, Str> {
    // The ? operator propagates errors
    let content = fs::read_file(path)?;
    let config = json::parse(&content)?;

    Ok(config)
}

Compiles to:

  • Babel: { ok: boolean, value?: T, error?: E }
  • SWC: Native Result<T, E> enum

Collections ​

Vec (Array/Vector) ​

reluxscript
let mut items = vec!["a", "b"];
items.push("c");

for item in &items {
    println!("{}", item);
}

let doubled: Vec<Str> = items.iter()
    .map(|s| format!("{}_{}", s, s))
    .collect();

HashMap (Map/Dictionary) ​

reluxscript
let mut map = HashMap::new();
map.insert("key", "value");

if let Some(value) = map.get("key") {
    println!("Found: {}", value);
}

HashSet (Unique Set) ​

reluxscript
let mut seen = HashSet::new();
seen.insert("item1");

if seen.contains("item1") {
    // Already seen
}

Control Flow ​

If/Else ​

reluxscript
if node.async {
    // Async function
} else if node.generator {
    // Generator function
} else {
    // Regular function
}

Loops ​

reluxscript
// For loop
for arg in &node.arguments {
    process_argument(arg);
}

// While loop
while has_more() {
    process_next();
}

// Loop with break
loop {
    if done() {
        break;
    }
}

Vector Alignment ​

ReluxScript only supports features that work identically in both Babel and SWC.

Supported ​

✅ Pattern matching on AST nodes ✅ Explicit mutations ✅ Type-safe collections ✅ Error handling with Result ✅ String operations ✅ Closures (limited)

Not Supported ​

❌ Async/await (different semantics) ❌ Direct DOM/Node.js APIs ❌ External library imports ❌ Regex literals (use string matching) ❌ Closures capturing mutable state

Escape Hatches ​

For platform-specific code, use verbatim blocks:

reluxscript
babel! {
    // Raw JavaScript - only in Babel output
    const recast = require('recast');
    // ...
}

swc! {
    // Raw Rust - only in SWC output
    use swc_common::DUMMY_SP;
    // ...
}

Use sparingly - this breaks the "write once" guarantee.

Best Practices ​

1. Keep Visitor Methods Focused ​

reluxscript
// ✅ Good: Single responsibility
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
    if self.should_transform(node) {
        self.transform(node);
    }
}

// ❌ Bad: Too much logic in visitor
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
    // 50 lines of complex logic...
}

2. Use Helper Functions ​

reluxscript
plugin MyPlugin {
    // Helper function
    fn is_console_method(&self, callee: &Expression) -> bool {
        matches!(callee, MemberExpression {
            object: Identifier { name: "console" }
        })
    }

    // Visitor uses helper
    fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
        if self.is_console_method(&node.callee) {
            *node = Statement::empty();
        }
    }
}

3. Track State Explicitly ​

reluxscript
plugin MyPlugin {
    struct State {
        component_name: Option<Str>,
        hook_calls: Vec<HookInfo>,
    }

    fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
        self.state.component_name = Some(node.id.name.clone());
        node.visit_children(self);
        self.state.component_name = None;
    }
}

4. Use Pattern Matching ​

reluxscript
// ✅ Good: Clear pattern matching
if matches!(node.operator, "+") {
    handle_addition();
}

// ❌ Less clear: String comparison
if node.operator == "+" {
    handle_addition();
}

Next Steps ​

Now that you understand the core concepts:

Released under the MIT License.