How to create a module-based Node.js executable
- Published at
- Updated at
- Reading time
- 3min
Many of my projects include Node.js scripts to perform setup or teardown steps. And while I could run all of them with the node
binary (node create-thumbnails
), I prefer to remove the file extension and make the scripts executables (
). This approach saves characters, and makes me feel like a hacker!
My steps to create an executable are:
- Remove the
file extension (.js mv create-thumbnails
)..js create-thumbnails - Make the file executable (
chmod 744 create-thumbnails
). - Add a shebang (
#!/usr/bin/env node
) to signal that the executing shell should use thenode
binary.
Et voilà, you just created a Node.js executable!
This approach has served me well for CommonJS-based scripts using the require
function. But it's 2022, and I planned to adopt ECMAScript modules in Node.js executables. Unfortunately, it's not that easy.
This article summarizes the most valuable parts of Axel Rauschmayer's extensive guide "Node.js: creating ESM-based shell scripts for Unix and Windows". Head on over if you want to dive into cross-platform executables.
But what's the problem?
There are three ways to enable ECMAScript modules in Node.js:
- use the
file extension.mjs - have a surrounding
package
with a.json "type": "module"
field - call
node
with the--input-type=module
flag
My executables should work without a file extension, which rules out the first option. I also don't want to declare all files as module files or add a package
, so the type
field is out, too.
The --input-type
flag looks promising at first, but it only works for strings you pipe into the Node binary yourself.
# pipe JavaScript code into the node binary
echo "import { mkdir } from 'node:fs/promises';" | node --input-type=module
Why's there no flag to enable modules when running a file?
I spent the last 15 minutes reading Node.js issues and discussions about the topic, and I still can't answer this question. If you want to read more, here's a very long GitHub discussion on why such a flag isn't available yet.
Knock yourself out, the discussion takes many turns, and folks have strong opinions!
There are multiple ways to make ECMAScript module-based executables work. They all come with slightly different spins but use the same trick.
Axel recommends the following for UNIX environments.
#!/bin/sh
':' // ; cat "$0" | node --input-type=module - $@ ; exit $?
import * as os from 'node:os';
const {username} = os.userInfo();
console.log(`Hello ${username}!`);
This snippet is wild! In short, it does the following:
- the
cat
command reads the current file (the executable itself) - the file content is then piped into
node
with the--input-type
flag - all the original parameters are handed over to
node
, too ($@
) - the
node
call's exit code is caught and propagated to the executable
This instruction makes the file read and pipe itself into the node
binary because there's no flag to enable modules. Wow! 🤯
As said, this post is only for my reference. Here are more resources if you want to learn more about other approaches or make it work on Windows.
Join 5.5k readers and learn something new every week with Web Weekly.