Webpack App Breaks When Entrypoints Share Functions or Components

A couple of days ago I was refactoring a client's web app after some major changes were made to it. All in all, it was a relatively uneventful refactor that took place over two or three weeks. The application itself was notable in that it was not a Single Page App (SPA), which is uncommon in the type of work that I'm doing -- although not unheard of.

Instead of a SPA-style app where every page is bundled together into one script file and navigation between URLs is instant, this app was set up as a traditional ASP.NET MVC affair with views and page loads. In most cases this would have been fine and nothing to write home about, but since this was an embedded Shopify app that had to be loaded in the Shopify admin dashboard, each page had its own dedicated JavaScript (actually TypeScript) script. That script was responsible for initializing Shopify's Polaris framework and running the form fields/other typical app stuff.

We decided to use Webpack to extract common vendor files (like React and Polaris) out into one shared script that would be loaded on every page. This would increase the speed of page loads, since the browser would only need to load the big vendor file once and then cache it. Subsequent page loads would only need to load the page's own dedicated script, which was much, much smaller.

The Webpack config looked something like this:

module.exports = {
    entry: {
        "home": "src/home/page.js",
        "auth": "src/auth/page.js",
        "account": "src/account/page.js"
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: {
                    // Extract any package from node_modules to the vendors file
                    test: /[\\/]node_modules[\\/]/,
                    name: "vendors",
                    chunks: "all"
                },
                shared: {
                    chunks: "initial",
                    minChunks: 2
                }
            }
        }
    },
    output: {
        filename: "[name].js",
        path: "public/js"
    },
    ...
}

Since each page had it's own corresponding entry, they wouldn't all be bundled together into one big blob. Rather, they'd be bundled into their own smaller blobs that just contained the components and functions used on that page. Seasoned Webpack veterans can most likely see the error here already, but even as somebody that's reasonably familiar with Webpack and has been using it for a couple years at this point, the issue I'm about to describe was not obvious to me. I lost the better part of a day trying to debug it!

I first learned that there was an issue after I finished refactoring and loaded up the app in my browser to do some manual testing. First page worked fine, but when I navigated to one of the pages that used a shared component or function, literally nothing would happen. There were no errors in the console, and no failed network requests. There was absolutely zero indication that anything was happening at all -- the script wasn't even being executed.

At first I couldn't even tell what was causing this behavior. It was only through dumb luck that I discovered, after commenting out entire swathes of code, that this would only happen when one of my shared components or functions was used by more than one Webpack entrypoint. If I had shared/TextField.jsx, and that was used by src/home/page.js and src/auth/page.js -- both entrypointes in my Webpack config -- the app would simply not load on those specific pages.

At first I thought it was maybe related to the name of the component, which was something like TypeAhead. Maybe that's some kind of reserved word in Webpack or React, I thought to myself. To test my theory, I duplicated the component into a different file and gave it a different name. When I replaced the original component with the duplicate in the page that I was testing, suddenly it started working again. "Aha! It must be the name! That's weird, maybe I should report this to Webpack later."

So I did what anybody would do in this situation: I renamed the original component, booted up the app and went back to work... only to be greeted with another blank page and no errors in the console. It wasn't working again.

I'll keep a long story short and skip over several hours of debugging (mostly because I was too stubborn to add live reloading to the app, so I'd have to run docker/yarn/dotnet build between each change). Toward the end of the day, I finally caught a glimpse of something that looked weird in my terminal.

To give some context on why I didn't catch this earlier, this was a dotnet app that was built and run inside Docker, and it would run Webpack as one of its pre-build steps. The output of the Webpack build was always instantly pushed off the screen when dotnet started building immediately afterwards. Since Webpack wasn't throwing errors, I didn't think there was anything wrong with the build process and never thought to check it.

Toward the end of the day, I finally caught a glimpse of something that looked weird in my terminal. Webpack was building all of the entrypoints just fine, but a couple of the entrypoints were generating some extra, strangely named files; the names looked something like shared~src~auth~page~src~home~page.js. That's what clued me in that there were maybe some extra files being generated, and because I didn't know about them, they weren't being loaded in my views. A quick ls path/to/built/files confirmed what I was seeing: a few of those strangely-named files were sitting in the output directory.

So here's what was happening: it turns out that the optimization.splitChunks.cacheGroups.shared object in my Webpack config was telling Webpack to take anything that was used by more than one entrypoint and put it into its own shared file. It was behaving just like the vendors.js file that I had set up for node packages, but because it didn't have a name property (like "vendors"), it was just choosing a random filename for those shared pieces of code. Once I gave it a name: "shared", Webpack started putting those components into a shared.js script, and after loading it in my views, the app started working again!

Now, obviously this was all my fault for not reading the docs completely. I thought that the shared object was just some necessary stuff for the vendor file, but it was actually configurating its own, separate file. However, even though it was my fault that this was happening, it still would have been nice if Webpack threw some kind of error or logged literally any kind of message to give a hint that it's waiting for additional files to be loaded.

This was incredibly hard to debug with no guidance. Hopefully you have better luck after reading this!


Learn how to build rock solid Shopify apps with C# and ASP.NET!

Did you enjoy this article? I wrote a premium course for C# and ASP.NET developers, and it's all about building rock-solid Shopify apps from day one.

Enter your email here and I'll send you a free sample from The Shopify Development Handbook. It'll help you get started with integrating your users' Shopify stores and charging them with the Shopify billing API.

We won't send you spam. Unsubscribe at any time.