jahed.dev

Upgrading to Webpack 5

Webpack 5 was released back around October 2020, but I decided to give it some time before upgrading. Webpack is a complicated project with a lot of moving pieces. A lot of plugins also have to migrate to support it so it's a good idea to give everything some time. Though, not too much time as Webpack 4 was no longer being maintained, meaning no security fixes, bug fixes and so on.

Within a month, most plugins I use seemed to have made the switch. Some dropped support for Webpack 4 and started introducing new features and removing old ones, which only made the upgrade even more necessary. So a few months later I decided to take the leap. This post will go through my approach and some of the more notable issues I encountered.

Version Management

In general, the NodeJS/NPM ecosystem makes it difficult to coordinate dependencies. The best we have are peerDependencies, which lets us know which dependency needs to be installed. The downside is that failures to adhere to those requirements don't lead to errors, just warnings. So you need to be perceptive when they occur and figure it out yourself. This means looking at documentation, release notes and package.jsons of dependencies before you install them to ensure compatibility. In my case, I kept a list of Webpack dependants and whether they support Webpack 5 or not.

Dependency Maintenance

Webpack 4 has been out for a while, so it's normal for a lot of plugins to no longer be maintained at this point. A decision needs to be made to look for alternatives or consider forking and maintaining it yourself.

Typically the best solution I've found is to minimise your dependency on Webpack. Don't use it to copy files or other basic tasks that can be done from the outside. There's no need to couple everything to a single tool. Use Webpack only for bundling and upgrades will be much simpler. Especially if you later want to move to a different bundler like esbuild.

For this reason, I've been removing a lot of Webpack plugins over the years. Some I've created and decided to archive. Sitemap generators, favicon generators and so on. Others maintained by third-parties I didn't have enough trust in. The quality of plugins are hit or miss so it's important to have tests to ensure they're functioning correctly. Don't just upgrade and hope they're still working.

Configuration

As APIs change between versions, it's important to know your configuration is still valid. The most reliable solution is to go through your entire configuration. Make sure things still mean what they did previously and if not, what's different.

The release notes and migration guide has a summary but you'll have to dig deeper for details. Do not rely on automation as types and validators can only check for flags and values, not the meaning behind them. Sadly, Webpack's documentation is still a bit inconsistent. It won't explain defaults clearly and some of is it hard to understand if you don't know the internals and its terminology. Some of it is the opposite and doesn't explain enough.

So it's a good idea to avoid configuring Webpack too much. The less configuration, the better. Let Webpack do its magic, unless if you have to time to keep up with how the magic works on every release.

Magic

On the topic of magic, there are things Webpack does that isn't immediately obvious... until it stops doing it.

NodeJS Modules

Webpack 4 used to automatically swap NodeJS modules like path and process with polyfills to maintain browser support. This was completely behind the scenes so I don't think many would realise. Webpack 5 removed this magic with good reason.

A few dependencies I used relied on these modules. Mostly the ones around graphics, compression, zipping and encryption. Luckily, Webpack knows when a module won't be found and will tell you. So that's convenient. Just reintroduce them. How? It's not immediately clear as the migration guide only talks about modules you have direct control over. For dependencies, the best way I've found is to install the missing polyfill and use an alias.

{
  // ...
  resolve: {
    // ...
    alias: {
      path: "path-browserify"; // require('path') becomes require('path-browserify')
    }
  }
}
If you need a full list of polyfills that Webpack used to install automatically, click here.
assert: 'assert',
buffer: 'buffer',
console: 'console-browserify',
constants: 'constants-browserify',
crypto: 'crypto-browserify',
domain: 'domain-browser',
events: 'events',
http: 'stream-http',
https: 'https-browserify',
os: 'os-browserify/browser',
path: 'path-browserify',
punycode: 'punycode',
process: 'process/browser',
querystring: 'querystring-es3',
stream: 'stream-browserify',
string_decoder: 'string_decoder',
sys: 'util',
timers: 'timers-browserify',
tty: 'tty-browserify',
url: 'url',
util: 'util',
vm: 'vm-browserify',
zlib: 'browserify-zlib'

NodeJS Globals

Similar to modules, Webpack also stopped handling NodeJS globals like Buffer. As these aren't imported, you'll only know about missing globals when your code executes that line and can't find it. Ideally you'll have tests to pick these up, otherwise you'll need to manually test various scenarios to make sure you're in the clear.

Again, install the polyfill but this time since it's not imported as a module, you'll need the ProvidePlugin to provide it whenever its used as a global.

{
  // ...
  plugins: [
    // ...
    new ProvidePlugin({
      Buffer: ["buffer", "Buffer"], // Buffer = require('buffer').Buffer
    }),
  ];
}

Module Resolution

There's an odd bug I encountered with JSZip which I think is a combination of changes in how modules are resolved and how global polyfills need to be provided. Essentially, the "." resolver in JSZip's package.json doesn't seem to be detected by Webpack 5 so weird stuff happens when it incorrectly goes into NodeJS-specific module and polyfills it. I made a PR for it on JSZip's end so hopefully that gets fixed. For more details, follow those last two links. Again, thank you tests.

Dynamic Imports

Dynamic imports also seem to have changed. They no longer resolve default exports automatically. I couldn't find the documentation on it so that was a surprise. Again, my tests caught this as it's a runtime failure for a very specific usecase so... good thing I write tests.

-  const jsMD5 = await import('js-md5')
+  const { default: jsMD5 } = await import('js-md5')

Development Server

This one was a bit unexpected as webpack-dev-server didn't have a major version change and it isn't mentioned in the migration guide. Using it as a command doesn't work anymore with Webpack 5. Instead you have to use webpack serve. A minor thing, but still a thing. I think they're planning a major change to make this more obvious but it hasn't been released yet.

Plugins

I won't go too much into the issues I faced with upgrading plugins. Most plugins upgraded fine. Some still don't support Webpack 5 so I removed them.

A few plugins no longer need their own cache as they use Webpack 5's new caching feature. Loaders like babel-loader still do as they don't have access to Webpack's. So it's kind of a half-solved problem right now.

The remaining issues were specific to certain plugins so I'll leave them out.

Conclusion

So after around 3 months since Webpack 5's release, there are still a lot of issues with its migration. That's a shame and really makes me doubt the current front-end ecosystem I'm building on top of. Webpack does do a lot, but I wonder if I need that complexity. At the end of all of this, nothing's really changed for me. Build times are similar, outputs are about the same size. It's one of the reasons to look forward to a new generation of bundlers that can hopefully learn from the previous generation.

If there's one positive outcome, it's that all that time I spent on writing tests clearly paid off here. I don't think I would've caught most of these issues without them.

Thanks for reading.