Over five years ago I wrote a short post about writing JavaScript scripts which also work as CJS modules. I wrote that I didn’t know how to perform a similar trick with ESM modules, and that it might even be impossible. Well, I was wrong. I found a solution in the book Shell scripting with Node.js by Axel Rauschmayer. He proposes the following solution in the chapter about file system paths and file URLs:
import {fileURLToPath} from 'node:url';
const modulePath = fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) {
main();
}
(well actually, his solution is a bit more complex, but this is the gist of it – head to his book for more details).
I started using it and it worked great… until it didn’t. It turned out that I was able to trick this code by symlinking to my script and calling the symlink instead. After a bit of poking around, I arrived at this code:
import {fileURLToPath} from 'node:url';
import {realpath} from 'node:fs/promises';
const modulePath = await realpath(fileURLToPath(import.meta.url));
if (await realpath(process.argv[1]) === modulePath) {
main();
}
Note that it uses top-level await, which Node.js has been supporting for some time now.
From now on, I can both call my ESM script directly (even as a symlink) or import it in other code (for example, unit tests).
That’s it for today, thanks for reading!