Better NPM Scripts

The common behavior of developers who uses NPM is to open package.json and read through scripts section to figure out what can be done with the new project, or to refresh in memory how to do things with the current one. Even though thousands of developers doing that every day this path lacks several important features: documentation, configuration, comfort (writing code in a one-line string isn’t a pleasure).

Scripts are the important part of the development process. They simplify a work a lot. But poor features stop developers from creating and maintaining them. There are plenty of tools that add those features. Some are adding descriptions. Others let you code in JavaScript. And I’d like to share with you how can you use them.

Interactive NPM Scripts

The first example is a quick and simple upgrade without enforcing anybody to switch from familiar to them NPM scripts. We’ll create an interactive menu for them with a description for each.

There is a package called npm-scripts-info that collects all commands from package.json and additional documenting sections. When using it from CLI it prints out script names and their descriptions. To see how it works, first, add a description to your scripts in package.json. This can be done in two ways, I’ll use the one with a suffix because it keeps the description near the script it describes:

{ ...,
"scripts": {
"?start": "starts the app on port 8000",
"start": "APP_PORT=8000 node app/main.js",
"?test": "runs all unit tests",
"test": "mocha tests/unit/"
}
}

By installing and running npm-scripts-info we’ll see a list of commands with their description in the output:

$ npm install -D npm-scripts-info
$ npx npm-scripts-info
start:
starts the app on port 8000
test:
runs all unit tests
For this article I’m gonna use npx in examples, as there is no need to install any dependency globally.

It’s nice but it can be better, so we won’t stop. I’ve promised you interaction! Let’s install Go toolkit and add gofile.js to our project:

$ npm install -D go

// gofile.js
const go = require('go')
const getScriptsInfo = require('npm-scripts-info')
const pkg = require('./package.json')
const { spawn } = require('child_process')
// Promise wrapper for spawn()
const execute = script => new Promise((resolve, reject) => {
spawn(script, { stdio: 'inherit', shell: true })
.on('error', err => reject(`exec error: ${err}`))
.on('exit', code => {
if (code) reject()
else resolve()
})
})
// parse script names and descriptions
const scripts = getScriptsInfo(pkg)
// generate commands for Go from parsed scripts
const commands = Object.entries(scripts)
.map(([name, description]) => ({
// the command (script) name
name,
// text that will be shown after the command name
description,
// function that will be called when the command is triggered
callback() {
// execute NPM script
return execute(pkg.scripts[name])
}
}))
// add commands to the Go list
go.registerCommand(commands)

Done! After running Go in a terminal app an interactive menu will appear with the list of NPM scripts and their descriptions. By selecting one and hitting Enter you’ll execute the script related to it. Try it out:

$ npx go
Interactive Go menu for NPM scripts

Advanced Scripts

There is still much more you can do to improve your scripting experience. We still didn’t solve the problem of inline coding. Let’s go back to our gofile.js and add a few more lines:

// gofile.js
const go = require('go')
// ...
go.registerCommand({
name: 'ping',
description: 'test that the scripting tool is working',
callback: () => console.log('pong')
})

In this way, you can add more commands to your CLI and program them using all the capabilities of JavaScript. It is not required to go through the interactive menu every time. You also can run commands directly:

$ npx go ping
pong

As we are writing our scripts in NodeJS environment we can use process.argv to change the behavior of our commands. With Go this arguments are parsed automatically. You can get them from callback argument. In addition, you can configure how to parse those arguments:

// gofile.js
// ...
go.registerCommand({
name: 'options',
description: 'formats and prints given options',
options: {
name: String
},
callback({ args }) {
console.log(args)
}
})
$ go options rename --name Daniel -f
{
"_": [
"options",
"rename"
],
"name": "Daniel",
"f": true
}

Sometimes, there is also a need to ask a user for something during execution. See how simple can it be:

// gofile.js
// ...
go.registerCommand({
name: 'clear',
description: 'cleans working directory from unnecessary files',
async callback({ args }) {
if (await go.confirm('Are you sure you want to clean your working directory?')) {
console.log('deleting files')
}
}
})

When you run this command it will show the given message and ask you to choose y (yes) or n (no). Let’s make it more interesting and combine it with options:

// gofile.js
// ...
go.registerCommand({
name: 'clear',
description: 'cleans working directory from unnecessary files',
options: {
force: { type: Boolean, alias: 'f' }
},
async callback({ args }) {
const shouldRemove = args.force
|| await go.confirm('Are you sure you want to clean your working directory?')
if (shouldRemove) console.log('deleting files')
}
})

Execute go clear with or without flag force (or its alias f). If you call it with no flags it will ask you to confirm the action. If you call it with –force or -f it won’t ask you and run the code in if statement immediately:

$ go clear --force
deleting files
$ go clear
? Are you sure you want to clean your working directory? (Y/n)
deleting files

There are more features to help you automate your work. Learn more about Go on http://gocli.io.