diff options
author | Alan Pearce | 2024-04-27 21:18:03 +0200 |
---|---|---|
committer | Alan Pearce | 2024-04-27 21:18:03 +0200 |
commit | 2a4c795d5a165f995e9f7dc84e07465b140f3770 (patch) | |
tree | 379ee13be7bf0e6f7db096222e782f86f8ea1caf | |
parent | 9b4ca4783a186c345d99f613aeaf73e1bc112bfa (diff) | |
download | website-2a4c795d5a165f995e9f7dc84e07465b140f3770.tar.lz website-2a4c795d5a165f995e9f7dc84e07465b140f3770.tar.zst website-2a4c795d5a165f995e9f7dc84e07465b140f3770.zip |
implement live-reloading dev server
Squashed commit of the following: commit 02f077432202af4d633eb2cad81dfdaa6921317f Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 21:09:14 2024 +0200 builder: only remove output directory if set and in dev mode commit 47001e01c55fa6e74aafeda04ebc3e4e7c47eba0 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 21:03:37 2024 +0200 implement live reload on dev server commit 411ec969f61e4b73439f1c54ea29f75135ecc618 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:59:26 2024 +0200 server: implement graceful shutdown commit 5400132eb6eb1b638e0b3fd4265f51611c92d473 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:41:07 2024 +0200 add some debug logs commit 3c9b678197c044603950232d222f501ef74d7873 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:39:09 2024 +0200 prefix log output with executable name commit 300e24c179e390e9d3f5aeab4471c97f17f1fa64 Author: Alan Pearce <alan@alanpearce.eu> Date: Sat Apr 27 20:29:42 2024 +0200 don't panic inside internal packages, return error instead commit fe2715d330402ad67fe866471bed89c7238ad2ec Author: Alan Pearce <alan@alanpearce.eu> Date: Fri Apr 26 01:18:29 2024 +0200 config: use a table to configure CSP headers commit d012553aaf78a436fa8871830b5d720a9e292d4b Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 17:13:39 2024 +0200 dev: create basic dev server to build and serve from a temporary directory commit a1d11d3e69650d9b43ca1b1d7b7ccc05a808d5c1 Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 13:02:22 2024 +0200 remove unused redirect_other_hostnames config option commit fd67b19b5c7f76f0c3579e8a05ef20a618e90be7 Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 12:58:53 2024 +0200 server: make port a string, which is what go uses commit c798e8e736c0649008cade337158399470a9099b Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 12:58:33 2024 +0200 config: remove unused port variable commit f94882b9001f3b0855e26b26b4a84b96e3deb22b Author: Alan Pearce <alan@alanpearce.eu> Date: Thu Apr 25 12:49:10 2024 +0200 re-organise module layout
-rw-r--r-- | cmd/build/main.go | 8 | ||||
-rw-r--r-- | cmd/cspgenerator/cspgenerator.go | 13 | ||||
-rw-r--r-- | cmd/dev/main.go | 313 | ||||
-rw-r--r-- | cmd/server/main.go | 50 | ||||
-rw-r--r-- | config.toml | 27 | ||||
-rw-r--r-- | go.mod | 19 | ||||
-rw-r--r-- | go.sum | 224 | ||||
-rw-r--r-- | internal/builder/builder.go (renamed from cmd/build/build.go) | 37 | ||||
-rw-r--r-- | internal/builder/posts.go (renamed from cmd/build/posts.go) | 2 | ||||
-rw-r--r-- | internal/builder/template.go (renamed from cmd/build/template.go) | 22 | ||||
-rw-r--r-- | internal/config/config.go | 29 | ||||
-rw-r--r-- | internal/config/csp.go | 44 | ||||
-rw-r--r-- | internal/config/cspgenerator.go | 79 | ||||
-rw-r--r-- | internal/server/filemap.go (renamed from cmd/server/filemap.go) | 2 | ||||
-rw-r--r-- | internal/server/logging.go (renamed from cmd/server/logging.go) | 2 | ||||
-rw-r--r-- | internal/server/server.go (renamed from cmd/server/server.go) | 84 | ||||
-rwxr-xr-x | justfile | 11 | ||||
-rw-r--r-- | nix/gomod2nix.toml | 57 | ||||
-rw-r--r-- | templates/dev.html | 8 |
19 files changed, 945 insertions, 86 deletions
diff --git a/cmd/build/main.go b/cmd/build/main.go index 9b6a79b..069f9bd 100644 --- a/cmd/build/main.go +++ b/cmd/build/main.go @@ -7,6 +7,8 @@ import ( "log/slog" "os" + "website/internal/builder" + "github.com/BurntSushi/toml" "github.com/ardanlabs/conf/v3" "github.com/pkg/errors" @@ -16,9 +18,11 @@ func main() { if os.Getenv("DEBUG") != "" { slog.SetLogLoggerLevel(slog.LevelDebug) } + log.SetFlags(log.LstdFlags | log.Lmsgprefix) + log.SetPrefix("build: ") slog.Debug("starting build process") - ioConfig := IOConfig{} + ioConfig := builder.IOConfig{} if help, err := conf.Parse("", &ioConfig); err != nil { if errors.Is(err, conf.ErrHelpWanted) { fmt.Println(help) @@ -34,7 +38,7 @@ func main() { } } - if err := buildSite(ioConfig); err != nil { + if err := builder.BuildSite(ioConfig); err != nil { switch cause := errors.Cause(err).(type) { case *fs.PathError: slog.Info("pathError") diff --git a/cmd/cspgenerator/cspgenerator.go b/cmd/cspgenerator/cspgenerator.go new file mode 100644 index 0000000..f79a591 --- /dev/null +++ b/cmd/cspgenerator/cspgenerator.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + "website/internal/config" +) + +func main() { + err := config.GenerateCSP() + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/dev/main.go b/cmd/dev/main.go new file mode 100644 index 0000000..1a6ccea --- /dev/null +++ b/cmd/dev/main.go @@ -0,0 +1,313 @@ +package main + +import ( + "context" + "fmt" + "io" + "io/fs" + "log" + "log/slog" + "net/http" + "net/http/httputil" + + "os" + "os/exec" + "os/signal" + "path" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + + "website/internal/config" + + "github.com/antage/eventsource" + "github.com/ardanlabs/conf/v3" + "github.com/gohugoio/hugo/watcher" + + "github.com/pkg/errors" +) + +type DevConfig struct { + Source string `conf:"default:.,short:s"` + TempDir string `conf:"required,short:t"` + BaseURL config.URL `conf:"default:http://localhost:3000"` + ServerURL config.URL `conf:"default:http://localhost:3001"` +} + +func RunCommandPiped(ctx context.Context, command string, args ...string) (cmd *exec.Cmd, err error) { + slog.Debug(fmt.Sprintf("running command %s %s", command, strings.Join(args, " "))) + cmd = exec.CommandContext(ctx, command, args...) + cmd.Env = append(os.Environ(), "DEBUG=") + cmd.Cancel = func() error { + slog.Debug("signalling child") + err := cmd.Process.Signal(os.Interrupt) + if err != nil { + slog.Error(fmt.Sprintf("signal error: %v", err)) + } + return err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return + } + stderr, err := cmd.StderrPipe() + if err != nil { + return + } + + go io.Copy(os.Stdout, stdout) + go io.Copy(os.Stderr, stderr) + + return +} + +type FileWatcher struct { + *watcher.Batcher +} + +func NewFileWatcher(pollTime time.Duration) (*FileWatcher, error) { + batcher, err := watcher.New(pollTime/5, pollTime, true) + if err != nil { + return nil, err + } + return &FileWatcher{batcher}, nil +} + +func (watcher FileWatcher) WatchAllFiles(from string) error { + slog.Debug(fmt.Sprintf("watching files under %s", from)) + err := filepath.Walk(from, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + // slog.Debug(fmt.Sprintf("adding file %s to watcher", path)) + if err = watcher.Add(path); err != nil { + return err + } + return nil + }) + return err +} + +func build(ctx context.Context, config DevConfig) error { + buildExe := filepath.Join(config.TempDir, "build") + cmd, err := RunCommandPiped(ctx, buildExe, + "--dest", path.Join(config.TempDir, "output"), + "--dev", + ) + // cmd, err := RunCommandPiped(ctx, "./devfakebuild") + + if err != nil { + return errors.WithMessage(err, "error running build command") + } + + err = cmd.Run() + slog.Debug(fmt.Sprintf("build command exited with code %d", cmd.ProcessState.ExitCode())) + if err != nil { + return errors.WithMessage(err, "error running build command") + } + return nil +} + +func server(ctx context.Context, devConfig DevConfig) error { + serverExe := path.Join(devConfig.TempDir, "server") + + cmd, err := RunCommandPiped(ctx, + serverExe, + "--port", devConfig.ServerURL.Port(), + "--root", path.Join(devConfig.TempDir, "output"), + "--in-dev-server", + ) + if err != nil { + return errors.WithMessage(err, "error running server command") + } + // cmd.Env = append(cmd.Env, "DEBUG=1") + + cmdErr := make(chan error, 1) + done := make(chan struct{}) + err = cmd.Start() + if err != nil { + return errors.WithMessage(err, fmt.Sprintf("error starting server binary")) + } + + go func() { + err := cmd.Wait() + if err == nil && cmd.ProcessState.Exited() { + err = errors.Errorf("server exited unexpectedly") + } + + cmdErr <- err + close(done) + }() + + for { + select { + case <-ctx.Done(): + slog.Debug("server context done") + cmd.Process.Signal(os.Interrupt) + <-done + case err := <-cmdErr: + return err + } + } +} + +func main() { + if os.Getenv("DEBUG") != "" { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + var wg sync.WaitGroup + + devConfig := DevConfig{} + help, err := conf.Parse("", &devConfig) + if err != nil { + if errors.Is(err, conf.ErrHelpWanted) { + fmt.Println(help) + os.Exit(1) + } + log.Panicf("parsing dev configuration: %v", err) + } + + slog.Debug(fmt.Sprintf("using folder %s for build output", devConfig.TempDir)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + slog.Debug("setting interrupt handler") + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + sig := <-c + slog.Info(fmt.Sprintf("shutting down on signal %d", sig)) + cancel() + sig = <-c + slog.Info(fmt.Sprintf("got second signal, dying %d", sig)) + os.Exit(1) + }() + + serverChan := make(chan bool, 1) + eventsource := eventsource.New(nil, nil) + defer eventsource.Close() + srv := http.Server{ + Addr: devConfig.BaseURL.Host, + } + devCtx, devCancel := context.WithTimeout(ctx, 1*time.Second) + + wg.Add(1) + go func() { + defer wg.Done() + defer devCancel() + slog.Debug("waiting for first server launch") + <-serverChan + slog.Debug("got first server launch event") + + http.Handle("/", &httputil.ReverseProxy{ + Rewrite: func(req *httputil.ProxyRequest) { + req.SetURL(devConfig.ServerURL.URL) + req.Out.Host = req.In.Host + }, + }) + http.Handle("/_/reload", eventsource) + done := make(chan bool) + go func() { + err := srv.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + slog.Error(err.Error()) + cancel() + } + done <- true + }() + go func() { + for { + select { + case ready := <-serverChan: + if ready { + slog.Debug("sending reload message") + eventsource.SendEventMessage("reload", "", "") + } else { + slog.Debug("server not ready") + } + } + } + }() + slog.Info(fmt.Sprintf("dev server listening on %s", devConfig.BaseURL.Host)) + <-done + slog.Debug("dev server closed") + }() + + fw, err := NewFileWatcher(500 * time.Millisecond) + if err != nil { + log.Fatalf("error creating file watcher: %v", err) + } + err = fw.WatchAllFiles("content") + if err != nil { + log.Fatalf("could not watch files in content directory: %v", err) + } + err = fw.WatchAllFiles("templates") + if err != nil { + log.Fatalf("could not watch files in templates directory: %v", err) + } + + var exitCode int + serverErr := make(chan error, 1) +loop: + for { + serverCtx, stopServer := context.WithCancel(ctx) + slog.Debug("starting build") + + err := build(ctx, devConfig) + if err != nil { + slog.Error(fmt.Sprintf("build error: %v", err)) + // don't set up the server until there's a FS change event + } else { + slog.Debug("setting up server") + wg.Add(1) + go func() { + defer wg.Done() + serverChan <- true + serverErr <- server(serverCtx, devConfig) + }() + } + + select { + case <-ctx.Done(): + slog.Debug("main context cancelled") + slog.Debug("calling server shutdown") + srv.Shutdown(devCtx) + exitCode = 1 + break loop + case event := <-fw.Events: + slog.Debug(fmt.Sprintf("event received: %v", event)) + stopServer() + serverChan <- false + slog.Debug("waiting for server shutdown") + <-serverErr + slog.Debug("server shutdown completed") + continue + case err = <-serverErr: + if err != nil && err != context.Canceled { + var exerr *exec.ExitError + slog.Error(fmt.Sprintf("server reported error: %v", err)) + if errors.As(err, &exerr) { + slog.Debug("server exit error") + exitCode = exerr.ExitCode() + } else { + slog.Debug("server other error") + exitCode = 1 + } + break + } + slog.Debug("no error or server context cancelled") + continue + } + + slog.Debug("waiting on server") + exitCode = 0 + break + } + + slog.Debug("waiting for wg before shutting down") + wg.Wait() + os.Exit(exitCode) +} diff --git a/cmd/server/main.go b/cmd/server/main.go index b6817d8..bae215a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -5,7 +5,10 @@ import ( "log" "log/slog" "os" - cfg "website/internal/config" + "os/signal" + "sync" + + "website/internal/server" "github.com/ardanlabs/conf/v3" "github.com/pkg/errors" @@ -16,20 +19,14 @@ var ( ShortSHA string ) -type Config struct { - Production bool `conf:"default:false"` - ListenAddress string `conf:"default:localhost"` - Port uint16 `conf:"default:3000,short:p"` - BaseURL cfg.URL `conf:"default:http://localhost:3000,short:b"` - RedirectOtherHostnames bool `conf:"default:false"` -} - func main() { if os.Getenv("DEBUG") != "" { slog.SetLogLoggerLevel(slog.LevelDebug) } + log.SetFlags(log.LstdFlags | log.Lmsgprefix) + log.SetPrefix("server: ") - runtimeConfig := Config{} + runtimeConfig := server.Config{} help, err := conf.Parse("", &runtimeConfig) if err != nil { if errors.Is(err, conf.ErrHelpWanted) { @@ -39,5 +36,36 @@ func main() { log.Panicf("parsing runtime configuration: %v", err) } - startServer(&runtimeConfig) + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt) + sv, err := server.New(&runtimeConfig) + if err != nil { + log.Fatalf("error setting up server: %v", err) + } + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + sig := <-c + log.Printf("signal captured: %v", sig) + <-sv.Stop() + slog.Debug("server stopped") + }() + + sErr := make(chan error) + wg.Add(1) + go func() { + defer wg.Done() + sErr <- sv.Start() + }() + if !runtimeConfig.InDevServer { + log.Printf("server listening on %s", sv.Addr) + } + + err = <-sErr + if err != nil { + // Error starting or closing listener: + log.Fatalf("error: %v", err) + } + wg.Wait() } diff --git a/config.toml b/config.toml index 603c8d3..55b2508 100644 --- a/config.toml +++ b/config.toml @@ -1,6 +1,5 @@ default_language = "en-GB" base_url = "https://alanpearce.eu" -redirect_other_hostnames = true title = "Alan Pearce" email = "alan@alanpearce.eu" @@ -13,10 +12,34 @@ original_domain = "alanpearce.eu" name = "tags" feed = true +[content-security-policy] +default-src = [ + "'none'", +] +image-src = [ + "'self'", + "http://gc.zgo.at", +] +script-src = [ + "'self'", + "http://gc.zgo.at", +] +style-src = [ + "'unsafe-inline'", +] +frame-ancestors = [ + "https://kagi.com", +] +connect-src = [ + "https://alanpearce-eu.goatcounter.com/count", +] +require-trusted-types-for = [ + "'script'", +] + [extra.headers] cache-control = "max-age=14400" x-content-type-options = "nosniff" -content-security-policy = "default-src 'none'; img-src 'self' https://gc.zgo.at; script-src 'self' https://gc.zgo.at; style-src 'unsafe-inline'; frame-ancestors https://kagi.com; connect-src https://alanpearce-eu.goatcounter.com/count; require-trusted-types-for 'script'" [[menus.main]] name = "Home" diff --git a/go.mod b/go.mod index d124c22..4e8682e 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,15 @@ require ( github.com/PuerkitoBio/goquery v1.9.1 github.com/a-h/htmlformat v0.0.0-20231108124658-5bd994fe268e github.com/adrg/frontmatter v0.2.0 + github.com/antage/eventsource v0.0.0-20220422142129-c4aae935d5bd github.com/antchfx/xmlquery v1.4.0 github.com/antchfx/xpath v1.3.0 github.com/ardanlabs/conf/v3 v3.1.7 + github.com/crewjam/csp v0.0.2 github.com/deckarep/golang-set/v2 v2.6.0 + github.com/fatih/structtag v1.2.0 github.com/getsentry/sentry-go v0.27.0 + github.com/gohugoio/hugo v0.125.4 github.com/otiai10/copy v1.14.0 github.com/pkg/errors v0.9.1 github.com/shengyanli1982/law v0.1.13 @@ -23,12 +27,23 @@ replace github.com/a-h/htmlformat => github.com/alanpearce/htmlformat v0.0.0-202 require ( github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/bep/godartsass v1.2.0 // indirect + github.com/bep/godartsass/v2 v2.0.0 // indirect + github.com/bep/golibsass v1.1.1 // indirect + github.com/cli/safeexec v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/hashstructure v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/tdewolff/parse/v2 v2.7.13 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 68fbd04..1368a63 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,6 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= +github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -5,64 +8,236 @@ github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VP github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4= github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE= -github.com/alanpearce/htmlformat v0.0.0-20240418170242-387207ca8d01 h1:mD01zfZPrqHj7OlyU1O2gJIBRN/kgIyMueres3CHPp8= -github.com/alanpearce/htmlformat v0.0.0-20240418170242-387207ca8d01/go.mod h1:FMIm5afKmEfarNbIXOaPHFY8X7fo+fRQB6I9MPG2nB0= github.com/alanpearce/htmlformat v0.0.0-20240425000139-1244374b2562 h1:7LpBXZnmFk8+RwdFnAYB7rKZhBQrQ4poPLEhpwwbmSc= github.com/alanpearce/htmlformat v0.0.0-20240425000139-1244374b2562/go.mod h1:FMIm5afKmEfarNbIXOaPHFY8X7fo+fRQB6I9MPG2nB0= +github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= +github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/antage/eventsource v0.0.0-20220422142129-c4aae935d5bd h1:FD3sn3oFA0wySr3iH+47H3lIts9qT9nZfoF935hxaH0= +github.com/antage/eventsource v0.0.0-20220422142129-c4aae935d5bd/go.mod h1:jXAZMa2S7OGjBCXWeQVOIZd+LXToszS2zCh0NiHRGvE= github.com/antchfx/xmlquery v1.4.0 h1:xg2HkfcRK2TeTbdb0m1jxCYnvsPaGY/oeZWTGqX/0hA= github.com/antchfx/xmlquery v1.4.0/go.mod h1:Ax2aeaeDjfIw3CwXKDQ0GkwZ6QlxoChlIBP+mGnDFjI= github.com/antchfx/xpath v1.3.0 h1:nTMlzGAK3IJ0bPpME2urTuFL76o4A96iYvoKFHRXJgc= github.com/antchfx/xpath v1.3.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/ardanlabs/conf/v3 v3.1.7 h1:p232cF68TafoA5U9ZlbxUIhGJtGNdKHBXF80Fdqb5t0= github.com/ardanlabs/conf/v3 v3.1.7/go.mod h1:zclexWKe0NVj6LHQ8NgDDZ7bQ1spE0KeKPFficdtAjU= +github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= +github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= +github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= +github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= +github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= +github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840= +github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY= +github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= +github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= +github.com/bep/godartsass v1.2.0 h1:E2VvQrxAHAFwbjyOIExAMmogTItSKodoKuijNrGm5yU= +github.com/bep/godartsass v1.2.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8= +github.com/bep/godartsass/v2 v2.0.0 h1:Ruht+BpBWkpmW+yAM2dkp7RSSeN0VLaTobyW0CiSP3Y= +github.com/bep/godartsass/v2 v2.0.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo= +github.com/bep/golibsass v1.1.1 h1:xkaet75ygImMYjM+FnHIT3xJn7H0xBA9UxSOJjk8Khw= +github.com/bep/golibsass v1.1.1/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= +github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY= +github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= +github.com/bep/lazycache v0.4.0 h1:X8yVyWNVupPd4e1jV7efi3zb7ZV/qcjKQgIQ5aPbkYI= +github.com/bep/lazycache v0.4.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc= +github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= +github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= +github.com/bep/overlayfs v0.9.2 h1:qJEmFInsW12L7WW7dOTUhnMfyk/fN9OCDEO5Gr8HSDs= +github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40= +github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= +github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= +github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crewjam/csp v0.0.2 h1:fIq6o0Z6bkABlvLT3kB0XgPnVX9iNXSAGMILs6AqHVw= +github.com/crewjam/csp v0.0.2/go.mod h1:0tirp4wHwMLZZtV+HXRqGFkUO7uD2ux+1ECvK+7/xFI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= +github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanw/esbuild v0.20.2 h1:E4Y0iJsothpUCq7y0D+ERfqpJmPWrZpNybJA3x3I4p8= +github.com/evanw/esbuild v0.20.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= +github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +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/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= +github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= +github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ= +github.com/gohugoio/hugo v0.125.4 h1:H4sg186C5bUbN1RCDGTNkly/TP/nUkOyxGoMTkX3z20= +github.com/gohugoio/hugo v0.125.4/go.mod h1:b2O1TXqyxQnMzr6wUpqTWJUuK83/U9i2kfYCovO9Gb0= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 h1:PCtO5l++psZf48yen2LxQ3JiOXxaRC6v0594NeHvGZg= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM= +github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc= +github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= +github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= +github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hairyhenderson/go-codeowners v0.4.0 h1:Wx/tRXb07sCyHeC8mXfio710Iu35uAy5KYiBdLHdv4Q= +github.com/hairyhenderson/go-codeowners v0.4.0/go.mod h1:iJgZeCt+W/GzXo5uchFCqvVHZY2T4TAIpvuVlKVkLxc= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= +github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kyokomi/emoji/v2 v2.2.12 h1:sSVA5nH9ebR3Zji1o31wu3yOwD1zKXQA2z0zUyeit60= +github.com/kyokomi/emoji/v2 v2.2.12/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= +github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= +github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= +github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= +github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= +github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= +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/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/shengyanli1982/law v0.1.13 h1:BuUYw/71w1dpGnbXLaCyFUHT36wueUQ7AoVephDut4E= github.com/shengyanli1982/law v0.1.13/go.mod h1:20k9YnOTwilUB4X5Z4S7TIX5Ek1Ok4xfx8V8ZxIWlyM= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk= +github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k= +github.com/tdewolff/parse/v2 v2.7.13 h1:iSiwOUkCYLNfapHoqdLcqZVgvQ0jrsao8YYKP/UJYTI= +github.com/tdewolff/parse/v2 v2.7.13/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52 h1:gAQliwn+zJrkjAHVcBEYW/RFvd2St4yYimisvozAYlA= +github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -71,17 +246,22 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -97,15 +277,45 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/cmd/build/build.go b/internal/builder/builder.go index 5daa940..88e3f02 100644 --- a/cmd/build/build.go +++ b/internal/builder/builder.go @@ -1,4 +1,4 @@ -package main +package builder import ( "fmt" @@ -16,11 +16,19 @@ import ( "github.com/pkg/errors" ) +type IOConfig struct { + Source string `conf:"default:.,short:s,flag:src"` + Destination string `conf:"default:website,short:d,flag:dest"` + BaseURL config.URL + Development bool `conf:"default:false,flag:dev"` +} + func mkdirp(dirs ...string) error { return os.MkdirAll(path.Join(dirs...), 0755) } func outputToFile(output io.Reader, filename ...string) error { + slog.Debug(fmt.Sprintf("outputting file %s", path.Join(filename...))) file, err := os.OpenFile(path.Join(filename...), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errors.WithMessage(err, "could not open output file") @@ -34,6 +42,7 @@ func outputToFile(output io.Reader, filename ...string) error { } func build(outDir string, config config.Config) error { + slog.Debug(fmt.Sprintf("output directory %s", outDir)) privateDir := path.Join(outDir, "private") if err := mkdirp(privateDir); err != nil { return errors.WithMessage(err, "could not create private directory") @@ -48,7 +57,7 @@ func build(outDir string, config config.Config) error { PermissionControl: cp.AddPermission(0755), }) if err != nil { - log.Panic(errors.Errorf("could not copy static files: %v", err)) + return errors.WithMessage(err, "could not copy static files") } if err := mkdirp(publicDir, "post"); err != nil { @@ -163,17 +172,12 @@ func build(outDir string, config config.Config) error { return nil } -type IOConfig struct { - Source string `conf:"default:.,short:s"` - Destination string `conf:"default:website,short:d"` - BaseURL config.URL -} - -func buildSite(ioConfig IOConfig) error { +func BuildSite(ioConfig IOConfig) error { config, err := config.GetConfig() if err != nil { log.Panic(errors.Errorf("could not get config: %v", err)) } + config.InjectLiveReload = ioConfig.Development if ioConfig.BaseURL.URL != nil { config.BaseURL.URL, err = url.Parse(ioConfig.BaseURL.String()) @@ -182,15 +186,12 @@ func buildSite(ioConfig IOConfig) error { } } - err = os.RemoveAll(ioConfig.Destination) - if err != nil { - log.Panic(errors.Errorf("could not remove public directory: %v", err)) - } - - if err := build(ioConfig.Destination, *config); err != nil { - return err + if ioConfig.Development && ioConfig.Destination != "website" { + err = os.RemoveAll(ioConfig.Destination) + if err != nil { + log.Panic(errors.Errorf("could not remove destination directory: %v", err)) + } } - slog.Debug("done") - return nil + return build(ioConfig.Destination, *config) } diff --git a/cmd/build/posts.go b/internal/builder/posts.go index f03caf3..223531b 100644 --- a/cmd/build/posts.go +++ b/internal/builder/posts.go @@ -1,4 +1,4 @@ -package main +package builder import ( "bytes" diff --git a/cmd/build/template.go b/internal/builder/template.go index 84cf57c..74d0418 100644 --- a/cmd/build/template.go +++ b/internal/builder/template.go @@ -1,4 +1,4 @@ -package main +package builder import ( "encoding/xml" @@ -22,10 +22,11 @@ import ( ) var ( - assetsOnce sync.Once - css string - countHTML *goquery.Document - templates map[string]*os.File = make(map[string]*os.File) + assetsOnce sync.Once + css string + countHTML *goquery.Document + liveReloadHTML *goquery.Document + templates map[string]*os.File = make(map[string]*os.File) ) func loadTemplate(path string) (file *os.File, err error) { @@ -111,6 +112,14 @@ func layout(filename string, config config.Config, pageTitle string, pageURL str } defer countFile.Close() countHTML, err = NewDocumentNoScript(countFile) + if config.InjectLiveReload { + liveReloadFile, err := os.OpenFile("templates/dev.html", os.O_RDONLY, 0) + if err != nil { + return + } + defer liveReloadFile.Close() + liveReloadHTML, err = goquery.NewDocumentFromReader(liveReloadFile) + } }) if err != nil { return nil, err @@ -127,6 +136,9 @@ func layout(filename string, config config.Config, pageTitle string, pageURL str doc.Find("title").Add(".p-name").SetText(pageTitle) doc.Find("head > style").SetHtml(css) doc.Find("body").setImgURL(pageURL, pageTitle) + if config.InjectLiveReload { + doc.Find("body").AppendSelection(liveReloadHTML.Find("body").Clone()) + } nav := doc.Find("nav") navLink := doc.Find("nav a") nav.Empty() diff --git a/internal/config/config.go b/internal/config/config.go index d2eabf0..578390e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,7 +5,6 @@ import ( "log/slog" "net/url" "os" - "strconv" "github.com/BurntSushi/toml" "github.com/pkg/errors" @@ -31,18 +30,17 @@ func (u *URL) UnmarshalText(text []byte) (err error) { } type Config struct { - DefaultLanguage string `toml:"default_language"` - BaseURL URL `toml:"base_url"` - RedirectOtherHostnames bool `toml:"redirect_other_hostnames"` - Port uint64 - Production bool - Title string - Email string - Description string - DomainStartDate string `toml:"domain_start_date"` - OriginalDomain string `toml:"original_domain"` - Taxonomies []Taxonomy - Extra struct { + DefaultLanguage string `toml:"default_language"` + BaseURL URL `toml:"base_url"` + InjectLiveReload bool + Title string + Email string + Description string + DomainStartDate string `toml:"domain_start_date"` + OriginalDomain string `toml:"original_domain"` + Taxonomies []Taxonomy + CSP *CSP `toml:"content-security-policy"` + Extra struct { Headers map[string]string } Menus map[string][]MenuItem @@ -71,10 +69,5 @@ func GetConfig() (*Config, error) { return nil, errors.Wrap(err, "config error") } } - port, err := strconv.ParseUint(getEnvFallback("PORT", "3000"), 10, 16) - if err != nil { - return nil, err - } - config.Port = port return &config, nil } diff --git a/internal/config/csp.go b/internal/config/csp.go new file mode 100644 index 0000000..536d9fc --- /dev/null +++ b/internal/config/csp.go @@ -0,0 +1,44 @@ +// Code generated DO NOT EDIT. +package config + +import ( + "github.com/crewjam/csp" +) + +type CSP struct { + BaseURI []string `csp:"base-uri" toml:"base-uri"` + BlockAllMixedContent bool `csp:"block-all-mixed-content" toml:"block-all-mixed-content"` + ChildSrc []string `csp:"child-src" toml:"child-src"` + ConnectSrc []string `csp:"connect-src" toml:"connect-src"` + DefaultSrc []string `csp:"default-src" toml:"default-src"` + FontSrc []string `csp:"font-src" toml:"font-src"` + FormAction []string `csp:"form-action" toml:"form-action"` + FrameAncestors []string `csp:"frame-ancestors" toml:"frame-ancestors"` + FrameSrc []string `csp:"frame-src" toml:"frame-src"` + ImgSrc []string `csp:"img-src" toml:"img-src"` + ManifestSrc []string `csp:"manifest-src" toml:"manifest-src"` + MediaSrc []string `csp:"media-src" toml:"media-src"` + NavigateTo []string `csp:"navigate-to" toml:"navigate-to"` + ObjectSrc []string `csp:"object-src" toml:"object-src"` + PluginTypes []string `csp:"plugin-types" toml:"plugin-types"` + PrefetchSrc []string `csp:"prefetch-src" toml:"prefetch-src"` + Referrer csp.ReferrerPolicy `csp:"referrer" toml:"referrer"` + ReportTo string `csp:"report-to" toml:"report-to"` + ReportURI string `csp:"report-uri" toml:"report-uri"` + RequireSRIFor []csp.RequireSRIFor `csp:"require-sri-for" toml:"require-sri-for"` + RequireTrustedTypesFor []csp.RequireTrustedTypesFor `csp:"require-trusted-types-for" toml:"require-trusted-types-for"` + Sandbox csp.Sandbox `csp:"sandbox" toml:"sandbox"` + ScriptSrc []string `csp:"script-src" toml:"script-src"` + ScriptSrcAttr []string `csp:"script-src-attr" toml:"script-src-attr"` + ScriptSrcElem []string `csp:"script-src-elem" toml:"script-src-elem"` + StyleSrc []string `csp:"style-src" toml:"style-src"` + StyleSrcAttr []string `csp:"style-src-attr" toml:"style-src-attr"` + StyleSrcElem []string `csp:"style-src-elem" toml:"style-src-elem"` + TrustedTypes []string `csp:"trusted-types" toml:"trusted-types"` + UpgradeInsecureRequests bool `csp:"upgrade-insecure-requests" toml:"upgrade-insecure-requests"` + WorkerSrc []string `csp:"worker-src" toml:"worker-src"` +} + +func (c *CSP) String() string { + return csp.Header(*c).String() +} diff --git a/internal/config/cspgenerator.go b/internal/config/cspgenerator.go new file mode 100644 index 0000000..4594d0d --- /dev/null +++ b/internal/config/cspgenerator.go @@ -0,0 +1,79 @@ +package config + +//go:generate go run ../../cmd/cspgenerator/ + +import ( + "fmt" + "os" + "reflect" + + "github.com/crewjam/csp" + "github.com/fatih/structtag" +) + +func GenerateCSP() error { + t := reflect.TypeFor[csp.Header]() + file, err := os.OpenFile("./csp.go", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0) + if err != nil { + return err + } + defer file.Close() + + _, err = fmt.Fprintf(file, `// Code generated DO NOT EDIT. +package config + +import ( + "github.com/crewjam/csp" +) + +`) + if err != nil { + return err + } + + _, err = fmt.Fprintf(file, "type CSP struct {\n") + if err != nil { + return err + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + var t reflect.Type + if field.Type.Kind() == reflect.Slice { + t = field.Type + } else { + t = field.Type + } + tags, err := structtag.Parse(string(field.Tag)) + if err != nil { + return err + } + cspTag, err := tags.Get("csp") + if err != nil { + return err + } + tags.Set(&structtag.Tag{ + Key: "toml", + Name: cspTag.Name, + }) + + _, err = fmt.Fprintf(file, "\t%-23s %-28s `%s`\n", field.Name, t, tags.String()) + if err != nil { + return err + } + } + _, err = fmt.Fprintln(file, "}") + if err != nil { + return err + } + + _, err = fmt.Fprintln(file, ` +func (c *CSP) String() string { + return csp.Header(*c).String() +} + `) + if err != nil { + return err + } + return nil +} diff --git a/cmd/server/filemap.go b/internal/server/filemap.go index 5f7e1bb..466db49 100644 --- a/cmd/server/filemap.go +++ b/internal/server/filemap.go @@ -1,4 +1,4 @@ -package main +package server import ( "fmt" diff --git a/cmd/server/logging.go b/internal/server/logging.go index 601baab..135f06e 100644 --- a/cmd/server/logging.go +++ b/internal/server/logging.go @@ -1,4 +1,4 @@ -package main +package server import ( "fmt" diff --git a/cmd/server/server.go b/internal/server/server.go index 9a1e48a..2e9796d 100644 --- a/cmd/server/server.go +++ b/internal/server/server.go @@ -1,6 +1,7 @@ -package main +package server import ( + "context" "fmt" "io" "log" @@ -9,6 +10,8 @@ import ( "net" "net/http" "os" + "path" + "slices" "strings" "time" @@ -16,17 +19,31 @@ import ( "github.com/getsentry/sentry-go" sentryhttp "github.com/getsentry/sentry-go/http" + "github.com/pkg/errors" "github.com/shengyanli1982/law" ) var config *cfg.Config +type Config struct { + Production bool `conf:"default:false"` + InDevServer bool `conf:"default:false"` + Root string `conf:"default:website"` + ListenAddress string `conf:"default:localhost"` + Port string `conf:"default:3000,short:p"` + BaseURL cfg.URL `conf:"default:http://localhost:3000,short:b"` +} + type HTTPError struct { Error error Message string Code int } +type Server struct { + *http.Server +} + func canonicalisePath(path string) (cPath string, differs bool) { cPath = path if strings.HasSuffix(path, "/index.html") { @@ -52,6 +69,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) *HTTPError { } w.Header().Add("ETag", file.etag) w.Header().Add("Vary", "Accept-Encoding") + w.Header().Add("Content-Security-Policy", config.CSP.String()) for k, v := range config.Extra.Headers { w.Header().Add(k, v) } @@ -93,20 +111,28 @@ func fixupMIMETypes() { } } -func startServer(runtimeConfig *Config) { +func applyDevModeOverrides(config *cfg.Config) { + config.CSP.ScriptSrc = slices.Insert(config.CSP.ScriptSrc, 0, "'unsafe-inline'") + config.CSP.ConnectSrc = slices.Insert(config.CSP.ConnectSrc, 0, "'self'") +} + +func New(runtimeConfig *Config) (*Server, error) { fixupMIMETypes() - c, err := cfg.GetConfig() + var err error + config, err = cfg.GetConfig() if err != nil { - log.Panicf("parsing configuration file: %v", err) + return nil, errors.WithMessage(err, "error parsing configuration file") + } + if runtimeConfig.InDevServer { + applyDevModeOverrides(config) } - config = c - prefix := "website/public" + prefix := path.Join(runtimeConfig.Root, "public") slog.Debug("registering content files", "prefix", prefix) err = registerContentFiles(prefix) if err != nil { - log.Panicf("registering content files: %v", err) + return nil, errors.WithMessagef(err, "registering content files") } env := "development" @@ -121,13 +147,14 @@ func startServer(runtimeConfig *Config) { Environment: env, }) if err != nil { - log.Panic("could not set up sentry") + return nil, errors.WithMessage(err, "could not set up sentry") } defer sentry.Flush(2 * time.Second) sentryHandler := sentryhttp.New(sentryhttp.Options{ Repanic: true, }) + top := http.NewServeMux() mux := http.NewServeMux() slog.Debug("binding main handler to", "host", runtimeConfig.BaseURL.Hostname()+"/") mux.Handle(runtimeConfig.BaseURL.Hostname()+"/", webHandler(serveFile)) @@ -143,7 +170,7 @@ func startServer(runtimeConfig *Config) { } else { logWriter = os.Stdout } - http.Handle("/", + top.Handle("/", sentryHandler.Handle( wrapHandlerWithLogging(mux, wrappedHandlerOptions{ defaultHostname: runtimeConfig.BaseURL.Hostname(), @@ -152,10 +179,43 @@ func startServer(runtimeConfig *Config) { ), ) // no logging, no sentry - http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + top.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) - listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, fmt.Sprint(runtimeConfig.Port)) - log.Fatal(http.ListenAndServe(listenAddress, nil)) + listenAddress := net.JoinHostPort(runtimeConfig.ListenAddress, runtimeConfig.Port) + return &Server{ + &http.Server{ + Addr: listenAddress, + Handler: top, + }, + }, nil +} + +func (s *Server) Start() error { + if err := s.ListenAndServe(); err != http.ErrServerClosed { + return err + } + return nil +} + +func (s *Server) Stop() chan struct{} { + slog.Debug("stop called") + + idleConnsClosed := make(chan struct{}) + + go func() { + slog.Debug("shutting down server") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + err := s.Server.Shutdown(ctx) + slog.Debug("server shut down") + if err != nil { + // Error from closing listeners, or context timeout: + log.Printf("HTTP server Shutdown: %v", err) + } + close(idleConnsClosed) + }() + + return idleConnsClosed } diff --git a/justfile b/justfile index e4021c0..74ee08f 100755 --- a/justfile +++ b/justfile @@ -34,6 +34,17 @@ nix-build what: watch-server: (watch-flake "watchexec -r -i content -i templates go run ./cmd/server") +dev: + #!/usr/bin/env bash + set -euxo pipefail + tmp="$(mktemp -d -t website-XXXXXX)" || exit 1 + echo using temp directory $tmp + trap "{ echo cleaning up $tmp; rm -rf \"$tmp\"; }" EXIT + go build -o $tmp ./cmd/dev ./cmd/build ./cmd/server + "${tmp}/dev" --temp-dir "${tmp}" + +watch-dev: (watch-flake "watchexec -r just dev") + docker-stream system=(arch() + "-linux"): @nix build --print-out-paths .#docker-stream-{{ system }} | sh diff --git a/nix/gomod2nix.toml b/nix/gomod2nix.toml index 1e2501d..8c6ac1f 100644 --- a/nix/gomod2nix.toml +++ b/nix/gomod2nix.toml @@ -17,6 +17,9 @@ schema = 3 [mod."github.com/andybalholm/cascadia"] version = "v1.3.2" hash = "sha256-Nc9SkqJO/ecincVcUBFITy24TMmMGj5o0Q8EgdNhrEk=" + [mod."github.com/antage/eventsource"] + version = "v0.0.0-20220422142129-c4aae935d5bd" + hash = "sha256-5JLLEwW3L3Mt756q3Qw0TKtrNjQ3VtEsV9S2JfdPHy0=" [mod."github.com/antchfx/xmlquery"] version = "v1.4.0" hash = "sha256-ReWP6CPDvvWUd7vY0qIP4qyxvrotXrx9HXbGbeProP4=" @@ -26,30 +29,69 @@ schema = 3 [mod."github.com/ardanlabs/conf/v3"] version = "v3.1.7" hash = "sha256-7H53l0JN5Q6hkAgBivVQ8lFd03oNmP1IG8ihzLKm2CQ=" + [mod."github.com/bep/godartsass"] + version = "v1.2.0" + hash = "sha256-kkKRFesHX8Yp1+/L7yFeRqltBRlAVKgdSN4d7Lc/uI8=" + [mod."github.com/bep/godartsass/v2"] + version = "v2.0.0" + hash = "sha256-ISvlb0UVikyLGPCrkQus//HVgAcgplQDhdIuTrmToRM=" + [mod."github.com/bep/golibsass"] + version = "v1.1.1" + hash = "sha256-rQK/w54sh57GtCG8plKbkFxWBZB0+7RLMvOGCV2jvqY=" + [mod."github.com/cli/safeexec"] + version = "v1.0.1" + hash = "sha256-74r/MHyMIxDSGA2D862d/OQ3lYLozxoUIRMyL0n03m8=" + [mod."github.com/crewjam/csp"] + version = "v0.0.2" + hash = "sha256-4vlGmDdQjPiXmueCV51fJH/hRcG8eqhCi9TENCXjzfA=" [mod."github.com/deckarep/golang-set/v2"] version = "v2.6.0" hash = "sha256-ni1XK75Q8iBBmxgoyZTedP4RmrUPzFC4978xB4HKdfs=" + [mod."github.com/fatih/structtag"] + version = "v1.2.0" + hash = "sha256-Y2pjiEmMsxfUH8LONU2/f8k1BibOHeLKJmi4uZm/SSU=" + [mod."github.com/fsnotify/fsnotify"] + version = "v1.7.0" + hash = "sha256-MdT2rQyQHspPJcx6n9ozkLbsktIOJutOqDuKpNAtoZY=" [mod."github.com/getsentry/sentry-go"] version = "v0.27.0" hash = "sha256-PTkTzVNogqFA/5rc6INLY6RxK5uR1AoJFOO+pOPdE7Q=" + [mod."github.com/gobwas/glob"] + version = "v0.2.3" + hash = "sha256-hYHMUdwxVkMOjSKjR7UWO0D0juHdI4wL8JEy5plu/Jc=" + [mod."github.com/gohugoio/hugo"] + version = "v0.125.4" + hash = "sha256-BHTawZLm/82SXJyGq17pIizqHCG2NJRuqTioKqcu880=" [mod."github.com/golang/groupcache"] version = "v0.0.0-20210331224755-41bb18bfe9da" hash = "sha256-7Gs7CS9gEYZkbu5P4hqPGBpeGZWC64VDwraSKFF+VR0=" - [mod."github.com/kr/pretty"] - version = "v0.3.1" - hash = "sha256-DlER7XM+xiaLjvebcIPiB12oVNjyZHuJHoRGITzzpKU=" + [mod."github.com/mattn/go-isatty"] + version = "v0.0.20" + hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" + [mod."github.com/mitchellh/hashstructure"] + version = "v1.1.0" + hash = "sha256-dNPVpLRsCa2XZHlCRRtkpBVqb8rpHIocpFPNCqZg2EY=" [mod."github.com/otiai10/copy"] version = "v1.14.0" hash = "sha256-xsaL1ddkPS544y0Jv7u/INUALBYmYq29ddWvysLXk4A=" + [mod."github.com/pelletier/go-toml/v2"] + version = "v2.2.1" + hash = "sha256-gmQ4CTz/MI97D3pYqU7mpxqo8gBTDccQ1Cp0lAMmJUc=" [mod."github.com/pkg/errors"] version = "v0.9.1" hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw=" - [mod."github.com/rogpeppe/go-internal"] - version = "v1.10.0" - hash = "sha256-vR7+d0aoKTuKeTYSgZxsGhH9e5Zvxix3Zrq9SPm5+NQ=" [mod."github.com/shengyanli1982/law"] version = "v0.1.13" hash = "sha256-gjXWxWR6XCpOUYKBzPaObw2hPOmkoVtuHd1aMHm/ljA=" + [mod."github.com/spf13/afero"] + version = "v1.11.0" + hash = "sha256-+rV3cDZr13N8E0rJ7iHmwsKYKH+EhV+IXBut+JbBiIE=" + [mod."github.com/spf13/cast"] + version = "v1.6.0" + hash = "sha256-hxioqRZfXE0AE5099wmn3YG0AZF8Wda2EB4c7zHF6zI=" + [mod."github.com/tdewolff/parse/v2"] + version = "v2.7.13" + hash = "sha256-mG6TO8hcXUmi9yKIogrfoDLWE6i6qs4LTjr7mSmqb7M=" [mod."github.com/yuin/goldmark"] version = "v1.7.1" hash = "sha256-3EUgwoZRRs2jNBWSbB0DGNmfBvx7CeAgEwyUdaRaeR4=" @@ -65,6 +107,9 @@ schema = 3 [mod."golang.org/x/text"] version = "v0.14.0" hash = "sha256-yh3B0tom1RfzQBf1RNmfdNWF1PtiqxV41jW1GVS6JAg=" + [mod."google.golang.org/protobuf"] + version = "v1.33.0" + hash = "sha256-cWwQjtUwSIEkAlAadrlxK1PYZXTRrV4NKzt7xDpJgIU=" [mod."gopkg.in/check.v1"] version = "v1.0.0-20201130134442-10cb98267c6c" hash = "sha256-VlIpM2r/OD+kkyItn6vW35dyc0rtkJufA93rjFyzncs=" diff --git a/templates/dev.html b/templates/dev.html new file mode 100644 index 0000000..0ca383e --- /dev/null +++ b/templates/dev.html @@ -0,0 +1,8 @@ +<body> + <script defer> + new EventSource("/_/reload").onmessage = event => { + console.log("got message", event) + window.location.reload() + }; + </script> +</body> |