April 13, 2016

Creating Modules in JavaScript with ES7 and Babel

by Peter van der Zee

ES7-Babel

Last year the new version of JavaScript was released and it gave us a lot of new goodies. Amongst those was syntax for importing and exporting of modules which finally codified “the only way” to do modules in JavaScript. Or well, eventually. Another nice thing is that it’s specced in such a way that you can statically analyze the whole module dependency tree. Pretty awesome.

Let’s take a quick look at what they are:

import v from "mod";
import * as obj from "mod";
import {
    x
}
from "mod";
import {
    x as v
}
from "mod";
import "mod";

export var v;
export default function f() {};
export default function() {};
export default 42;
export {
    x
};
export {
    x as v
};
export {
    x
}
from "mod";
export {
    x as v
}
from "mod";
export * from "mod";

So basically you can import the main value of a module (the “default”), or a specific property from explicit exports, a combination of this, or everything. In symmetry you can export one value for the module as the default, or an object with multiple properties. You can also export these properties one by one. I’ll leave the preferred style to the style guides.

For ES7 there are some small additions proposed to extend this syntax.

export * as ns from "mod";
export v from "mod";

Nothing shocking, but when can we use this? Well. There’s no time like the present. As with many syntactical features from ES6, you can use a tool called Babel to translate them back to ES5 as long as support for them doesn’t cover your runtime targets. Then once your targets do support them out of the box you can tell Babel not to translate them anymore.

Let’s take a look at the setup required for this. We’ll do this on Node.js and NPM. Let’s try to execute this file;

src/letter_keys.js

// you would have a constant for each key
// (I would normally uppercase all constants)
const a = 119;
const d = 100;
const s = 115;
const w = 119;

// you would export all keys here
// note: you can't say `w: 119` here. It just isn't valid.
// This destructures to `w: w, a: a, ...`
export {
    w,
    a,
    d,
    s,
}

src/arrow_keys.js

const UP = 38;
const RIGHT = 39;
const DOWN = 40;
const LEFT = 37;

export {
    UP,
    RIGHT,
    DOWN,
    LEFT,
}

src/move.js

export {
    a, w, s, d
}
from './letter_keys';
export * as ARROWS from './arrow_keys';

The idea is that there is a main index.js file which exports stuff from internal modules. It assumes these keys are exported from the other files. The example is convoluted but that’s not very relevant.

src/index.js

import * as keys from './move';
console.log(keys);

This would be part of a project that depends on this module and it should print out the awsd keys as well as the arrows object. Let’s get crackin’ with npm first. Create the repo dir and initialize it.

~$ mkdir foo
    ~$ cd foo~/foo$ mkdir src#
put src files above in ~/foo/src~/foo$ npm init -yes~/foo$ npm install babel-cli babel-preset-es2015 babel-preset-stage-1 -D

This may take a minute. As you may have already guessed, babel-cli allows us to run Babel (6) from the command line and the babel-preset-stage-1 package contains the relevant ES7 module translation stuff (at the time of writing…). The -yes flag will cause npm to create a default package.json without asking questions. The -D flag is short for --save-dev which adds the packages under the devDependency entry in package.json for you. We add the presets to the default babel configuration file:

.babelrc

{
    "presets": ["es2015", "stage-1"]
}

If this works for you, that’s awesome, hello future! But these examples wouldn’t run in ES6, let alone Node.js at the time of writing. With these translation steps it can be executed anyways.

There should now also be a near empty package.json file, which contains those three dev dependencies we added. Let’s add a script to that package.json to do our translation:

  ...
  "scripts": {
      "test": "echo "
      Error: no test specified " && exit 1",
      "translate": "node_modules/babel-cli/bin/babel-node.js src/index.js"
  },
  ...

(Only added the “translate” line and the comma behind the “test” line).
The translate script is a build step. The final package.json file contents as used for this post (with fixed versions) can be found at the end of this article. Now all that is left is to call npm run translate to translate and run the code;

~/foo$ npm run translate --silent

{
    A: [Getter],
    W: [Getter],
    S: [Getter],
    D: [Getter],
    ARROWS: {
        UP: 38,
        RIGHT: 39,
        DOWN: 40,
        LEFT: 37
    }
}

Hurray! Now for bonus points we can use Jscrambler to mangle that a little further. We can pass on the Babel translated code, so why not!

~/foo$ npm install jscrambler -D

Our (final) package.json now looks like this:

package.json

{
    "name": "foo",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo "
        Error: no test specified " && exit 1",
        "translate": "node_modules/babel-cli/bin/babel-node.js src/index.js"
    },
    "keywords": [],
    "author": "Your Name <[email protected]> (http://localhost/)",
    "license": "ISC",
    "devDependencies": {
        "babel-cli": "6.6.5",
        "babel-preset-es2015": "6.6.0",
        "babel-preset-stage-1": "6.5.0",
        "jscrambler": "0.7.5"
    }
}

Set up the config like you normally do (using Node.js requires a pro account), here’s the file I used (If you want to know more about how to setup this file let this serve as an example and npm for further documentation):

.jscramblerrc

{
    "keys": {
        "accessKey": "See https://jscrambler.com/en/account/api_access",
        "secretKey": "See https://jscrambler.com/en/account/api_access"
    },
    "params": {
        "constant_folding": "%DEFAULT%",
        "dead_code": "%DEFAULT%",
        "dead_code_elimination": "%DEFAULT%",
        "dictionary_compression": "%DEFAULT%",
        "dot_notation_elimination": "%DEFAULT%",
        "function_outlining": "%DEFAULT%",
        "function-reorder": "%DEFAULT%",
        "literal_duplicates": "%DEFAULT%",
        "literal_hooking": "2;8",
        "member_enumeration": "%DEFAULT%",
        "mode": "nodejs",
        "rename_local": "%DEFAULT%",
        "string_splitting": "0.3",
        "whitespace": "%DEFAULT%"
    }
}

We’ll use a script to wrap it all together. This script will translate the original files with Babel, output them to /build folder, then have Jscrambler mangle them and put the result in /dist folder where we can run it as we normally would without using ES7 features.

run.sh

#!/bin/sh

echo "Babelifying src/*.js"
node_modules / babel - cli / bin / babel.js - d build src
    /*.js
    echo "Scrambling build/*.js"
    node_modules/jscrambler/bin/jscrambler -o dist build/src/**
    echo "Clean up artifacts"
    mv dist/build/src/* dist/
    rmdir dist/build/src
    rmdir dist/build
    echo "Done! See dist/scrambled.js"
    echo "Running:"
    node dist/index.js*/

Make it runnable:

chmod + x run.sh

And… run it!

~/foo$ ./run.sh
Babelifying src
/*.js
src/arrow_keys.js -> build/src/arrow_keys.js
src/index.js -> build/src/index.js
src/letter_keys.js -> build/src/letter_keys.js
src/move.js -> build/src/move.js
Scrambling build/*.js
Clean up artifacts
Done! See dist/ for your scrambled files
Running:
{ a: [Getter],
  w: [Getter],
  s: [Getter],
  d: [Getter],
  ARROWS: { UP: 38, RIGHT: 39, DOWN: 40, LEFT: 37 } }*/

You can investigate the results in the /dist folder yourself. It’ll be a far cry from the original source since it has been protected with Jscrambler, but still run.

And there you go, have fun working in ES7, right now!