From 66b66e6118dd43ccbd1e85e33d44a02de7a2b812 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Tue, 11 Jun 2024 10:07:25 +0200 Subject: server: automatically build, re-build and reload in dev --- internal/server/dev.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++ internal/server/server.go | 54 +++++++++++++++++++++++++++++------ 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 internal/server/dev.go (limited to 'internal') diff --git a/internal/server/dev.go b/internal/server/dev.go new file mode 100644 index 0000000..f7ebb82 --- /dev/null +++ b/internal/server/dev.go @@ -0,0 +1,71 @@ +package server + +import ( + "fmt" + "io/fs" + "log/slog" + "os" + "path/filepath" + "time" + "website/internal/log" + + "github.com/fsnotify/fsnotify" + "github.com/pkg/errors" +) + +type FileWatcher struct { + *fsnotify.Watcher +} + +func NewFileWatcher() (*FileWatcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, errors.WithMessage(err, "could not create watcher") + } + + return &FileWatcher{watcher}, nil +} + +func (watcher FileWatcher) AddRecursive(from string) error { + log.Debug("walking directory tree", "root", from) + err := filepath.WalkDir(from, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return errors.WithMessagef(err, "could not walk directory %s", path) + } + if entry.IsDir() { + log.Debug("adding directory to watcher", "path", path) + if err = watcher.Add(path); err != nil { + return errors.WithMessagef(err, "could not add directory %s to watcher", path) + } + } + + return nil + }) + + return errors.WithMessage(err, "error walking directory tree") +} + +func (watcher FileWatcher) Start(callback func(string)) { + for { + select { + case event := <-watcher.Events: + if event.Has(fsnotify.Create) || event.Has(fsnotify.Rename) { + f, err := os.Stat(event.Name) + if err != nil { + slog.Error(fmt.Sprintf("error handling %s event: %v", event.Op.String(), err)) + } else if f.IsDir() { + err = watcher.Add(event.Name) + if err != nil { + slog.Error(fmt.Sprintf("error adding new folder to watcher: %v", err)) + } + } + } + if event.Has(fsnotify.Rename) || event.Has(fsnotify.Write) { + callback(event.Name) + time.Sleep(500 * time.Millisecond) + } + case err := <-watcher.Errors: + slog.Error(fmt.Sprintf("error in watcher: %v", err)) + } + } +} diff --git a/internal/server/server.go b/internal/server/server.go index 31db347..4692965 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,10 +9,12 @@ import ( "slices" "time" + "website/internal/builder" cfg "website/internal/config" "website/internal/log" "website/internal/website" + "github.com/osdevisnot/sorvor/pkg/livereload" "github.com/pkg/errors" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -25,8 +27,7 @@ var ( ) type Config struct { - Production bool `conf:"default:false"` - InDevServer bool `conf:"default:false"` + Development bool `conf:"default:false,flag:dev"` Root string `conf:"default:website"` ListenAddress string `conf:"default:localhost"` Port string `conf:"default:3000,short:p"` @@ -68,12 +69,43 @@ func New(runtimeConfig *Config) (*Server, error) { } listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, runtimeConfig.Port) + top := http.NewServeMux() - if !runtimeConfig.Production { + if runtimeConfig.Development { applyDevModeOverrides(config, listenAddress) + builderConfig := builder.IOConfig{ + Source: "content", + Destination: runtimeConfig.Root, + BaseURL: config.BaseURL, + Development: true, + } + builder.BuildSite(builderConfig) + + liveReload := livereload.New() + top.Handle("/_/reload", liveReload) + liveReload.Start() + fw, err := NewFileWatcher() + if err != nil { + return nil, errors.WithMessage(err, "could not create file watcher") + } + for _, dir := range []string{"content", "static", "templates"} { + err := fw.AddRecursive(dir) + if err != nil { + return nil, errors.WithMessagef( + err, + "could not add directory %s to file watcher", + dir, + ) + } + } + go fw.Start(func(filename string) { + log.Debug("file updated", "filename", filename) + builder.BuildSite(builderConfig) + liveReload.Reload() + }) } - top := http.NewServeMux() + loggingMux := http.NewServeMux() mux, err := website.NewMux(config, runtimeConfig.Root) if err != nil { return nil, errors.Wrap(err, "could not create website mux") @@ -81,13 +113,19 @@ func New(runtimeConfig *Config) (*Server, error) { log.Debug("binding main handler to", "host", listenAddress) hostname := config.BaseURL.Hostname() - top.Handle(hostname+"/", mux) + loggingMux.Handle(hostname+"/", mux) - top.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + loggingMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { newURL := config.BaseURL.JoinPath(r.URL.String()) http.Redirect(w, r, newURL.String(), 301) }) + top.Handle("/", + serverHeaderHandler( + wrapHandlerWithLogging(loggingMux), + ), + ) + top.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }) @@ -97,9 +135,7 @@ func New(runtimeConfig *Config) (*Server, error) { Addr: listenAddress, ReadHeaderTimeout: 1 * time.Minute, Handler: http.MaxBytesHandler(h2c.NewHandler( - serverHeaderHandler( - wrapHandlerWithLogging(top), - ), + top, &http2.Server{ IdleTimeout: 15 * time.Minute, }, -- cgit 1.4.1