2024-02-14 17:16:41 +01:00
|
|
|
package commands
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2024-02-15 03:54:46 +01:00
|
|
|
"github.com/facundoolano/blorg/site"
|
2024-02-14 17:16:41 +01:00
|
|
|
"github.com/fsnotify/fsnotify"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Generate and serve the site, rebuilding when the source files change.
|
|
|
|
func Serve() error {
|
2024-02-15 03:54:46 +01:00
|
|
|
site, err := site.Load(SRC_DIR, LAYOUTS_DIR)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-02-14 17:16:41 +01:00
|
|
|
|
2024-02-15 03:54:46 +01:00
|
|
|
if err := site.Build(SRC_DIR, TARGET_DIR, false, true); err != nil {
|
2024-02-14 17:16:41 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// watch for changes in src and layouts, and trigger a rebuild
|
2024-02-15 03:54:46 +01:00
|
|
|
watcher, err := setupWatcher(site)
|
2024-02-14 17:16:41 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer watcher.Close()
|
|
|
|
|
|
|
|
// serve the target dir with a file server
|
|
|
|
fs := http.FileServer(HTMLDir{http.Dir("target/")})
|
|
|
|
http.Handle("/", http.StripPrefix("/", fs))
|
|
|
|
fmt.Println("server listening at http://localhost:4001/")
|
|
|
|
http.ListenAndServe(":4001", nil)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tweaks the http file system to construct a server that hides the .html suffix from requests.
|
|
|
|
// Based on https://stackoverflow.com/a/57281956/993769
|
|
|
|
type HTMLDir struct {
|
|
|
|
d http.Dir
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d HTMLDir) Open(name string) (http.File, error) {
|
|
|
|
// Try name as supplied
|
|
|
|
f, err := d.d.Open(name)
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
// Not found, try with .html
|
|
|
|
if f, err := d.d.Open(name + ".html"); err == nil {
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return f, err
|
|
|
|
}
|
|
|
|
|
2024-02-15 03:54:46 +01:00
|
|
|
func setupWatcher(site *site.Site) (*fsnotify.Watcher, error) {
|
2024-02-14 17:16:41 +01:00
|
|
|
watcher, err := fsnotify.NewWatcher()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case event, ok := <-watcher.Events:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// chmod events are noisy, ignore them
|
|
|
|
isChmod := event.Has(fsnotify.Chmod)
|
|
|
|
|
|
|
|
// I've seen issues with temporary files, eg. .#name.org generated by emacs
|
|
|
|
// I'll just ignore changes to dotfiles to stay on the safe side
|
|
|
|
isDotFile := strings.HasPrefix(filepath.Base(event.Name), ".")
|
|
|
|
|
|
|
|
if !isChmod && !isDotFile {
|
|
|
|
fmt.Printf("\nFile %s changed, triggering rebuild.\n", event.Name)
|
|
|
|
|
|
|
|
// since new nested directories could be triggering this change, and we need to watch those too
|
|
|
|
// and since re-watching files is a noop, I just re-add the entire src everytime there's a change
|
|
|
|
if err := addAll(watcher); err != nil {
|
|
|
|
fmt.Println("error:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-15 03:54:46 +01:00
|
|
|
if err := site.Build(SRC_DIR, TARGET_DIR, false, true); err != nil {
|
2024-02-14 17:16:41 +01:00
|
|
|
fmt.Println("error:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
case err, ok := <-watcher.Errors:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
fmt.Println("error:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
err = addAll(watcher)
|
|
|
|
|
|
|
|
return watcher, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the layouts and all source directories to the given watcher
|
|
|
|
func addAll(watcher *fsnotify.Watcher) error {
|
|
|
|
err := watcher.Add(LAYOUTS_DIR)
|
|
|
|
// fsnotify watches all files within a dir, but non recursively
|
|
|
|
// this walks through the src dir and adds watches for each found directory
|
|
|
|
filepath.WalkDir(SRC_DIR, func(path string, entry fs.DirEntry, err error) error {
|
|
|
|
if entry.IsDir() {
|
|
|
|
watcher.Add(path)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return err
|
|
|
|
}
|