DEV Community

nausaf
nausaf

Posted on • Edited on

Creating an NPM package that runs on command line

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:

  1. 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.

  2. In package.json, place a "bin" field:

    "bin": "./cli.js",
    "type": "module",
    

    "bin": "./cli.js" tells npx that when the package is run from the command line using npx <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 (using import statement). Otherwise you would only be able to import Common JS modules (using require) which, this being the tail-end of 2024, you probably don't want to do.

  3. On top of the file declared in "bin" in package.json, place the line #!/usr/bin/env node. This allows the cli.js to run using the Node.js executable when it is launched by npx.

    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:

Image description

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",
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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" };
Enter fullscreen mode Exit fullscreen mode

or using the assert keyword as:

import packageJson from "./package.json" assert { type: "json" };
Enter fullscreen mode Exit fullscreen mode

In either case, I got the following warning when I ran the code:

Image description

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.

Top comments (0)