[ANN] LiveServer: A simple development server with live-reload capability

Hi everyone, we (@tlienart and myself) have developed a simple and lightweight pure-Julia development web-server, inspired from Python’s http.server and Node’s browsersync. It is based on HTTP.jl and has live-reload capability, i.e. when changing files, every browser (tab) currently displaying a corresponding page is automatically refreshed.

It is tested for Linux/Mac and Windows and Julia 1.0, 1.1, and nightly. It is in the General registry, i.e. you can install it by

pkg> add LiveServer

Functionality

The main function LiveServer exports is serve which starts observing the current (or specified) folder and serves its contents. The following code creates an example directory and serves it:

julia> using LiveServer
julia> LiveServer.example() # creates an "example/" folder with some files
julia> cd("example")
julia> serve() # starts the local server & the file watching
✓ LiveServer listening on http://localhost:8000/ ...
  (use CTRL+C to shut down)

Open a Browser and go to http://localhost:8000/ to see the content being rendered; try modifying files (e.g. index.html) and watch the changes being rendered immediately in the browser.

Note: This package in no way is, nor tries to be or become a production server. It is meant for developing static sites, let you efficiently work on your docs (see below), or anything else that requires a mock file-server with auto-reload of the browser.

Serve docs

Derived from serve, servedocs runs Documenter.jl along with LiveServer to render your docs and will track and live-reload any modifications. Assuming you are in directory/to/YourPackage.jl and that you have a docs/ folder as prescribed by Documenter, just run:

julia> using LiveServer
julia> servedocs()

Go to http://localhost:8000/ to see your docs; try modifying files (e.g. docs/index.md) and watch the changes being rendered in the browser.

Status

The main functionality works flawlessly and has been tested thoroughly (… but only by two people so far). There’s also a complete set of tests; the coverage is currently at 81% since we have to monkey-patch HTTP.jl to fix #405 by PR #406 – the guys are working on a “clean” solution, and we will remove our “hacky” patch as soon as it is cleanly fixed.

We put quite some effort in having good documentation.

We welcome comments and suggestions for improvements!

27 Likes

Side note: for servedocs, the module whose docs you would be working on should be activated so if you were in a new julia session, it would be:

julia> using YourPackage, LiveServer
julia> servedocs()

otherwise the cross-references from Documenter.jl will not render (the rest will). I’ll update the docs to clarify this.

1 Like

Seems useful. In conjunction with Revise.entr you might even have something that handles non-static sites?

6 Likes

The file-watching system of LiveServer can be flexibly modified or extended, cf. this section in the docs. The serve() function also allows to pass in a coreloopfun, i.e. a function that is called from the main loop. This can also be used to extend the serving to “dynamic” files, e.g. by triggering a re-generation of HTML files when markdown or code files change.

BTW, from a quick glance, entr seems to use FileWatching and an @async task for each file to be watched. FYI, we started out usingFileWatching as well, but abandoned it for a simpler and “more sluggish” watching mechanism.

The advantage of Revise is that you don’t have to recompile everything when code changes: it just changes the specific methods that get modified. If your content doesn’t rely on that much Julia code, you may not care, but if you have a big stack you will notice recompiles.

WRT FileWatching, yes, Revise.entr was a quick hack and its use of FileWatching is naive; the rest of the Revise is more sophisticated, in that it (1) watches directories rather than files on platforms that support it (and then checks mtimes to see which files need attention), thus reducing the number of inodes allocated by the system, and (2) aggregates all triggers until the next REPL command, thus avoiding “double triggers.” If LiveServer can handle all the use cases of entr, I’d be happy to modify Revise’s docs to recommend LiveServer instead of entr (and maybe even deprecate entr). Conversely, you may wish to consider how to integrate Revise into the LiveServer workflow.

It’s worth pointing out that watch_file doesn’t trigger when a file is opened, only when files are modified. (You can verify this for yourself with two REPL sessions, one with status = watch_file(filename) and another with readlines(filename).) But editors do all sorts of weird things with the files they are editing, which may be the source of the impression that simply opening the file can trigger it.

You appear to be using a strategy known as polling. FileWatching also supports polling, and indeed on some filesystems (notably NFS) polling is your only option. (Revise makes this configurable.) The downside of polling is that it doesn’t scale very well to large numbers of files. That may not be relevant to your use case, of course.

4 Likes

Yes, we do simple (short) polling and trigger reloads upon changes of any individual file (i.e. don’t aggregate all changes that happened during the sleep time and then fire all relevant reloads only once). But the intended usage is for someone to manually work on the files being watched (or the one-to-one corresponding source file such as markdown). Resolution of the minimal set of reloads would also involve keeping track of inner-file references (e.g. a HTML file linking to CSS, JS or picture files), which we have not implemented yet. We just wanted to keep everything as simple as possible, at least for the first implementation.

I’d have to get deeper into Revise and entr specifically to see whether replacing it (or parts of LiveServer by it) could be an option.

One thing that’s not solved right now is that with servedocs, the docstrings etc. are only parsed during the first (slow) run of Documenter. In update-passes, only the markdown is re-evaluated to have a “real-time” update. But when changing docstrings, one has to stop and restart the server, and then wait for the full pass of Documenter again to see those changes in the docs. Could Revise help here?

I use python -m http.server a lot (not as often as I used to because the network topologies are really screwed up lately where I work). It’s nice to have a Julia alternative!

3 Likes

But when changing docstrings, one has to stop and restart the server, and then wait for the full pass of Documenter again to see those changes in the docs. Could Revise help here?

With Revise alone, Documenter needs a full pass to generate the doc pages and so any changes require you to do a second full pass. However, that full pass is faster because you don’t have to reload the package. For example, the first time I build the JuliaImages documentation, it’s ~80s; the second time (in the same REPL session) it’s ~36s. Obviously the time required will depend a lot on how many doctests etc. the package has; IIUC doctests spin up a new Julia session, so you don’t get the benefit of Revise there.

1 Like

So the equivalent would be julia -e "using LiveServer; serve()" ?