diff options
Diffstat (limited to 'commands/server.go')
-rw-r--r-- | commands/server.go | 1101 |
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( |