Repos / s4g / 8606ea563e
commit 8606ea563e4331c6c56afa3aaa44498e7a83de6c
Author: Nhân <hi@imnhan.com>
Date: Sat Jul 8 17:16:23 2023 +0700
implement livereload
Not ideal right now: if multiple tabs are open, only 1 will be reloaded.
diff --git a/Makefile b/Makefile
index 83c5cf1..920c8df 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ build:
go build -o dist/
watch:
- find . -name '*.go' -or -name '*.js' \
+ find . -name '*.go' -or -name '*.js' -or -name 'livereload.html' \
| entr -rc go run .
# Cheating a little because the djot.js repo on github does not provide builds
diff --git a/livereload/livereload.go b/livereload/livereload.go
new file mode 100644
index 0000000..d440790
--- /dev/null
+++ b/livereload/livereload.go
@@ -0,0 +1,90 @@
+package livereload
+
+import (
+ "bytes"
+ _ "embed"
+ "io/fs"
+ "net/http"
+ "strings"
+ "sync"
+
+ "go.imnhan.com/webmaker2000/writablefs"
+)
+
+const endpoint = "/_livereload"
+
+//go:embed livereload.html
+var lrScript []byte
+
+var pleaseReload = []byte("1")
+var dontReload = []byte("0")
+
+var state struct {
+ shouldReload bool
+ mut sync.RWMutex
+}
+
+func init() {
+ lrScript = bytes.ReplaceAll(lrScript, []byte("{{LR_ENDPOINT}}"), []byte(endpoint))
+ lrScript = bytes.ReplaceAll(lrScript, []byte("{{SHOULD_RELOAD}}"), pleaseReload)
+}
+
+// For html pages, insert a script tag to enable livereload
+func Middleware(fsys writablefs.FS, f http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+
+ // Handle AJAX endpoint
+ if path == endpoint {
+ state.mut.RLock()
+ shouldReload := state.shouldReload
+ state.mut.RUnlock()
+
+ if shouldReload {
+ w.Write(pleaseReload)
+ state.mut.Lock()
+ state.shouldReload = false
+ state.mut.Unlock()
+ } else {
+ w.Write(dontReload)
+ }
+ return
+ }
+
+ // For non-html requests, fall through to default FileServer handler
+ if !strings.HasSuffix(path, ".html") && !strings.HasSuffix(path, "/") {
+ f.ServeHTTP(w, r)
+ return
+ }
+
+ if strings.HasSuffix(path, "/") {
+ path += "index.html"
+ }
+
+ // Filesystem access doesn't expect leading slash "/"
+ path = strings.TrimPrefix(path, "/")
+
+ originalContent, err := fs.ReadFile(fsys, path)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+
+ w.Write(withLiveReload(originalContent))
+ })
+}
+
+func Trigger() {
+ state.mut.Lock()
+ state.shouldReload = true
+ state.mut.Unlock()
+}
+
+func withLiveReload(original []byte) []byte {
+ bodyEndPos := bytes.LastIndex(original, []byte("</body>"))
+ result := make([]byte, len(original)+len(lrScript))
+ copy(result, original[:bodyEndPos])
+ copy(result[bodyEndPos:], lrScript)
+ copy(result[bodyEndPos+len(lrScript):], original[bodyEndPos:])
+ return result
+}
diff --git a/livereload/livereload.html b/livereload/livereload.html
new file mode 100644
index 0000000..8f35245
--- /dev/null
+++ b/livereload/livereload.html
@@ -0,0 +1,11 @@
+<script>
+ setInterval(() => {
+ fetch("{{LR_ENDPOINT}}")
+ .then((resp) => resp.text())
+ .then((text) => {
+ if (text === "{{SHOULD_RELOAD}}") {
+ location.reload();
+ }
+ });
+ }, 500);
+</script>
diff --git a/main.go b/main.go
index d5d1826..abaadff 100644
--- a/main.go
+++ b/main.go
@@ -14,6 +14,8 @@
"github.com/BurntSushi/toml"
"go.imnhan.com/webmaker2000/djot"
+ "go.imnhan.com/webmaker2000/livereload"
+ "go.imnhan.com/webmaker2000/writablefs"
)
const DJOT_EXT = ".dj"
@@ -32,7 +34,7 @@ func main() {
panic(err)
}
- fsys := WriteDirFS(absolutePath)
+ fsys := writablefs.WriteDirFS(absolutePath)
regenerate(fsys)
@@ -45,18 +47,19 @@ func main() {
closeWatcher := WatchLocalFS(fsys, func() {
fmt.Println("Change detected. Regenerating...")
regenerate(fsys)
+ livereload.Trigger()
})
defer closeWatcher()
println("Serving local website at http://localhost:" + port)
- http.Handle("/", http.FileServer(http.FS(fsys)))
+ http.Handle("/", livereload.Middleware(fsys, http.FileServer(http.FS(fsys))))
err = http.ListenAndServe("127.0.0.1:"+port, nil)
if err != nil {
panic(err)
}
}
-func regenerate(fsys WritableFS) {
+func regenerate(fsys writablefs.FS) {
defer timer("Took %s")()
site := readSiteMetadata(fsys)
articles := findArticles(fsys)
@@ -117,7 +120,7 @@ type SiteMetadata struct {
}
}
-func readSiteMetadata(fsys WritableFS) SiteMetadata {
+func readSiteMetadata(fsys writablefs.FS) SiteMetadata {
sm := SiteMetadata{
HomePath: "/",
ShowFooter: true,
@@ -131,7 +134,7 @@ func readSiteMetadata(fsys WritableFS) SiteMetadata {
}
type Article struct {
- Fs WritableFS
+ Fs writablefs.FS
Path string
WebPath string
DjotBody string
@@ -192,7 +195,7 @@ func (a *Article) WriteHtmlFile(
}
func WriteHomePage(
- fsys WritableFS,
+ fsys writablefs.FS,
site SiteMetadata,
articlesInFeed, articlesInNav []Article,
startYear int,
@@ -229,7 +232,7 @@ func WriteHomePage(
fsys.WriteFile("index.html", buf.Bytes())
}
-func findArticles(fsys WritableFS) (result []Article) {
+func findArticles(fsys writablefs.FS) (result []Article) {
fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if d.IsDir() || !strings.HasSuffix(d.Name(), DJOT_EXT) {
diff --git a/watcher.go b/watcher.go
index 36d9dc9..2430b32 100644
--- a/watcher.go
+++ b/watcher.go
@@ -8,6 +8,7 @@
"time"
"github.com/fsnotify/fsnotify"
+ "go.imnhan.com/webmaker2000/writablefs"
)
var WATCHED_EXTS = []string{DJOT_EXT, SITE_EXT, ".tmpl"}
@@ -17,7 +18,7 @@
// Watches for relevant changes in FS, debounces by debounceInterval,
// then executes callback.
// Returns cleanup function.
-func WatchLocalFS(fsys WritableFS, callback func()) (Close func() error) {
+func WatchLocalFS(fsys writablefs.FS, callback func()) (Close func() error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
panic(err)
diff --git a/writablefs.go b/writablefs/writablefs.go
similarity index 86%
rename from writablefs.go
rename to writablefs/writablefs.go
index 3930b1b..2451f8a 100644
--- a/writablefs.go
+++ b/writablefs/writablefs.go
@@ -1,4 +1,4 @@
-package main
+package writablefs
import (
"io/fs"
@@ -6,14 +6,14 @@
"path/filepath"
)
-type WritableFS interface {
+type FS interface {
fs.FS
WriteFile(path string, content []byte) error
Path() string
}
// Like os.DirFS but is writable
-func WriteDirFS(path string) WritableFS {
+func WriteDirFS(path string) FS {
return writeDirFS(path)
}