npm-free LiveReload(ish): Simple scripts for asset watching and auto-reload in the browser

I recently read Chris Coyier’s “Fine, I’ll Use a Super Basic CSS Processing Setup” on and was inspired by the tools he referenced to add a super simple asset-watching script (in bash and then in PHP using server-side events) and “LiveReload” functionality. All fitting with my values. Let’s see what it looks like!

Previous references that this post follows on from:


If you’re not bored of me talking about building small static web apps yet, read the links above and you soon will be!

If you’re following along though, one of the things that was annoying about my almost-npm-free build script is that it didn’t have a file watcher or any kind of hot-reloading.

I always thought there must be a way I could do this that fitted my needs:

  • No NPM or wearisome dependency hell with all the risks of future breakage that brings.
  • Use the tools I have to hand.
  • Make it simple and “done”.
  • Make it something others can use and learn from.

The “Aha!” moment

Then, the other day, I read Chris Coyier’s article about using Lightning CSS in a lightweight build setup.

In it, Chris outlines a simple watch/reload setup using:

  • Turbowatch for file watching – It’s an “extremely fast file change detector and task orchestrator for Node.js.”. This isn’t a huge thing to download, and the config file isn’t terrible, but it’s still overkill. We can do better!
  • Live.js for reloading in the browser. This is old and clunky, but works. It basically polls for changes using special, small, fast, HTTP requests and re-loads stuff if it finds changes.

Finding Live.js was the “Aha!” moment. Polling is fine for local dev on small static sites. Inject the script in development and away you go! The only real downside is clutter in your dev tools network panel with all the requests.

The initial soution: A simple file-watching shell script

Spoiler: This was not the final solution! But it was an important step towards it. Skip this bit if you don’t care about the journey.

So here’s the thing… Unix/Linux can tell you file change times quickly, and shell scripting is kinda perfect for this kind of thing. So why don’t we just do that!

We can use the `find` command to get the latest timestamp of files in a directory.

We can provide a simple list of directories to watch and loop through them seeing what has updated.

When change is detected we can run the build process and then leave some file lying around that front-end polling can detect to prompt a reload.

So I made a script that does that. It does leave .modified_time files in the source directories – you will want to add them to your .gitignore.

It also creates a project-global .modified_time file with a timestamp in it. Read on to find out why we need this.

It should work on both MacOS and Linux, but I’d not tried Linux properly yet.

You can see the script in this gist.

Updating Live.js

I then added Live.js to my site and hoped it would just work. But sadly, it did not.

I’m running Laravel Valet locally and that uses nginx which is fine for .css and .js files, but seems to strip out ETag and Last-Modified headers for HTTP requests that aren’t for resources, like it’s expecting PHP or something to provide them. Live.js depends on these headers for spotting changes. And it was broken! Grrrr.

I could probably hack the nginx config, but this felt like it was awkward and hard for people to do, and could be overwritten in future. I want this to “just work”. So there needs to be another way.

Hold on though… I control this end-to-end. I know the watcher on the back-end as well. So I can link them up!

So I decided to:

  • make the file watcher script add a timestamp to a file on the server
  • modify Live.js to check for the value in that timestamp file as well as the HTTP headers for resources on the page
  • generally tidy up and modernise Live.js code that I heavily based this on

The result of that is in this gist. I include this only in my development environment and it works a treat.

But it still clutters up the dev tools network panel with its polling requests. Can we do better?

A final solution: A simple PHP file watcher server using server-side events

I thought hard about this. Was there a better way for the back-end to notify the front-end. Perhaps using some kind of persistent connection?

There’s websockets, but this looked like it would either require me to custom code a LOT more stuff, or use a library, which I’m trying to avoid because it will almost certainly do a thousand things that I don’t need it to. I’m going for a minimal solution that I can write and maintain that only does what I need.

I’d recently read something about streaming responses in the browser. Something like this wouldn’t work for a real site as you’d need loads of long-running processes and open connections on the server.

But it could work for a single browser connecting to a locally-run server for local development.

And then I found Server-Side Events. These are ancient technology (Chrome version 6 anyone? I think this was from September 2010!) and pretty simple to implement on both sides.

You basically run your server in a loop, outputting events in a particular format, and the browser can respond. There are quriks around closing connections properly and keeping the server side alive. But nothing we can’t handle.

So here’s where I landed:

  • A PHP file watcher that runs using PHP’s built in web server
  • A simple shell script to run this server
  • A further updated and modernised version of Live.js that uses EventSource to receive the events and trigger reloads.

Detecting nginx changes with my Valet setup wasn’t quite working, so I implement some content hashing. It fetches the current page, hashes the content, and compares to a saved hash. Again, inefficient really, but fine for a development environment.

Here it is then: A PHP file watching watching server and JS reloading script for local development.

I shall work this into my small-site-template soon as I want this in my new small site projects in future.