rename packages, refactor out tailwind compiler,
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
//go:build !(js && wasm)
|
||||
|
||||
// Package wasmdevserver is a reusable development server for gowasm apps: it serves
|
||||
// the built web assets, renders routes server-side (SSR) at request time, hosts
|
||||
// the /rsc server-component endpoint, and hot-swaps the freshly built wasm into
|
||||
@@ -33,14 +35,21 @@ import (
|
||||
// Config wires an app into the dev server. Render and Document are called per
|
||||
// request; Build is called for the initial build and on every source change.
|
||||
type Config struct {
|
||||
Addr string // listen address (default ":8085")
|
||||
Dir string // static assets dir; Build writes app.wasm here (default "./wwwroot")
|
||||
Watch bool // rebuild on change + hot reload
|
||||
WatchDirs []string // source dirs to watch when Watch is set
|
||||
Build func() ([]byte, error) // (re)build the wasm bundle; combined output on failure
|
||||
Render func(path string) (inner string, ok bool) // SSR the #app inner HTML for a route (ok=false => client-rendered)
|
||||
Document func(inner string) string // wrap #app inner HTML in a full HTML document
|
||||
Handle func(mux *http.ServeMux) // optional: register extra routes (e.g. app API endpoints)
|
||||
Addr string // listen address (default ":8085")
|
||||
Dir string // static assets dir; Build writes app.wasm here (default "./wwwroot")
|
||||
Watch bool // rebuild on change + hot reload
|
||||
WatchDirs []string // source dirs to watch when Watch is set
|
||||
Build func() ([]byte, error) // (re)build everything; combined output on failure
|
||||
|
||||
// BuildCSS recompiles ONLY the stylesheet. When a save touched stylesheets and no
|
||||
// Go code, this runs instead of Build and the browser swaps the stylesheet in
|
||||
// place — no wasm rebuild, no page reload, no lost state. Without it a colour
|
||||
// tweak waits on the whole Go compiler, which is the difference between a design
|
||||
// loop and a coffee break.
|
||||
BuildCSS func() ([]byte, error)
|
||||
Render func(path string) (inner string, ok bool) // SSR the #app inner HTML for a route (ok=false => client-rendered)
|
||||
Document func(inner string) string // wrap #app inner HTML in a full HTML document
|
||||
Handle func(mux *http.ServeMux) // optional: register extra routes (e.g. app API endpoints)
|
||||
}
|
||||
|
||||
// Serve builds once (in watch mode), wires the routes, and blocks serving.
|
||||
@@ -116,15 +125,45 @@ func rootHandler(cfg Config) http.HandlerFunc {
|
||||
|
||||
// ---- build + watch ------------------------------------------------------
|
||||
|
||||
// watchLoop rebuilds on change, and treats a stylesheet edit differently from a code
|
||||
// edit — because they cost wildly different amounts.
|
||||
//
|
||||
// A Go change means recompiling a multi-megabyte wasm binary. A CSS change means
|
||||
// re-running Tailwind, which takes a moment. Putting both through the same path
|
||||
// would make every tweak to a colour wait on the compiler, so a save that touched
|
||||
// ONLY stylesheets runs BuildCSS and pushes a stylesheet swap: no wasm rebuild, no
|
||||
// page reload, no lost state — the new CSS just appears.
|
||||
func watchLoop(cfg Config, h *hub) {
|
||||
prev := fingerprint(cfg.WatchDirs)
|
||||
prevGo := fingerprintExt(cfg.WatchDirs, ".go")
|
||||
prevCSS := fingerprintExt(cfg.WatchDirs, ".css")
|
||||
|
||||
for {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
fp := fingerprint(cfg.WatchDirs)
|
||||
if fp == prev {
|
||||
|
||||
fpGo := fingerprintExt(cfg.WatchDirs, ".go")
|
||||
fpCSS := fingerprintExt(cfg.WatchDirs, ".css")
|
||||
codeChanged := fpGo != prevGo
|
||||
cssChanged := fpCSS != prevCSS
|
||||
if !codeChanged && !cssChanged {
|
||||
continue
|
||||
}
|
||||
prev = fp
|
||||
prevGo, prevCSS = fpGo, fpCSS
|
||||
|
||||
// Styles only: the cheap path.
|
||||
if !codeChanged && cfg.BuildCSS != nil {
|
||||
log.Println("stylesheet changed, recompiling CSS…")
|
||||
h.broadcast(`{"type":"building"}`)
|
||||
if out, err := cfg.BuildCSS(); err != nil {
|
||||
log.Printf("CSS build failed: %v\n%s", err, out)
|
||||
h.setError(string(out))
|
||||
continue
|
||||
}
|
||||
log.Println("CSS ok — swapping stylesheets")
|
||||
h.clearError()
|
||||
h.broadcast(`{"type":"css"}`)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("change detected, rebuilding…")
|
||||
h.broadcast(`{"type":"building"}`)
|
||||
if cfg.Build == nil {
|
||||
@@ -141,12 +180,20 @@ func watchLoop(cfg Config, h *hub) {
|
||||
}
|
||||
}
|
||||
|
||||
// fingerprint changes whenever any .go file under dirs is modified.
|
||||
func fingerprint(dirs []string) int64 {
|
||||
// fingerprintExt changes whenever a file of the given extension under dirs is
|
||||
// modified.
|
||||
//
|
||||
// .css is watched at all for a reason: the build compiles Tailwind, and the entry
|
||||
// stylesheet (its @theme block, its @font-face rules, any hand-written CSS) is an
|
||||
// INPUT to that build. Watching only .go meant saving app.css did nothing — no
|
||||
// rebuild, no reload — and the change simply never reached the browser. Editing some
|
||||
// unrelated Go file would then sweep it up by accident, which is a maddening way to
|
||||
// find out.
|
||||
func fingerprintExt(dirs []string, ext string) int64 {
|
||||
var fp int64
|
||||
for _, d := range dirs {
|
||||
filepath.WalkDir(d, func(path string, e fs.DirEntry, err error) error {
|
||||
if err != nil || e.IsDir() || !strings.HasSuffix(path, ".go") {
|
||||
if err != nil || e.IsDir() || !strings.EqualFold(filepath.Ext(path), ext) {
|
||||
return nil
|
||||
}
|
||||
if info, err := e.Info(); err == nil {
|
||||
@@ -206,6 +253,28 @@ const clientJS = `// Injected by the dev server in watch mode.
|
||||
start(); // renders synchronously — no await between dispose and first paint
|
||||
}
|
||||
|
||||
// Swap every stylesheet for a freshly-fetched copy — no page reload, so scroll
|
||||
// position, form state and the running wasm instance all survive a style tweak.
|
||||
//
|
||||
// The NEW link is loaded and only then does the old one go, on its onload. Removing
|
||||
// it first would leave the page unstyled for however long the fetch takes, which
|
||||
// reads as a flash of naked HTML every time you save.
|
||||
function swapCSS() {
|
||||
hideOverlay();
|
||||
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
(function (old) {
|
||||
var url = old.href.split("?")[0] + "?v=" + Date.now();
|
||||
var next = old.cloneNode();
|
||||
next.href = url;
|
||||
next.onload = function () { if (old.parentNode) old.parentNode.removeChild(old); };
|
||||
next.onerror = function () { if (next.parentNode) next.parentNode.removeChild(next); };
|
||||
old.parentNode.insertBefore(next, old.nextSibling);
|
||||
})(links[i]);
|
||||
}
|
||||
console.log("[hot reload] stylesheet updated");
|
||||
}
|
||||
|
||||
// Full-screen overlay showing the Go compiler output when a build fails. The
|
||||
// app underneath keeps running (and its state), so fixing the code and saving
|
||||
// clears the overlay and hot-swaps without losing anything.
|
||||
@@ -246,6 +315,7 @@ const clientJS = `// Injected by the dev server in watch mode.
|
||||
var msg = {};
|
||||
try { msg = JSON.parse(e.data); } catch (_) { return; }
|
||||
if (msg.type === "reload") { hotSwap(); } // build ok: swap in place
|
||||
else if (msg.type === "css") { swapCSS(); } // styles only: swap the stylesheet
|
||||
else if (msg.type === "error") { showOverlay(msg.msg); } // build failed: show compiler output
|
||||
else if (msg.type === "building") { console.log("[hot reload] rebuilding…"); }
|
||||
};
|
||||
|
||||
11
go/wasmdevserver/devserver_wasm.go
Normal file
11
go/wasmdevserver/devserver_wasm.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build js && wasm
|
||||
|
||||
// This file exists only so the package is not EMPTY under js/wasm.
|
||||
//
|
||||
// wasmdevserver is a server: it compiles Go, serves HTTP, and walks the filesystem —
|
||||
// none of which a browser can do. The real file is excluded from the wasm build (it
|
||||
// depends on rsc.Handler, which is itself server-only). But a package whose files are
|
||||
// all excluded is a build error, not an empty package, and `GOOS=js go build ./...`
|
||||
// would fail on it — which is exactly the command you run to check the half of kjol
|
||||
// that DOES target wasm. So the package stays present here, and empty.
|
||||
package wasmdevserver
|
||||
Reference in New Issue
Block a user