Loading JavaScript

Kea - High level abstraction between React and Redux - Interview with Marius Andra

Webpack processes ES6 module definitions by default and transforms them into code. It does not transform ES6 specific syntax apart, such as const. The resulting code can be problematic especially in the older browsers.

To get a better idea of the default transform, consider the example output below:

build/app.js

webpackJsonp([1],{

/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony default export */ __webpack_exports__["a"] = function (text = 'Hello world') {
  const element = document.createElement('div');

  element.className = 'fa fa-hand-spock-o fa-1g';
  element.innerHTML = text;

  return element;
};

...

The problem can be worked around by processing the code through Babel, a popular JavaScript compiler that supports ES6 features and more. It resembles ESLint in that it's built on top of presets and plugins. Presets are collections of plugins, and you can define your own as well.

Given sometimes extending existing presets is not be enough, modify-babel-preset allows you to go a step further and configure the base preset in a more flexible way.

Using Babel with Webpack Configuration#

Even though Babel can be used standalone, as you can see in the Package Authoring Techniques chapter, you can hook it up with webpack as well. During development, it can make sense to skip processing if you are using language features supported by your browser.

Skipping processing is a good option especially if you don't rely on any custom language features and work using a modern browser. Processing through Babel becomes almost a necessity when you compile your code for production, though.

You can use Babel with webpack through babel-loader. It can pick up project level Babel configuration or you can configure it at the webpack loader itself. babel-webpack-plugin is another lesser known option.

Connecting Babel with a project allows you to process webpack configuration through it. To achieve this, name your webpack configuration using the webpack.config.babel.js convention. interpret package enables this and it supports other compilers as well.

Given that Node supports the ES6 specification well these days, you can use a lot of ES6 features without having to process configuration through Babel.
Babel isn't the only option although it's the most popular one. Buble by Rich Harris is another compiler worth checking out. There's experimental buble-loader that allows you to use it with webpack. Buble doesn't support ES6 modules, but that's not a problem as webpack provides that functionality.
If you use webpack.config.babel.js, take care with the "modules": false, setting. If you want to use ES6 modules, you could skip the setting in your global Babel configuration and then configure it per environment as discussed below.

Setting Up babel-loader#

The first step towards configuring Babel to work with webpack is to set up babel-loader. It takes the code and turns it into a format older browsers can understand. Install babel-loader and include its peer dependency babel-core:

npm install babel-loader babel-core --save-dev

As usual, let's define a part for Babel:

webpack.parts.js

exports.loadJavaScript = ({ include, exclude }) => ({
  module: {
    rules: [
      {
        test: /\.js$/,
        include,
        exclude,

        loader: 'babel-loader',
        options: {
          // Enable caching for improved performance during
          // development.
          // It uses default OS directory by default. If you need
          // something more custom, pass a path to it.
          // I.e., { cacheDirectory: '<path>' }
          cacheDirectory: true,
        },
      },
    ],
  },
});

Next, you need to connect this with the main configuration. If you are using a modern browser for development, you can consider processing only the production code through Babel. To play it safe, it's used for both production and development environments in this case. In addition, only application code is processed through Babel.

Adjust as below:

webpack.config.js

const commonConfig = merge([
  ...
parts.loadJavaScript({ include: PATHS.app }),
]);

Even though you have Babel installed and set up, you are still missing one bit: Babel configuration. This can be achieved using a .babelrc dotfile as other tooling can pick it up as well.

There are times when caching Babel compilation can surprise you if your dependencies change in a way that babel-loader default caching mechanism doesn't notice. Override cacheIdentifier with a string that has been derived based on data that should invalidate the cache for better control. Node crypto API and especially its MD5 related functions can come in handy.
If you try to import files outside of your configuration root directory and then process them through babel-loader, this fails. It's a known issue, and there are workarounds including maintaining .babelrc at a higher level in the project and resolving against Babel presets through require.resolve at webpack configuration.

Setting Up .babelrc#

At a minimum, you need babel-preset-env. It's a Babel preset that enables the needed plugins based on the environment definition you pass to it. It follows the browserslist definition discussed in the Autoprefixing chapter.

Install the preset first:

npm install babel-preset-env --save-dev

To make Babel aware of the preset, you need to write a .babelrc. Given webpack supports ES6 modules out of the box, you can tell Babel to skip processing them. Skipping this step would break webpack's HMR mechanism although the production build would still work. You can also constrain the build output to work only in recent versions of Chrome.

Adjust the target definition as you like. As long as you follow browserslist, it should work. Here's a sample configuration:

.babelrc

{
  "presets": [
    [
      "env",
      {
        "modules": false,
        "targets": {
          "browsers": ["last 2 Chrome versions"]
        }
      }
    ]
  ]
}
If you omit the targets definition, babel-preset-env compiles to ES5 compatible code. If you are using UglifyJS, see the Minifying chapter for more information on why this is required. You can also target Node through the node field. Example: "node": "current".
babel-preset-env does not support .browserslistrc file yet. See issue #26 for more information.

If you execute npm run build now and examine build/app.js, the result should be similar to the earlier since it supports the features you are using in the code.

To see that the target definition works, change it to work such as "browsers": ["IE 8"]. Since IE 8 doesn't support consts, the code should change. If you build (npm run build), now, you should see something different:

build/app.js

webpackJsonp([1],{

/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony default export */ __webpack_exports__["a"] = function () {
  var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'Hello world';

  var element = document.createElement('div');

  element.className = 'fa fa-hand-spock-o fa-1g';
  element.innerHTML = text;

  return element;
};

...

Note especially how the function was transformed. You can try out different browser definitions and language features to see how the output changes based on the selection.

Polyfilling Features#

babel-preset-env allows you to polyfill certain language features for older browsers. For this to work, you should enable its useBuiltIns option ("useBuiltIns": true) and install babel-polyfill. You have include it to your project either through an import or an entry (app: ['babel-polyfill', PATHS.app]). babel-preset-env rewrites the import based on your browser definition and loads only the polyfills that are needed.

babel-polyfill pollutes the global scope with objects like Promise. Given this can be problematic for library authors, there's transform-runtime option. It can be enabled as a Babel plugin, and it avoids the problem of globals by rewriting the code in such way that they aren't be needed.

Certain webpack features, such as Code Splitting, write Promise based code to webpack's bootstrap after webpack has processed loaders. The problem can be solved by applying a shim before your application code is executed. Example: entry: { app: ['core-js/es6/promise', PATHS.app] }.

Babel Tips#

There are other possible .babelrc options beyond the ones covered here. Like ESLint, .babelrc supports JSON5 as its configuration format meaning you can include comments in your source, use single quoted strings, and so on.

Sometimes you want to use experimental features that fit your project. Although you can find a lot of them within so-called stage presets, it's a good idea to enable them one by one and even organize them to a preset of their own unless you are working on a throwaway project. If you expect your project to live a long time, it's better to document the features you are using well.

Babel Presets and Plugins#

Perhaps the greatest thing about Babel is that it's possible to extend with presets and plugins:

It's possible to connect Babel with Node through babel-register or babel-cli. These packages can be handy if you want to execute your code through Babel without using webpack.

Enabling Presets and Plugins per Environment#

Babel allows you to control which presets and plugins are used per environment through its env option. You can manage Babel's behavior per build target this way.

env checks both NODE_ENV and BABEL_ENV and functionality to your build based on that. If BABEL_ENV is set, it overrides any possible NODE_ENV. Consider the example below:

.babelrc

{
  ...
  "env": {
    "development": {
      "plugins": [
        "react-hot-loader/babel"
      ]
    }
  }
}

Any shared presets and plugins are available to all targets still. env allows you to specialize your Babel configuration further.

It's possible to pass the webpack environment to Babel with a tweak:

webpack.config.js

module.exports = (env) => {
process.env.BABEL_ENV = env;
... };
The way env works is subtle. Consider logging env and make sure it matches your Babel configuration or otherwise the functionality you expect is not applied to your build.
The technique is used in the Server Side Rendering chapter to enable the Babel portion of react-hot-loader for development target only.

Setting Up TypeScript#

Microsoft's TypeScript is a compiled language that follows a similar setup as Babel. The neat thing is that in addition to JavaScript, it can emit type definitions. A good editor can pick those up and provide enhanced editing experience. Stronger typing is valuable for development as it becomes easier to state your type contracts.

Compared to Facebook's type checker Flow, TypeScript is a more established option. As a result, you find more premade type definitions for it, and overall, the quality of support should be better.

You can use TypeScript with webpack using the following loaders:

There's a TypeScript parser for ESLint. It's also possible to lint it through tslint.

Setting Up Flow#

Flow performs static analysis based on your code and its type annotations. You have to install it as a separate tool and then run it against your code. There's flow-status-webpack-plugin that allows you to run it through webpack during development.

If you use React, the React specific Babel preset does most of the work through babel-plugin-syntax-flow. It can strip Flow annotations and convert your code into a format that is possible to transpile further.

There's also babel-plugin-typecheck that allows you to perform runtime checks based on your Flow annotations. flow-runtime goes a notch further and provides more functionality. These approaches complement Flow static checker and allow you to catch even more issues.

flow-coverage-report shows how much of your code is covered by Flow type annotations.

Conclusion#

Babel has become an indispensable tool for developers given it bridges the standard with older browsers. Even if you targeted modern browsers, transforming through Babel is an option.

To recap:

  • Babel gives you control over what browsers to support. It can compile ES6 features to a form the older browser understand. babel-preset-env is valuable as it can choose which features to compile and which polyfills to enable based on your browser definition.
  • Babel allows you to use experimental language features. You can find numerous plugins that improve development experience and the production build through optimizations.
  • Babel functionality can be enabled per development target. This way you can be sure you are using the correct plugins at the right place.
  • Besides Babel, webpack supports other solutions like TypeScript of Flow. Flow can complement Babel while TypeScript represents an entire language compiling to JavaScript.
Previous chapterLoading Fonts
Next chapterBuilding

This book is available through Leanpub (digital), Amazon (paperback), and Kindle (digital). By purchasing the book you support the development of further content. A part of profit (~30%) goes to Tobias Koppers, the author of webpack.

Need help?