How to quickly transpile JavaScript using Babel alone? A brief introduction to Babel.js.
By: Uday Hiwarale
Tags: Webpack, JavaScript, Web Design, Web Development, Babel

You might have heard of Webpack, Rollup, or perhaps Parcel. These are some of the widely popular (web) bundling tools. What is bundling you ask? I think you are a little late to the party. Bundling, in a nutshell, is merging on two or more .js files together to form a single bundle file such as main.js . But this would be a far simplified definition. You could have used Gulp or Grunt for that.

Bundling tools such as Webpack webpack can do much more than that. It can transpile your code from ES6 (or higher) to ES5 (or lower). It can also transpile programs written TypeScript or React to vanilla JavaScript as well as SASS/LESS to CSS. It can also automate a lot of things such as creating asynchronous bundles (chunks), compressing images, relocating builds, etc.

💡 The word transpile means to compile a program from a higher programming language such as TypeScript to another high programming language such as JavaScript. The output programming language could be the same as input such as ES6 to ES5 transpilation. I will be using this word interchangeably with “compile” in this article but it means the same thing.

As you can see, Webpack takes away a lot of manual work from the web development process and all you are left away with is the actual coding which is what every organization thrives to achieve. Hence, you will see Webpack (or others) being used in almost every organization and most projects.

The famous Angular framework uses Webpack under the hood to compile the Angular project which is primarily written in TypeScript. Though React is not a framework, but the create-react-app CLI tool helps you initialize an empty React project with a boilerplate that contains Webpack configuration to transpile React (JSX) to JavaScript and bundle your code for deployment.

Trust me, setting up Webpack is no joke. I have written an article on how to create JavaScript plugins using Webpack and you can follow this lesson to understand this setup process. But the effort you put in setting up a better Webpack configuration earlier will immensely pay off in the future.

However, sometimes you do not want the full capability of the bundling process that Webpack provides. Most of the time, we just want to transpile ES6 code to ES5. For example, you may want to use ES6 Arrow Function expression and ES6 Class syntax in the program but since they are ES6 features and may not be supported in all browsers, you want to transpile them to normal function declarations and constructor function expression.

For this purpose, setting up Webpack probably isn’t worth the effort. This is a very small task and there must be a tool that can do this easily, perhaps just by invoking a CLI command. Luckily, we have such a tool.

Before we dive into this tool, let me clarify something. One misunderstanding people have about Webpack is that Webpack does everything on its own. In reality, Webpack is merely just a bundling tool and it can’t do transpilation of TypeScript to JavaScript or SASS to CSS. So who does that?

Webpack provides APIs to hook into the compilation process and manipulate the source code and build artifacts (output files). Webpack depends on loaders and plugins for this.

A loader is an external program that Webpack invokes with the source code of a input program file. The loader transforms this code and returns it back to Webpack. This the crux of the transpilation process that the loader is performing. So if Webpack imports .ts file, it may call ts-loader which transforms TypeScript code to JavaScript.

A plugin is an external program that hooks into different lifecycle events of the Webpack and performs some operation. For example, the widely used HtmlWebpackPlugin plugin listens to Webpack’s emit event and injects path of the output JavaScript and CSS files inside a .html file. This way, you do not need to manually import compiled files inside the .html file.

Now lets come to the topic of the article. Babel is a JavaScript compiler and not a webpack loader. It compiles (transpiles) higher JavaScript code (ES6+) to lower JavaScript code (such as ES5). However, Babel doesn’t come as a single package but rather as individual NPM modules.

The @babel/core package is the brain of Babel that orchestrates the compilation process. It provides APIs to transpile a JavaScript program file from a higher JavaScript version to a lower JavaScript version. You can import this package inside your own JavaScript program and use the APIs make a custom JavaScript compiler out of it.

The @babel/cli package gives you the babel command to compile JavaScript files from the command-line interface. For example, $ babel main.js command would transpile the main.js file. This package uses the @babel/core package under the hood for the compilation.

Another misunderstood bit is that “the @babel/core package does the compilation of JavaScript” which is not quite right. The @babel/core package is just an orchestrator. Consider it as a middle person between a program that imports it (or the babel command) and the program that actually does the transpilation.

In the Babel universe, we have two more pieces of the puzzle, viz. plugin and preset. The plugin is the program that does the actual transpilation of a JavaScript feature. For example, @babel/plugin-transform-arrow-functions plugin transpiles ES6 Arrow Functions to ES5 function expressions.

// from (input)

var a = () => {}; // to (output)

var a = function () {};

You will also find Babel plugins to transpile ES6 class declarations, let and const declarations, spread operator, and much more. You can find the full list of official plugins from this document. You can configure Babel to only support plugins of your choice and leave other JavaScript features intact.

So when a program asks @babel/core package to transpile a JavaScript program, it uses these plugins to perform transpilation of the individual JavaScript features. After the input program is passed through all the provided plugins, Babel will output the compiled result.

However, as you can see from this document, the list of plugins is huge. If you want to transpile all the JavaScript features, then it would be a nightmare to install these plugins manually one at a time and configure Babel for them.

This is where presets come into play. A preset in a nutshell is a collection of Babel plugins. For example, the @babel/preset-es2015 preset is a collection of Babel plugins that transpiles ES2015 (ES6) features. However, year-wise presets are deprecated and you should use @babel/preset-env instead.

The @babel/preset-env preset is a smart preset that takes a call on what Babel plugins to use for transpilation based on a configuration. For example, you can provide this plugin a configuration about the browsers and their minimal version you want to support and it will figure out what JavaScript features are supported by the browser and what needs to be transpiled

{

"targets": {

"chrome": "58",

"ie": "11"

}

}

💡 Babel officially provides the @babel/preset-typescript preset to transpile TypeScript code to JavaScript and the @babel/preset-react preset to transpile React (JSX) code to JavaScript.

When Babel transpile a JavaScript feature, it may use helper functions. A helper function is just a normal Javascript function that contains the logic to support a new JavaScript feature in the old JavaScript version. For example, the @babel/plugin-transform-instanceof plugin transpiles instanceof operator for older browsers using the _instanceof helper function.

// from (input)

foo instanceof Bar; // to (output)

function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return right[Symbol.hasInstance](left); } else { return left instanceof right; } } _instanceof(foo, Bar);

💡 Since instanceof operator is it is not a new JavaScript feature, you must be thinking why we need to transpile it in the first place. This is because its behavior is now changed, thanks to Symbol.hasInstance method. You can read more about it from my article on the Symbol object.

In the above example, as you can see, Babel has replaced instanceof operator to _instanceof helper function call. This helper functions will be added to every input program file in the compilation if needed. This will lead to code duplication when multiple program files are involved in the compilation.

Therefore Babel recommends implementing the @babel/plugin-transform-runtime plugin that uses @babel/runtime library to import these helper function. When this plugin is used, we get the following output.

// from (input)

foo instanceof Bar; // to (output)

import { _instanceof } from '@babel/runtime';

_instanceof(foo, Bar);

Though transpilation is the process of converting a feature implementation from a higher version to a lower version while talking about JavaScript transpilation, it doesn’t necessarily apply for all the features. For example, Promise and Symbol APIs aren’t transpiled by Babel and they are left intact.

Unlike let , const , class , arrow functions and JavaScript features for which you will find Babel plugins, Promise and Symbol aren’t syntactical sugar. These are new JavaScript APIs that do not exist in older versions.

In such cases, Babel won’t transpile these and leave their implementations to us. Well, how would you implement these if they do not exist in older versions of JavaScript? This is where polyfills come into the picture. A polyfill is a JavaScript program that provides a missing JavaScript feature at runtime.

The promise-polyfill JavaScript package when imported using the simple import "promise-polyfill" statement or using the <script> tag injects Promise object in the global scope. Therefore a program that uses Promise API wouldn’t even notice if it is a polyfilled feature or native feature.

Babel provides @babel/polyfill package which provides polyfills for the major JavaScript APIs. But since Babel v7.4.0 , this package has been deprecated in favor of core-js . Read more about this from here.

import "core-js/stable";

import "regenerator-runtime/runtime";

The core-js and regenerator-runtime packages provide a perfect polyfilled runtime environment for modern JavaScript to run consistently on almost all browsers and JavaScript platforms. The regenerator-runtime package provides helper functions for code transpiled using the @babel/plugin-transform-regenerator plugin.

The last piece of the puzzle is babel-loader . The babel-loader packge is the actual Webpack loader that transpiles JavaScript. It uses @babel/core and other plugins/presets we talked about. Its job is to take commands from Webpack and translate them to @babel/core API calls.

⇨ Local Babel Installation

First of all, let’s set up a simple project with a minimal package.json . You can create it using npm init or npm init -y command. Then we need to install @babel/core , @babel/preset-env packages for the development (compilation) and some polyfills for the runtime.

$ npm init -y

$ npm install --save-dev @babel/core @babel/preset-env

$ npm install --save core-js regenerator-runtime

Now we are just missing one more piece and that is @babel/cli . This package provides us the ability to invoke @babel/core from the command-line interface. We can install this package locally or globally. First, we will install it locally and for that, we just need to install it in the normal fashion.

$ npm install --save-dev @babel/cli

When we install @babel/cli locally, NPM creates a babel file (symlink to @babel/cli ) inside node_modules/.bin directory of the project. This file is executed when the babel command is invoked by the user. But we can’t invoke this command globally as this package is installed locally.

(./ node_modules/.bin/babel )

We need to use $ node_modules/.bin/babel command to make it work. We could also use the babel command inside scripts of the package.json and NPM would gladly execute node_modules/.bin/babel for us.

// package.json {

"name": "babel-starter",

"scripts": {

"compile": "babel -f program.js"

}

}

Another cool trick is to use npx command. You can read more about this command from here. When you invoke npx babel command with some command-line arguments, it invokes the babel file inside node_modules/.bin which is neater than invoking $ node_modules/.bin/babel . So let’s test local installation of Babel using the npx babel --version command.

As you can see from the above results, I have installed v7.10.5 of @babel/cli and v7.11.4 of @babel/core package. It’s time to compile program.js JavaScript program file using npx babel program.js command.

If we look at the result, Babel didn’t do much here. It removed some unnecessary spaces and characters from the program and outputted the compiled program to the console (STDOUT).

This is actually the default Babel behavior. Since we haven’t configured Babel to use presets or plugins, it won’t transpile arrow function and let used in the program.js . To instruct Babel to use plugins and/or presets we have installed, we need a babel.config.json file in the project.

This JSON file mentions the plugins and presets we want to use in the compilation process. When babel command is invoked, Babel looks for this file in the root of the project.

💡 There are multiple ways to provide Babel configuration apart from the babel.config.json file. Read this documentation to know more.

Now that we have configured Babel to use @babel/preset-env preset to transpile JavaScript features suitable for the Chrome 58 and Internet Explorer 11, we should be able to see different compilation result.

As you can see from the above result, Babel now compiled the arrow function expression to function expression as well as let keyword to var . Since you might want to store this result to a file, you can either redirect ( > ) the STDOUT to a file in the command itself or use the --out-file or simply the -o flag with the output file name.

Since you would need to execute this command every time you make changes to the program.js file, Babel provides the --watch or simply -w flag to watch for the file changes ( program.js here) and compile whenever it detects the change in the source code of the file.

You may want to generate a source map for the input file ( program.js here) to aid the debugging process. Babel can generate a source map file for you if you invoke the above command with --source-maps or simply -s flag.

As you can see from the above results, Babel has generated the out.js.map file which is the source map file for the out.js and it also injected the source map comment at the bottom of out.js file.

💡 If you want to keep the source maps inline in the source file itself, use the inline value for the --source-maps flag such as --source-maps inline .

⇨ Global Babel Installation

So far we have installed @babel/cli package locally due to which we had to use npx babel command. Local installation of @babel/cli package is ideal when you have multiple projects that support different versions of the Babel and/or different versions of Babel CLI.

If you want to invoke babel command from anywhere on your system, we need to install @babel/cli globally so that you always have the babel command in the terminal. Since @babel/cli needs @babel/core package, we need to globally install that too.

$ npm install --global @babel/core @babel/cli

Once these packages are installed, you can execute babel command from anywhere. This command would work exactly like the npx babel command as it would use the babel.config.json file in the project’s root directory.

Though the global installation might seem enticing, I would recommend the local installation when it comes to commercial development. Since the global installation is up to the user, keeping the version of the @babel/core and @babel/cli consistent across multiple development machines is not possible with this approach.

Since the versions of @babel/core and @babel/cli is locked by package-loca.json when the local installation is implemented, you will get the 100% reproducible builds across all development machines (when npm install command is used to install the project dependencies).

Handling Polyfills

If you are working with Promises, Symbols, Generators, or any other JavaScript APIs that need polyfills, make sure you import the polyfills for them in the source program as shown below.

In the above example, the polyfill.js program implements Generators and Promise API. Since Babel do not transpile promises, we need to provide a polyfill for it which was done using the core-js/stable import.

Since Generators are not supported in IE11, Babel transpiled it using the @babel/plugin-transform-regenerator plugin which has the dependency on regenerator-runtime package. Hence we imported that too. The output of the above program ( polytest.out.js ) looks like below.

You can spot the helper function emitted by Babel in the output such as _toConsumableArray , _nonIterableSpread etc. By default, Babel does not use @babel/plugin-transform-runtime plugin to import them from the @babel/runtime package. So let’s install this plugin first.

$ npm install --save-dev @babel/plugin-transform-runtime

$ npm install --save @babel/runtime

The @babel/runtime package also needs to be installed since Babel would import these helper functions from this package (at runtime). Let’s add this plugin inside babel.config.json so that Babel can start using it.

Now when we compile the polytest.js file using the same command, we get the following output.

From the above result, now you can clearly see that the output does not contain the implementation of any helper function, rather the helper functions are being imported from the @babel/runtime package.

There is one issue, however in the above file. We have imported the regenrator-runtime package but after integrating the @babel/plugin-transform-runtime plugin, Babel uses _regenrator import from the @babel/runtime package to support generators.

This happens because by default, the @babel/plugin-transform-runtime plugin implements Regenerator aliasing. If we look at the default settings of this plugin, this will more evident.

{

"plugins": [

[

"@babel/plugin-transform-runtime",

{

"absoluteRuntime": false,

"corejs": false,

"helpers": true,

"regenerator": true,

"useESModules": false,

"version": "7.0.0-beta.0"

}

]

]

}

Since the regenrator is set to true by default, Babel replaces regeneratorRuntime with _regenerator.default where _regenerator is imported from the @babel/runtime package. This is an effort to stop pollution of the global scope. But you can avoid this by simply setting regenrator to false in the babel.config.json as shown above.