summaryrefslogtreecommitdiffstats
path: root/commands/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'commands/server.go')
-rw-r--r--commands/server.go1101
1 files changed, 699 insertions, 402 deletions
diff --git a/commands/server.go b/commands/server.go
index 121a649d4..81a5120ef 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
+// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,357 +16,217 @@ package commands
import (
"bytes"
"context"
+ "encoding/json"
+ "errors"
"fmt"
"io"
+ "io/ioutil"
"net"
"net/http"
"net/url"
"os"
+ "sync"
+ "sync/atomic"
+
"os/signal"
"path"
"path/filepath"
"regexp"
- "runtime"
"strconv"
"strings"
- "sync"
"syscall"
"time"
- "github.com/gohugoio/hugo/common/htime"
- "github.com/gohugoio/hugo/common/paths"
+ "github.com/bep/debounce"
+ "github.com/bep/simplecobra"
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/common/urls"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib"
- "github.com/gohugoio/hugo/tpl"
- "golang.org/x/sync/errgroup"
-
+ "github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
-
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/transform"
+ "github.com/gohugoio/hugo/transform/livereloadinject"
"github.com/spf13/afero"
"github.com/spf13/cobra"
- jww "github.com/spf13/jwalterweatherman"
+ "github.com/spf13/fsync"
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/sync/semaphore"
)
-type serverCmd struct {
- // Can be used to stop the server. Useful in tests
- stop chan bool
-
- disableLiveReload bool
- navigateToChanged bool
- renderToDisk bool
- renderStaticToDisk bool
- serverAppend bool
- serverInterface string
- serverPort int
- liveReloadPort int
- serverWatch bool
- noHTTPCache bool
-
- disableFastRender bool
- disableBrowserError bool
-
- *baseBuilderCmd
-}
-
-func (b *commandsBuilder) newServerCmd() *serverCmd {
- return b.newServerCmdSignaled(nil)
-}
-
-func (b *commandsBuilder) newServerCmdSignaled(stop chan bool) *serverCmd {
- cc := &serverCmd{stop: stop}
+var (
+ logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
+ logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`)
+ logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`)
+)
- cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
- Use: "server",
- Aliases: []string{"serve"},
- Short: "A high performance webserver",
- Long: `Hugo provides its own webserver which builds and serves the site.
-While hugo server is high performance, it is a webserver with limited options.
-Many run it in production, but the standard behavior is for people to use it
-in development and use a more full featured server such as Nginx or Caddy.
+var logReplacer = strings.NewReplacer(
+ "can't", "can’t", // Chroma lexer doesn't do well with "can't"
+ "*hugolib.pageState", "page.Page", // Page is the public interface.
+ "Rebuild failed:", "",
+)
-'hugo server' will avoid writing the rendered and served content to disk,
-preferring to store it in memory.
+const (
+ configChangeConfig = "config file"
+ configChangeGoMod = "go.mod file"
+ configChangeGoWork = "go work file"
+)
-By default hugo will also watch your files for any changes you make and
-automatically rebuild the site. It will then live reload any open browser pages
-and push the latest content to them. As most Hugo sites are built in a fraction
-of a second, you will be able to save and see your changes nearly instantly.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- err := cc.server(cmd, args)
- if err != nil && cc.stop != nil {
- cc.stop <- true
+func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder {
+ return &hugoBuilder{
+ r: r,
+ s: s,
+ visitedURLs: types.NewEvictingStringQueue(100),
+ fullRebuildSem: semaphore.NewWeighted(1),
+ debounce: debounce.New(4 * time.Second),
+ onConfigLoaded: func(reloaded bool) error {
+ for _, wc := range onConfigLoaded {
+ if err := wc(reloaded); err != nil {
+ return err
+ }
}
- return err
+ return nil
},
- })
-
- cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen")
- cc.cmd.Flags().IntVar(&cc.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
- cc.cmd.Flags().StringVarP(&cc.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
- cc.cmd.Flags().BoolVarP(&cc.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
- cc.cmd.Flags().BoolVar(&cc.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
- cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL")
- cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
- cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
- cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "serve all files from disk (default is from memory)")
- cc.cmd.Flags().BoolVar(&cc.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
- cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
- cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
-
- cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
- cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
-
- return cc
+ }
}
-type filesOnlyFs struct {
- fs http.FileSystem
+func newServerCommand() *serverCommand {
+ var c *serverCommand
+ c = &serverCommand{
+ quit: make(chan bool),
+ }
+ return c
}
-type noDirFile struct {
- http.File
+type countingStatFs struct {
+ afero.Fs
+ statCounter uint64
}
-func (fs filesOnlyFs) Open(name string) (http.File, error) {
- f, err := fs.fs.Open(name)
- if err != nil {
- return nil, err
+func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) {
+ f, err := fs.Fs.Stat(name)
+ if err == nil {
+ if !f.IsDir() {
+ atomic.AddUint64(&fs.statCounter, 1)
+ }
}
- return noDirFile{f}, nil
+ return f, err
}
-func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) {
- return nil, nil
+// dynamicEvents contains events that is considered dynamic, as in "not static".
+// Both of these categories will trigger a new build, but the asset events
+// does not fit into the "navigate to changed" logic.
+type dynamicEvents struct {
+ ContentEvents []fsnotify.Event
+ AssetEvents []fsnotify.Event
}
-func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
- // If a Destination is provided via flag write to disk
- destination, _ := cmd.Flags().GetString("destination")
- if destination != "" {
- sc.renderToDisk = true
- }
-
- var serverCfgInit sync.Once
+type fileChangeDetector struct {
+ sync.Mutex
+ current map[string]string
+ prev map[string]string
- cfgInit := func(c *commandeer) (rerr error) {
- c.Set("renderToMemory", !(sc.renderToDisk || sc.renderStaticToDisk))
- c.Set("renderStaticToDisk", sc.renderStaticToDisk)
- if cmd.Flags().Changed("navigateToChanged") {
- c.Set("navigateToChanged", sc.navigateToChanged)
- }
- if cmd.Flags().Changed("disableLiveReload") {
- c.Set("disableLiveReload", sc.disableLiveReload)
- }
- if cmd.Flags().Changed("disableFastRender") {
- c.Set("disableFastRender", sc.disableFastRender)
- }
- if cmd.Flags().Changed("disableBrowserError") {
- c.Set("disableBrowserError", sc.disableBrowserError)
- }
- if sc.serverWatch {
- c.Set("watch", true)
- }
-
- // TODO(bep) see issue 9901
- // cfgInit is called twice, before and after the languages have been initialized.
- // The servers (below) can not be initialized before we
- // know if we're configured in a multihost setup.
- if len(c.languages) == 0 {
- return nil
- }
-
- // We can only do this once.
- serverCfgInit.Do(func() {
- c.serverPorts = make([]serverPortListener, 1)
-
- if c.languages.IsMultihost() {
- if !sc.serverAppend {
- rerr = newSystemError("--appendPort=false not supported when in multihost mode")
- }
- c.serverPorts = make([]serverPortListener, len(c.languages))
- }
-
- currentServerPort := sc.serverPort
-
- for i := 0; i < len(c.serverPorts); i++ {
- l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort)))
- if err == nil {
- c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
- } else {
- if i == 0 && sc.cmd.Flags().Changed("port") {
- // port set explicitly by user -- he/she probably meant it!
- rerr = newSystemErrorF("Server startup failed: %s", err)
- return
- }
- c.logger.Println("port", sc.serverPort, "already in use, attempting to use an available port")
- l, sp, err := helpers.TCPListen()
- if err != nil {
- rerr = newSystemError("Unable to find alternative port to use:", err)
- return
- }
- c.serverPorts[i] = serverPortListener{ln: l, p: sp.Port}
- }
-
- currentServerPort = c.serverPorts[i].p + 1
- }
- })
-
- if rerr != nil {
- return
- }
-
- c.Set("port", sc.serverPort)
- if sc.liveReloadPort != -1 {
- c.Set("liveReloadPort", sc.liveReloadPort)
- } else {
- c.Set("liveReloadPort", c.serverPorts[0].p)
- }
-
- isMultiHost := c.languages.IsMultihost()
- for i, language := range c.languages {
- var serverPort int
- if isMultiHost {
- serverPort = c.serverPorts[i].p
- } else {
- serverPort = c.serverPorts[0].p
- }
+ irrelevantRe *regexp.Regexp
+}
- baseURL, err := sc.fixURL(language, sc.baseURL, serverPort)
- if err != nil {
- return nil
- }
- if isMultiHost {
- language.Set("baseURL", baseURL)
- }
- if i == 0 {
- c.Set("baseURL", baseURL)
- }
- }
+func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
+ f.Lock()
+ defer f.Unlock()
+ f.current[name] = md5sum
+}
+func (f *fileChangeDetector) PrepareNew() {
+ if f == nil {
return
}
- if err := memStats(); err != nil {
- jww.WARN.Println("memstats error:", err)
- }
-
- // silence errors in cobra so we can handle them here
- cmd.SilenceErrors = true
+ f.Lock()
+ defer f.Unlock()
- c, err := initializeConfig(true, true, true, &sc.hugoBuilderCommon, sc, cfgInit)
- if err != nil {
- cmd.PrintErrln("Error:", err.Error())
- return err
+ if f.current == nil {
+ f.current = make(map[string]string)
+ f.prev = make(map[string]string)
+ return
}
- err = func() error {
- defer c.timeTrack(time.Now(), "Built")
- err := c.serverBuild()
- if err != nil {
- cmd.PrintErrln("Error:", err.Error())
- }
- return err
- }()
- if err != nil {
- return err
+ f.prev = make(map[string]string)
+ for k, v := range f.current {
+ f.prev[k] = v
}
+ f.current = make(map[string]string)
+}
- // Watch runs its own server as part of the routine
- if sc.serverWatch {
-
- watchDirs, err := c.getDirList()
- if err != nil {
- return err
- }
-
- watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
-
- for _, group := range watchGroups {
- jww.FEEDBACK.Printf("Watching for changes in %s\n", group)
- }
- watcher, err := c.newWatcher(sc.poll, watchDirs...)
- if err != nil {
- return err
+func (f *fileChangeDetector) changed() []string {
+ if f == nil {
+ return nil
+ }
+ f.Lock()
+ defer f.Unlock()
+ var c []string
+ for k, v := range f.current {
+ vv, found := f.prev[k]
+ if !found || v != vv {
+ c = append(c, k)
}
-
- defer watcher.Close()
-
}
- return c.serve(sc)
+ return f.filterIrrelevant(c)
}
-func getRootWatchDirsStr(baseDir string, watchDirs []string) string {
- relWatchDirs := make([]string, len(watchDirs))
- for i, dir := range watchDirs {
- relWatchDirs[i], _ = paths.GetRelativePath(dir, baseDir)
+func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
+ var filtered []string
+ for _, v := range in {
+ if !f.irrelevantRe.MatchString(v) {
+ filtered = append(filtered, v)
+ }
}
-
- return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",")
+ return filtered
}
type fileServer struct {
baseURLs []string
roots []string
errorTemplate func(err any) (io.Reader, error)
- c *commandeer
- s *serverCmd
-}
-
-func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request {
- r2 := new(http.Request)
- *r2 = *r
- r2.URL = new(url.URL)
- *r2.URL = *r.URL
- r2.URL.Path = toPath
- r2.Header.Set("X-Rewrite-Original-URI", r.URL.RequestURI())
-
- return r2
+ c *serverCommand
}
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string, string, error) {
+ r := f.c.r
+ conf := f.c.conf()
baseURL := f.baseURLs[i]
root := f.roots[i]
port := f.c.serverPorts[i].p
listener := f.c.serverPorts[i].ln
+ logger := f.c.r.logger
- // For logging only.
- // TODO(bep) consolidate.
- publishDir := f.c.Cfg.GetString("publishDir")
- publishDirStatic := f.c.Cfg.GetString("publishDirStatic")
- workingDir := f.c.Cfg.GetString("workingDir")
-
- if root != "" {
- publishDir = filepath.Join(publishDir, root)
- publishDirStatic = filepath.Join(publishDirStatic, root)
- }
- absPublishDir := paths.AbsPathify(workingDir, publishDir)
- absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic)
-
- jww.FEEDBACK.Printf("Environment: %q", f.c.hugo().Deps.Site.Hugo().Environment)
+ r.Printf("Environment: %q", f.c.hugoTry().Deps.Site.Hugo().Environment)
if i == 0 {
- if f.s.renderToDisk {
- jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
- } else if f.s.renderStaticToDisk {
- jww.FEEDBACK.Println("Serving pages from memory and static files from " + absPublishDirStatic)
+ if f.c.renderToDisk {
+ r.Println("Serving pages from disk")
+ } else if f.c.renderStaticToDisk {
+ r.Println("Serving pages from memory and static files from disk")
} else {
- jww.FEEDBACK.Println("Serving pages from memory")
+ r.Println("Serving pages from memory")
}
}
- httpFs := afero.NewHttpFs(f.c.publishDirServerFs)
+ httpFs := afero.NewHttpFs(conf.fs.PublishDirServer)
fs := filesOnlyFs{httpFs.Dir(path.Join("/", root))}
-
if i == 0 && f.c.fastRenderMode {
- jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
+ r.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
}
// We're only interested in the path
u, err := url.Parse(baseURL)
if err != nil {
- return nil, nil, "", "", fmt.Errorf("Invalid baseURL: %w", err)
+ return nil, nil, "", "", fmt.Errorf("invalid baseURL: %w", err)
}
decorate := func(h http.Handler) http.Handler {
@@ -375,16 +235,16 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
// First check the error state
err := f.c.getErrorWithContext()
if err != nil {
- f.c.wasError = true
+ f.c.errState.setWasErr(false)
w.WriteHeader(500)
r, err := f.errorTemplate(err)
if err != nil {
- f.c.logger.Errorln(err)
+ logger.Errorln(err)
}
port = 1313
- if !f.c.paused {
- port = f.c.Cfg.GetInt("liveReloadPort")
+ if !f.c.errState.isPaused() {
+ port = conf.configs.Base.Internal.LiveReloadPort
}
lr := *u
lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port)
@@ -394,19 +254,21 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
}
}
- if f.s.noHTTPCache {
+ if f.c.noHTTPCache {
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
w.Header().Set("Pragma", "no-cache")
}
+ serverConfig := f.c.conf().configs.Base.Server
+
// Ignore any query params for the operations below.
requestURI, _ := url.PathUnescape(strings.TrimSuffix(r.RequestURI, "?"+r.URL.RawQuery))
- for _, header := range f.c.serverConfig.MatchHeaders(requestURI) {
+ for _, header := range serverConfig.MatchHeaders(requestURI) {
w.Header().Set(header.Key, header.Value)
}
- if redirect := f.c.serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
+ if redirect := serverConfig.MatchRedirect(requestURI); !redirect.IsZero() {
// fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
doRedirect := true
// This matches Netlify's behaviour and is needed for SPA behaviour.
@@ -416,7 +278,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
if root != "" {
path = filepath.Join(root, path)
}
- fs := f.c.publishDirServerFs
+ fs := f.c.conf().getFs().PublishDir
fi, err := fs.Stat(path)
@@ -459,7 +321,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
}
- if f.c.fastRenderMode && f.c.buildErr == nil {
+ if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") {
if !f.c.visitedURLs.Contains(requestURI) {
// If not already on stack, re-render that single page.
@@ -488,48 +350,368 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
} else {
mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
}
+ if r.IsTestRun() {
+ var shutDownOnce sync.Once
+ mu.HandleFunc("/__stop", func(w http.ResponseWriter, r *http.Request) {
+ shutDownOnce.Do(func() {
+ close(f.c.quit)
+ })
+ })
+ }
- endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port))
+ endpoint := net.JoinHostPort(f.c.serverInterface, strconv.Itoa(port))
return mu, listener, u.String(), endpoint, nil
}
-var (
- logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `)
- logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`)
- logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`)
-)
+func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request {
+ r2 := new(http.Request)
+ *r2 = *r
+ r2.URL = new(url.URL)
+ *r2.URL = *r.URL
+ r2.URL.Path = toPath
+ r2.Header.Set("X-Rewrite-Original-URI", r.URL.RequestURI())
-func removeErrorPrefixFromLog(content string) string {
- return logErrorRe.ReplaceAllLiteralString(content, "")
+ return r2
}
-var logReplacer = strings.NewReplacer(
- "can't", "can’t", // Chroma lexer doesn't do well with "can't"
- "*hugolib.pageState", "page.Page", // Page is the public interface.
- "Rebuild failed:", "",
-)
+type filesOnlyFs struct {
+ fs http.FileSystem
+}
-func cleanErrorLog(content string) string {
- content = strings.ReplaceAll(content, "\n", " ")
- content = logReplacer.Replace(content)
- content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
- content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
- seen := make(map[string]bool)
- parts := strings.Split(content, ": ")
- keep := make([]string, 0, len(parts))
- for _, part := range parts {
- if seen[part] {
- continue
+func (fs filesOnlyFs) Open(name string) (http.File, error) {
+ f, err := fs.fs.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ return noDirFile{f}, nil
+}
+
+type noDirFile struct {
+ http.File
+}
+
+func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) {
+ return nil, nil
+}
+
+type serverCommand struct {
+ r *rootCommand
+
+ commands []simplecobra.Commander
+
+ *hugoBuilder
+
+ quit chan bool // Closed when the server should shut down. Used in tests only.
+ serverPorts []serverPortListener
+ doLiveReload bool
+
+ // Flags.
+
+ renderToDisk bool
+ renderStaticToDisk bool
+ navigateToChanged bool
+ serverAppend bool
+ serverInterface string
+ serverPort int
+ liveReloadPort int
+ serverWatch bool
+ noHTTPCache bool
+ disableLiveReload bool
+ disableFastRender bool
+ disableBrowserError bool
+}
+
+func (c *serverCommand) Commands() []simplecobra.Commander {
+ return c.commands
+}
+
+func (c *serverCommand) Name() string {
+ return "server"
+}
+
+func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
+ err := func() error {
+ defer c.r.timeTrack(time.Now(), "Built")
+ err := c.build()
+ if err != nil {
+ c.r.Println("Error:", err.Error())
}
- seen[part] = true
- keep = append(keep, part)
+ return err
+ }()
+ if err != nil {
+ return err
}
- return strings.Join(keep, ": ")
+
+ // Watch runs its own server as part of the routine
+ if c.serverWatch {
+
+ watchDirs, err := c.getDirList()
+ if err != nil {
+ return err
+ }
+
+ watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
+
+ for _, group := range watchGroups {
+ c.r.Printf("Watching for changes in %s\n", group)
+ }
+ watcher, err := c.newWatcher(c.r.poll, watchDirs...)
+ if err != nil {
+ return err
+ }
+
+ defer watcher.Close()
+
+ }
+
+ return c.serve()
+}
+
+func (c *serverCommand) WithCobraCommand(cmd *cobra.Command) error {
+ cmd.Short = "A high performance webserver"
+ cmd.Long = `Hugo provides its own webserver which builds and serves the site.
+While hugo server is high performance, it is a webserver with limited options.
+Many run it in production, but the standard behavior is for people to use it
+in development and use a more full featured server such as Nginx or Caddy.
+
+'hugo server' will avoid writing the rendered and served content to disk,
+preferring to store it in memory.
+
+By default hugo will also watch your files for any changes you make and
+automatically rebuild the site. It will then live reload any open browser pages
+and push the latest content to them. As most Hugo sites are built in a fraction
+of a second, you will be able to save and see your changes nearly instantly.`
+ cmd.Aliases = []string{"serve"}
+
+ cmd.Flags().IntVarP(&c.serverPort, "port", "p", 1313, "port on which the server will listen")
+ cmd.Flags().IntVar(&c.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)")
+ cmd.Flags().StringVarP(&c.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind")
+ cmd.Flags().BoolVarP(&c.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
+ cmd.Flags().BoolVar(&c.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching")
+ cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL")
+ cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
+ cmd.Flags().BoolVar(&c.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
+ cmd.Flags().BoolVar(&c.renderToDisk, "renderToDisk", false, "serve all files from disk (default is from memory)")
+ cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
+ cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
+ cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
+
+ cmd.Flags().String("memstats", "", "log memory usage to this file")
+ cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
+ return nil
+}
+
+func (c *serverCommand) Init(cd, runner *simplecobra.Commandeer) error {
+ c.r = cd.Root.Command.(*rootCommand)
+
+ c.hugoBuilder = newHugoBuilder(
+ c.r,
+ c,
+ func(reloaded bool) error {
+ if !reloaded {
+ if err := c.createServerPorts(cd); err != nil {
+ return err
+ }
+ }
+ if err := c.setBaseURLsInConfig(); err != nil {
+ return err
+ }
+
+ if !reloaded && c.fastRenderMode {
+ c.conf().fs.PublishDir = hugofs.NewHashingFs(c.conf().fs.PublishDir, c.changeDetector)
+ c.conf().fs.PublishDirStatic = hugofs.NewHashingFs(c.conf().fs.PublishDirStatic, c.changeDetector)
+ }
+
+ return nil
+ },
+ )
+
+ destinationFlag := cd.CobraCommand.Flags().Lookup("destination")
+ c.renderToDisk = c.renderToDisk || (destinationFlag != nil && destinationFlag.Changed)
+ c.doLiveReload = !c.disableLiveReload
+ c.fastRenderMode = !c.disableFastRender
+ c.showErrorInBrowser = c.doLiveReload && !c.disableBrowserError
+ if c.r.environment == "" {
+ c.r.environment = hugo.EnvironmentDevelopment
+ }
+
+ if c.fastRenderMode {
+ // For now, fast render mode only. It should, however, be fast enough
+ // for the full variant, too.
+ c.changeDetector = &fileChangeDetector{
+ // We use this detector to decide to do a Hot reload of a single path or not.
+ // We need to filter out source maps and possibly some other to be able
+ // to make that decision.
+ irrelevantRe: regexp.MustCompile(`\.map$`),
+ }
+
+ c.changeDetector.PrepareNew()
+
+ }
+
+ err := c.loadConfig(cd, true)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *serverCommand) setBaseURLsInConfig() error {
+ if len(c.serverPorts) == 0 {
+ panic("no server ports set")
+ }
+ isMultiHost := c.conf().configs.IsMultihost
+ for i, language := range c.conf().configs.Languages {
+ var serverPort int
+ if isMultiHost {
+ serverPort = c.serverPorts[i].p
+ } else {
+ serverPort = c.serverPorts[0].p
+ }
+ langConfig := c.conf().configs.LanguageConfigMap[language.Lang]
+ baseURLStr, err := c.fixURL(langConfig.BaseURL, c.r.baseURL, serverPort)
+ if err != nil {
+ return nil
+ }
+ baseURL, err := urls.NewBaseURLFromString(baseURLStr)
+ if err != nil {
+ return fmt.Errorf("failed to create baseURL from %q: %s", baseURLStr, err)
+ }
+
+ baseURLLiveReload := baseURL
+ if c.liveReloadPort != -1 {
+ baseURLLiveReload, _ = baseURLLiveReload.WithPort(c.liveReloadPort)
+ }
+ langConfig.C.SetBaseURL(baseURL, baseURLLiveReload)
+ }
+ return nil
}
-func (c *commandeer) serve(s *serverCmd) error {
- isMultiHost := c.hugo().IsMultihost()
+func (c *serverCommand) getErrorWithContext() any {
+ errCount := c.errCount()
+
+ if errCount == 0 {
+ return nil
+ }
+
+ m := make(map[string]any)
+
+ //xwm["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors())))
+ m["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors())))
+ m["Version"] = hugo.BuildVersionString()
+ ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.errState.buildErr())
+ m["Files"] = ferrors
+
+ return m
+}
+
+func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
+ flags := cd.CobraCommand.Flags()
+ isMultiHost := c.conf().configs.IsMultihost
+ c.serverPorts = make([]serverPortListener, 1)
+ if isMultiHost {
+ if !c.serverAppend {
+ return errors.New("--appendPort=false not supported when in multihost mode")
+ }
+ c.serverPorts = make([]serverPortListener, len(c.conf().configs.Languages))
+ }
+ currentServerPort := c.serverPort
+ for i := 0; i < len(c.serverPorts); i++ {
+ l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort)))
+ if err == nil {
+ c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
+ } else {
+ if i == 0 && flags.Changed("port") {
+ // port set explicitly by user -- he/she probably meant it!
+ return fmt.Errorf("server startup failed: %s", err)
+ }
+ c.r.Println("port", currentServerPort, "already in use, attempting to use an available port")
+ l, sp, err := helpers.TCPListen()
+ if err != nil {
+ return fmt.Errorf("unable to find alternative port to use: %s", err)
+ }
+ c.serverPorts[i] = serverPortListener{ln: l, p: sp.Port}
+ }
+
+ currentServerPort = c.serverPorts[i].p + 1
+ }
+ return nil
+}
+
+// fixURL massages the baseURL into a form needed for serving
+// all pages correctly.
+func (c *serverCommand) fixURL(baseURL, s string, port int) (string, error) {
+ useLocalhost := false
+ if s == "" {
+ s = baseURL
+ useLocalhost = true
+ }
+
+ if !strings.HasSuffix(s, "/") {
+ s = s + "/"
+ }
+
+ // do an initial parse of the input string
+ u, err := url.Parse(s)
+ if err != nil {
+ return "", err
+ }
+
+ // if no Host is defined, then assume that no schema or double-slash were
+ // present in the url. Add a double-slash and make a best effort attempt.
+ if u.Host == "" && s != "/" {
+ s = "//" + s
+
+ u, err = url.Parse(s)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ if useLocalhost {
+ if u.Scheme == "https" {
+ u.Scheme = "http"
+ }
+ u.Host = "localhost"
+ }
+
+ if c.serverAppend {
+ if strings.Contains(u.Host, ":") {
+ u.Host, _, err = net.SplitHostPort(u.Host)
+ if err != nil {
+ return "", fmt.Errorf("failed to split baseURL hostport: %w", err)
+ }
+ }
+ u.Host += fmt.Sprintf(":%d", port)
+ }
+
+ return u.String(), nil
+}
+
+func (c *serverCommand) partialReRender(urls ...string) error {