From b181e3d855efb102783a24aa0c96413f125aed8c Mon Sep 17 00:00:00 2001 From: facundoolano Date: Wed, 14 Feb 2024 13:16:41 -0300 Subject: [PATCH] implement blorg serve command Squashed commit of the following: commit 5e3c3f35c73f884bbf89f5daabcdc2aec5e0af75 Author: facundoolano Date: Wed Feb 14 11:24:45 2024 -0300 cleanup commit e8881df9f27fc37c46120c946dfad107d551ef67 Author: facundoolano Date: Wed Feb 14 11:21:54 2024 -0300 add basic src watching commit 4e61add89c632b0cc7a740d15be671de3df12157 Author: facundoolano Date: Wed Feb 14 00:19:25 2024 -0300 move serve command to a separate file commit abc2100582b71179e0eade154d2af3e86b48574f Author: facundoolano Date: Wed Feb 14 00:17:05 2024 -0300 first stab at serve command --- commands/commands.go | 11 +--- commands/serve.go | 120 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 4 ++ 4 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 commands/serve.go diff --git a/commands/commands.go b/commands/commands.go index e526d59..433df60 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -2,12 +2,13 @@ package commands import ( "fmt" - "github.com/facundoolano/blorg/site" "io" "io/fs" "os" "path/filepath" "strings" + + "github.com/facundoolano/blorg/site" ) const SRC_DIR = "src" @@ -98,11 +99,3 @@ func New() error { fmt.Println("not implemented yet") return nil } - -func Serve() error { - // build - // serve target with file server - // (later watch and live reload) - fmt.Println("not implemented yet") - return nil -} diff --git a/commands/serve.go b/commands/serve.go new file mode 100644 index 0000000..1b13fd1 --- /dev/null +++ b/commands/serve.go @@ -0,0 +1,120 @@ +package commands + +import ( + "fmt" + "io/fs" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/fsnotify/fsnotify" +) + +// Generate and serve the site, rebuilding when the source files change. +func Serve() error { + // TODO tweak the building logic to inject js snippet that reloads the browser on rebuild + + // first rebuild the target + if err := Build(); err != nil { + return err + } + + // watch for changes in src and layouts, and trigger a rebuild + watcher, err := setupWatcher() + 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 +} + +func setupWatcher() (*fsnotify.Watcher, error) { + 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 + } + + if err := Build(); err != nil { + 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 +} diff --git a/go.mod b/go.mod index ecbd94c..d2a88b6 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,10 @@ require ( ) require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/osteele/liquid v1.3.2 // indirect github.com/osteele/tuesday v1.0.3 // indirect golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect + golang.org/x/sys v0.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 0af66db..1d937fa 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= github.com/osteele/liquid v1.3.2 h1:G+MvVYt1HX2xuv99JgdrhV7zRVdlvFnNi8M5rN8gQmI= @@ -13,6 +15,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=