Saturday, March 5, 2016

5 npm script secrets

No Secret

Last week I was hosting a workshop about Electron Shell, ES6 and Reactjs at ForwardJS in San Francisco. Preparing all the sample code, I was inspired by this blog post to avoid any build tool like grunt or gulp at all. In fact, I wanted to remove anything that could distract the attendees of the workshop by throwing another unknown toolset at them. Instead, leveraging the build in capabilities of npm.

The Idea

Why use grunt, gulp, etc. if you can leverage the power of npm scripts? Not having to maintain a build script is a good idea and helps to remove another piece of complexity from your project.


Different types of scripts

npm supports custom scripts defined by scripts property of package.json. npm distinguishes between 2 different kind of scripts

  • lifecycle scripts

  • directly-run scripts

Lifecycle scripts tie into a specific phase of your project lifecycle, like install, publish, start, stop, etc. These scripts can be executed with npm lifecycle-script. Each lifecycle script can have a pre and post script for that phase, e.g. preinstall and postinstall.

The directly-run scripts need the extra run command after npm, e.g. npm run *my-script-name*. Your directly-run scripts can have a pre and post step with the same name as your script name, e.g. premy-script-name and *postmy-script-name. Always without space between the prefix and the script name.

You can read more about it in the official docs.

Tip Call your own scripts always with npm run <script-name>.

Platform differences

Sometimes you need to do things differenly when it comes to build your project on multiple platforms. I ran into the issue, where I needed to copy files. On OSX this is as simple as cp srcfile.txt destfile.txt. On Windows, this didn’t work, since cp is an unknown command, unless you have cygwin installed. Building your project in the default Windows command prompt fails with an error, once you want to copy those important files. On Windows you should use copy instead.

Tip Create an external copy.js file and do the OS specific tasks there. You can use process.platform === 'win32' to determine if you are running Windows.

DRY - refer to anything defined in your package.json

DRY (don’t repeat yourself) is a great principle when it comes to software development. You can even refer to anything defined in your package.json when you execute your script. Let’s assume, you want to create a zip for distribution.

"scripts": {
    "create-zip": "zip -r dist"

Now you can call npm run create-zip to create a zip containing all the files to distribute your app.

But why can’t we just refer to the name of the app and the version already defined in package.json? Well, we can. npm exposes all config keys from package.json in the form npm_package_configkey for you to use in your scripts.

"scripts": {
    "create-zip": "zip -r $npm_package_name-$ $npm_package_distdir"
Tip Stay DRY! Refer to information already available in package.json. You can define whatever config option in package.json and refer to it in the same way.

Call any script in another script

What if you want to run more than one script at the same time? You want for example watch *.js files and *.scss files and preprocess them? How can you do this with one npm script?

You can concatenate different scripts and run them all at the same time:

"scripts": {
    "sass-watch": "node-sass -watch src/style.scss dest/style.css",
    "babel-watch": "babel -w *.js dist",
    "watch": "npm run sass-watch & npm run babel-watch" // 1
1 npm run watch will call both custom scripts and run them at the same time. This will create two processes that run independently from each other.
Tip Concatenate multiple scripts with & to run them as one script.

Run any script in your node_modules/.bin directory

Installing any node module that adds it’s own executable script, will be available from projectDir/node_modules/.bin. Let’s say, you need babel for your project and you installed it with npm install babel-cli --save-dev, you can run babel from within your project directory by typing ./node_modules/.bin/babel. That is great, since you don’t have to rely on a global installation of babel.

If you want to run babel as part of a script, you would probably specify the whole path to the babel script:

"scripts": {
    "babel-watch": "./node_modules/.bin/babel -w *.js dist"

Since npm adds all executable script to the PATH for the script, you can instead write

"scripts": {
    "babel-watch": "babel -w *.js dist"
Tip Don’t specify the full path to a script in your projects node_modules/.bin folder. Use the command without the path.

Thank you very much for reading. If you have any comments or questions, please leave them in the comments below.


Burger 01 said...

> zip -r dist

'zip' is not recognized as an internal or external command,
operable program or batch file.

Ingo Richter said...

Oh well, you are right. Doing this on Windows will most likely end up in that error. I should have noted that. You need to install some zip tool and add it to the path. Would be better to have a good cross platform solution, when it comes to zipping, instead of calling zip directly.