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:
vendor.js, which contains third-party libraries,main.js, which contains the project's source code, andmanifest.js, which contains the webpack runtime and loads the webpack manifest for the project.
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.