You Don’t Need NPM Scripts

If you have done any Node.js development, you’ve likely used NPM, and you should know that on your package.json file, you can add simple scripts that can be executed using npm run <name of script>. At first, this might seem convenient, and since most projects use it, why not use it.

In my experience, NPM scripts have a few problems.

  • They are encoded as JSON strings that don’t have syntax highlighting or linting by default on most editors.
  • Strings in JSON must be double-quoted, which produces the need to escape double quotes inside your scripts.
  • Longer scripts make your package.json difficult to read and understand.
  • On larger projects, you might need many scripts which can become unmanageable quickly. 
  • Scripts are all on a single namespace forcing you to invent naming conventions like build:css, build:js, build:production:js, etc.

The only good reason to use NPM scripts is to hook into life cycle events like postinstall, prestart, and friends. If you need something more than mocha src/**/*.test.js, you will be better off avoiding NPM scripts.

My suggestion is to create a top-level folder named scripts and organize it however you like. Inside the scripts folder, you will have executable files without file extensions, and each file must begin with the correct shebang line. 

Here’s an example from one of my projects. This file is located at scripts/build, which can be invoked by running ./scripts/build on the root of the project.

#!/usr/bin/env bash
#
# Build project for production
#
set -euf -o pipefail

echo "-> Building App..."

# Remove old files if they exist
rm -rf ./build

# Run postcss build
./scripts/css-build

# Copy images to build
./scripts/img-build

# Run server TypeScript build
echo "-> Compiling server-side TypeScript..."
npx tsc

# Run client side JS build and minify
./scripts/js-build

# Copy other files to build dir
echo "-> Copiying *.hbs and *.json files..."
npx copyfiles --up 1 ./src/**/*.{hbs,json} ./build

echo "-> Build done"

This example is written in bash because it is a very short list of commands, each calling another smaller and much more focused script. You don’t have to use bash. Actually, if your script is more than a few lines long and requires some sort of logic, you might be better off using a more familiar programming language like JavaScript or Python or whatever you like.

Here’s the smallest possible example using Node.js as the runtime. This script can be invoked with ./scripts/node-env.

#!/usr/bin/env node
/**
 * Print the current NODE_ENV
 */
console.log(`NODE_ENV='${process.env.NODE_ENV}'`);

That’s it, that’s the idea. Just to recap:

  • Create a top-level scripts folder and add your scripts to it.
  • Scripts must not use a file extension. This will make it easier to change the language later.
  • Add the required shebang like to the file. 
  • Make sure the scripts are executable by running chmod +x scripts/<name of script>.
  • Invoke scripts by running ./scripts/name.
  • Only use NPM scripts to hook into the life cycle events like preinstall, postinstall, etc.
  • If you use one of the life cycle event hooks, just call one of your scripts.

Let me know what you think.

Photo: Unsplash