How I Organize my NPM Scripts

The typical script names in package.json and how I run them

In the web applications I come across or write myself I typically have a few scripts to build and start the application. This blog post describes the scripts and how I run them every day.

Scripts names

Typically and application needs to be built and served or started. For example in bahmutov/cypress-example-forms web application we have these three scripts at the start:

package.json
1
2
3
4
5
6
7
{
"scripts": {
"format": "prettier --write 'cypress/**/*.js'",
"start": "parcel serve index.html",
"build": "parcel build index.html"
}
}

If you are inside the project's folder, you can see all available scripts by executing npm run command

1
2
3
4
5
6
7
8
9
10
$ npm run
Lifecycle scripts included in cypress-example-forms:
start
parcel serve index.html

available via `npm run-script`:
format
prettier --write 'cypress/**/*.js'
build
parcel build index.html

If we want to format our code, we can use

1
$ npm run-script format

The run-script has an alias run, thus we can execute the scripts by running simply npm run <script name>:

1
2
3
$ npm run format
## the start command is special, you can skip the "run" word
$ npm start

When using Yarn we can usually skip even the "run" command:

1
2
3
$ yarn run build
## is the same as
$ yarn build

Later we add Cypress end-to-end tests and thus need to add two more scripts for running Cypress interactively and on CI.

package.json
1
2
3
4
5
6
7
8
9
{
"scripts": {
"format": "prettier --write 'cypress/**/*.js'",
"start": "parcel serve index.html",
"build": "parcel build index.html",
"cy:open": "cypress open",
"cy:run": "cypress run"
}
}

Tip: for my personal projects I prefer the short "cy:open" and "cy:run" script names. When sharing projects with others I prefer the explicit "cypress:open" and "cypress:run" script names.

Watch the script organization video below

Start and test

Right now to work on Cypress tests we need to start the application and then use the second terminal to execute npm run cy:open command. When we are done with testing, we would close the Cypress Test Runner, switch back to the first terminal and stop the app.

We can do it all using a single command by using start-server-and-test utility. Install it as a dev dependency with npm i -D start-server-and-test or yarn add -D start-server-and-test and then add a new script. For example, I would like to develop the application which means starting it and starting Cypress. Thus I would add dev script:

package.json
1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"format": "prettier --write 'cypress/**/*.js'",
"start": "parcel serve index.html",
"build": "parcel build index.html",
"cy:open": "cypress open",
"cy:run": "cypress run",
"dev": "start-test-and-test start 1234 cy:open"
}
}

Thus I can simply use npm run dev to do my local development. The script itself refers to other NPM scripts: it runs npm start in the background to start the app. The 1234 in the script refers to the port number - the utility start-server-and-test waits for this local port to respond. Once the localhost:1234 responds, it runs npm run cy:open. When I close Cypress, the utility automatically stops my application.

The utility registers two aliases in the node_modules/.bin folder: start-test-and-test and start-test. Thus I can use shorter command name:

1
"dev": "start-test start 1234 cy:open"

Since the npm start command is very command, start-test understands it could be the default, thus we can skip it:

1
"dev": "start-test 1234 cy:open"

Tip: for all options, consult the start-server-and-test README

If the script name dev is unavailable, I use something like e2e. For example, Next.js typically sets the dev script to start the next command, so my scripts in bahmutov/next-and-cypress-example uses e2e:

package.json
1
2
3
4
5
6
7
8
9
10
11
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"e2e": "start-test dev 3000 cy:open",
"cy:open": "cypress open",
"pree2e": "rm -rf .nyc_output coverage .next || true",
"check-coverage-limits": "check-total --min 100"
}
}

The CI script

Sometimes we want to simply run all the tests, without opening Cypress. Thus I typically have the second script to execute cy:run via npm test or npm t:

package.json
1
2
3
4
5
6
7
8
9
10
11
{
"scripts": {
"format": "prettier --write 'cypress/**/*.js'",
"start": "parcel serve index.html",
"build": "parcel build index.html",
"cy:open": "cypress open",
"cy:run": "cypress run",
"dev": "start-test 1234 cy:open",
"test": "start-test 1234 cy:run"
}
}

Whenever we clone the project we can install and test it:

1
2
3
4
npm it
## equivalent to
npm install
npm test

We can use the package-lock.json file when doing the install

1
2
3
4
npm cit
## equivalent to
npm ci
npm test

Watch the "start server and test" in action in this video

Quick run

For larger projects, the number of scripts grows, and the average length of the script name creeps towards Icelandic volcano's name. Sometimes we get variations on the cypress run script: one for recording the test results, another one for running in Chrome browser, etc

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"scripts": {
"format": "prettier --write 'cypress/**/*.js'",
"start": "parcel serve index.html",
"build": "parcel build index.html",
"cy:open": "cypress open",
"cy:run": "cypress run",
"cy:run:record": "cypress run --record",
"cy:run:chrome": "cypress run --browser chrome",
"cy:run:record:chrome": "cypress run --record --browser chrome",
"dev": "start-test 1234 cy:open",
"test": "start-test 1234 cy:run"
}
}

To quickly run the right script I suggest using my npm-quick-run utility. Install it globally with npm i -g npm-quick-run and then use nr alias to quickly run a script by its prefix:

1
2
3
nr d
## same as
npm run dev

If there are multiple scripts starting with the given prefix, the npm-quick-run lists them and exits

1
2
3
4
$ nr cy
running command with prefix "cy"
Several scripts start with "cy"
cy:open, cy:run, cy:run:record, cy:run:chrome, cy:run:record:chrome

Notice that our scripts use several words separated by the : character (the - works as well). We can give the prefix for several words to find the right script. We can use : or - when calling nr. I personally prefer using - since I can type it without pressing Shift key.

For example, let's open Cypress

1
2
$ nr c-o
## finds the script "cy:open" and runs it

Let's run tests in Chrome

1
2
$ nr c-r-c
## finds the script "cy:run:chrome" and runs it

The npm-quick-run utility also passes the arguments to the script it finds. For example, we could keep our original cypress run script and just pass CLI arguments when needed:

package.json
1
2
3
4
5
6
7
8
9
10
11
{
"scripts": {
"format": "prettier --write 'cypress/**/*.js'",
"start": "parcel serve index.html",
"build": "parcel build index.html",
"cy:open": "cypress open",
"cy:run": "cypress run",
"dev": "start-test 1234 cy:open",
"test": "start-test 1234 cy:run"
}
}
1
2
3
4
## record tests
nr c-r --record
## record tests in Chrome
nr c-r --record --browser chrome

Watch the NPM quick run in action in the video below

Stop dot

The npm-quick-run has a little utility for handling the number of words. If you place a dot character . at the end of the prefix, it will only match that number of words. Consider the following package.json file:

1
2
3
4
5
6
7
8
{
"scripts": {
"cypress": "cypress -help",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:record": "cypress run --record"
}
}

In order to run "cypress" script use prefix with "." at the end:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# same as "npm run cypress"
$ nr c. # finds the script with a single word starting with "c"

# same as "npm run cypress:open"
$ nr c-o.

# same as "npm run cypress:run"
$ nr c-r.

# these commands are equivalent
$ npm run cypress:run:record
$ yarn cypress:run:record
$ nr c-r-r
$ nr c-r-r.

Why did I typically use - and . characters to split the words and stop the match? Because I can type them without pressing the Shift key 🥳

Happy testing!