(Updated: )
/ #Productivity #javascript 

10 minute JavaScript: Library development in ES6 with Babel, Mocha and npm scripts

Warning: the ecosystem has moved on since this post was originally published, it’s now easier than ever to set up a library for development: see Writing an npm module with microbundle for an up to date view on how to develop a library

JavaScript has a thriving ecosystem of libraries delivered as packages on npm. Node has a high degree of compatibility with ES6 but it doesn’t have some features yet, namely import. This means that if we want to distribute useable packages over npm and we want to be writing ES6, we have to transpile back to ES5.

We’ll be setting up a project that allows us to write ES6, and distribute it in ES5 (with Babel) and to test it with Mocha, all through npm scripts. We’ll also discuss some workflows that we can use to speed up our development.

TL;DR Use this package.json, create a src folder, add an index.js and some .js and .test.js files inside it and off you go. You can also check out https://github.com/HugoDF/recursive-js, it’s a decent example project. :)

{
  "name": "library-js",
  "version": "1.0.0",
  "description": "Starter for library development with ES6, Babel, Mocha and npm scripts",
  "main": "./dist/index.js",
  "scripts": {
    "start": "npm run dev",
    "dev": "npm test -- -w",
    "init": "mkdir dist",
    "clean": "rm -rf dist",
    "prebuild": "npm run clean && npm run init",
    "build": "babel ./src -d ./dist --ignore test.js",
    "pretest": "npm run build",
    "test": "mocha --compilers js:babel-core/register ./src/**/*.test.js",
    "test:single": "mocha --compilers js:babel-core/register"
  },
  "author": "Hugo Di Francesco",
  "devDependencies": {
    "babel-cli": "^6.16.0",
    "babel-core": "^6.17.0",
    "babel-preset-es2015": "^6.16.0",
    "chai": "^3.5.0",
    "mocha": "^3.1.2"
  },
  "files": [
    "dist"
  ]
}
Table of Contents

Why npm scripts?

npm scripts live in your package.json. This means you don’t have to create a new configuration file just to run your tasks.

These scripts use the packages installed inside the project ( ./node_modules). This means you don’t have to install dependencies globally just to build your project.

This setup makes it easy to compose your tasks. For example our build task will require both build:js and test to run but we can run the JavaScript build and the tests independently as well.

There are also built-in pre and post hooks like pretest, preinstall or postpublish.

Basic boilerplate

We’re creating a library that implements map, filter and reduce in a recursive manner using ES6.

To learn more about these functions you can read this post: Recursion in JavaScript with ES6, destructuring and rest/spread The latest ECMA standard for JavaScript (ECMAScript 6) makes JavaScript more readable

We’ll runnpm init to generate a package.json that we will add our project scripts and dependencies to.

Our package.json should look something like this:

{
  "name": "recursive-js",
  "version": "1.0.0",
  "description": "Recursive JavaScript with ES6, destructuring and rest/spread",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Hugo Di Francesco"
}

This line:

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

Describes an npm script that can be called using npm run test. Since tests are run often, npm is kind enough to also alias npm test and npm t to npm run test. We can try it out:

$ npm t
> [email protected] test
> echo "Error: no test specified" && exit 1

Error: no test specified
npm ERR! Test failed.  See above for more details.

Which errors out, as expected since we don’t have any tests set up.

We should create a src directory that will contain our code, as well as a couple of files in there.

$ mkdir src
$ touch src/index.js src/map.js src/filter.js src/reduce.js

We can check that they’ve been created:

$ ls src
filter.js index.js map.js reduce.js

Let’s now put some ES6 code in our files (for more details on how these functions were implemented see Recursive JavaScript with ES6, destructuring and rest/spread).

function map([ head, ...tail ], fn) {
  if (head === undefined && !tail.length) return [];
  return tail.length ? [ fn(head), ...(map(tail, fn)) ] : [ fn(head) ];
}
function reduce([ head, ...tail ], fn, initial) {
  if(head === undefined && tail.length === 0) return initial;
  if(!initial) {
    const [ newHead, ...newTail] = tail;
    return reduce(newTail, fn, fn(head, newHead));
  }
  return tail.length ? reduce(tail, fn, fn(initial, head)) : [ fn(initial, head) ];
}
function filter([ head, ...tail ], fn) {
  const newHead = fn(head) ? [ head ] : [];
  return tail.length ? [ ...newHead, ...(filter(tail, fn)) ] : newHead;
}

Right now none of these functions can be loaded into another file, we’ll use ES6 default export to expose the functions so:

// map.js
export function map() { /* ... */ }

// filter.js
export function filter() { /* ... */ }

// reduce.js
export function reduce() { /* ... */ }

Node hasn’t implemented this export syntax yet (you can find out more about it on MDN), so we’ll need to transpile the code back to ES5 using something like Babel so that everyone using our package on npm is able to load it.

Let’s create a single entry point to all the functions in index.js:

// index.js
export * from './map';
export * from './filter';
export * from './reduce';

Initialise and teardown tasks

We will store our code *source *(ES6) in the src directory and the transpiled, distributable version of the code in a dist directory.

Let’s start adding some npm scripts:

init

We’ll create the dist folder:

// package.json in the "scripts" JSON object
"init": "mkdir dist"

We can try it out:

$ npm run init

> [email protected] init
> mkdir dist

$ ls
dist package.json src

clean

In this task we’ll want to remove the dist folder and all its content:

// package.json in the "scripts" JSON object
"clean": "rm -rf dist"

We can try it out:

$ ls 
dist package.json src

$ npm run clean

> [email protected] init
> rm -rf dist

$ ls
package.json src

prebuild

Before building we’ll want to reset the dist folder. We can easily do this by deleting it and recreating it. Using the && operator, we express that the first operation should complete successfully before the second one

// package.json in the "scripts" JSON object
"prebuild": "npm run clean && npm run init"

This is a case of a pre hook. This runs before the build task every time we run npm run build at the command line. Unfortunately we don’t have a build task yet but we can check that it works properly by running the right-hand operation at the command line (in sh or bash).

$ npm run clean && npm run init

> [email protected] clean
> rm -rf dist

> [email protected] init
> mkdir dist

Our package.json now looks like this:

{
  "name": "recursive-js",
  "version": "1.0.0",
  "description": "Recursive JavaScript with ES6, destructuring and rest/spread",
  "main": "index.js",
  "scripts": {
    "init": "mkdir dist",
    "clean": "rm -rf dist",
    "prebuild": "npm run clean && npm run init",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Hugo Di Francesco"
}

Building your JavaScript with Babel

To compile ES6 to ES5 we’ll need Babel. Install and save babel-cli and babel-preset-es2015 as devDependencies from npm:

$ npm install --save-dev babel-cli babel-preset-es2015

Build only one file

The Babel CLI command to compile one file takes an argument (source file) and a couple of flags ( -o for output file and –presets for Babel presets):

$ babel ./src/index.js -o ./dist/index.js --presets es2015
# compiles ./src/index.js to ES5 in ./dist/index.js

We could add this to package.json:

// package.json in the "scripts" JSON object
"build:single": "babel ./src/index.js -o ./dist/index.js --presets es2015"

However we want to transpile all the files.

Build all files

The flag to compile all files in a directory to a set output directory is -d, the command is therefore:

$ babel src -d dist --presets es2015 
# you need babel-cli installed globally for this to work

src/filter.js -> dist/filter.js
src/index.js -> dist/index.js
src/map.js -> dist/map.js
src/reduce.js -> dist/reduce.js

We can add it to the package.json:

// package.json in the "scripts" JSON object
"build": "babel src -d dist --presets es2015"

And we can test it:

$ npm run build

> [email protected] prebuild 
> npm run clean && npm run init

> [email protected] clean 
> rm -rf dist

> [email protected] init 
> mkdir dist

> [email protected] build 
> babel ./src -d ./dist --presets es2015

src/filter.js -> dist/filter.js
src/index.js -> dist/index.js
src/map.js -> dist/map.js
src/reduce.js -> dist/reduce.js

Notice how prebuild gets run before build, just like we said. :)

We should also change the “main” property of our package.json to ./dist/index.js.

Current state of package.json:

{
  "name": "recursive-js",
  "version": "1.0.0",
  "description": "Recursive JavaScript with ES6, destructuring and rest/spread",
  "main": "./dist/index.js",
  "scripts": {
    "init": "mkdir dist",
    "clean": "rm -rf dist",
    "prebuild": "npm run clean && npm run init",
    "build": "babel ./src -d ./dist --presets es2015",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Hugo Di Francesco",
  "devDependencies": {
    "babel-cli": "^6.16.0",
    "babel-preset-es2015": "^6.16.0"
  }
}

Running your tests with Mocha

*Mocha is a feature-rich JavaScript test framework that we’ll use to run the tests we write for our library. We’ll also use the *Chai Assertion Library to write our tests using the expect style. So we need to install both of these libraries.

npm install --save-dev mocha chai

Getting the environment ready

Mocha is able to run ES6 tests but we need to have babel-core available, so we should install that:

npm install --save-dev babel-core

Now we did say that we didn’t need configuration files for Babel but it’s just easier to create one for consistency across library and testing presets, put this in a file called .babelrc in the root of your project:

{
  "presets": ["es2015"]
}

There are currently multiple ways to separate the test files from the library source files. One of them is to use a test folder, the other is to name all the test files *.test.js. We’ll demonstrate how to use the latter style (the difference between the two is just a case of changing a UNIX-style path).

Adding some tests

We’ll add some tests for map.js:

import { expect } from 'chai';

import { map } from './map';

describe('map', () => {
  it('should maintain array length', () => {
    const arr = [ 1, 2, 3 ];
    expect(map(arr, x => x*2)).to.have.length(arr.length);
  });
  it('should return an empty array when passed an empty array', () => {
    expect(map([], x => x*2)).to.deep.equal([]);
  });
});

Save this under ./src/map.test.js.

And some tests for filter.js:

import { expect } from 'chai';

import { filter } from './filter';

describe('filter', () => {
  it('should remove items that don\'t evaluate to true when passed to predicate function', () => {
    const arr = [ 1, 2, 3 ];
    const expected = [ 2, 3 ];
    expect(filter(arr, x => x > 1)).to.deep.equal(expected);
  });
  it('should return an empty array when passed an empty array', () => {
    expect(filter([], x=> x === 1)).to.deep.equal([]);
  });
});

Run all the tests

We could theoretically run this at the command line, it’s a valid mocha command, although depending on how our machine is currently set up it may complain about some missing dependencies.

mocha --compiler js:babel-core ./src/**/*.test.js

If we pop that into our package.json replacing the test line:

// package.json in the "scripts" JSON object
"test": "mocha --compiler js:babel-core ./src/**/*.test.js"

We can now try running the tests:

$ npm t

> [email protected] test 
> mocha --compilers js:babel-core/register ./src/**/*.test.js

filter
    ✓ should remove items that don't evaluate to true when passed to predicate function
    ✓ should return an empty array when passed an empty array

map
    ✓ should maintain array length
    ✓ should return an empty array when passed an empty array

4 passing (293ms)

All of which pass! :)

Running a single test file

The path passed to the npm run command will be forwarded recursively so if we want to run just one test (and we want to pass a path):

mocha --compiler js:babel-core "./src/map.test.js"

We can call this test:single:

// package.json in the "scripts" JSON object
"test:single": "mocha --compiler js:babel-core"

To use it:

$ npm run test:single ./src/map.test.js

> [email protected] test:single
> mocha --compilers js:babel-core/register "./src/map.test.js"

map
    ✓ should maintain array length
    ✓ should return an empty array when passed an empty array

2 passing (279ms)

Using a test folder

Left as homework to work out the kinks, but the path is should be something like ./test/**/*.js.

Cleaning up the “build” task

We can remove –presets es2015 from the build task since we’ve added a .babelrc.

Let’s check that the build task still works:

$ npm run build

> [email protected] prebuild
> npm run clean && npm run init

> [email protected] clean
> rm -rf dist

> [email protected] init
> mkdir dist

> [email protected] build
> babel ./src -d ./dist --presets es2015

src/filter.js -> dist/filter.js
src/filter.test.js -> dist/filter.test.js
src/index.js -> dist/index.js
src/map.js -> dist/map.js
src/map.test.js -> dist/map.test.js
src/reduce.js -> dist/reduce.js

It works but:

src/filter.test.js -> dist/filter.test.js
src/map.test.js -> dist/map.test.js

This means it copies the test files over to dist which we don’t really need or want. Thankfully there’s a babel-cli flag for this, –ignore. We also now have a .babelrc so we don’t need to specify our –presets at the command line.

We can change build to:

"build": "babel ./src -d ./dist --ignore test.js",

So our final package.json is:

{
  "name": "recursive-js",
  "version": "1.0.0",
  "description": "Recursive JavaScript with ES6, destructuring and rest/spread",
  "main": "./dist/index.js",
  "scripts": {
    "init": "mkdir dist",
    "clean": "rm -rf dist",
    "prebuild": "npm run clean && npm run init",
    "build": "babel ./src -d ./dist --ignore test.js",
    "test": "mocha --compilers js:babel-core/register ./src/**/*.test.js",
    "test:single": "mocha --compilers js:babel-core/register"
  },
  "author": "Hugo Di Francesco",
  "devDependencies": {
    "babel-cli": "^6.16.0",
    "babel-core": "^6.17.0",
    "babel-preset-es2015": "^6.16.0",
    "chai": "^3.5.0",
    "mocha": "^3.1.2"
  }
}

Workflow suggestions

Build the code before every test

A lot of library development work is going to be test-driven (or at the command line). It’s a good idea to rebuild your dist files so that you don’t commit stale code (ie you actually commit what you were testing).

"pretest": "npm run build"

That was easy! Thanks to the the hooks npm scripts provide out of the box, all we had to do was add the command we would like to be run before tests run.

Build the code/run tests as you write

We can add the following npm scripts:

"start": "npm run dev",
"dev": "npm test -- -w",

We’re aliasing dev to start because start is one of the few scripts we can call without using npm run we can just do npm start (the other commands for which this works are: test, stop and restart).

– in npm scripts means “pass the following arguments straight to the called npm script”:

$ npm run dev

> [email protected] dev
> npm test -- -w

> [email protected] pretest
> npm run build

> [email protected] prebuild
> npm run clean && npm run init

> [email protected] clean
> rm -rf dist

> [email protected] init
> mkdir dist

> [email protected] build
> babel ./src -d ./dist --ignore test.js

src/filter.js -> dist/filter.js
src/index.js -> dist/index.js
src/map.js -> dist/map.js
src/reduce.js -> dist/reduce.js

> [email protected] test
> mocha --compilers js:babel-core/register ./src/**/*.test.js **"-w"**

filter
    ✓ should remove items that don't evaluate to true when passed to predicate function
    ✓ should return an empty array when passed an empty array

map
    ✓ should maintain array length
    ✓ should return an empty array when passed an empty array

4 passing (274ms)

The -w option for mocha will watch for file changes and rerun the command when they do. Which means our builds and tests get run automatically whenever anything changes.

Use the files field to describe what files should be installed

This is about being a good package maintainer. Just like we don’t babel-compile our tests into the dist folder, we should keep as much code out of the version of the package that is npm-installed by the end user.

The “files” field in package.json allows us to specify which files are required by the end user. In our case it’s just the dist folder.

"files": [
  "dist"
]

Note: instead of whitelisting which files we need, we could also blacklist items using an .npmignore file.

So that’s how to use npm scripts, Babel and Mocha to create a library using ES6 :). Feel free to write a reply with any questions.

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Get The Jest Handbook (100 pages)

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.