What I Learned at Work this Week: package, webpack, Babel, and plugins

As I’ve gained more experience in front end development, I’ve started to hear a little voice in my head. That voice asks questions like did you remove the event listener?, did you put a period in front of your class selector?, or will this work on Internet Explorer? It’s that final question that brought me to this topic today. I was doing a PR review for a teammate when I noticed that their code used JavaScript’s find method. Here’s an example for reference:

[1,2,3].find(num => num> 1)
// 2

The method returns the first element in an array that meets the qualifications we set forth. This functionality is really useful in my work because I’m often searching through a window’s dataLayer for an object that contains some values relevant to a transaction. Since I can’t always count on my array to be organized in the same way, I can use find to pick out the element with certain attributes. There’s just one problem:

What does “no support” mean? It means that, as soon as my script is loaded on Internet Explorer, I’ll be presented with a console error and my script will fail. Not just the one line with .find in it, but my entire script. Fortunately the webpage that hosts the script will still work, but if the client I’m working with has a significant number of users who prefer to use Internet Explorer — the service I‘ve coded won’t work for them at all.

So what do we do?

Compilation

Ideally, everyone on my team would get together before we wrote a single line of JavaScript and decide which browsers we wanted to be compatible with our code. We’d request changes on any PR that came through with .find or .includes or .startsWith because they weren’t supported by IE. We’d teach this to each new member of our team and we’d make sure that any imported libraries also followed these guidelines. There’s just one problem with that ideal solution: humans make mistakes.

I’ve been very fortunate to be on a team that is accepting of inexperienced devs and has given fresh boot camp grads like me an opportunity to learn on the job. The downside there is that we didn’t necessarily have the foresight to think about browser compatibility when putting together everything else that our platform needs to run. In checking the code our team is responsible for, I found about 250 instances of .find. It’s impractical to manually open and rewrite that part of each file, but what’s better than a manual solution? That’s right — an automatic one!

You might know that the code we write often needs to be altered, or compiled before it can be run. Compilation reads the syntax of our written code and transforms it into a version that can be processed by our runtime environment. JavaScript is actually an interpreted language, which means it doesn’t require compilation to run — but that doesn’t mean that it can’t be compiled. So what if there was a way for us to use compilation to search through our JavaScript and change the ES6 content, like .find, back into an ES5 version?

Babel

When you first visit https://babeljs.io/, you’ll learn that Babel is a JavaScript compiler. And if you check out the docs, you’ll quickly see that Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. What are the odds?

Besides newer functions, there is also inconsistent support for JS features like arrow functions or even let and const. Babel offers a series of modules that we can use to transform our syntax and make it compatible with the largest possible variety of browsers. If you already knew all this and came here to see how it actually works…this is your moment!

Package.json

Before we spend any more time reading Babel docs, we’ll set up our project. And believe it or not, our very first step will be creating a package.json file! It’s possible to do this manually, but the most common method is probably to run npm init.

If we read the first line after our command, we’ll see that this utility will walk you through creating a package.json file. Great! That’s exactly what we want to do. We’ll receive a series of prompts that ask us for the details of our project. Those include things like the project’s name, license, and a description. Since this is purely for demonstration/practice purpose, we can leave everything blank. None of these values are critical to having our code run. Skipping all the prompts will generate a package.json file that looks something like this:

Package.json is automatically generated and populated by a lot of well-known frameworks, so if you’re a bootcamp grad like me, you’ve likely seen one in previous projects you’ve worked on. If you’ve never given much thought to what it does, don’t worry because it’s pretty straightforward. It contains metadata like a project’s name or version, but it is also used to identify scripts that can be run in association with the project and libraries that the project requires to run, known as dependencies.

We’re already aware of one such dependency: Babel. So let’s install it:

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

On this line, we’re installing three different parts of Babel:

  1. The core compiler code.
  2. The ability to interact with Babel through our command line
  3. An environment module that will establish that we want to compile code to ES5.

If you run this command in your terminal, you might see some warnings, but don’t let those concern you too much. The warnings likely explain that certain modules are deprecated or that our project doesn’t have a description. The important result here is that our project has been updated:

The very first thing we see is that our JSON includes a new key, devDependencies, whose value is an object that includes @babel/cli, @babel/core and @babel/preset-env as keys. We’re planning to convert some ES6 to ES5, so we’ll have to use Babel, hence the appearance in devDependencies. We also see that we’ve got two new files: node_modules and package-lock.json. If you’ve never looked into it, node_modules actually contains the JS logic that makes our imported libraries work. There are a lot of them because Babel is responsible for a lot of different processes. The package-lock.json file is something of a reference point for our project because our npm command altered node_modules or package.json (it altered both). It keeps track of the dependency tree and if you don’t understand how or why, don’t worry because we won’t be touching it. If you want to read more about package-lock.json, check out this resource from the University of Washington.

Webpack

Babel is going to do the heavy-lifting in compiling our code, but we’re going to use a module-bundler called webpack to connect our files and dependencies. We don’t necessarily need webpack to make Babel work, but webpack is ubiquitous when it comes to transforming our projects into webpages. My Babel is sent through webpack at work, so I’ll simulate that flow here. We’ll again run an npm command, this time to install webpack. Note that we’re also installing babel-loader, which helps Babel work with webpack.

npm install webpack webpack-cli babel-loader

Unlike last time, we won’t see any new files created, but if we look in package.lock, we should see that we now have a dependencies object which contains our webpack object. But if we actually want to use webpack, we have to create a file to define some parameters. We create a file called webpack.config.js and populate it with an object called module.exports. Here’s what the final product looks like:

Let’s break it down:

const path = require('path');

Path is a node module (feel free to check it out in node_modules) that allows us to navigate the path of our project. That’s important because we’re going to be identifying file locations in our webpack config file.

module.exports = {

We’re writing a webpack module right now. The module is going to be referenced by other files, so we’re going to put some important information in its exports.

entry: {
app: './src/app.js'
},

Here we declare where webpack is going to start searching for files to convert (and it becomes clear why our path module is helpful). We could choose any file we’d like, but we’re going to go with the classic src/app.js. That file doesn’t exist in our project yet — we’ll write it later.

output: {
path: path.resolve(__dirname, 'build'),
filename: 'app.bundle.js'
},

Webpack is going to take our file or files, bundle them together, create a new file, and then populate that file with the bundled code. Here we declare a path, which is where the new file will live (__dirname gives us the same name as our input file, meaning we want to drop our build directory back into our existing project). The filename will be the name of our new…file. We used app.bundle.js because this file is going to be a “bundle” of JavaScript. We’re taking all of our disparate files and dependencies and wrapping them into one file.

module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
]
}]
},

If you’re following the video I linked in my sources, this is where our code starts to diverge. We’re working with a newer version of Babel, so our syntax has to be a bit different. First we see the module object, which is where we define our rules, which are bundling conditions. Under rules we have test, which lets us decide which files qualify for bundling. In this case we use regex to specify files that end with .js. In exclude, we declare that we do not want to bundle our node_modules. Use is where we bring Babel into the picture by setting our loader as babel-loader. Finally, options allows us to pass parameters to our loader in the form of presets or plugins (which we’ll see later). You might recognize the preset here since we installed it earlier. This is how we tell babel-loader which environment we’d like our code compiled for. In this case, we’re aiming broadly for ES5, which is the default.

Last but not least in our config file:

mode: 'production'

We have to tell webpack whether we’re compiling for development or production. I’ve chosen production in this case because it again better emulates what I see in the workplace.

Running a command

Now that we’ve got that settled, we can actually write some code and try to compile it! In our config file, we declared that our entry point would be src/app.js, so we’ll create that file and write some simple code that uses a bunch of ES6 syntax. Below we’ll see const, an arrow function, and .find, all of which are native to ES6 but not ES5:

Next, we’ll write a script that can call webpack, which as we just saw, will call Babel and print out a new file for us. Since all that logic is living in node_modules and webpack.config.js, all we have to do is name our script and tell it to run webpack. Where can we name the script? Under scripts in package.json, of course!

Yep, that’s all there is to it. If you’ve ever had package.json built by a scaffolding command, you might have noticed a bunch of different scripts with different webpack or other options. For our example, we just want to be able to run build using webpack. Writing this line of code was a big ‘aha!’ moment for me, because I finally understood what was happening when I ran:

npm run build

I was asking my Node package manager to run the build command. As we know, that command calls webpack, which generates bundles our code into a new file. That file has been transpiled by Babel, which means our ES6 should now be ES5. So what’s the result?

Wow! Webpack created the build directory and populated it with app.bundle.js just like we asked it to. We see that our code was slightly altered — the variable name has been removed because it’s never referenced after being declared, so it’s ultimately not necessary. It replaces our arrow function with the classic function(){} syntax and as for the ES6 .find method that started all this…it’s still there.

It turns out that @babel/core doesn’t take care of ES6 methods like I hoped it would. But there is good news! Since we’ve now got a good grasp on webpack, we can very easily install and invoke what’s called a “plugin.” This looks like it might work.

So we run

npm install babel-plugin-transform-array-prototype-find

and add

plugins: ['transform-array-prototype-find'] 

to the options in webpack.config.js. Here’s the updated version of that file:

And then when we npm run build again…

.find is gone! It’s been replaced by .filter, which returns an array of all elements that meet the stated qualification. Our Babel plugin is smart enough to know this, so to emulate the .find result, it takes the 0th index of that return. Most importantly, though, .filter is ES5 compatible, which means it’s supported by Internet Explorer. At long last, we’ve done it!

Plugins

If you’re really into documentation, you might have noticed that our plugin doesn’t always replace .find outright. We used it directly on an array, but if that array is saved as a variable and .find called on that variable, the plugin will create a ternary that includes both .filter and .find. Sadly, that meant that the plugin didn’t work for my codebase at work because .find still appeared in the bundled code, which means IE still threw an error and failed to execute our script. As is an inevitable part of a programmer’s life, I’ll be heading back to the drawing board next week.

Sources

Solutions Engineer