DEV Community

Rense Bakker
Rense Bakker

Posted on • Updated on

Publishing a NodeJS CLI tool to NPM

Project setup

Create a new folder (my-cli-demo for example) and use npm init command. Answer the questions with default options, except the entry point, which should be changed to bin/index.js

package name: (cli-demo)
version: (1.0.0)
description:
entry point: (index.js) bin/index.js
test command:
keywords:
author: 
license: (ISC)
Enter fullscreen mode Exit fullscreen mode

After that, we can use npm to install the project dependencies for our CLI tool:

npm i yargs chalk@~4 && npm i typescript @types/yargs @types/node --save-dev
Enter fullscreen mode Exit fullscreen mode

Lastly we need to tell npm that our package has an executable file and since we are using typescript, we should also add a watch script, to recompile our code when we make changes. This will allow us to test our CLI tool locally, without having to run a compile command manually every time:

{
  "name": "my-cli-demo",
  "version": "1.0.0",
  "bin": {
    "my-cli-demo": "bin/index.js" // <- command name of our cli script and the executable file it points to
  },
  "main": "bin/index.js",
  "scripts": {
    "watch": "tsc -w" // <- watch script
  },
  "license": "ISC",
  "dependencies": {
    "chalk": "^4.1.2",
    "yargs": "^17.7.2"
  },
  "devDependencies": {
    "@types/node": "^20.1.0",
    "@types/yargs": "^17.0.24",
    "typescript": "^5.0.4"
  }
}
Enter fullscreen mode Exit fullscreen mode

Typescript configuration

We should also add a minimal tsconfig.json:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "moduleResolution": "node",
    "declaration": true, // creates d.ts type declarations
    "declarationMap": true, // creates map files for our d.ts files
    "sourceMap": true, // creates map files for our source code
    "outDir": "bin", // compile source code into "bin" folder
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitAny": true,
    "esModuleInterop": true
  },
  "include": [
    "src/index.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Parsing command line arguments

It’s time to start writing our CLI script! We’re going to use yargs to parse the command line arguments, that can be passed to our CLI tool. Yargs makes it easier to define which command line arguments can be used and it can automatically add documentation for our CLI tool that can be show with the --help flag.

Start by creating src/index.ts and adding the following contents:

#! /usr/bin/env node
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'

yargs(hideBin(process.argv))
  .help()
  .argv
Enter fullscreen mode Exit fullscreen mode

Let’s start by getting our watch script up and running by typing npm run watch in a terminal. Now everytime we make a change, our code will be recompiled into the bin folder, which is configured as our main entry point. If we open another terminal and run node . --help, we should see the following output from our CLI script:

Options:
  --version  Show version number                                       [boolean]
  --help     Show help                                                 [boolean]
Enter fullscreen mode Exit fullscreen mode

Adding a command module

Yargs allows us to define commands for our CLI tool, for example, we could define an init command that gets executed like this:

node . init
Enter fullscreen mode Exit fullscreen mode

To add a command module, let’s start by creating src/commands/init.ts and adding the following content:

import { CommandModule, Argv, ArgumentsCamelCase } from 'yargs'
import chalk from 'chalk'

// the builder function can be used to define additional
// command line arguments for our command
function builder(yargs: Argv) {
  return yargs.option('name', {
    alias: 'n',
    string: true
  })
}

// the handler function will be called when our command is executed
// it will receive the command line arguments parsed by yargs
function handler(args: ArgumentsCamelCase) {
  console.log(chalk.green('Hello world!'), args)
}

// name and description for our command module
const init: CommandModule = {
  command: 'init',
  describe: 'Init command',
  builder,
  handler
}

export default init
Enter fullscreen mode Exit fullscreen mode

We should also change our src/index.ts to let yargs know about our new command module:

#! /usr/bin/env node
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import init from './commands/init'

yargs(hideBin(process.argv))
  .command(init) // registers the init command module
  // or to register everything in the commands dir: .commandDir('./commands')
  .demandCommand()
  .help()
  .argv
Enter fullscreen mode Exit fullscreen mode

Now if we run node . init --name Rense in our terminal, we will see the following output:

Hello world! Rense
Enter fullscreen mode Exit fullscreen mode

Publishing to NPM

To publish our new CLI tool to NPM, we first need to add a git repository. Run the git init command in our project root directory and then create a repository on github or any other git host and add the remote url to our project with: git remote add origin git@github.com:[username]/[repository_name].git now add our files to the registry with git add . and push our initial commit: git commit -m "init" && git push.

We will use np for publishing, let’s install some additional dependencies to make this happen:

npm i np cross-env --save-dev
Enter fullscreen mode Exit fullscreen mode

We’re going to add a release script to our package.json, but we need a way to prevent someone from accidentally running npm publish in the root of our project. To achieve this we can create this prepublish.js file with the following content:

const RELEASE_MODE = !!(process.env.RELEASE_MODE)

if (!RELEASE_MODE) {
  console.log('Run `npm run release` to publish the package')
  process.exit(1)
}
Enter fullscreen mode Exit fullscreen mode

Now we can add the following scripts to our package.json:

[
  "prepublishOnly": "node prepublish.js && tsc",
  "release": "cross-env RELEASE_MODE=true np --no-tests"
]
Enter fullscreen mode Exit fullscreen mode

And then run npm run release and answer the questions that np asks us. The prePublishOnly script will prevent anyone from manually running npm publish.

Using our CLI tool

After the CLI tool is published to NPM, we can install it anywhere we want to use it:

npm i my-cli-demo
my-cli-demo init --name Rense
Enter fullscreen mode Exit fullscreen mode

Links

Top comments (0)