Thank you for your interest in contributing to wakaru! This guide covers the practical steps for getting started, adding features, and submitting changes. For deeper dives, see the docs linked throughout.
- Fork the repo and create your branch from
main. - Install a stable Rust toolchain (via rustup).
- Run
cargo testfrom the workspace root to verify everything builds and passes. - Make your changes.
Optionally install cargo-insta for interactive snapshot review:
cargo install cargo-instaThe formatter currently depends on several OXC crates from a pinned git revision. From a fresh Cargo cache, Cargo may need to fetch the OXC monorepo, which can be slow.
If you use nightly Cargo, you can opt into Cargo's unstable shallow git fetch mode before building or testing:
cargo +nightly fetch -Zgit=shallow-depsThis is optional and not required for normal development. The default workflow uses stable Cargo.
Before submitting a PR, run the relevant checklist in docs/testing.md. For most changes, the local gate is:
cargo fmt --check # formatting
cargo nextest run --workspace
cargo test --workspace --doc
cargo clippy --workspace --all-targets -- -D warnings.cargo/config.toml sets INSTA_UPDATE=new, so a changed snapshot fails
the test and writes a .snap.new file. Review the diff, then accept intentional
changes with cargo insta accept.
wakaru is a Cargo workspace with three crates:
| Crate | Path | Purpose |
|---|---|---|
wakaru-core |
crates/core/ |
Decompile pipeline, transformation rules, unpackers, and public API |
wakaru-cli |
crates/cli/ |
CLI binary (wakaru) built on clap |
wakaru-wasm |
crates/wasm/ |
WASM bindings for browser-based decompilation |
Almost all development happens in wakaru-core. Key directories within it:
src/rules/-- one file per transformation rule;pipeline.rsdefines the ordered rule registrysrc/unpacker/-- bundle format detection and module extractionsrc/driver.rs-- orchestrates the full decompile and unpack pipelinestests/-- per-rule test files, pipeline integration tests, and snapshot fixtures
For the full architecture overview, see docs/architecture.md.
This is the most common type of contribution. Here is a step-by-step walkthrough.
Add a new file at crates/core/src/rules/my_rule.rs. A minimal rule looks like this:
use swc_core::ecma::ast::Expr;
use swc_core::ecma::visit::{VisitMut, VisitMutWith};
pub struct MyRule;
impl VisitMut for MyRule {
fn visit_mut_expr(&mut self, expr: &mut Expr) {
expr.visit_mut_children_with(self);
// your transformation logic here
}
}If your rule needs to distinguish free variables (globals) from locally-bound identifiers, take unresolved_mark:
use swc_core::common::Mark;
pub struct MyRule {
unresolved_mark: Mark,
}
impl MyRule {
pub fn new(unresolved_mark: Mark) -> Self {
Self { unresolved_mark }
}
}Then guard identifier matches with id.ctxt.outer() == self.unresolved_mark. See docs/architecture.md for details on why this is necessary.
- In
crates/core/src/rules/mod.rs, addmod my_rule;andpub use my_rule::MyRule;. - In
crates/core/src/rules/pipeline.rs, add arunner!macro invocation for your rule and aRuleDescriptorentry in thedefine_rule_registry!block at the right position. Entries specify a name, stage, runner function, enablement gate, and optional dependency list.
Pipeline placement matters -- see the "Pipeline Ordering" section below.
Create crates/core/tests/my_rule_rule.rs:
mod common;
use common::{assert_eq_normalized, render_rule};
use wakaru_core::rules::MyRule;
fn apply(input: &str) -> String {
render_rule(input, |_| MyRule)
}
#[test]
fn transforms_target_pattern() {
let input = r#"/* minified input */"#;
let expected = r#"/* readable output */"#;
assert_eq_normalized(&apply(input), expected);
}
#[test]
fn leaves_unrelated_code_alone() {
let input = r#"/* code that should not change */"#;
assert_eq_normalized(&apply(input), input);
}Cover both positive cases (pattern is transformed) and negative cases (unrelated code is untouched).
After adding the rule, run the full test suite to check for snapshot changes in other tests:
cargo testIf other snapshots changed, review them carefully. If your rule is placed early in the pipeline, it can cascade through later rules.
Tests live in crates/core/tests/. The key test helpers are:
| Helper | Use when... |
|---|---|
render(source) |
You want to test the full decompile pipeline |
render_rule(source, builder) |
You want to test a single rule in isolation |
render_pipeline_until(source, stop_after) |
You want to test up to a specific pipeline stage |
render_pipeline_between(source, start, stop) |
You want to test a range of rules |
assert_eq_normalized(actual, expected) |
Comparing output (normalizes whitespace) |
Common pitfalls:
- Do not use bare expression statements as test inputs (e.g.,
65536;) --SimplifySequencedrops them as dead code. Useconst x = 65536;instead. - When
render()gives unexpected results, switch torender_rule()to isolate your rule, or userender_pipeline_until()to stop at a specific point.
For the full testing guide (snapshot workflows, test organization, all available helpers), see docs/testing.md.
When a rule is not working as expected, start with the rule trace CLI:
cargo run -p wakaru-cli -- debug trace path/to/input.jsThis prints a git-style diff for each rule that changes the output, making it easy to see where transformations happen (or fail to happen).
Useful options:
# Show all rules, including ones that did not change output
cargo run -p wakaru-cli -- debug trace path/to/input.js --all
# Trace only a range of rules
cargo run -p wakaru-cli -- debug trace path/to/input.js --from RemoveVoid --until UnEsmCommon symptoms and what to check:
- Rule not firing -- An earlier rule may have changed the AST shape. Use
debug traceto see what the input looks like by the time your rule runs. - Unexpected variable names -- Check for a missing
unresolved_markguard. - Too many snapshots changed -- An early pipeline rule is cascading. Check early rules like
SimplifySequenceandFlipComparisons. cargo testhangs -- Likely infinite recursion. Run withRUST_BACKTRACE=1 cargo test -- --nocapture.
For the full debugging guide (snapshot layers, fixture repo workflow), see docs/debugging.md.
Rules run in a fixed order defined in crates/core/src/rules/pipeline.rs. The pipeline has six stages:
- Stage 1: Syntax normalization -- simplify sequences, flip comparisons, remove void, etc.
- Stage 2: Transpiler helper unwrapping -- remove Babel/TypeScript helpers, reconstruct module systems
- Stage 3: Structural restoration -- template literals, while loops, nullish coalescing, optional chaining
- Stage 4: Complex pattern restoration -- IIFEs, conditionals, parameters, enums, JSX, classes
- Stage 5: Modernization -- arrow functions, let/const, object shorthand, exponentiation
- Stage 6: Cleanup and renaming -- import/export rename, smart inline, smart rename, dead code elimination
Order matters because rules depend on earlier ones having run. For example:
UnEsm(Stage 2) depends onUnCurlyBraces,UnEsmoduleFlag, andUnAssignmentMerginghaving normalized the AST first.ArrowFunction(Stage 5) should run afterUnEs6Class(Stage 4) so that class methods are not incorrectly converted to arrows.
When placing a new rule, consider:
- What AST shape does your rule expect? Place it after the rule that produces that shape.
- Will your rule's output be consumed by a later rule? Make sure it runs first.
- Use
cargo run -p wakaru-cli -- debug traceon real-world samples to verify your rule fires at the right point. - Run the full test suite and review any snapshot changes to catch ordering issues.
For the full list of rule dependencies, see docs/rule-dependency-inventory.md.
This project follows the Conventional Commits specification. Please make sure your commit messages are formatted correctly.
Examples:
feat: add UnNullishCoalescing rule
fix: handle nested ternary in UnConditionals
test: add edge case for arrow function with rest params
refactor: extract shared helper into babel_helper_utils
docs: update architecture diagram for two-phase pipeline
Please mention the issue number in the commit message or the PR description.