Webpack-Dev-Server (3.6.0): What I Wish I Knew

Published Oct 06, 2017Last updated Apr 04, 2018
Webpack-Dev-Server (3.6.0): What I Wish I Knew

I tend to get frustrated when debugging config issues. This leads to me making stupid mistakes and haphazardly changing config options hoping one of them sticks. I decided to break the cycle and try to actually figure out what’s going on. I’ll refer to webpack-dev-server as WDS for this article.

First, 3 big things to be aware of:

  1. WDS does not auto rebuild if you change the config files. So, if you’re changing things, and nothing seems to be changing, this observation is correct.

  2. WDS does not just compile your bundle and then serve it from files. Rather, it holds your bundle in-memory. This means that if you’re trying to figure out where your compiled files are going, you won’t see them in a directory. To see where they’re going, go to localhost:8080/webpack-dev-server.

  3. Nothing changing even though you can load your site and the compile is successful? There’s a good chance you’re actually serving up your non-WDS built bundle files.

    If you’re debugging your WDS config, delete your built webpack files. This way, if your WDS config is messed up, you won’t get any false positives. The catch is, after getting WDS correct, you then need to make sure that your built files are going to the right place.

Docs too vague? Read the code.

When I get consistently frustrated by something like this, I try to force myself to understand it at a deeper level. Thus, to the source.

The most confusing thing to me is figuring out where WDS considers your file to be (since you can’t just look at the directory), and how that’s different from the normal build.

There’s this odd page that WDS gives you, which effectively shows you a directory listing of your built files.

app.get('/webpack-dev-server', (req, res) => {
  res.setHeader('Content-Type', 'text/html');
  res.write('<!DOCTYPE html><html><head><meta charset="utf-8"/></head><body>');
  const outputPath = this.middleware.getFilenameFromUrl(options.publicPath || '/');
  const filesystem = this.middleware.fileSystem;
  function writeDirectory(baseUrl, basePath) {
    const content = filesystem.readdirSync(basePath);
    content.forEach((item) => {
      const p = `${basePath}/${item}`;
      if (filesystem.statSync(p).isFile()) {
        res.write('<li><a href="');
        res.write(baseUrl + item);
        if (/\.js$/.test(item)) {
          const htmlItem = item.substr(0, item.length - 3);
          res.write('<li><a href="');
          res.write(baseUrl + htmlItem);
          res.write('</a> (magic html for ');
          res.write(') (<a href="');
          res.write(baseUrl.replace(/(^(https?:\/\/[^\/]+)?\/)/, "$1webpack-dev-server/") + htmlItem); // eslint-disable-line
      } else {
        writeDirectory(`${baseUrl + item}/`, p);
  writeDirectory(options.publicPath || '/', outputPath);

I confess, I don’t really know what the magical HTML is supposed to do from lines 19–29, but it’s not necessary to understand for this. It seems be a way of having your app wrapped in some HTML, which tells you when WDS is reloading and such. It doesn’t work for me due to the way my routing is set up

This is what renders for me at localhost:8080/webpack-dev-server.

It appears to be the files that webpack is serving — you can tell my setup here is messed up. Everything should be in dist. But, dist should also be the root.

The business of this route appears to be the writeDirectory function, which takes baseUrl and basePath. At the bottom, we see writeDirectory is called with publicPath and outputPath, respectively. This function gets the list of files at the basePath and iterates over them, recursively calling itself if one of those files is actually another directory.

This bit of code reveals how WDS’s paths actually works, specifically the relationship between outputPath and publicPath. First off, some relevant bits of my config:

output: {
  path: path.resolve(__dirname, 'dist'),
  publicPath: 'dist'
plugins: [
   new HtmlWebpackPlugin({
   template: 'public/indexTemplate.html',
   filename: 'dist/index.html'
   favicon: 'favicon.ico'
devServer: {
  historyApiFallback: {
    rewrites: [
      { from: /list\/*/, to: 'index.html' }
  https: false,
  publicPath: '/dist/'

Obviously, this config is a little messed up, but I arrived here out of a more sensible working config. You can see I’m using html-webpack-plugin, which is what started all of this confusion because it meant that I needed to get my index out of dist instead of public.

Let’s look at the rendered page again:


That first link, my bundle, actually points to
How did dist get there? Well..

res.write('<li><a href="');
res.write(baseUrl + item);

The baseUrl (remember, this is publicPath) is appended in the URL but not in the text. If I click this link, the bundle is indeed there. This seems to make sense, although if publicPath was '/' instead of /dist/, this means that the bundle would be built to dist/bundle.js but accessible in WDS at localhost:8080/bundle.js.

Fixing the config

The worst part of this config, is that I used filename: 'dist/index.html for the html-webpack-plugin config. This means that the file is actually accessible at localhost:8080/dist/dist/index.html. This can be particularly confusing if you’re playing around with the config. As with publicPath: '/', you’d have the index.html accessible at localhost:8080/dist/index.html, which almost makes sense.

So, we remove dist from the html-webpack-plugin filename because outputPath is already putting it in dist/.
With this change, I can now access my built index.html at localhost:8080/dist/index.html, which is still not what I want.

I really want it to be at /. But if we recall that line where the link is created:

res.write(baseUrl + item)

Knowing baseUrl is really publicPath, the only way this can happen is if the WDS publicPath is /.

Success! Or, sort of. Now I get index.html just by going to localhost:8080/ (which will request index.html because index has magical properties). However..


html-webpack-plugin is generating the URL to the bundle, but it’s including dist in the URL, why?

Well, it turns out, I only changed the publicPath in WDS. It needs to be / in both cases. Save yourself the trouble, and don’t set publicPath in your WDS config.

SPA Woes

Okay, so now we’re back to the problem that led me to change everything in the first place. When I navigate to another page and refresh, I get a 404 due to the fact that this is a single page app (SPA). I’d set up re-routing rules, but it appears they aren’t working.

historyApiFallback: {
  rewrites: [
    { from: /list\/*/, to: 'index.html' }

When running WDS, it helpfully informs us of the following:

Okay, so if output is coming from / and 404s fall back to index.html, then when I 404 on localhost:8080/list/1/1 this should re-write to localhost:8080/index.html, which veritably loads a page. So there’s something going on here that’s not obvious from the information given.

index: 'index.html'

What’s wrong with this? It’s a relative path. Relative, to whatever URL you’re currently at. So, when the 404 for localhost:8080/list/1/1 occurs, we’re actually loading localhost:8080/list/1/index.html. This really was not obvious to me. The worst part? If you look at the request in your network tab, you don’t see this attempt at re-routing the request.


One last gotcha

Now that publicPath is /, html-webpack-plugin will generate this in your index.html:

<script type="text/javascript" src="/3bf45b6412afeda22cbc.js"></script></body>

Your bundle is built to dist because of outputPath. html-webpack-plugin tells the browser to look for it at / because of publicPath. How to handle this? Either start your server at dist (seems to make sense), or use a different publicPath in your prod webpack config.

Up to you.

That’s as much Webpack pain as I can summon up at the moment. If you find anything that could be improved about this, I’d be more than happy to hear it.

Lastly, if you’re in need of a freelance developer, I’m in the market for work. I specialize in modern front-end web dev using the latest technologies. I’m also brushing up on my full-stack skills and trying to help connect rock climbers with my web app, letsclimbstuff.com.

You can contact me at narthur157@gmail.com or on my Codementor profile.

Discover and read more posts from Nicholas Arthur
get started