DEV Community

Cover image for Mastering NPM Scripts
Paula Santamaría
Paula Santamaría

Posted on

Mastering NPM Scripts

You may have come across the scripts property in the package.json file and even write some scripts yourself. But do you know all you can do with NPM Scripts?

I've been using NPM Scripts for years, but I wanted to pass a parameter to a script a few weeks ago and realized I didn't know how to do that. That's when I decided to learn everything I could about NPM scripts and write this article.

In this article, I'll share my research about how to take full advantage of NPM scripts.


Introduction

NPM Scripts are a set of built-in and custom scripts defined in the package.json file. Their goal is to provide a simple way to execute repetitive tasks, like:

  • Running a linter tool on your code
  • Executing the tests
  • Starting your project locally
  • Building your project
  • Minify or Uglify JS or CSS

You can also use these scripts in your CI/CD pipeline to simplify tasks like build and generate test reports.

To define an NPM script, all you need to do is set its name and write the script in the script property in your package.json file:

{
    "scripts": {
        "hello-world": "echo \"Hello World\""
    }
}
Enter fullscreen mode Exit fullscreen mode

It's important to notice that NPM makes all your dependencies' binaries available in the scripts. So you can access them directly as if they were referenced in your PATH. Let's see it in an example:

Instead of doing this:

{
    "scripts": {
        "lint": "./node_modules/.bin/eslint .",
    }
}
Enter fullscreen mode Exit fullscreen mode

You can do this:

{
    "scripts": {
        "lint": "eslint ."
    }
}
Enter fullscreen mode Exit fullscreen mode

npm run

Now all you need to do is run npm run hello-world on the terminal from your project's root folder.

> npm run hello-world

"Hello World"
Enter fullscreen mode Exit fullscreen mode

You can also run npm run, without specifying a script, to get a list of all available scripts:

> npm run

Scripts available in sample-project via `npm run-script`:
    hello-world
        echo "Hello World"
Enter fullscreen mode Exit fullscreen mode

As you can see, npm run prints both the name and the actual script for each script added to the package.json.

ℹ️ npm run is an alias for npm run-script, meaning you could also use npm run-script hello-world. In this article, we'll use npm run <script> because it's shorter.

Built-in scripts and Aliases

In the previous example, we created a custom script called hello-world, but you should know that npm also supports some built-in scripts such as test and start.

Interestingly, unlike our custom scripts, these scripts can be executed using aliases, making the complete command shorter and easier to remember. For example, all of the following commands will run the test script.

npm run-script test
npm run test
npm test
npm t
Enter fullscreen mode Exit fullscreen mode

Similarly to the test command, all of the following will run the start command:

npm run-script start
npm run start
npm start
Enter fullscreen mode Exit fullscreen mode

For these built-in scripts to work, we need to define a script for them in the package.json. Otherwise, they will fail. We can write the scripts just as any other script. Here's an example:

{
    "scripts": {
        "start": "node app.js",
        "test": "jest ./test",
        "hello-world": "echo \"Hello World\""
    }
}
Enter fullscreen mode Exit fullscreen mode

Executing multiple scripts

We may want to combine some of our scripts and run them together. To do that, we can use && or &.

  • To run multiple scripts sequentially, we use &&. For example: npm run lint && npm test
  • To run multiple scripts in parallel, we use &. Example: npm run lint & npm test
    • This only works in Unix environments. In Windows, it'll run sequentially.

So, for example, we could create a script that combines two other scripts, like so:

{
    "scripts": {
        "lint": "eslint .",
        "test": "jest ./test",
        "ci": "npm run lint && npm test"
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding errors

When a script finishes with a non-zero exit code, it means an error occurred while running the script, and the execution is terminated.

That means we can purposefully end the execution of a script with an error by exiting with a non-zero exit code, like so:

{
    "scripts": {
        "error": "echo \"This script will fail\" && exit 1"
    }
}
Enter fullscreen mode Exit fullscreen mode

When a script throws an error, we get a few other details, such as the error number errno and the code. Both can be useful for googling the error.

And if we need more information, we can always access the complete log file. The path to this file is provided at the end of the error message. On failure, all logs are included in this file.

Run scripts silently or loudly

Use npm run <script> --silent to reduce logs and to prevent the script from throwing an error.

The --silent flag (short for --loglevel silent) can be helpful when you want to run a script that you know may fail, but you don't want it to throw an error. Maybe in a CI pipeline, you want your whole pipeline to keep running even when the test command fails.

It can also be used as -s: npm run <script> -s

ℹ️ If we don't want to get an error when the script doesn't exists, we can use --if-present instead: npm run <script> --if-present.

About log levels

We saw how we can reduce logs using --silent, but what about getting even more detailed logs? Or something in between?

There are different log levels: "silent", "error", "warn", "notice", "http", "timing", "info", "verbose", "silly". The default is "notice". The log level determines which logs will be displayed in the output. Any logs of a higher level than the currently defined will be shown.

We can explicitly define which loglevel we want to use when running a command, using --loglevel <level>. As we saw before, the --silent flag is the same as using --loglevel silent.

Now, if we want to get more detailed logs, we'll need to use a higher level than the default ("notice"). For example: --loglevel info.

There are also short versions we can use to simplify the command:

  • -s, --silent, --loglevel silent
  • -q, --quiet, --loglevel warn
  • -d, --loglevel info
  • -dd, --verbose, --loglevel verbose
  • -ddd, --loglevel silly

So to get the highest level of detail we could use npm run <script> -ddd or npm run <script> --loglevel silly.

Referencing scripts from files

You can execute scripts from files. This can be useful for especially complex scripts that would be hard to read in the package.json file. However, it doesn't add much value if your script is short and straightforward.

Consider this example:

{
    "scripts": {
        "hello:js": "node scripts/helloworld.js",
        "hello:bash": "bash scripts/helloworld.sh",
        "hello:cmd": "cd scripts && helloworld.cmd"
    }
}
Enter fullscreen mode Exit fullscreen mode

We use node <script-path.js> to execute JS files and bash <script-path.sh> to execute bash files.

Notice that you can't just call scripts/helloworld.cmd for CMD and BAT files. You'll need to navigate to the folder using cd first. Otherwise, you'll get an error from NPM.

Another advantage of executing scripts from files is that, if the script is complex, it'll be easier to maintain in a separate file than in a single line inside the package.json file.

Pre & Post

We can create "pre" and "post" scripts for any of our scripts, and NPM will automatically run them in order. The only requirement is that the script's name, following the "pre" or "post" prefix, matches the main script. For example:

{
    "scripts": {
        "prehello": "echo \"--Preparing greeting\"",
        "hello": "echo \"Hello World\"",
        "posthello": "echo \"--Greeting delivered\""
    }
}
Enter fullscreen mode Exit fullscreen mode

If we execute npm run hello, NPM will execute the scripts in this order: prehello, hello, posthello. Which will result in the following output:

> script-test@1.0.0 prehello
> echo "--Preparing greeting"

"--Preparing greeting"

> script-test@1.0.0 hello
> echo "Hello World"

"Hello World"

> script-test@1.0.0 posthello
> echo "--Greeting delivered"

"--Greeting delivered"
Enter fullscreen mode Exit fullscreen mode

ℹ️ If we run prehello or posthello individually, NPM will not automatically execute any other scripts. It only works if you run the "main" script, in this case, hello.

Access environment variables

While executing an NPM Script, NPM makes available a set of environment variables we can use. These environment variables are generated by taking data from NPM Configuration, the package.json, and other sources.

Configuration parameters are put in the environment using the npm_config_ prefix. Here are a few examples:

{
    "scripts": {
        "config:loglevel": "echo \"Loglevel: $npm_config_loglevel\"",
        "config:editor": "echo \"Editor: $npm_config_editor\"",
        "config:useragent": "echo \"User Agent: $npm_config_user_agent\""
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's see what we get after executing the above commands:

> npm run config:loglevel
# Output: "Loglevel: notice"

> npm run config:editor
# Output: "Editor: notepad.exe"

> npm run config:useragent
# Output: "User Agent: npm/6.13.4 node/v12.14.1 win32 x64"
Enter fullscreen mode Exit fullscreen mode

ℹ️ You can also run npm config ls -l to get a list of all the configuration parameters available.

Similarly, package.json fields, such as version and main, are included with the npm_package_ prefix. Let's see a few examples:

{
    "scripts": {
        "package:main": "echo \"Main: $npm_package_main\"",
        "package:name": "echo \"Name: $npm_package_name\"",
        "package:version": "echo \"Version: $npm_package_version\""
    }
}
Enter fullscreen mode Exit fullscreen mode

The results from these commands will be something like this:

> npm run package:main
# Output: "Main: app.js"

> npm run package:name
# Output: "Name: npm-scripts-demo"

> npm run package:version
# Output: "Version: 1.0.0"
Enter fullscreen mode Exit fullscreen mode

Finally, you can add your own environment variables using the config field in your package.json file. The values setup there will be added as environment variables using the npm_package_config prefix.

{
    "config": {
        "my-var": "Some value",
        "port": 1234
    },
    "script": {
        "packageconfig:port": "echo \"Port: $npm_package_config_port\"",
        "packageconfig:myvar": "echo \"My var: $npm_package_config_my_var\""
    }
}
Enter fullscreen mode Exit fullscreen mode

If we execute both commands we'll get:

> npm run packageconfig:port
# Output: "Port: 1234"

> npm run packageconfig:myvar
# Output: "My var: Some value"
Enter fullscreen mode Exit fullscreen mode

ℹ️ In Windows' cmd instead of $npm_package_config_port you should use %npm_package_config_port% to access the environment variables.

Passing arguments

In some cases, you may want to pass some arguments to your script. You can achieve that using -- that the end of the command, like so: npm run <script> -- --argument="value".

Let's see a few examples:

{
    "scripts": {
        "lint": "eslint .",
        "test": "jest ./test",
    }
}
Enter fullscreen mode Exit fullscreen mode

If I wanted to run only the tests that changed, I could do this:

> npm run test -- --onlyChanged
Enter fullscreen mode Exit fullscreen mode

And if I wanted to run the linter and save the output in a file, I could execute the following command:

> npm run lint -- --output-file lint-result.txt
Enter fullscreen mode Exit fullscreen mode

Arguments as environment variables

Another way of passing arguments is through environment variables. Any key-value pairs we add to our script will be translated into an environment variable with the npm_config prefix. Meaning we can create a script like this:

{
    "scripts": {
        "hello": "echo \"Hello $npm_config_firstname!\""
    }
}
Enter fullscreen mode Exit fullscreen mode

And then use it like so:

> npm run hello --firstname=Paula
# Output: "Hello Paula"
Enter fullscreen mode Exit fullscreen mode

Naming conventions

There are no specific guidelines about how to name your scripts, but there are a few things we can keep in mind to make our scripts easier to pick up by other developers.

Here's my take on the subject, based on my research:

  • Keep it short: If you take a look at Svelte's NPM Scripts, you'll notice that most script names are one word only. If we can manage to keep our script names short, it'll be easier to remember them when we need them.
  • Be consistent: You may need to use more than one word to name your script. In that case, choose a naming style and stick to it. It can be camelCase, kebab-case, or anything you prefer. But avoid mixing them.

Prefixes

One convention that you may have seen is using a prefix and a colon to group scripts, for example, "build:prod". This is simply a naming convention. It doesn't affect your scripts' behavior but can be helpful to create groups of scripts that are easier to identify by their prefixes.

Example:

{
    "scripts": {
        "lint:check": "eslint .",
        "lint:fix": "eslint . --fix",
        "build:dev": "...",
        "build:prod": "..."
    }
}

Enter fullscreen mode Exit fullscreen mode

Documentation

Consider adding documentation for your scripts so other people can easily understand how and when to use them. I like to add a few lines explaining each script on my Readme file.

The documentation for each available script should include:

  • Script name
  • Description
  • Accepted arguments (optional)
  • Links to other documentation (optional): For example, if your script runs tsc --build, you may want to include a link to Typescript docs.

Conclusion

This is all I managed to dig up about NPM Scripts. I hope you find it useful! I certainly learned a lot just by doing this research. It took me way more time than I thought it would, but it was totally worth it.

Let me know if there's anything missing that you'll like to add to make this guide even more complete! 💬

Oldest comments (32)

Collapse
 
bgrand_ch profile image
Benjamin Grand

Thanks, very helpful ☺️

Collapse
 
paulasantamaria profile image
Paula Santamaría

Thanks, I'm glad to hear that!

Collapse
 
jonton profile image
Jonathan Wong

Loved this article - definitely helped refresh some topics for me when I moved to a new codebase!

Collapse
 
paulasantamaria profile image
Paula Santamaría

Thanks, Jonathan! Writing it definitely helped me refresh some stuff and learn new tricks. Happy to see it helped others as well.

Collapse
 
ignassedunovas profile image
ignas-sedunovas

On Windows "hello:cmd": "cd scripts && helloworld.cmd" could be written as "hello:cmd": "scripts\\helloworld.cmd"

Collapse
 
paulasantamaria profile image
Paula Santamaría

That didn't work for me on Windows (I tried it in CMD and PowerShell), that's why I went for the cd alternative. Did it work for you?

Collapse
 
ignassedunovas profile image
ignas-sedunovas

Yes. But only when using double backslashes as directory separator.

Thread Thread
 
paulasantamaria profile image
Paula Santamaría

I see it now, thank you! I'll update the post ASAP.

Collapse
 
activenode profile image
David Lorenz • Edited

I am seeing a lot of posts these days that I barely read cause it's always the same ish. But this one is a nice one. Really. Good job here

Collapse
 
paulasantamaria profile image
Paula Santamaría

Thank you, David! I'm glad you enjoyed it.

Collapse
 
anonymousm profile image
mludovici

very cool, some nice new stuff that I learned :)

Collapse
 
paulasantamaria profile image
Paula Santamaría

Thank you! Glad to hear that :)

Collapse
 
rajmohanpdy profile image
rajmohan s

cool tricks. useful information regarding scripts.

Collapse
 
paulasantamaria profile image
Paula Santamaría

Great! I'm glad you found it useful :)

Collapse
 
ruyadorno profile image
Ruy Adorno

Super useful reference! Thanks for putting it all together ❤️

Collapse
 
paulasantamaria profile image
Paula Santamaría • Edited

Thanks, Ruy! Happy to help :)

Collapse
 
johnb21 profile image
Johnb21

This article is filled with so much goodness and very useful information! Thank you!

Collapse
 
paulasantamaria profile image
Paula Santamaría

Glad to hear that 😊

Collapse
 
weakish profile image
Jang Rush

Hi, I'd like to translate this awesome tutorial to Chinese. Can you give me the permission? The translated text will be published at nextfe.com

Collapse
 
paulasantamaria profile image
Paula Santamaría

Sure! It's ok, as long as you credit me :)

Collapse
 
weakish profile image
Jang Rush

Chinese translation published: nextfe.com/mastering-npm-scripts/

There is a backlink to this original post at the beginning of the translated text.

Thread Thread
 
paulasantamaria profile image
Paula Santamaría

Thank you!

Collapse
 
wojtekmaj profile image
Wojciech Maj

I thought I knew everything about NPM scripts, or most of it at least. You proved me wrong. Thanks!

Collapse
 
paulasantamaria profile image
Paula Santamaría

Writing it had the same effect on me :P Thanks, Wojciech!

Collapse
 
jaballadares profile image
John Balladares

This is the most useful article I have seen on NPM scripts. Thank you for:

  1. Being so thorough yet clear
  2. Providing examples (I feel like I knew I could have a package.json to hold global configs like ES Lint, but this really drove it home)

Wish there was a repo with useful NPM scripts.

Collapse
 
paulasantamaria profile image
Paula Santamaría

Thanks, John! I'm glad it was helpful.
I was going to create a repo with examples to reference in this post, but in the end, it was taking me too long, and decided to just include the examples in the post. Maybe I'll keep working in the repo later 🤔

Collapse
 
jaballadares profile image
John Balladares

Yay! I would definitely take a look at that.

Is it common for folks to have a package.json file in their home directory for global access to NPM scripts/

Collapse
 
damiensavoldelli profile image
damienSavoldelli

Thanks, very helpful. I was looking for how passing arguments as variable and I find this article with useful informations ! Thanks a lot ! 😃

Collapse
 
paulasantamaria profile image
Paula Santamaría

Glad you find it helpful! :)

Collapse
 
vincenguyen94 profile image
VINCENGUYEN94

Thank you ! You made my day :)

Collapse
 
strdr4605 profile image
Dragoș Străinu

Here is a tool to document npm scripts:
npx why

Collapse
 
igorsantos07 profile image
Igor Santos • Edited

This article is very concise and complete! That's rare to see around 🙏

One thing to keep in mind about parallel scripts: when you put & at the end of a bash command you are, actually, placing that command in background. This means that, if you use that with a server command, you'll never be able to close it with Ctrl^C!

I realized that when trying to run the Firebase emulators + Svelte dev server in a single command. I can't find a clean solution to that, unfortunately 🤷🏻‍♂️
The best I could do is run a couple of tmux commands to create a single process with two outputs, but it's a bit convoluted and makes your project dependent on a platform feature (having tmux installed). I got a quick introduction to tmux in this article, and ChatGPT gave me the right commands to control the tmux session (since tmux --help is of no help lol).