goldsmith/devserver/devserver.go
2024-03-03 21:39:08 -08:00

116 lines
2.5 KiB
Go

// Package devserver makes it easy to view statically generated websites and
// automatically rebuild them when source data changes. When combined with the
// "livejs" plugin, it is possible to have a live preview of your site.
package devserver
import (
"fmt"
"log"
"net/http"
"os"
"path"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
// Builder interface should be implemented by you to contain the required
// goldsmith chain to generate your website.
type Builder interface {
Build(sourceDir, targetDir, cacheDir string)
}
// DevServe should be called to start a web server using the provided builder.
// While the source directory will be watched for changes by default, it is
// possible to pass in additional directories to watch; modification of these
// directories will automatically trigger a site rebuild. This function does
// not return and will continue watching for file changes and serving your
// website until it is terminated.
func DevServe(builder Builder, port int, sourceDir, targetDir, cacheDir string, watchDirs ...string) {
dirs := append(watchDirs, sourceDir)
build(dirs, func() {
builder.Build(sourceDir, targetDir, cacheDir)
})
httpAddr := fmt.Sprintf(":%d", port)
httpHandler := http.FileServer(http.Dir(targetDir))
log.Fatal(http.ListenAndServe(httpAddr, httpHandler))
}
func build(dirs []string, callback func()) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
var mutex sync.Mutex
timestamp := time.Now()
dirty := true
go func() {
for {
select {
case event := <-watcher.Events:
mutex.Lock()
timestamp = time.Now()
dirty = true
mutex.Unlock()
if event.Op&fsnotify.Create == fsnotify.Create {
info, err := os.Stat(event.Name)
if os.IsNotExist(err) {
continue
}
if err != nil {
log.Fatal(err)
}
if info.IsDir() {
watch(event.Name, watcher)
} else {
watcher.Add(event.Name)
}
}
case err := <-watcher.Errors:
log.Fatal(err)
}
}
}()
go func() {
for range time.Tick(10 * time.Millisecond) {
if dirty && time.Now().Sub(timestamp) > 100*time.Millisecond {
mutex.Lock()
dirty = false
mutex.Unlock()
callback()
}
}
}()
for _, dir := range dirs {
watch(dir, watcher)
}
}
func watch(dir string, watcher *fsnotify.Watcher) {
watcher.Add(dir)
items, err := os.ReadDir(dir)
if err != nil {
log.Fatal(err)
}
for _, item := range items {
fullPath := path.Join(dir, item.Name())
if item.IsDir() {
watch(fullPath, watcher)
} else {
watcher.Add(fullPath)
}
}
}