Babel Npm Scripts
I don’t say it often enough, although I did gush on @sebmck at
jsconf2015), I Babel.
If, like me, you want to…
…then this post is for you. Let’s Babelify our npm scripts.
If you haven’t used npm scripts for your build/development process yet, this just stands on the shoulders of giants like @linclark’s articles at the npm blog, @keithamus’s amazing article, and numerous examples from the repos of masters like tj. I’m still a noob at the commandline stuff, so if you have suggestions for improvement, please let me know!
- Get a
package.json
if you don’t already have one:npm init
. - Install Babel to your project:
npm -i --save-dev babel
. - Create scripts in a
/scripts
folder. - Wire it up to npm by editing your
package.json
file.
"scripts": {
"myscript": "./scripts/myscript.js",
....
}
At the top of your script file, include the shebang #! /usr/bin/env babel-node
. Now you can write your
script in ESnext syntax. If you aren’t passing flags (--
style arguments), everything will just work from here.
Here’s an example that reads an .env
file, spins up redis, then spins up a server.
#! /usr/bin/env babel-node | |
import {spawn} from 'child_process'; | |
import dotenv from 'dotenv'; | |
const [a, b, envName = 'development'] = process.argv; | |
dotenv.config({path: `./${envName}.env`}); | |
const log = console.log.bind(console); | |
const stdio = 'inherit'; | |
const commands = [ | |
spawn('redis-server', {stdio}), | |
spawn('./node_modules/.bin/babel-node', ['src/server'], {stdio}) | |
]; | |
commands.forEach(child => ['error', 'close'].forEach(e => child.on(e, log))); | |
process.on('SIGINT', () => commands.forEach(child => child.kill('SIGHUP'))); |
Here’s the development.env
file:
NODE_ENV=development
PORT=4000
REDIS_CONNECTION_STRING=redis://localhost:6379
JWT_TOKEN=super-secret
DEBUG=*
I can invoke this with npm start
or npm start <environment>
. The argument is passed through.
If you need more control over the execution, you can try using commanderjs.
I created a simple script to create a jwt token for my server based
on the .env
file specified as well as a user.
#! /usr/bin/env babel-node | |
import dotenv from 'dotenv'; | |
import program from 'commander'; | |
import jwt from 'jsonwebtoken'; | |
program | |
.usage('[options]') | |
.option('-u, --user <user id>', 'user id') | |
.option('-E, --env [value]', 'environment, defaults to development', 'development') | |
.parse(process.argv); | |
const {user, env} = program; | |
dotenv.config({path: `./${program.env}.env`}); | |
console.log(`creating ${env} token: ${JSON.stringify({user})}...`); | |
console.log(jwt.sign({user}, process.env.JWT_SECRET)); |
After adding this to your package.json
scripts, you have to pass flags using --
.
However, node will intercept its flags first, so don’t repeat any flags listed in node --help
(including --help
).
For example, npm run jwt -- --help
will output node’s help. But npm run jwt -- -h
will output commanderjs’s
help for our script. That’s the one flag you can’t control because it’s assumed by commanderjs. Here, I’ve
used -E
to avoid conflict with node’s -e --eval
. Creating a token is as simple as npm run jwt -- -u <username>
.
Lastly, I created a script to execute my tests and coverage. While it’s a bash one-liner, it’s pretty long and
I didn’t want to maintain it in that form. It was hard to break up because of how I
loaded the .env
file (it was a bash script that export
ed each variable). It also didn’t manage redis.
"scripts": {
"test-cov": "(. ./test.env && babel-node node_modules/.bin/isparta cover --report text --report html node_modules/.bin/_mocha --compilers js:babel/register)",
"test": "(. ./test.env && _mocha --compilers js:babel/register --reporter spec)",
}
Was replaced with:
#! /usr/bin/env babel-node | |
import {spawn} from 'child_process'; | |
import dotenv from 'dotenv'; | |
const [a, b, coverage] = process.argv; | |
dotenv.config({path: `./test.env`}); | |
const log = console.log.bind(console); | |
const stdio = 'inherit'; | |
const mocha = './node_modules/.bin/_mocha'; | |
const babel = './node_modules/.bin/babel-node'; | |
const mochaParams = ['--compilers', 'js:babel/register']; | |
const mochaReport = ['--reporter', 'spec']; | |
const covParams = ['node_modules/.bin/isparta', 'cover', '--report', 'text', '--report', 'html']; | |
const commands = []; | |
if (!process.env.CIRCLECI) { | |
commands.push(spawn('redis-server', {stdio})); | |
} | |
if (coverage) { | |
commands.push(spawn(babel, [...covParams, mocha, ...mochaParams], {stdio})); | |
} else { | |
commands.push(spawn(mocha, [...mochaParams, ...mochaReport], {stdio})); | |
} | |
commands.forEach(child => child.on('error', log)); | |
if (!process.env.CIRCLECI) { | |
commands[1].on('close', () => commands[0].kill('SIGINT')); | |
} | |
process.on('SIGINT', () => commands.forEach(child => child.kill('SIGHUP'))); |
The only take-away here is that because of the execution context, you can’t just use the npm
reference of a .bin
file, you have to use the full path. Because this script can run on the CI server, and it already has redis,
I made the redis commands conditional.