Front-end automation

using npm scripts

Nick Ribal

image/svg+xml

Front-end consultant, freelancer and a family man

    @elektronik

Do you gulp Mimosa?

Grunt over make?

Have broccoli for brunch?

I'll talk about

  • JS fatigue disclaimer

  • Why use them?

  • Basics and features:

    • Pre/Post hooks
    • Package metadata in environment variables
    • Configuration and overrides via arguments
    • Package executables path resolution
    • Passing arguments to scripts
  • Composing scripts

  • Limitations and workarounds

  • Sources and reference

npm scripts !== silver bullet

If it ain't broken - don't fix it

But when you start a new project or change a current one, try npm scripts

Because npm scripts are AWESOME!

How are npm scripts awesome?

No installation or deps: if you use node, it's built-in!

Aware of context: your package.json and system

Shell, unlike BEST-NEXT-THING-SINCE-SLICED-BREAD.js, is a long term investment and will remain relevant in years to come

Simple for simple tasks

Powerful for complex tasks: running and composing commands is actually what shell is made for

Uses shell scripts and environment variables: mechanisms which are battle tested for decades now

Integrate seamlessly with CI and CLI apps

Pick your poison

or cmd

use Bash

Neither is perfect, both beat JS at OS operations: files, streams, error handling, etc

Write business logic in general purpose programming languages, do systems stuff in shell

Let's create a package!

➜  mkdir fatigue-js; cd fatigue-js; npm init -y

Wrote to package.json:
{
  "name": "fatigue-js",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test":
      "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

"scripts"

{
  // ...
  "scripts": {
    "test":
      "echo \"Error: no test specified\" && exit 1"
  },
  // ...
}
➜  npm run test

> echo "Error: no test specified" && exit 1
Error: no test specified

Lets run and test our package!

declerative pre/post hooks

➜  npm run hello

> fatigue-js@1.0.0 prehello
> echo creating world...
creating world...

> fatigue-js@1.0.0 hello
> echo Hello, World!
Hello, World!

> fatigue-js@1.0.0 posthello
> echo destroying world...
destroying world...
"scripts": {
  "prehello": "echo creating world...",
  "hello": "echo Hello, World!",
  "posthello": "echo destroying world..."
}

package.json data in scripts & node

{
  "name": "fatigue-js",
  "version": "1.0.0",
  // ...
  "scripts": {
    "prehello":
      "echo preparing $npm_package_name@$npm_package_version...",
    "hello":
      "node -e 'console.log(`hello ${ process.env.npm_package_name }@${ process.env.npm_package_version }!`)'"
  },
  // ...
}

Readable by any environment aware app

package.json properties: in scripts and node (output)

➜  npm run hello

> fatigue-js@1.0.0 prehello
> echo preparing $npm_package_name@$npm_package_version...
preparing fatigue-js@1.0.0...

> fatigue-js@1.0.0 hello
> node -e 'console.log(`hello ${ process.env.npm_package_name }@${ process.env.npm_package_version }!`)'
hello fatigue-js@1.0.0!

package config in scripts/node + overriding via arguments

{
  // ...
  "config": {
    "port": 3000
  },
  "scripts": {
    "prestart":
      "echo Checking port $npm_package_config_port availability...",
    "start":
"node -e 'console.log(`Running on port`, process.env.npm_package_config_port)'",
    "dev":
      "npm --$npm_package_name:port=8080 run start"
  },
  // ...
}
➜  npm run start

> fatigue-js@1.0.0 prestart
> echo Checking port $npm_package_config_port availability...
Checking port 3000 availability...

> fatigue-js@1.0.0 start
> node -e "console.log('Running on port', process.env.npm_package_config_port)"
Running on port 3000
➜  npm run dev

> fatigue-js@1.0.0 dev
> npm --$npm_package_name:port=8080 run start

> fatigue-js@1.0.0 prestart
> echo Checking port $npm_package_config_port availability...
Checking port 8080 availability...

> fatigue-js@1.0.0 start
> node -e 'console.log(`Running on port`, process.env.npm_package_config_port)'
Running on port 8080
"config": {
  "port": 3000
}
➜  eslint --version
zsh: eslint: command not found...
➜  npm install --save-dev eslint
fatigue-js@1.0.0 /home/code/fatigue-js
└── eslint@3.10.2

Let's install a CLI package

Yay, let's see which version it is!

OK, let's see it... awkwardly?

➜  ./node_modules/eslint/bin/eslint.js --version
v3.10.2
➜  npm run lint -- --version

> fatigue-js@1.0.0 lint
> eslint "--version"
v3.10.2

npm adds package executables to path!

Let's try that again

{
  // ...
  "scripts": {
    "lint": "eslint"
  },
  "devDependencies": {
    "eslint": "3.10.2"
  }
}

-- arg1 arg2 argN

Built-in scripts

  • install

  • start

  • restart

  • stop

  • version

  • publish
  • test
  • uninstall

Same as custom scripts, but "run" can be omitted:

npm start

Can hook into npm lifecycle events

Composing serial and parallel commands

Serial execution

  • first && second

  • first; second

Parallel execution

parallel1 & parallel2 &

Serial after multiple parallels

(parallel1 & parallel2 & wait); serial

{
  // ...
  "scripts": {
    "lint": "eslint",
    "test": "jest",
    "build": "webpack -p",
    "prepublish":
      "npm run lint && npm run test && npm run build"
  },
  "devDependencies": {
    "eslint": "3.10.2",
    "jest": "17.0.3",
    "webpack": "2.1.0-beta.27"
  }
}

Serial execution

stops immediately on any failure

Serial execution failure

➜  npm publish
> fatigue-js@1.0.0 prepublish
> npm run lint && npm run test && npm run build

> fatigue-js@1.0.0 lint
> eslint
eslint [options] file.js [file.js] [dir]
# [full eslint output was here]

> fatigue-js@1.0.0 test
> jest
No tests found
# [full jest output was here]

> fatigue-js@1.0.0 build
> webpack -p
No configuration file found and no output filename configured via CLI option.
A configuration file could be named 'webpack.config.js' in the current directory.
Use --help to display the CLI options.

npm ERR! code ELIFECYCLE
npm ERR! fatigue-js@1.0.0 build: `webpack -p`
npm ERR! Exit status 255
{
  // ...
  "scripts": {
    "lint": "time sleep 2s; echo lint took 2s!",
    "test": "time sleep 1s; echo test took 1s!",
    "watch":
      "time (npm run lint & npm run test & wait) && echo done!"
  },
  // ...
}

Parallel execution

Parallel execution output:

➜  npm run watch

> fatigue-js@1.0.0 watch
> time (npm run lint & npm run test & wait) && echo done!

> fatigue-js@1.0.0 test
> time sleep 1s; echo test took 1s!

> fatigue-js@1.0.0 lint
> time sleep 2s; echo lint took 2s!

real	0m1.003s
user	0m0.000s
sys	0m0.001s
test took 1s!

real	0m2.001s
user	0m0.001s
sys	0m0.000s
lint took 2s!

real	0m2.579s
user	0m1.036s
sys	0m0.107s
done!

✗ Bash/cmd.exe interop is nearly impossible

✗  Very verbose output, especially for errors :(

  package.json is strict JSON:

  • Escaping required for anything that may be interpreted as JSON
  • no comments, no documentation
  • no newlines and formatting

Limitations and workarounds

✓ Use npm packages and call external scripts

Sources & reference

Thank you and

stay curious :)

Front-end automation using npm scripts

By Nick Ribal

Front-end automation using npm scripts

Modern front-end consists of many moving parts which must be glued together to help you build awesome apps. This is a practical guide to curing JavaScript task-runners' fatigue by using npm scripts!

  • 2,218