Add support for Node native addons by geekuillaume · Pull Request #837 · vercel/pkg

Very-Early Announcement of a New Approach to Packaging: caxa

I did my investigation and learned that there isn’t a satisfactory solution to my problem, so I’m cooking something up which I’m calling caxa. It isn’t ready for consumption yet, but I wanted to check in and let y’all in the loop. Here’s a few details (more to come in the near future):

My Requirements

  • I just want to distribute Node.js applications easily.
  • I don’t care about hiding the source code.
  • I want builds to be fast.
  • I want no surprises at all. If the application works in development, then it should work after packaged. Native modules and all.
  • I don’t a patched version of Node.js. That’s bound to go stale (see the situation with pkg and fs/promises, which I had to patch in @leafac/pkg).
  • I want the packaged application to be as small as possible.

Prior Art

Unfortunately, all packaging solutions I could find either require you to compile Node.js from scratch, which is slow (hours!) and consumes ~10GB of disk(!) (boxednode, Node Packer, and so forth), or they don’t support native extensions transparently (pkg, nexe, and so forth).

I decided not to invest in pkg anymore, and when I release caxa in the next few days I’ll deprecate @leafac/pkg. (Note that I’m far from being the maintainer of pkg, I’m just a random dude who passed by…)

How caxa Works

The root of the problem seems to be that Node.js insists on loading .node files from disk.

The workaround for native extensions in this PR and in other packaging solutions is to put those .node files in a temporary directory. Your packaged application becomes a sort of self-extracting archive.

caxa takes this approach to the next level and achieves blissful simplicity and robustness. It puts your project alongside the Node.js in a self-extracting archive.

Limitations

  • Cross-compilation is out of the table. You can’t, for example, generate a Windows .exe from macOS. I learned that not even the best in the cross-compilation business, Go, supports cross-compiling native extensions (or, as they call it, CGO). I think that in the Go community they try to avoid CGO if they can, but here in Node.js there’s no way around them; think of SQLite, Sharp, and so forth. This isn’t a big deal, because GitHub Actions provides runners for Linux, macOS, and Windows.
  • You must bundle the same version of node that you’re running. The reasoning is pretty much the same as on the point above: Native modules break when compiled against an incompatible version of node. Also, there’s no shenanigans of downloading different versions of node in the packaging process. And you don’t have to wait for anyone to release a new pre-compiled binary (or worse, compile it yourself). Minimal work; minimal surprises. And again, GitHub Actions are a good solution here: just use a matrix build.

What We Have so Far

  • caxa produces self-extracting archives for Linux & macOS.
  • The binaries are smaller than the competition (because they’re gzipped).
  • Support for every Node.js version.
  • Support for native modules.
  • Fast builds.
  • caxa can also produce macOS Application Bundles (.app). It’s a cute trick.

What’s Coming in the Next Few Days

  • Windows support. (As far as I can tell the idea of self-extracting archives came from the Windows world, so I expect things to work great there, but I don’t have a Windows machine and VirtualBox with modern.ie is painful to work with because my development machine can barely handle it. So Windows support comes last… If you’re reading this, you have an old Windows machine laying around, and you want to support my work, please send the machine my way).
  • Documentation.

What’s in the Name

caxa is a misspelling of caixa, which is Portuguese for box. I find it amusing to say that I’m putting my application in the caxa.