Developping for the web is fun, Webpack is a marvel of modern enginerering, and when you have a project fully set up with the latest and greatest tools, things work amazingly.

But when you have a small, throwaway web projet, Webpack and all that stuff starts to feel a bit like overkill. I'm not that interested in tracking my dependencies explicitly, writing up a package.json, trying ot remember if I want css-loader or style-loader or both for the 153rd time. Seeing a bunch of npm audit results interspersed with fundraising for the same package 3 times over. I get why it's all there, and it totally makes sense, but it doesn't feel very artisinal.


I know about create-react-app and I don't want to use it. I don't have any good reasons beyond my own personal aesthetics.


So when I want to work on a tiny web thing, I tend to just work with static files and a small python script (or a direct python -m http.server call).

I, of course, instantly regrest this! The whole "save my file in an editor, tab over to a browser, and then hit refresh to see the results" flow gets tiring really quickly (especially if you're trying to iterate pretty quickly). It's not a huge deal, but when you have a taste of the magic of live reloading, it's hard to not want it everywhere.

This is where I decided to finally figure out more about what livereload was.

If you ever have used "auto-refresh" stuff, you have surely seen livereload.js somewhere in the stack. I have always known that livereload was a "thing", but I hadd never really looked into what it entailed until recently. I was able to find one Livereload client that was even more flexible thn I could have imagined.

The fundamental thing to understand about livereload is that there are two sides to it:

  • A file watching server, that will broadcast to connected clients that a page is updated and should get refreshed.
  • A javascript snippet that connects to the server, that will actually do the automatic refreshing.

Though a lot of the servers come with some helpers for things like automatically compiling your .less files or whatever, for the most part the basic stuff is really just "tell the browser your code is refreshed".

When looking into this, I landed onto a Python livereload implementation that has a very simple and user-friendly interface.

The simple case: let's say you have just some HTML files. You can set things up like the following, then (after running the script with python serve.py) just navigate to the file and get automatic refreshing on every save of your file save:

#!html
<!-- index.html -->
<html>
  <script src="/livereload.js"></script>
  <body>
    Hi there, everyone!
  </body>
</html>
#!python
# serve.py
from livereload import Server

server = Server()

server.watch("*.html")

server.serve()

You can also watch *.js/*.css files as well and have that livereload.js script trigger reloads on there just as easily.

It gets a bit more interesting with the help of the shell command. You can use this to process files after you notice changes. This is a very simple interface, with no parameters about what file changed or anything. So if you want to do "clever" things like set up compilation across several files, well... you gotta write the cleverness yourself!

#!python

from livereload import Server, shell

server = Server()

for less_file in ['main.less']:
    out_file = less_file.split('.')[0] + '.css'
    # every time less_file changes, run the shell command and signal
    # that a refresh is needed
    server.watch(less_file, shell(f"lessc {less_file} {out_file}"))

server.watch("*.html")

server.serve()

With this, I can add a list of less files amd have them be watched individually. I then make sure to include links to the output over in my index.html and I get live reloading for my file as well.

#!html
<html>
  <link rel="stylesheet" href="/main.css" />
  <script src="/livereload.js"></script>
  <body>
    <div class='important'> This is an important alert!</div>
    Hi there, everyone!
  </body>
</html>

I like this because there's very little mystery about what's going on. It's very manual in one sense, but if your needs are small this can be enough. And if you want to go off the beaten path for whatever eason, it'll be somewhat doable.

You can go even further with this, of course. What if you want to write your amazing blog posts in markdown, and output to HTML?

You can set up file watchers to look at some input files, and then generate output from there:

#!python
import glob
import markdown

from jinja2 import Environment, FileSystemLoader
from livereload import Server, shell

jinja_env = Environment(loader=FileSystemLoader('.'))

# ...
def render_index():
    post_files = glob.glob("posts/*.md")

    posts = [
        markdown.markdown(open(f, 'r').read())
        for f in post_files
    ]

    index_tpl = jinja_env.get_template('index.tpl.html')

    with open('index.html', 'w') as index:
        index.write(
            index_tpl.render(
                posts=posts
            )
        )

server.watch("posts/*.md", render_index)
server.watch("index.tpl.html", render_index)
# ...
#!html
    <!-- inside index.tpl.html -->
    <h1>Recent Posts</h1>

    {% for post in posts %}
       {{ post }}
    {% endfor %}

My render_index function takes a bunch of markdown files and the index.tpl.html, and renders my new home page from those inputs. It'll auto-refresh

This is, of course, where things start to get messier than with more structured tools. I have to be watching the right files, tracking inputs, this is all some real tricky stuff! But on the other hand, with this I'm not having to try and convince Jekyll how to do something that is conceptually simple.

Another thing to notice here is that you can take an existing build process without live reloading, and just add a bunch of watches with this (and the <script> tag in your page), and get the extra functionality in a minute or so of work.

#!python
for files in ["*.py", "*.js", "*.html"]:
    server.watch(files, shell("python existing_build_process.py"))

# you might not need to use this for the static file serving, but
# you need it to serve livereload.js and the file watcher code
server.serve()

I'm also going to let you in on a little secret..... livereload is a useful set of tooling even when you are not building a webpage. You can use some tiny tricks to make this useful with other toolchains as well!

Let's start by making a named pipe:

#!text
mkfifo /tmp/livereload

We'll also set up a small C program:

#!c
#include <stdio.h>

int main(int argv, char** argc){
  printf("Hello there!!");
  return 0;
}

We then set up the following server script, that will track our single C file and recompile when needed:

#!python
from livereload import Server, shell

server = Server()

def signal_change():
    # /tmp/livereload is a named pipe made with
    # mkfifo /tmp/livereload
    with open('/tmp/livereload', 'w') as f:
        f.write('1')


server.watch("hello.c", shell("gcc hello.c -o hello"))
server.watch("hello", signal_change)

server.serve()

Every time our final executable hello is updated, we will write 1 to the named pipe.

Finally, we run the following script in a shell:

#!text
while true; do
  echo "Running hello...."
  ./hello
  cat /tmp/livereload > /dev/null
done

Running hello....
Hello world!
Running hello....
Hello there!
Running hello....
Hello there!!

This shell script will hang on the cat call to the named pipe, until we end up writing to it with the livereload script. That ends up being the signal to the script to try running hello again!

I now have live reloading for my C script, allowing for quick iteration and a very simple protocol for setting it up. Depending on the sort of project you're working on, you can tailor-make tricks to get what you want to work smoothly, all in a couple lines of scaffolding.

Lots of the setups described here are wonky to say the least. But I hope that we can get more and more of our tooling to work in this kind of straightforward way, all while still providing as much value as it can. Trying to balance understandability with offering good features is tough, but I think it's a goal worth striving for.