A Webpack Setup that Makes Sense

Idan Cohen
BigPanda Engineering
7 min readMay 8, 2017

TL;DR

Webpack baffles the best of us. Whether you’re a n00b or a pro, you can find in this article a tested and maintainable setup for a frontend project using webpack 2. This is the very same setup we at BigPanda use for our FE projects. This article will not focus on specific plugins or loaders, but rather on an architecture that will serve your project’s unique implementation, whatever it may be.

Seed Project

No better way to learn than to try yourself. So I’ve created a minimal yet robust scaffold of the setup described here. you can find it at -

Preface: What is webpack?

If you’re already familiar with webpack, feel free to move to the next part.

Common mistake when introduced to webpack is the attempt to place it somewhere between tools we’re familiar with, such as gulp, grunt, bower, etc. But webpack is more than just a tool, it’s a project architecture, one that takes time and patience to fully comprehend. I’ll start with a little line of code, commonly found in index.js files, which always puts me in the webpack mood -

I’m assuming you’re already familiar with imports in ES6, but what twists the mind of every javascript developer is of course ‘style.css’. How the fudge can you import a CSS file into a JS file? It’s an abomination!

Well, you can’t. No browser alive can compile this code. But none should: Before this code reaches the browser, webpack will bundle it.

Let’s stop for a minute and ponder about this: Most build tools manipulate our code in some way or the other (concat, transpile, etc.), but it is still the same code running in the client’s browser. Webpack on the other hand rebuilds our code into a bundle, based on these imports. let’s see how it’s done.

STAGE 1: Entry Point

During the bundling process, webpack analyses your project, starting at a file defined as entry in your webpack configuration:

From this entry point, webpack looks for and follows the imports throughout your project to create a dependency tree. This method offers a few advantages -

  • Only resources you actually use are bundled
  • No more folder scanning and bulk file concatenation
  • Resources are always loaded in the order they’re required
  • Common code chunks can be grouped together

STAGE 2: Loaders

Once this dependency tree is fully mapped, a second stage begins: rules and loaders. Rules in webpack configuration tell it how to treat each file type it encounters throughout the tree. Here is an example of such a rule set -

Notice that test sets the filetypes for which this rule will apply. Then under use we define a set of loaders which will be applied on the files matching the test. Notice that loaders are applied in reverse order. So in the above case, files matching the regex /\.(css)$/ will first be converted into javascript modules, And secondly be injected into the page.

But which page? Well, that’s easy, there should be only one — index.html. Webpack was designed for SPA (single-page-applications).

STAGE 3: Plugins

The third important stage in the bundling process is plugins. Plugins usually take affect after all the rules were applied and all the branches in the hierarchy are prepped and ready. They’re used for general actions preformed on the tree as a whole or peripheral tasks. For example, minification and uglification are defined in the plugins section -

Other common actions preformed by plugins are:

  • Injection of resources into index.html
  • Split code between vendor chunks (angular, jquery, etc.) and app chunks
  • Clean up dist folder before build
  • And so much more…

STAGE 4: Output

Last in our list is the output. Output sets the target folder for the final outcome of the bundle:

These were the four pillars of webpack — entry, loaders, plugins and output. You can use webpack 2 wonderful documentation if you wish to dive deeper. But if you feel ready to get your hands dirty , let’s start building a strong, modular webpack configuration.

Kicking Off

Continue here if you skipped the preface

Creating your Own Task Manager

Since we don’t want to rely on too many different technologies, we’re gonna drop grunt, gulp, and all those other task managers. Which leaves the question — how are we going to run all our different tasks? We still want a task for building, testing, serving and so on. The answer is we’re simply going to use npm scripts. Just to feel the hang of it, add this to your package.json -

and from the project’s root folder type -

npm run tryme

As you can see, npm scripts are just a fancy way to run command line code. And fortunately, webpack has a very strong CLI that allows you to perform very complicated tasks through the command line, or in our case — through the npm scripts. The problem with this approach is that those commands can also get too complicated, and become hard to maintain, not to mention debug.

Luckily, webpack also has a very strong node.js api. So as suggested in this great article, we can simply pass all the logic to a separate node file, and use npm script to run it. Like this -

The problem with this approach is that you might end up with multiple node.js files for each task (build.js, serve.js, etc.), many of which are doing very similar tasks. So I suggest taking this approach one step further, and removing all logic out of package.json and move it into a new file called tasks.js by implementing NPM lifecycle event. This nifty little variable lets you know exactly which npm command initiated the current process, so you no longer need package.json to point to the right file.

So your package.json file deprived of all logic will look something like this -

And this new tasks.js file will look something like this -

Building Tasks

So now that we’ve created our own personal task manager, it’s time to build the tasks themselves. mine looks something like this -

Here we can see two methods: serve and build. Both initiate a compiler by passing a webpack configuration file (lines #18 & #26). But then they diverge:

  • The serve method pass the compiler into an instance of a webpackDevServer (#19) and then starts it (#21).
  • The build method simply runs the compiler (#28).

Different Configurations for Different Tasks

Some of you may have noticed that both serve and build use the same webpack configuration file (#2). You don’t have to be a webpack expert the guess that these tasks require different configurations. Take minification and uglification for example: you wouldn’t want your development environment to have unreadable JS, nor would you want your production environment to download large un-minified JS. So how can we achieve this separation without complicating our tasks file too much?

Let’s take a look at ./webpack.config.js :

As you can see, this file actually doesn’t hold any configuration of its own, but rather proxies the appropriate configuration file from inside the conf folder, according to the current npm event. Actually this switch could easily reside inside tasks.js, but having a webpack.config.js file in your project’s root folder is a good convention that’s worth keeping: it lets people know your project uses webpack.

Modular Configuration

So we already established that different tasks require different configurations. But more importantly, even different configurations have a lot in common. Take transpiling JS files for example: both serve and build requires babel loader. So how can you share common code between different webpack configurations? This is where tools like webpack-config and webpack-merge come into play: they allow us to recycle some basic configuration, and reuse it by adding, removing or changing specific parts.

This practice allows us to have one file, let’s call it webpack.base.config.js, which holds configuration common to all tasks, and many other task-specific files, such as webpack.build.config.js and webpack.serve.config.js

Here are simplified versions of base and build using webpack-config (for complete setup checkout the sample project):

Base configuration
Build configuration

Notice how in webpack.build.config.js we call the base configuration (line #6) and merge it with specific tasks unique to the build configuration, such as:

ProTips

Here are some tips for making the best out of this architecture -

Common Configuration File

When using a modular architecture for your build, many parameters may repeat themselves many times, such as folder paths or ports. For this reason it’s recommended to have a common configuration file that will hold all these constants for a more efficient and centralized control. Here is an example of such file -

Checkout the full version in the sample project.

ExtractTextPlugin

The ExtractTextPlugin requires a little different approach to make it work in a modular architecture. This plugin is used to extract the css into a separate file, instead of injecting it into the header. Since its syntax wraps the rule it refers to (usually .css or .scss), it’s a little harder to reuse. Luckily, it has an on/off switch and a fallback rule, which combined make it completely modular. So the base configuration can look like this -

And now, if we want to disable this module for development server (because it doesn’t work well with live reload), we can simply add these lines to the serve configuration -

While in the distribution build we definitely want to switch it on, so in the build configuration we could add this::

Summary

Webpack is a great tool, but configuring it can become very complicated very fast. Instead of trying to maintain a huge blob of configurations, or using a third party task manager, simply implement the architecture described here, and enjoy all the free time you suddenly have to -

  • Go to the beach
  • Work on your side project
  • Write Medium articles

Let me know in the comments what you decided, plus any other questions or ideas you might have. Enjoy!

--

--