Browsersync and Deleting Files

This is a little blog about an issue (and a solution) when using Browsersync in combination with a bundler.

Some background

I was working on an Angular project with webpack and lite-server. Webpack bundles and transpiles my JavaScript and CSS and lite-server hosts my files. As you might know lite-server uses browsersync internally to make sure your app in the browser is always up to date with any code changes. Most sensible people would use webpack-dev-server to host their files when using webpack. But not me, oh no, I had to be stubborn and use lite-server.

The issue

Browsersync uses a bs-config.json file for configuration, this is what mine looked like:

{
  "port": 8000,
  "files": ["./dist/**/*.{html,htm,css,js}"],
  "server": { "baseDir": "./dist" }
}

It makes sure that my files from the dist folder are hosted on port 8000. The files entry specifies a subset of files in my dist folder. Whenever there is a change to one of these files, the browser will update accordingly.

Now this is a simplified version of my webpack.config.js file:

module.exports = {
  entry: ['./src/index'], // file extension after index is optional for .js files
  watch: true,
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.[hash].js'
  },
  module: {
    rules: [ ... ]
  },
  plugins: [
    new CleanWebpackPlugin('dist/**.*', { watch: true }),
    new ExtractTextPlugin('theme.[hash].css'),
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new HtmlWebpackPlugin({ template: 'src/index.html' })
  ]
}

As you can see I have watch mode enabled, so the bundles will rebuild whenever I make a change to my code. I'm also using the CleanWebpackPlugin to clear out any old files in the dist folder. This is necessary because the bundle has a different hash every time and I don't want to keep the old ones. Finally the HtmlWebpackPlugin comes into play and regenerates my index.html file in dist folder. This plugin injects the script tag referring my bundle into index.html.

So what is the problem? The problem was that when I made the change to my code I would see this:

/cantget.png

Webpack did pick up the change and did re-create the bundle. A new index.html was created and the browser refreshed. However, it could no longer find index.html. Refreshing the browser again resolved the problem, but it was annoying to do.

Looking lite-server's output allowed me to find the problem.

[Browsersync] Reloading Browsers...
17.09.22 11:59:13 404 GET /index.html
[Browsersync] Reloading Browsers...
[Browsersync] Reloading Browsers...

"I was wondering why the ball was getting larger. Then it hit me"

Initially I thought the three reloads where due to the fact that I had three files being generated (HTML, JS and CSS). But then it hit me.

The first event is not the change of the index.html file, it was the removal. That explains the 404. One of the next reloads was triggered by my new index.html, but the browser did not reload.

Browsersync injects code into the served html file to set up a websocket connection. This code is also responsible for refreshing the browser. But since we don't have an html page in the browser at that point, there is no way that the browser could know about the change.

The solution

The solution is quite clear: make sure the browser does not reload on the removal of the html file. But how?

The frist step is to switch from bs-config.json to bs-config.js. The major difference between the two is that you can write code in the second one. This is the equivalent to the original bs-config.json, except that I limited the scope to only index.html.

bs-config.js

module.exports = {
  port: 8000,
  files: [
    {
      match: ["./dist/index.html"]
    }
  ],
  server: { baseDir: "./dist" }
};

The files entry allows you to pass a function fn that is executed when a matching file causes an event.

module.exports = {
  port: 8000,
  files: [
    {
      match: ["./dist/index.html"],
      fn: function (event, file) {
        console.log("event: " + event);
        this.reload();
      }
    }
  ],
  server: { baseDir: "./dist" }
};

Next to reloading (which is the default behavior), I've also printed the event. The output from lite-server changed to:

event: unlink
[Browsersync] Reloading Browsers...
17.09.22 11:59:13 404 GET /index.html
event: change
[Browsersync] Reloading Browsers...
event: add
[Browsersync] Reloading Browsers...

I encountered three kinds of events: unlink, change and add. Unlink is causing the problem so the solution is quite apparent:

fn: function (event, file) {
  console.log("event: " + event);
  if (event !== 'unlink') {
    this.reload();
  }
}

The resulting output:

event: unlink
event: change
[Browsersync] Reloading Browsers...
event: add
[Browsersync] Reloading Browsers...
17.09.22 13:24:03 200 GET /index.html

Bingo!

/bingo.png

(I have no idea who that guy is)

Anyway I really love small victories like this and I hope it helps out some people.