Modern JavaScript Tooling: Compiling

Published on 2021-10-13

One thing that I commonly see new JavaScript developers struggle with, and that I found particularly annoying when jumping back in, was this concept of having to compile when developing with modern JavaScript, as well as all of the other tooling in the ecosystem.

In this series, I want to jump into the "why" around having a build for your front end code, and walk through setting up a brand new project from scratch.

What Happened to the Good Ole Days?

If I were to tell you to open the developer tools in your browser of choice, you could easily start writing JavaScript and executing it. In FireFox I can just hop into the console and type const f = a => a * a; and after executing that code I could type f(5); and expect to see an output of 25.

In fact, I can continue this path and open up my favorite text editor, create an HTML 5 document with a script tag, and put most JavaScript that I would want in there:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My Test Page</title>
</head>

<body>
    <section>
        Hello, <span id="name"></span>!
    </section>
    <script type="text/javascript">
        const name = prompt("Please enter your name");
        if (name !== null) {
            document.getElementById("name").innerHTML = name;
        }
    </script>
</body>

</html>

This is all fine, and you will see that I made sure to still use "newer" syntax such as const with no issues. But lets say that I want to get a little fancier and use the rest parameters syntax:

<script type="text/javascript">
    function sum(...nums) {
        return nums.reduce((prev, curr) => prev + curr);
    }

    console.log(sum(1, 2, 3, 4, 5));
</script>

When I open this up in FireFox it appears to work as intended, but as a good front end developer I start testing in all of my browsers. I notice that I have issues with IE, Edge, and Safari! A quick consultation of caniuse.com shows me that this syntax is not supported in those browsers 😞

So, what are my options? I can choose to move away from this new syntax and find something that works in all browsers, I can drop support for these three browsers (good luck with that!), or I can look into setting up process that will either polyfill or build backwards compatible code for me!

This article is focusing on the last option, the build option.

I Can Live Without Rest Syntax

That may be true, but what was illustrated above was just one instance of backwards compatibility issues - many more exist today, and this will be an issue moving forward as new versions of ECMAScript are defined.

ECMAScript is the specification that JavaScript conforms to. The latest version as of this writing is ECMAScript 2019 which was published in June of 2019. The rest syntax was added in the ECMAScript 2018 version, so that can show you how browsers lag behind!

JavaScript will most likely always have this issue - new standards will be created and released well before all of browser vendors will be able to update their JavaScript engine. In some cases, like Internet Explorer, new updates will be dropped all together as the browser is sunset. Unfortunately, depending on who your target audience is, you may have to support these browsers long after they have been "retired".

Since we want to take advantage of the new specification of ECMAScript while also targeting as many platforms as reasonably possible we need to look at solutions to help us achieve that!

Babel to The Rescue

Babel is what I am used to using when wanting to have my cake and eat it to. The tagline on the banner of the webpage is even "Use next generation JavaScript, today." which is exactly what we are looking to do.

One thing that I suggest is to navigate to the "Try it Out" link, which will take you to a REPL (Read-Evaluate-Print Loop) with babel enabled. If you write any next generation JavaScript, it will compile it to backwards compatible JavaScript!

An example that scrolls by on the main page takes the following snippet

[1,2,3].map(n => n **2);

and the result is as follows:

"use strict";

[1, 2, 3].map(function (n) {
  return Math.pow(n, 2);
});

Awesome! Take some time to play around with different functionality to see what compiled JavaScript is returned.

And just for completion, and because one can never have enough code snippets, here is the rest operator code we wrote earlier, and its compiled output.

Before:

function sum(...nums) {
    return nums.reduce((prev, curr) => prev + curr);
}

console.log(sum(1, 2, 3, 4, 5));

After:

"use strict";

function sum() {
  for (var _len = arguments.length, nums = Array(_len), _key = 0; _key < _len; _key++) {
    nums[_key] = arguments[_key];
  }

  return nums.reduce(function (prev, curr) {
    return prev + curr;
  });
}

console.log(sum(1, 2, 3, 4, 5));

I don't know about you, but I much prefer writing the first snippet 😉

Basics of Using Babel

Back over on the Babel webpage, there is a section for setup so lets take a look at that! The first thing you will notice is there are a TON of different options, and if you are new to the ecosystem it will be overwhelming. So, lets take a different approach for this first step - we will get back to this phase in a later post specifically about asset bundling , specifically looking at Webpack.

For now, we will focus on the usage guide in the documentation. You will also need to install Node.js and npm on your preferred platform to follow along.

The first step will to be to create a new directory that we will call, build-tooling and we will change into that directory and instantiate a new node project.

mkdir build-tooling

cd build-tooling

npm init -y

That will create a package.json for your project with the default settings! Next we will follow step 1 from the babel usage guide:

npm i --save-dev @babel/core @babel/cli @babel/preset-env

npm i @babel/polyfill

@babel/core is what it sounds like, the meat and potatoes of the compiler! This is what will take the code we write, and output the backwards compatible code we saw earlier.

@babel/cli is the command line interface (CLI) that allows us to run a command that will specify our input directory and output directory.

@babel/preset-env is a preset that makes our lives a lot easier by not needing to micromanage which transforms to apply, and which polyfills to provide.

Next we will follow along and create a new file, babel.config.js to setup some basic properties for our project:

const presets = [
  [
    "@babel/env",
    {
      targets: {
        edge: "17",
        firefox: "60",
        chrome: "67",
        safari: "11.1",
      },
      useBuiltIns: "usage",
    },
  ],
];

module.exports = { presets };

Now we just need to write some code and compile it! Lets write the most interesting code in the world, and place it in src/interesting.js!

const sum = (...nums) => {
    return nums.reduce((prev, curr) => prev + curr);
};

const mergeState = (curr, next) => {
    const updated = { ...curr, ...next };
    return [next, updated];
};

export { sum, mergeState };

This is obviously a very useful section of code, so lets get it compiled so we can ship it ASAP! 🚀

We then go to our command line and type the following command to invoke the babel CLI:

./node_modules/.bin/babel src --out-dir dist

When you execute the command, you should see a successful compilation method with a file called interesting.js in a new directory called dist. If we open up that file, we are hit with this!

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.mergeState = exports.sum = void 0;

require("core-js/modules/es6.symbol");

require("core-js/modules/web.dom.iterable");

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

const sum = function sum() {
  for (var _len = arguments.length, nums = new Array(_len), _key = 0; _key < _len; _key++) {
    nums[_key] = arguments[_key];
  }

  return nums.reduce((prev, curr) => prev + curr);
};

exports.sum = sum;

const mergeState = (curr, next) => {
  const updated = _objectSpread({}, curr, {}, next);

  return [next, updated];
};

exports.mergeState = mergeState;

Wow! That is pretty neat, and we hardly had to do anything. Lets add a script to our package.json so we don't have to remember the CLI syntax every time we want to compile. Your "scripts" key can be modified to look like this:

"scripts": {
    "build": "./node_modules/.bin/babel src --out-dir dist"
  },

now executing npm run build should take everything in the src directory and compile it and place it in dist.

Now What?

Well, technically you have all you need to write next generation JavaScript and compile it in a backwards compatible way. If you keep creating new files and compiling, you will start noticing that this will just create a new compiled of the same name in the dist directory - that doesn't seem very manageable or scalable. That is where something like Webpack comes into play! Next time we will take a look into what its purpose is, how it can be configured, and we will also take a look at some of the other tools in its space such as Rollup and Parcel.

If you have any questions, need clarifications, or want me to go into more detail about this first tool and process let me know! I want to try and make this a holistic introduction to JavaScript tooling!