Every now and then I need to create an NPM package that runs on the command line, as a CLI (Command Line Interace).
For example, npx eslint
runs and lints code in a directory and can accept command line arguments also. Similarly npx create-next-app --eslint --tailwind
would scaffold a Next.js app that has eslint and tailwind configured.
There are three things you need to do to turn an NPM package into one that can be run from the command line:
Create a file that contains the code that would run when the package is run using
npx
from the command line, e.g.cli.js
.-
In package.json, place a
"bin"
field:
"bin": "./cli.js", "type": "module",
"bin": "./cli.js"
tellsnpx
that when the package is run from the command line usingnpx <package name>
, then./cli.js
should be run.ASIDE:
"type": "module"
tells NPM utilities that the type of modules which would be imported in the code files in this package would be ES6 (usingimport
statement). Otherwise you would only be able to import Common JS modules (usingrequire
) which, this being the tail-end of 2024, you probably don't want to do. -
On top of the file declared in
"bin"
in package.json, place the line#!/usr/bin/env node
. This allows thecli.js
to run using the Node.js executable when it is launched bynpx
.For example, my
cli.js
in package root would look like this:
#!/usr/bin/env node import { createRequire } from "module"; const require = createRequire(import.meta.url); const packageJson = require("./package.json"); console.log("Hello World!"); console.log(`Version number of the package is ${packageJson.version}`);
If I publish the package to NPM by running npm publish
on the terminal in the project folder, then go to a completely different folder on the terminal and execute npx show-version-number
(where show-version-number
is the name of the package in package.json
and therefore in NPM registry), it would still run:
I checked that npx
downloaded and stored the package in C:\Users\{My User Name}\AppData\Local\npm-cache\_npx\
on my Windows machine.
Code is in this GitHub repo. If you want to publish it to NPM, please change "name"
in package.json
to a different value as I have already published a package with the name "show-version-number"
. I use this tool to check if a package name is available in NPM registry.
You can also create named commands by creating named values in "bin"
in package.json
like this:
"bin": {
"showver": "./cli.js",
}
Now when you publish the package to NPM, and run it as npx show-version-number
on the command line, cli.js
would still run as before. I believe npx <package name>
picks up the first entry in "bin"
(in this case the key "showver"
) and runs the code file defined there (in this case ./cli.js
).
However, you can also install the package on your machine:
npm install --global show-version-number
The npm install --global
command would register a command named showversion
with the operating system (as a cmd file on Windows and as a symlink on Unix-based system such as Linux, as described here) that aps to cli.js
file.
Now, you can say the following on the terminal in any folder on your machine:
showver
This would run cli.js
with Node executable and you would have the same output as when you ran npx show-version-number
.
Of course, a single package may provide multiple named commands.
ASIDE: I had to import package.json
using the following lines in cli.js
:
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const packageJson = require("./package.json");
instead of import packageJson from "package.json"
because in my version of Node (v20+), this latter import throws a ERR_IMPORT_ASSERTION_TYPE_MISSING
error when I try to run the package using npx .
.
To fix the error I had to either rewrite the import as:
import packageJson from "./package.json" with { type: "json" };
or using the assert
keyword as:
import packageJson from "./package.json" assert { type: "json" };
In either case, I got the following warning when I ran the code:
However, the three lines I use to import package.json
instead, clunky as they are, get rid of the warning. See this StackOverflow thread for more details.