Skip to main content

Template store & hot reload

The store

Templates live behind the templates.Store interface (Fetch/List/Put). The loaded store is a fallback store — a chain of sub-stores (one per loader type) tried in order — so a name resolves to the first loader that provides it.

Loading

StoreLoader.BuildFreshStore (app/templates/fileloader/store_loader.go):

  1. loads plain templates from the templates directory using the ordered loaders (file → raw → remote-http);
  2. builds dependent loaders (compositions) that need the already-loaded store;
  3. loads compositions from the compositions directory;
  4. combines everything into the fallback store.

Per directory, loaders are tried in priority order; "not found" errors are non-fatal (try the next loader). With --templates-recursive, loading descends into nested directories, naming children parent/child.

BuildFreshStore is side-effect-free and returns a full snapshot, so the same code path serves the initial load, the always-mode list re-scan, and every watch-mode rebuild.

The LazyStore seam

The internal render server needs the store to serve assets, but the store is built later in the DI graph. A LazyStore implements both StoreProvider and StoreConsumer: Wire binds one instance to both interfaces, ProvideLiveStore fills it, and the internal server reads it. This is the late-binding seam that lets the graph wire up before the store exists.

ProvideLiveStore wraps the loaded store in a LiveStore — a Store backed by an atomic.Pointer whose inner store can be replaced with Swap. The render, spec and validate services hold it by value and the callback path reads it through the LazyStore, but all of them point at the same LiveStore, so a single Swap is observed everywhere at once with no locking.

Hot reload

--templates-reload-mode:

  • none — a static MapStore built once at startup (production default).
  • always — a DevStore that re-reads each template from disk on every fetch; wrapped in a reloadingListStore whose List re-scans the directory, so templates added or removed after startup also show up in listings (tester menu, spec) without a restart (development default).
  • watch — the watcher rebuilds the whole store on filesystem changes and pushes a live update to the tester UI over SSE.

Live watch

In watch mode app/templates/watcher runs an fsnotify watch over the template and composition trees. A debounce (--templates-reload-debounce, default 250 ms) collapses a burst of file events into one rebuild via BuildFreshStore; on success the watcher swaps the new store into the LiveStore and only then publishes a changed event on the in-process events.Hub. That order is load-bearing: the tester UI refetches on the event, so publishing before the swap would let it read the old store. A rebuild failure (e.g. a half-written file mid-edit) keeps the old store and emits an error event instead.

The EventsHandler streams hub events to the browser as Server-Sent Events on GET /_events — a route mounted only in watch mode. The tester UI's LiveReloadProvider consumes them with a native EventSource, refreshing the menu and re-rendering the current preview. Outside watch mode the endpoint is absent and the UI runs unchanged without live updates.

The watch set is seeded from the template and composition directories as they exist at startup, and new subdirectories are added as they are created. One directory is not covered: a top-level templates or compositions directory that is absent at startup is not watched if it is created later (nothing watches its parent), so restart the server after creating it. Existing directories and any nesting beneath them are picked up live.

Engines & loaders

Engine dispatch (instance_loader.go) tries engines in priority order per part (body/header/footer), first match wins. Sibling loaders handle schema, params, assets, examples, and partials. The composition sub-package holds the YAML/jq and JS planners and the pdfcpu merge; the remote sub-package holds the HTTP template loader.

See Extension points to add a new engine, loader, or planner.