Saturday, 23 July 2016

Understanding Webpack's DefinePlugin (and using with TypeScript)

I've been searching for a way to describe what the DefinePlugin actually does. The docs say*:

Define free variables. Useful for having development builds with debug logging or adding global constants.

* Actually that should read "used to say". I've made some changes to the official docs.... (Surprisingly easy to do that by the way; it's just a wiki you can edit at will.)

I think I would describe it thusly: the DefinePlugin allows you to create global constants which can be configured at compile time. I find this very useful for allowing different behaviour between development builds and release builds. This post will demonstrate usage of this approach, talk about what's actually happening and how to get this working nicely with TypeScript.

If you just want to see this in action then take a look at this repo and keep your eyes open for usage of __VERSION__ and __IN_DEBUG__.

What Globals?

For our example we want to define 2 global constants; a string called __VERSION__ and a boolean called __IN_DEBUG__. The names are deliberately wacky to draw attention to the fact that these are not your everyday, common-or-garden variables. Them's "special". These constants will be initialised with different values depending on whether we are in a debug build or a production build. Usage of these constants in our code might look like this:


    if (__IN_DEBUG__) {
        console.log(`This app is version ${ __VERSION__ }`);
    }

So, if __IN_DEBUG__ is set to true this code would log out to the console the version of the app.

Configuring our Globals

To introduce these constants to webpack we're going to add this to our webpack configuration:


var webpack = require('webpack');

// ...

  plugins: [ 
      new webpack.DefinePlugin({
          __IN_DEBUG__: JSON.stringify(false),
          __VERSION__: JSON.stringify('1.0.0.' + Date.now())
      }),
      // ...
  ]
// ...

What's going on here? Well, each key of the object literal above represents one of our global constants. When you look at the value, just imagine each outer JSON.stringify( ... ) is not there. It's just noise. Imagine instead that you're seeing this:


          __IN_DEBUG__: false,
          __VERSION__: '1.0.0.' + Date.now()

A little clearer, right? __IN_DEBUG__ is given the boolean value false and __VERSION__ is given the string value of 1.0.0. plus the ticks off of Date.now(). What's happening here is well explained in Pete Hunt's excellent webpack howto: "definePlugin takes raw strings and inserts them". JSON.stringify facilitates this; it produces a string representation of a value that can be inlined into code. When the inlining takes place the actual output would be something like this:


    if (false) { // Because at compile time, __IN_DEBUG__ === false
        console.log(`This app is version ${ "1.0.0.1469268116580" }`); // And __VERSION__ === "1.0.0.1469268116580"
    }

And if you've got some UglifyJS or similar in the mix then, in the example above, this would actually strip out the statement above entirely since it's clearly a NOOP. Yay the dead code removal! If __IN_DEBUG__ was false then (perhaps obviously) this statement would be left in place as it wouldn't be dead code.

TypeScript and Define

The final piece of the puzzle is making TypeScript happy. It doesn't know anything about our global constants. So we need to tell it:


declare var __IN_DEBUG__: boolean;
declare var __VERSION__: string;

And that's it. Compile time constants are a go!