An NPM Package Script Strategy

Zachary Leighton
6 min readMay 17, 2018

Most developers new to the NPM or NodeJS ecosystem are unaware of the power 🔌 available to them in the package.json file.

However, this file also is critical to any open or closed source project, as it is the entry point of the application. After working with a few projects I have put together a strategy for managing the file and also for naming conventions of the scripts as well as some helpers to make sure developers are using the correct versions of NPM and NodeJS.

Organizing and naming your scripts

If you’re currently using gulp, bower, or any of the other build systems for the web you can use NPM to centralize all your build systems in one spot, and in many cases simplify them.

Naming your scripts

Naming is always half the battle of building something and can have effects down the line so it should be done with care and correctly.

As a note, feel free to change any of the specific names here like api to server or client to app etc. Just be consistent!

The first thing to do is lay out the broad structure of what your package script includes. In this example we’ll assume we have a NodeJS api and a “webpacked” client app in the same git repository.

You now have a few basic areas:

  • API
  • Client
  • NPM life cycle scripts
  • Testing Scripts
  • Other build scripts, environmental, etc.

From these you can start to build your functional subgroups. For the api and client you will probably have a few environments you need to build, also for test you may have a few types of tests (i.e.— integration, end to end, unit), and maybe different reporting for different environments.

The next step is to take these areas and name the sub-functions. Here are some examples of common bad practices and improvements on them.

Bad names

“api-production”: “…”,
“api-dev”: “…”,
“client-watch”: “…”,
“client-build-test”: “…”,
“build:watch-dev”: “…”,
“testdebug”: “…”,
“e2e — inspect”: “…”,
“something-custom:dev”: “…”,

Better names

"api": "node server.js", // Recommend the base ones to be prod
“api:dev”: “…”,
“api:dev:inspect”: “…”,
“client:dev”: “…”,
“build:client:test”: “…”,
“build:client:dev:watch”: “…”,
“test”: “…”, // note: this is a life-cycle script
“test:unit:debug”: “…”,
“test:e2e:inspect”: “…”,
“custom:something:dev”: “…” // maybe this a svg build, etc.

Notice the difference between naming in the good and bad examples. The bad examples are random and don’t follow a strict pattern, while the good examples follow something along the lines of:

<function>:<action/environment>:<sub action/environment>

Obviously you can chain even deeper if your project needs this, but try not to chain more than 3–4 levels deep as it can be hard to manage. This usually indicates a need to split the repository/functions.

Another thing to recommend is to name consistently across the areas, don’t use dev for the api and then develop for the client. This will cause confusion (bad!) 😕 and opens up room for error.

Organizing the scripts

It is best to organize the scripts in a way that keeps the logical divisions together, so all the scripts that are related using the tiered architecture are together.

Other than this, I recommend using one of two things, organizing them alphabetically or organizing them logically.

Bad organization

"api": "node server.js",
“api:dev”: “…”,
“custom:something:dev”: “…”,
“start”: “…”,
“test:e2e:inspect”: “…”
“api:dev:inspect”: “…”,
“test”: “…”,
“client:dev”: “…”,
“build:client:test”: “…”,
“build:client:dev:watch”: “…”,
“test:unit:debug”: “…”

Better organization

"api": "node server.js",
“api:dev”: “…”,
“api:dev:inspect”: “…”,
“build:client:test”: “…”,
“build:client:dev:watch”: “…”,
“client:dev”: “…”,
“custom:something:dev”: “…”,
“start”: “…”,
“test”: “…”,
“test:unit:inspect”: “…”,
“test:e2e:inspect”: “…”

Notice the chaos in the first example, things are out of order and the logical areas are not grouped together. This is confusing and hard to follow, as we will see in the next section it also helps to be organized to see where you can run multiple things in parallel or in sequence.

Using npm-run-all

A great package for running multiple scripts in order or parallel, and it works amazingly well with the naming scheme proposed in this article.

A common issue with web projects is the need to run a few things in sequence for preparing a production build, unit testing in some scenarios, or just running a development server.

This is where npm-run-all comes into play. It can be used to build simple and complex build sequences. I recommend reading their documentation for advanced features.

You’ll want to keep this as a project dependency and not just as a development dependency. To get started run npm i npm-run-all

Running a build sequence

Common use cases for this are build sequences for SVGs, styles, or test scripts.

Here is an example for running a few types of tests, but running them in sequence so that we will fail out if a suite fails.

"test": "run-s test:**",
"test:client": "karma start ...",
"test:e2e": "nightwatch ...",
"test:integration": "mocha ...",
"test:unit": "mocha ..."

By running npm run test or npm test (a shortcut) it will run everything matching the pattern test:** which will run the test:XXX scripts one after the other unless a test fails, in which case it will fail and return a non-zero exit code.

Running scripts in parallel

A common example of this is running a watch on webpack (or a webpack-dev-server) as well as running a node server.

Let’s say you have client:dev:watch and api:dev:watch where client is webpack-watch and api is using nodemon.

You can add a script dev:watch which will use run-p to run the two scripts in parallel so you only need one cmd/powershell/terminal window.

An example is as follows:

"dev": "run-p dev:client dev:server",
"dev:server": "nodemon server/server.js",
"dev:client": "webpack-dev-server"

Here we can just run npm run dev and it will run both the server and client in parallel to get you up and running.

Enforcing common Node/NPM versioning

Something that can be extremely frustrating to have developers working on different versions of NodeJS and NPM. There are many unintended consequences such as using features not in an LTS version, or having lock files not be respected by older NPM.

So a good way to prevent these problems is to enforce the versions from the get-go.

The version checking script

The inspiration for this script came from a VueJS template.

Here is a gist with the script called check-versions.js which uses the package.json file (which we will configure below) to check the semantic versions of NodeJS and NPM to make sure developers are using compatible versions.

The important parts of this package are the version requirements, the rest is code to check the warnings and display it with a proper exit code.

const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
{
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
}
];

Here we check the version of NodeJS by checking the runtime process.version and it checks NPM by running exec('npm --version') which will assign the result from the console to the currentVersion property.

Later in the code line 25 the semver package checks what we have versus what we specified.

Make sure to place this file in the root with package.json

Changes in package.json

Here we will add a new script call using the hook postinstall which is run after npm i finishes.

"postinstall": "node check-versions.js",

As we saw above the file needs some specification as to what is valid to use. I recommend trying to keep the major version the same across developers and only check minor/patch versions, for this use the ^ semantic operator. Feel free to be more or less flexible with your requirements.

This goes in your package.json :

"engines": {
"node": "^8.9.4",
"npm": "^5.6.0"
},

This will restrict developers to use NodeJS 8.9.4 or above but less than 9.0 and the same idea for NPM, which allows 5.6.0 up until 6.0 .

Now you can be assured you are on the same versions across your development environments.

--

--

Zachary Leighton

Architect at Tipalti living in Israel trying not to spill tahini on the keyboard