Recently I’ve been hooking up numerous plugins to our Webpack based build system. As it currently stands most of the plugins lack good documentation which inevitably leads to misconfiguration and consequently build time errors. Of course this is part of the learning process and shouldn’t be a big problem by itself. However, the big obstacle is that at the moment, Webpack doesn’t exactly tell you where the error occurs. And so I often ended up with the following unspecified errors:

Webpack does its job and reports the error, but doesn’t give you any clue as to where exactly this error comes from. Debugging this error can literally take days if you don’t know where to look. You’d have to remove plugins one by one to identify the plugin that causes the error. That’s very time consuming and inefficient.

This article will show a simple way to very quickly identify the culprit for the error. It’s a very useful technique that potentially can save you days of the effort. It worked extremely well for me when I was trying to configure ngtools/webpack AOT plugin to be used outside of angular-cli. I believe it will help you as well.

HelloWorldCheckerPlugin#

Before I show you how to debug the error, let’s first see how these unspecified errors might occur. For that we will write a simple plugin ourselves that takes a path to a file as an option and checks whether this file contains a Hello World! string. It then simply logs the result of the check to the console.

plugins: [
   new HelloWorldCheckerPlugin({path: 'toinspect.txt'})
]

The plugin seems to be very easy to configure. But, suppose the author of the plugin forgot to mention that the plugin expects the absolute path to the file and not the relative path. It’s very common to first write a plugin for one’s own usage and afterwards make it public on Github. And as a result the documentation, if any, may not contain something that the author knows and assumes is obvious.

The plugin seems to be very easy to configure but suppose the author of the plugin forgot to mention that the plugin expects the absolute path to the file, not relative. It’s very common to first write a plugin for one’s own usage and afterwards make it public on github. And as a result the documentation, if any, may not contain something that the author knows and assumes is obvious.

So here is how our plugin implementation:

const fs = require('fs');
const path = require('path');

class HelloWorldCheckerPlugin {
    constructor(options) {
        this.options = options;
    }

    apply(compiler) {
        compiler.plugin('make', (compilation, cb) => this._make(compilation, cb));
    }

    _make(compilation, cb) {
        try {
            const file = fs.readFileSync(path.resolve('/', this.options.path), 'utf8');
            if (file.includes('Hello World!')) {
                console.log(`The file ${this.options.path} contains 'Hello World!' string`);
            } else {
                console.log(`The file ${this.options.path} doesn't contain 'Hello World!' string`)
            }
            cb();
        } catch (e) {
            compilation.errors.push(e);
            cb();
        }
    }
}

exports.HelloWorldCheckerPlugin = HelloWorldCheckerPlugin;

Here we simply hook into make stage of compilation process and try to perform the check in the _make method. And this line shows that current implementation expects an absolute pat to the file:

fs.readFileSync(path.resolve('/', this.options.path), 'utf8');

Now suppose you downloaded this plugin and configure it in the following way specifying the relative path:

const HelloWorldCheckerPlugin = require('./plugin').HelloWorldCheckerPlugin;
const path = require('path');

module.exports = {
    entry: "./main",
    output: {
        path: __dirname + "/dist",
        filename: "bundle.js"
    },
    plugins: [
        new HelloWorldCheckerPlugin({path: 'toinspect.txt'})
    ]
};

And when you run the webpack and you get the unspecified error:

It might be easy to realize that the error is in the HelloWorldCheckerPlugin. However, imagine:

  • the less descriptive error like the one we’ve seen in the introduction
  • you have 10+ plugins configured and some of them use toinspect.txt file
  • the file toinspect.txt is not only used in the plugin but is also required by a bunch of JS files in the bundle

Now suddenly you’re stuck. It’s not even clear where to start the debugging. It’d be nice if webpack provided at least the file name where the error occurred. But as we will see soon it’s not really possible to do that for webpack in its current implementation and so as I understand it relies on an author of a plugin to provide all the required error details to troubleshoot the problem.

Compilation errors#

It’s important to understand that webpack stores all errors related to the compilation process in the Compilation.errors array:

class Compilation extends Tapable {
   constructor(compiler) {
      super();
        ...
      this.errors = [];
   }
}

And every plugin is expected to populate that array if some error occurs. And our awesome HelloWorldCheckerPlugin certainly follows that requirements and adds the error to the array:

_make(compilation, cb) {
    try {
       ...
    } catch (e) {
        compilation.errors.push(e);
        cb();
    }
}

So to understand where the unspecified comes from we would simply need to intercept the call to push and then inspect the callstack. There we will see the place where the error is being pushed into the array. Perfect, let’s do exactly that.

Intercepting `push` to errors array#

I’m using Chrome to debug Node scripts now. The approach I‘ll show can be used with any Node debugger as well. I use Chrome because it works much faster than built-in Webstorm’s node debugger. See this article for more information on how to use Chrome with Node scripts.

As explained in the article to debug Node with Chrome we simply need to run node with the --inspect option. I’m using its variation here --inspect-brk to stop at the first statement since I need a pause to put relevant breakpoints in other files. So to run webpack in a debugging mode we run the following commands:

node --inspect-brk node_modules/webpack/bin/webpack.js

I also usually alias this to something, for example dlwpc (stands for debug local webpack), to be able to debug Webpack from any folder where it’s installed:

alias dlwpc="node --inspect-brk node_modules/webpack/bin/webpack.js"

Now, to intercept the push we’re going to replace:

this.errors = []

with

this.errors.push = ()=> { Array.prototype.push.call(this, arguments); debugger }

In this way whenever there’s a call to push we’ll get the execution stopped and we will be able to explore where the call is made from. There’s no need to make that change permanent in the webpack sources. We can utilize the console for that and make substitution there. So, let’s first run the webpack in the debug mode:

$ dlwpc

And as described in the article navigate to

chrome://inspect

and click on inspect in the Remote Target section:

Sometimes it takes a few seconds for the entry to appear, so give it a little time. One you click on it, this will open a standalone Chrome debugging tools and stop on the first statement:

Now we need to access Compilation class in the node_modules/webpack/lib/Compilation.js file but it won’t be available since Chrome haven’t yet loaded it. You have three options:

  1. Put a debugger statement in the Compilation class constructor in the sources. That's the least convenient option because you'll need to clean these statements but I still sometimes use this approach when I have source code open and want to get to this point without extra hassle.
  2. Upload Webpack files manually from the file system. That's probably the most convenient option, but it doesn't always solve the problem. When you know you debug just one package, it's easy to find it and upload it. But often multiple packages are used so you don't know which packages to upload and uploading entire node_modules folder isn't feasible. Besides, the file could easily be outside the node_modules.  
  3. Run Webpack till the end one time and then the inspector will load and keep all the files used during previous execution. Then you’ll be able to open the the required file and put a breakpoint there. I use this approach quite often too.

Let me show the second and the third approach.

Running the webpack till the end#

To run Webpack first time, I put a breakpoint at the end of the webpack.js main file:

And then resume script execution with Resume script execution (F8) and wait until webpack has reached the breakpoint. At this moment all the files used during execution have been loaded so I can simply open the file with Compilation class using Ctrl+P on Windows:

and put a breakpoint after the this.errors=[] have been defined:

Since Webpack have processed this file already let’s restart webpack again and after we resume the execution the debugger should stop at this breakpoint. I’ve sometimes experienced Chrome not remembering breakpoints from the previous session, so you might need to re-run Webpack twice or use the debugger statement approach.

Uploading webpack files#

To upload Webpack files, go to Sources tab and Filesystem section. Click on the Add folder to workspace and select webpack module in the file system:

Once you've done that, the Compilation class is available, you can open it

and put a breakpoint there:

Overriding errors array#

Now, once you’ve got execution paused at the breakpoint we need to override this.errors with our custom object. As I said earlier we will use the console for that:

I open console by pressing escape button Esc on Windows. If it doesn’t open a console for your check the documentation. It’s important to replace this.errors with the custom object before any code wrote anything to it.

Now once we’ve done that let’s just simply resume the execution and see what happens. The breakpoint is stopped and I can clearly see where the error comes from using the callstack:

And if I click on the plugin.js:22 in the callstack I can see our plugin adding the error:

Voila! We’ve just pinpointed the error. Now we know where the error comes from and we can experiment with the configuration or read the plugin sources and understand what we did incorrectly.

Just to show you that our plugin is powerful and correctly implemented let’s provide an absolute path:

plugins: [
    new HelloWorldCheckerPlugin({path: path.resolve(__dirname, 'toinspect.txt')})
]

And here is what we have:

And that’s it.

GitHub#

If you want to play with the setup I’ve created the github repository here. Once you've download the repository, change the path supplied for the HelloWorldCheckerPlugin in the webpack.config.js to generate an error:

plugins: [
    new HelloWorldCheckerPlugin({path: 'toinspect.txt'})
]