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.js), I prefer to remove the file extension and make the scripts executables (./create-thumbnails). This approach saves characters, and makes me feel like a hacker!

My steps to create an executable are:

  • Remove the .js file extension (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 the node 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?

The Node.js binary is missing a flag to specify module files

There are three ways to enable ECMAScript modules in Node.js:

  1. use the .mjs file extension
  2. have a surrounding package.json with a "type": "module" field
  3. 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.json, 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!

A "hacky" solution to run module-based JavaScript executables

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.

If you enjoyed this article...

Join 5.5k readers and learn something new every week with Web Weekly.

Web Weekly — Your friendly Web Dev newsletter
Reply to this post and share your thoughts via good old email.
Stefan standing in the park in front of a green background

About Stefan Judis

Frontend nerd with over ten years of experience, freelance dev, "Today I Learned" blogger, conference speaker, and Open Source maintainer.

Related Topics

Related Articles