Eventually Consistent

Dispatches from the grey area between correct and working

Java for Web Engineers: Recursively Decaffeinating a Codebase

You're a frontend engineer. You're used to Node and TypeScript. But the backend is written in Java — and it's time to get your hands dirty. This post walks through a practical Java program that scans directories and runs decaffeinate on old CoffeeScript files.

Java for Web Engineers: Recursively Decaffeinating a Codebase

You’re a frontend developer. You write in TypeScript, React, maybe some Node. But the service behind that app — the one you need to integrate with or debug — is written in Java.

This happens a lot.

And when it does, it helps to know just enough Java to be dangerous.

In this post, we’ll write a Java program that:

  • Walks a directory tree
  • Finds .coffee files
  • Runs decaffeinate on each

It’s a nice excuse to learn:

  • How to read and recurse through directories in Java
  • How to check file types
  • How to spawn subprocesses (like Node’s child_process.exec)
  • And how to write Java that feels at least somewhat ergonomic

What We’re Building

We want to write a command-line program that looks at your current working directory and finds every .coffee file under it. For each file, it will:

  • Spawn a decaffeinate subprocess in the file’s directory
  • Run decaffeinate somefile.coffee --use-js-modules
  • Print the exit status

The catch: decaffeinate wants to be run from the directory that holds the file — so we’ll need to manage CWDs as we go.


Getting Started

Install decaffeinate globally:

npm install -g decaffeinate

Create a new file: Decaf.java

You can compile and run it with:

javac Decaf.java && java Decaf

Let’s start with a minimal skeleton:

public class Decaf {
  public static void main(String[] args) {
    System.out.println("Hello from Java");
  }
}

Yes, everything in Java is in a class. And yes, the filename and class name have to match. Roll with it.


Reading the Current Working Directory

Java exposes environment info through System.getProperty().

String dir = System.getProperty("user.dir");
System.out.println("Working Directory = " + dir);

We’ll pass that to a recursive scan() function that does all the real work.


Recursively Scanning Directories

To scan a directory, we’ll:

  • Turn the path into a File
  • Get the list of files using .listFiles()
  • Convert that array into a Stream via Arrays.asList()ArrayList
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;

private static void scan(String path) {
  File folder = new File(path);
  ArrayList<File> files = new ArrayList<>(Arrays.asList(folder.listFiles()));

  files.stream().forEach(file -> {
    String fullPath = file.getAbsolutePath();
    if (file.isDirectory()) {
      scan(fullPath);
    } else if (fullPath.matches(".*\\.coffee$")) {
      runDecaffeinate(file);
    }
  });
}

We’re using Java 8’s stream API here because it’s a bit more expressive — closer to JavaScript’s forEach. This is purely for readability. You could just as easily use a for loop.


Spawning Subprocesses (the Good Stuff)

Here’s the code to spawn decaffeinate on each file:

private static void runDecaffeinate(File file) {
  String fileName = file.getName();
  String command = String.format("decaffeinate %s --use-js-modules", fileName);
  File directory = new File(file.getParent());

  System.out.println(String.format("Executing: %s in %s", command, directory.getPath()));

  try {
    Process subDecaf = Runtime.getRuntime().exec(command, null, directory);
    subDecaf.waitFor();
    System.out.println("Exit code: " + subDecaf.exitValue());
  } catch (Throwable t) {
    t.printStackTrace();
  }
}

Java’s Runtime.getRuntime().exec() is basically its version of child_process.exec. It’s clunky but works.

Important bits:

  • We pass the file’s parent directory as the working directory
  • We use waitFor() before asking for the exit code
  • We catch and print any exceptions

Putting It All Together

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;

public class Decaf {
    public static void main(String[] args) {
        String dir = System.getProperty("user.dir");
        System.out.println("Working Directory = " + dir);
        scan(dir);
    }

    private static void scan(String path) {
        File folder = new File(path);
        ArrayList<File> files = new ArrayList<>(Arrays.asList(folder.listFiles()));

        files.stream().forEach(file -> {
            String fullPath = file.getAbsolutePath();
            if (file.isDirectory()) {
                scan(fullPath);
            } else if (fullPath.matches(".*\\.coffee$")) {
                runDecaffeinate(file);
            }
        });
    }

    private static void runDecaffeinate(File file) {
        String fileName = file.getName();
        String command = String.format("decaffeinate %s --use-js-modules", fileName);
        File directory = new File(file.getParent());

        System.out.println(String.format("Executing: %s in %s", command, directory.getPath()));

        try {
            Process subDecaf = Runtime.getRuntime().exec(command, null, directory);
            subDecaf.waitFor();
            System.out.println("Exit code: " + subDecaf.exitValue());
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

Final Thoughts

Java’s not glamorous. But it’s sturdy. And knowing just enough to recurse over a filesystem, run a CLI, and work with the Stream API can save you a ton of pain when backend services don’t speak your preferred language.

I wrote this around the time I was promoted from WDE to SDE II at Amazon, and I’ve since lost count of how many frontend engineers I’ve seen benefit from a working knowledge of “boring old Java.”

Hopefully this gives you a good jumpstart.

Thanks for reading! If you found this helpful, consider sharing it.