01 October 2015

I don’t say it often enough, although I did gush on @sebmck at jsconf2015), I :heart: Babel.

If, like me, you want to…

Babel all the things

…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!

  1. Get a package.json if you don’t already have one: npm init.
  2. Install Babel to your project: npm -i --save-dev babel.
  3. Create scripts in a /scripts folder.
  4. 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')));
view raw script_start.js hosted with ❤ by GitHub

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));
view raw script_jwt.js hosted with ❤ by GitHub

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 exported 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')));
view raw script_test.js hosted with ❤ by GitHub

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.



Discussion: