Debugging a webpack project

February 26, 2018

I was working on a project that uses webpack to bundle assets together and HtmlWebpackPlugin to autogenerate an index.html file. The plugin reads the names of each bundle emitted by webpack and injects <script> tags into the index file for them. My setup creates three bundles:

The webpack manifest is a JSON file that contains information about the different bundles generated by webpack. The runtime uses it to know which files to load. HtmlWebpackPlugin injects three <script> tags into index.html—one for each of the three files listed above.

On Mac OS, the project loaded in the browser with no problem—no errors in the JavaScript console. After pushing the project to a server running Ubuntu Linux, building it, and deploying it, the project would not load. The JavaScript console displayed the following error:

Method 'call' does not exist for undefined!

This is a generic error in JavaScript, so the message alone doesn't give much of a clue as to its cause. I traced the message back to the line that throws the error:

modules[moduleId].call(module.exports, ...);

This line of code comes from manifest.js, the bundle containing the webpack runtime. The runtime can be hard to follow, but it appears that this line attempts to evaluate (execute) a JavaScript module when a dependent module requires it for the first time. For some reason, on Linux, modules[moduleId] was undefined, but on Mac OS it was not.

One issue that made debugging harder was that the bundles had been minified using webpack's UglifyJsPlugin. It took me too long to discover that the problem occurred even with minification disabled. If I had spent more time testing different conditions to see if the error persisted, I might not have burned so much time trying to get source maps to work effectively.

I also spent a lot of time googling variations on "webpack build works on mac but not on linux", but none of the results seemed to be related to my issue.

After rebuilding the project in development mode (which omits UglifyJsPlugin), I found that the missing moduleId was './node_modules/react/react.js'. Inspecting the modules object, the only moduleIds present corresponded to project source files. Vendor modules, like React, were not present. At this point, astute readers might discern the cause of the problem, but I was still grasping at straws.

As a next step, I copied all the built files from the Linux server to my Mac laptop to compare them with the locally built files (the working ones.) I used the comm command, which compares two files and outputs lines that differ between the two. Since the line that threw the error came from manifest.js, I tried

$ comm -3 manifest.js server/manifest.js

The -3 argument suppresses printing of identical lines from the two files. The output was empty, signifying that the two files were identical. So I tried running comm over each of the pairs of built JavaScript files. All were identical! At this point I was severely head-scratching. Flailing, and not a little bit frustrated, I tried running comm on the two index.html files. This was the string-tug that caused the mystery to unwind. Two lines were output, for the lines that differed in each file. In the Mac-built index.html, the line (split into three for readability)

<script type="text/javascript" src="manifest.js"></script>
<script type="text/javascript" src="vendor.js"></script>
<script type="text/javascript" src="main.js"></script>

differed from the line in the Linux-built index.html:

<script type="text/javascript" src="manifest.js"></script>
<script type="text/javascript" src="main.js"></script>
<script type="text/javascript" src="vendor.js"></script>

On Linux, the HtmlWebpackPlugin was swapping the order of the injected <script> tags, causing main.js to be loaded before vendor.js. Because main.js (which contains the project source code) tries to import third-party libraries (React, in this case), when it was evaluated before vendor.js was evaluated, the moduleId for React didn't yet exist in the modules object—and modules[moduleId] evaluated to undefined. In other words, the React module hadn't yet been loaded!

Once I understood the problem, I consulted the documentation for HtmlWebpackPlugin and found a configuration option that solved it. By setting chunksSortMode to 'dependency', the plugin would ensure that the <script> tags would be injected in the correct order.

new HtmlWebpackPlugin({
  chunksSortMode: 'dependency',
})

The default value for this option is 'auto', which I guess injects the tags in the order they're read in from the file system? It must be that a difference between Linux and Mac OS causes the file names to be read in different orders.