Using webpack Build System in Existing Codebases

There are many tutorials, blog posts, and articles in the internets that deal with using cool and shiny new tools in cool and shiny new projects. As any developer knows, useful codebases that are anything more than “Hello World” don’t stay in that hipster-happy state for too long. Even in the most disciplined teams, the tech debt grows and consumes the code; once active and well-maintained libraries gradually become forgotten and slowly await death in dusty corners of GitHub. Unfortunately, adding shiny new tools to older projects is not always well covered.

At Threat Stack, we discovered that when the time came to revamp our front-end project’s build system. The existing combination of gulp and browserify was adequate but started to show its age. Each rebuild was taking close to 15 seconds, and our custom-crafted gulpfile with many steps and permutations was becoming hard to understand and maintain.

Enter webpack. Our goal was to improve the speed and maintainability of the build process. A stretch goal was to incorporate the ability to do cool and productive things like live code and style reload. Both goals were met, and our rebuild time is now under two seconds with dynamic in-browser code reload.

Challenges

Our project has a combination of modern React code and older jQuery + Handlebars code, which we’re actively factoring out. Additionally, we have a healthy dose of 3rd-party jQuery plugins. We also take advantage of ES6 syntax which needs to be transpiled to plain Javascript. Each of those requires a different approach to incorporate into the webpack build. Webpack is similar to a regular Node.js application that has a single entry file, which in turn uses require to load any other files in the app. It then can generate the application bundle that contains all of the code and possibly even assets like CSS or images (even though those are better packaged separately, and webpack has readily available tools for that).

But since front-end code and libraries do not always follow proper AMD format, we need to handle:

  • Custom code that expects certain things to be available in the global context (i.e. code that expects $ to be jQuery)
  • 3rd-party libraries and plugins that expect jQuery or other libraries to be available in the global context
  • 3rd-party libraries that are not AMD or CommonJS compatible and require special handling to properly export their functionality
  • Code that requires pre-processing or prebuilding (i.e. Handlebars templates, less styles, ES6 Javascript code)

Solutions

Webpack comes with plugins and loaders to deal with the aforementioned fussy code. Loaders tell webpack how it is supposed to process particular file types. Plugins augment the build process to do things like define build-specific variables.

Dealing with various file types using loaders

As I mentioned earlier, each file in the webpack universe is presented as a commonjs module. We already had the Javascript code tree properly handled by require() and module.exports. But how do you deal with JSON files, CSS files, or handlebars templates? That’s where the webpack concept of loaders comes into play.

We can instruct webpack to use json-loader to handle JSON files. It will then automatically wrap that file in proper CommonJS definition.

var jsonFileData = require(‘json!./data.json’)

It makes sense to define often-used loaders in webpack’s config files under module.loaders:

loaders: [

{

   test: /.js$/,

  include: [

    path.resolve(__dirname, ‘src/)

  ],

     [ ‘babel-loader’, ‘eslint’]

},

{ test: /.json$/, loader: ‘json-loader’ },

]

This tells webpack that all *.js files should be processed by babel-loader (which will transpile any ES6 or JSX code back to plain Javascript) and run through eslint linter.

Dealing with code that expects globally available variables

It was once common for front-end libraries to attach themselves to the global context (browser’s window object). This way you can just use $(‘blah’) anywhere to run jQuery. So any jQuery code likely expects $ to be available. We can include jQuery in a script tag and let it do its naughty thing (attaching itself to window), but this will keep it outside of our bundle. It is also not practical to go to every file and manually require jQuery. This is where webpack’s ProvidePlugin is useful. It can be loaded and configured under plugins in webpack config:

new webpack.ProvidePlugin({

‘_’: ‘lodash’,

‘$’: ‘jquery’,

‘window.$’: ‘jquery’,

‘moment’: ‘moment’

})

This does the following: As webpack processes the code and generates the bundle, it will look for any mentions of $,_ or moment and will provide a reference to our project’s jQuery, lodash, or moment js in that file.

You may ask, how will it find those libraries? That is defined in webpack’s resolve config option:

resolve : {

root: [

  path.resolve(__dirname, ‘src’),

  path.resolve(__dirname, ‘node_modules’)

   ]

}

This way if we can either drop the jquery.js file under the src directory(very naughty) or we can use npm to install it under node_modules where webpack will automatically find it.

One last note on this. The ProvidePlugin uses simple text search to look for modules that need to be defined in a particular file, and it’s not uncommon for libraries to use window.$ syntax. So we can define the provided module for ‘window.$’, too (see above).

Dealing with 3rd-party jQuery plugins

Since $(and certain other libraries) is commonly available in the global namespace, any plugins that augment or extend its functionality expect it to be there too. Also those plugins typically just tack themselves onto jQuery’s $ object. Those plugins also rarely follow CJS format. Two problems arise: how do you load those plugins, and how do you tell them about our project’s jQuery. Webpack provides a special loader for this: imports-loader. Using that loader we can load bootstrap plugin as such:

require(‘imports?jQuery=jquery,$=jquery,this=>window!l3rdparty/bootstrap.min.js’)

This will look for bootstrap.min js under the directories we have previously defined in resolve config param. Once it finds it, it will prepend that file with something like:

var $ = require(‘jquery’);

So the plugin code can run and modify our project’s jquery.

Using the => symbol it’s possible to set arbitrary variables in that plugin file. For example our imports code will add:

var this = window;

I have also discovered that certain plugins try to check whether they are running in a CJS or AMD environment by checking if AMD’s “define” function is available. Webpack defines both of those, and few plugins failed very confusingly. This can be remediated with:

require(‘imports?jQuery=jquery,$=jquery,define=>false!l3rdparty/naughtyplugin.js’)

Libraries that do not export their functionality via AMD or CJS

In the previous section we discussed 3rd-party jQuery plugins. Most of those are not proper CJS or AMD modules, but we don’t really care since they just need a jquery reference to modify, and we provide that with imports-loader. But what if we need to use a library that provides an object or a function by just setting it in the global namespace. Since webpack wraps all files with a function, that value will only be accessible inside that webpack wrapper function, which is pretty useless to us. Yet another handy loader, exports-loader comes to the rescue:

var usefulModule = require(“exports?calculate!./useful.js”)

This will take the variable calculate in that module and set it as a return value of require.

Conclusion

While dealing with legacy code is always challenging, it’s very important to continuously pay down tech debt as your architecture evolves. Having a robust and efficient build system helps to keep your code clean, well modularized, and maintainable by allowing you to focus on the important parts (building things) instead of dealing with the minutiae of getting things built. Webpack provides a great set of tools to achieve this goal with minimal modifications to the existing code.