unModified()

Break stuff. Now.

Trying Node.js SEA

A little closer to native binaries but not quite

July 23, 2023

Top View Photo of Rocky Shore Photo by Pok Rie

This weekend I randomly thought about trying Node.js Single Executable Applications (SEA). The gist of this feature is that it lets you "turn your Node.js app into a native executable". I say that in quotes because technically, it's not doing any compiling of JavaScript code into native code. Instead, it does the next best thing: Embed the JavaScript into a copy of the Node.js executable. And once that modified executable is executed, it runs the embedded script if present. It's a very clever solution in my opinion.

The preparation process is quite simple:

  1. Create a config file that tells Node.js which script to convert into a blob.
  2. Use the previous config to convert a script into a blob.
  3. Grab a copy of the Node.js executable installed in your system.
  4. Inject the created blob into the copy of the Node.js executable.
  5. ???
  6. Profit!

Of course, given that this feature is experimental, constraints and differences exist. However, I find that the choice of going for CommonJS instead of ES modules to be most intriguing, as ES modules have been supported for a while now. While it doesn't matter much, as we have tools to translate from one format to another, it's just not seamless in most cases.

For small standalone scripts, everything works fine. It's when you try it with real third-party modules that it starts to show its experimental status. Not the fault of Node.js SEA, just features of regular Node.js that don't translate over cleanly. For instance, following are the things I found while trying to bundle standard into one CommonJS script so that I can turn it into an executable:

So far, my workaround to the above is to patch the modules with patch-package or replace lines using @rollup/plugin-replace. Not the most elegant solution, I'd have to patch/replace every quirk I find which can easily become a game of cat-and-mouse, but this lets me move forward.


PS: Now you might be wondering, why not just learn a language that compiles to a native executable (e.g. C, C++, Rust, Go)? I did try that (Rust) but the learing curve is quite steep on the more advanced end, like once you start building stuff like parsers and renderers. Also, JavaScript's syntax is more concise and flexible, allowing you to be clever with its use. For instance, you can create an HTML-like structure in Mithril.js with just JavaScript - something that you'd have to create a template DSL/elaborate macros for for with typed languages.