Understanding JavaScript Modules: Bundling & Transpiling

Mark Brown
Share

This article was peer reviewed by Dan Prince and Ravi Kiran. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Most folks consider modules, dependency management and dynamic loading a basic requirement for any modern programming language—these are some of the most important features added to JavaScript in 2015.

Modules are used extensively in Node but our focus here will be on how we can use modules inside of the browser. We’ll explore a little history, navigate through the hazardous current landscape with the end goal of having a clear path forward and appreciation for the most important module bundlers for JavaScript today: Browserify, Webpack and jspm.

Finally we’ll look at how to use these tools with transpilers like CoffeeScript, TypeScript and Babel.

Modules through the Ages

JavaScript has existed since 1995 and to this day no browser supports modules natively. Node and CommonJS were created in 2009 and the vast majority of packages in npm use CommonJS modules.

Browserify was released in 2011 and brought CommonJS modules to the browser allowing client-side JavaScript to require npm packages. The tool bundles up all of the required dependencies into a single JavaScript file.

The Past

A library such as jQuery adds $ to the global scope or window.

window.$ = function() { ... };

We include a script to a library and use the global objects it exposes.

<script src="jquery.js"></script>
<script>
$(function() { ... });
</script>

Your own application code was typically namespaced under a global like App to prevent polluting the global scope. Without this it’s only so long before you have name collisions and things fall apart.

var App = {};
App.Models = {};
App.Models.Note = function() {};

The Future

Libraries export objects in a common module format (ES6 modules).

export default function $() { ... }

We import a module into a local scope and use it.

import $ from 'jquery';

$(function() { ... });
  • No globals required 👍
  • Source order independence
  • Access to npm
  • No need to namespace your own application code
  • Dynamically load modules at any time as required

The Present

It’s really really complicated. First, there’s a variety of module formats out there in use:

Tools for bundling assets come in a variety of shapes and sizes:

Then there’s transpilers that you may want to use:

In addition, there are various libraries that allow dynamic loading of modules:

These are shortened lists of popular tools currently in use—it’s a minefield for beginners and experts alike. The cost of transpiling also highlights that you can mix and match a lot of these tools and get different results.

Let’s Consolidate Tooling in 2016

Front-end developers have been using build tools for a very long time but it’s only in the last few years that we’ve seen a build step become the norm. Tools like Sass and CoffeeScript helped make pre-processing mainstream but the momentum around ES6 has now got everyone on board.

I agree.

Gulp and Grunt have been very popular in the past few years, these tools allow you to write a series of transforms to pipe your assets through. They’ve been used to great effect and are still popular, though a lot of people choose to use the tools directly through npm – see Why I left Gulp and Grunt for npm Scripts and Guide to using npm as a Build Tool.

Personally, I don’t care for building asset pipelines any longer, what I’m looking for is minimal config tools that allow me to use modern tooling as needed: Things like Sass, Autoprefixer, Babel and Coffeescript, a proper module system and loader without having to worry about the implementation, configuration and ongoing maintenance. In essence, every developer has been investing time into creating asset pipelines over the last few years, that’s a lot of wheel re-invention going on and a lot of wasted hours.

The community is divided across tools like Browserify, Webpack, jspm, Sprockets and Gulp. That’s not really a problem, it’s just confusing for everyone trying to understand a clear path forward.

Clear Starting Points

There’s a few things we can agree on:

  • ES2015 modules are the one true future module format for JavaScript.
  • Babel is the ES2015 compiler of choice today.
  • Native loaders are still a while away from being available in browsers, a report on the Future of JavaScript by Telerik suggests that complete ES2015 support could take over two years given the module loading hurdle.
  • If you want to use modules now, that will very likely involve CommonJS at some point.

Let’s look at what minimal configuration setups look like using Browserify, Webpack and jspm, these are the most important JavaScript bundlers to know about today.

A New Project

mkdir modules-app
cd modules-app
npm init -y
npm install --save-dev browserify webpack jspm
mkdir src
touch src/{entry,lib}.js index.html

Update index.html in your favourite text editor

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

We’ll also need a server to run the code—for example live-server which is a great little zero-config HTTP server with live reload capability. Install it globally with npm install -g live-server and run live-server from the project root to start.

Browserify

Browserify lets you require('modules') in the browser by bundling up all of your dependencies.

Open up src/lib.js and add our very first module.

var double = function(number) {
  return number * 2;
}

module.exports = {
  double: double
}

Open up src/entry.js and we’ll require our module and use it.

var lib = require('./lib.js');
console.log(lib.double(2));

Update the scripts section in package.json

"scripts": {
  "browserify": "browserify ./src/entry.js -o ./bundle.js"
},

Run this script with npm run browserify

Browserify will create bundle.js in the project root and you should see a most exiting 4 output to the console. To learn more about what Browserify is doing and how this bundle gets created I recommend watching Introduction to Browserify at egghead.io

Congratulations! We now have modules in the browser! 🎉

Another key benefit of Browserify is that it gives you access not only to modules that you author, but to npm modules as well, let’s install lodash to see.

npm install lodash --save-dev

Edit src/lib.js

var sum = require('lodash/sum');

var double = function(number) {
  return number * 2;
}
var addFive = function(number) {
  return sum([number, 5]);
}

module.exports = {
  double: double,
  addFive: addFive
}

Edit src/entry.js and call our new addFive function

var lib = require('./lib.js');
console.log(lib.double(2));
console.log(lib.addFive(2));

Create the bundle again with npm run browserify and in the browser you should see a 4 and a 7 which shows that we’ve successfully imported and used lodash’s sum function.

If you’ve followed along this far you now know everything you need to get started using modules in the browser today, this brings many benefits that we outlined at the start.

  • No globals required 👍
  • Source order independence
  • Access to npm
  • No need for namespacing your own application code

We’ll look at dynamic loading of modules at runtime later.

Webpack

Webpack is a module bundler. Webpack takes modules with dependencies and generates static assets representing those modules.

Let’s add a new script to package.json for calling webpack

"webpack": "webpack ./src/entry.js bundle.js"

Run it with npm run webpack

Webpack will have rewritten bundle.js and the output in the browser should be exactly the same.

Try running npm run browserify and npm run webpack and examining the differences in the compiled bundle.js file. It’s not really important to understand how these tools work internally, the important thing to note is that whilst the implementations are different they are essentially doing the same task of compiling the same code with CommonJS modules into standard browser-friendly JavaScript. Each module is put inside a function within bundle.js and assigned an ID so that it can be loaded as required.

There’s far more to Webpack than this though! It truly is the swiss army knife of module bundlers. Webpack also comes with great tools for development out of the box, things like hot module replacement which will automatically reload individual modules in the browser as they are changed—similar to LiveReload but without the page refresh.

There is a growing list of loaders for different asset types too, even CSS with the css-loader and style-loader—loaders which can compile CSS into the JavaScript bundle and inject it into the page at runtime. This is outside of the scope of this article but more can be found on this at getting started with Webpack.

JavaScript Transpilers

These are three of the most popular transpilers used today, you may also want to use another from the very long list of languages that compile to JS.

Before looking at how we can use them with our module bundlers let’s look at how to use the tools directly first.

npm install --save-dev coffee-script typescript babel-cli babel-preset-es2015
touch src/{coffee-lib.coffee,ts-lib.ts,es6-lib.js}

CoffeeScript

Edit coffee-lib.coffee

sum = require 'lodash/sum'

double = (number)-> number * 2
addFive = (number)-> sum([number, 5])

module.exports =
  double: double
  addFive: addFive

Note: CoffeeScript uses the CommonJS syntax for modules

Add a script to package.json to run the coffee executable

"coffee": "coffee --output ./dist ./src/coffee-lib.coffee"

Run it with npm run coffee

TypeScript

Edit ts-lib.ts

/// <reference path="lodash.d.ts" />
import * as _ from 'lodash';

const double = (value: number)=> value * 2
const addFive = (value: number)=> _.sum([value, 5])

export = {
  double,
  addFive
}

Note: TypeScript has its own syntax for modules that look like a mix of ES2015 module syntax and CommonJS.

Add a script to package.json to run the tsc executable

"tsc": "tsc --outDir ./dist ./src/ts-lib.ts"

Run it with npm run tsc

The compiler will complain about not being able to find lodash as it requires a type definition to know how to work with external modules that aren’t TypeScript files. You can fetch a definition file with:

cd src
curl -O https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/lodash/lodash.d.ts
cd ..
npm run tsc

Babel

Edit es6-lib.js

import sum from 'lodash/sum';

const double = (number)=> number * 2
const addFive = (number)=> sum([number, 5])

export {
  double,
  addFive
}

Note: Babel understands the lovely new ES2015 module syntax.

Babel requires a config file for specifying which presets to use

echo '{ "presets": ["es2015"] }' > .babelrc

Add a script to package.json to run the babel cli

"babel": "babel ./src/es6-lib.js -o ./dist/es6-lib.js"

Run it with npm run babel

The files in /dist now contain ES5 code in CommonJS module format that will work perfectly with Browserify or Webpack as we used previously. You can either transpile down to ES5 with CommonJS first and then bundle, or you can use other packages to do both in a single step.

For Browserify there are plugins coffeeify, tsify and babelify to transpile and bundle.

For Webpack there are loaders coffee-loader, ts-loader and babel-loader to require modules in different languages.

jspm

jspm is a package manager for the SystemJS universal module loader, built on top of the dynamic ES6 module loader

jspm takes a different approach and starts with the module loader System.js. System.js is a project that will follow the loader spec as it’s developed.

Install and initialize a jspm project

npm install -g jspm
jspm init

Accept all of the defaults and ensure that Babel is used as the transpiler, that will configure System.js to use Babel when it runs into ES6 style modules.

Update index.html to load and configure System.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="jspm_packages/system.js"></script>
  <script src="config.js"></script>
  <!--<script src="bundle.js"></script>-->
  <script>
   System.import('src/entry.js');
  </script>
</body>
</html>

In the browser you’ll see a few requests being made and a 404 for lodash, this is because jspm loads packages from the jspm_packages directory by default.

Run jspm install lodash to install lodash in that directory and you should see the expected output in the console, a 4 and a 7, here’s what’s happening:

  • Our entry.js file is being dynamically loaded with System.import('src/entry.js');.
  • System.js loads entry.js, sees that it requires our lib module so fetches it at runtime.
  • System.js loads lib.js, sees that it requires lodash/sum and fetches it too.

System.js also knows how to work directly with ES6, update entry.js to dynamically require our ES6 module and compile on the fly.

import lib from './es6-lib';
// import lib from '../dist/coffee-lib';
// import lib from '../dist/ts-lib';

console.log(lib.double(2));
console.log(lib.addFive(2));

You can also try loading the ES5 compiled versions of our CoffeeScript or TypeScript modules by uncommenting those lines one at a time. Another option is to use System.js plugins to transpile the code, rather than requiring precompiled ES5 code.

Add a final script to package.json for creating a bundle with jspm

"jspm": "jspm bundle src/entry bundle.js"

Run it with npm run jspm

Finally uncomment the script tag for bundle.js in index.html and the browser should load a production-ready bundle without any extra http requests.

<script src="bundle.js"></script>

Revisiting Webpack

Our Webpack example earlier was the simplest example using the default options, it compiled entry.js with CommonJS modules down into a single bundle. When doing more fancy things with Webpack you’ll want to create a custom config file for all of the loader configuration.

Create webpack.config.js in the root of the project

module.exports = {
  context: __dirname + "/src",
  entry: "./entry",
  output: {
    path: __dirname,
    filename: "bundle.js"
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel-loader',
      query: {
        presets: ['es2015']
      }
    },{
      test: /\.coffee$/,
      loader: 'coffee-loader'
    },{
      test: /\.ts$/,
      loader: 'ts-loader'
    }]
  }
}

Update index.html to load only the bundled file again.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Modules!</title>
</head>
<body>
  <script src="bundle.js"></script>
</body>
</html>

Install the loaders for transpiling with Babel, CoffeeScript and TypeScript

npm install --save-dev babel-loader coffee-loader ts-loader

Install webpack globally and run without arguments to create the bundle from our config file.

npm install -g webpack
webpack

Now that webpack knows to use these loaders for these file extensions we’re free to use ES6, CoffeeScript or TypeScript from entry.js, give it a try by uncommenting these one by one.

import lib from './es6-lib.js';
// import lib from './coffee-lib.coffee';
// import lib from './ts-lib.ts';

There is so much more to Webpack than I’ve covered here, but these simple setups are a great starting point.

There and Back Again

And so we end our exploration of modules, they do solve a lot of problems and can really reduce the complexity of our applications—if the tooling doesn’t get in our way. If you’re not already using modules, now is the time. No need to spend unnecessary hours building asset pipelines, instead use simple tools that Just Work™.

Webpack is the current juggernaut and you’ll be able to configure it to do almost anything. jspm is a great tool for all your bundling needs and works with a variety of formats and has a nice developer experience. Browserify is still a solid option, the grandfather of modern module bundlers—it’s ecosystem has grown to include some of Webpack’s much loved features (such as bundle splitting and hot reloading). Finally System.js is perfect for when you need to be able to load extra modules at runtime.

You won’t want to use all of the above tools in one project but it’s important to have an understanding of these three popular options, as well as how you can use transpilers when the need arises. If you just want to use modules, then Browserify, jspm or Webpack with the default options will do the job.

Keep the tooling simple and the configuration light. Happy Bundling.