From a5e5be234c33016cd44a611ea4b8c6e57e2468e4 Mon Sep 17 00:00:00 2001 From: Bjørn Erik Pedersen Date: Fri, 13 Dec 2024 13:57:23 +0100 Subject: Fix panic on server rebuilds when using both base templates and template.Defer Fixes #12963 --- common/types/evictingqueue.go | 3 ++ hugolib/integrationtest_builder.go | 27 +++++++++++-- internal/js/esbuild/batch_integration_test.go | 40 ------------------- tpl/tplimpl/template.go | 56 +++++++++++++++++---------- tpl/tplimpl/tplimpl_integration_test.go | 49 +++++++++++++++++++++++ 5 files changed, 110 insertions(+), 65 deletions(-) diff --git a/common/types/evictingqueue.go b/common/types/evictingqueue.go index 88add59d5..5ab715aa8 100644 --- a/common/types/evictingqueue.go +++ b/common/types/evictingqueue.go @@ -65,6 +65,9 @@ func (q *EvictingStringQueue) Len() int { // Contains returns whether the queue contains v. func (q *EvictingStringQueue) Contains(v string) bool { + if q == nil { + return false + } q.mu.Lock() defer q.mu.Unlock() return q.set[v] diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index b593cd965..7a6c040b1 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -25,6 +25,7 @@ import ( "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/security" @@ -466,6 +467,28 @@ func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder { return s } +func (s *IntegrationTestBuilder) BuildPartial(urls ...string) *IntegrationTestBuilder { + if _, err := s.BuildPartialE(urls...); err != nil { + s.Fatal(err) + } + return s +} + +func (s *IntegrationTestBuilder) BuildPartialE(urls ...string) (*IntegrationTestBuilder, error) { + if s.buildCount == 0 { + panic("BuildPartial can only be used after a full build") + } + if !s.Cfg.Running { + panic("BuildPartial can only be used in server mode") + } + visited := types.NewEvictingStringQueue(len(urls)) + for _, url := range urls { + visited.Add(url) + } + buildCfg := BuildCfg{RecentlyVisited: visited, PartialReRender: true} + return s, s.build(buildCfg) +} + func (s *IntegrationTestBuilder) Close() { s.Helper() s.Assert(s.H.Close(), qt.IsNil) @@ -747,10 +770,6 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error { s.counters = &buildCounters{} cfg.testCounters = s.counters - if s.buildCount > 0 && (len(changeEvents) == 0) { - return nil - } - s.buildCount++ err := s.H.Build(cfg, changeEvents...) diff --git a/internal/js/esbuild/batch_integration_test.go b/internal/js/esbuild/batch_integration_test.go index 55528bdf0..b4a2454ac 100644 --- a/internal/js/esbuild/batch_integration_test.go +++ b/internal/js/esbuild/batch_integration_test.go @@ -721,43 +721,3 @@ console.log("config.params.id", id3); b.EditFileReplaceAll("assets/other/bar.css", ".bar-edit {", ".bar-edit2 {").Build() b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit2 {") } - -func TestEditBaseofManyTimes(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -disableKinds = ["taxonomy", "term"] --- layouts/_default/baseof.html -- -Baseof. -{{ block "main" . }}{{ end }} -{{ with (templates.Defer (dict "key" "global")) }} -Now. {{ now }} -{{ end }} --- layouts/_default/single.html -- -{{ define "main" }} -Single. -{{ end }} --- --- layouts/_default/list.html -- -{{ define "main" }} -List. -{{ end }} --- content/mybundle/index.md -- ---- -title: "My Bundle" ---- --- content/_index.md -- ---- -title: "Home" ---- -` - - b := hugolib.TestRunning(t, files) - b.AssertFileContent("public/index.html", "Baseof.") - - for i := 0; i < 100; i++ { - b.EditFileReplaceAll("layouts/_default/baseof.html", "Now", "Now.").Build() - b.AssertFileContent("public/index.html", "Now..") - } -} diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index 9e2af046d..0a593593b 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -30,6 +30,7 @@ import ( "unicode" "unicode/utf8" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/output/layouts" @@ -191,8 +192,10 @@ func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) { func newTemplateNamespace(funcs map[string]any) *templateNamespace { return &templateNamespace{ - prototypeHTML: htmltemplate.New("").Funcs(funcs), - prototypeText: texttemplate.New("").Funcs(funcs), + prototypeHTML: htmltemplate.New("").Funcs(funcs), + prototypeText: texttemplate.New("").Funcs(funcs), + prototypeHTMLCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](), + prototypeTextCloneCache: maps.NewCache[prototypeCloneID, *texttemplate.Template](), templateStateMap: &templateStateMap{ templates: make(map[string]*templateState), }, @@ -688,7 +691,7 @@ func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) { if overlay.isText { var ( - templ = t.main.prototypeTextClone.New(overlay.name) + templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name) err error ) @@ -713,7 +716,7 @@ func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Tem } var ( - templ = t.main.prototypeHTMLClone.New(overlay.name) + templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name) err error ) @@ -953,27 +956,37 @@ func (t *templateHandler) postTransform() error { return nil } +type prototypeCloneID uint16 + +const ( + prototypeCloneIDBaseof prototypeCloneID = iota + 1 + prototypeCloneIDDefer +) + type templateNamespace struct { - prototypeText *texttemplate.Template - prototypeHTML *htmltemplate.Template - prototypeTextClone *texttemplate.Template - prototypeHTMLClone *htmltemplate.Template + prototypeText *texttemplate.Template + prototypeHTML *htmltemplate.Template + + prototypeHTMLCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template] + prototypeTextCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template] *templateStateMap } -func (t *templateNamespace) getPrototypeText() *texttemplate.Template { - if t.prototypeTextClone != nil { - return t.prototypeTextClone +func (t *templateNamespace) getPrototypeText(id prototypeCloneID) *texttemplate.Template { + v, ok := t.prototypeTextCloneCache.Get(id) + if !ok { + return t.prototypeText } - return t.prototypeText + return v } -func (t *templateNamespace) getPrototypeHTML() *htmltemplate.Template { - if t.prototypeHTMLClone != nil { - return t.prototypeHTMLClone +func (t *templateNamespace) getPrototypeHTML(id prototypeCloneID) *htmltemplate.Template { + v, ok := t.prototypeHTMLCloneCache.Get(id) + if !ok { + return t.prototypeHTML } - return t.prototypeHTML + return v } func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) { @@ -989,9 +1002,10 @@ func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) { } func (t *templateNamespace) createPrototypes() error { - t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone()) - t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone()) - + for _, id := range []prototypeCloneID{prototypeCloneIDBaseof, prototypeCloneIDDefer} { + t.prototypeHTMLCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone())) + t.prototypeTextCloneCache.Set(id, texttemplate.Must(t.prototypeText.Clone())) + } return nil } @@ -1021,7 +1035,7 @@ func (t *templateNamespace) addDeferredTemplate(owner *templateState, name strin var templ tpl.Template if owner.isText() { - prototype := t.getPrototypeText() + prototype := t.getPrototypeText(prototypeCloneIDDefer) tt, err := prototype.New(name).Parse("") if err != nil { return fmt.Errorf("failed to parse empty text template %q: %w", name, err) @@ -1029,7 +1043,7 @@ func (t *templateNamespace) addDeferredTemplate(owner *templateState, name strin tt.Tree.Root = n templ = tt } else { - prototype := t.getPrototypeHTML() + prototype := t.getPrototypeHTML(prototypeCloneIDDefer) tt, err := prototype.New(name).Parse("") if err != nil { return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err) diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go index 1e7aa3111..dbadece4e 100644 --- a/tpl/tplimpl/tplimpl_integration_test.go +++ b/tpl/tplimpl/tplimpl_integration_test.go @@ -649,3 +649,52 @@ E: An _emphasized_ word. "
\n Details\n

D: An emphasized word.

\n
", ) } + +// Issue 12963 +func TestEditBaseofParseAfterExecute(t *testing.T) { + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableLiveReload = true +disableKinds = ["taxonomy", "term", "rss", "404", "sitemap"] +[internal] +fastRenderMode = true +-- layouts/_default/baseof.html -- +Baseof! +{{ block "main" . }}default{{ end }} +{{ with (templates.Defer (dict "key" "global")) }} +Now. {{ now }} +{{ end }} +-- layouts/_default/single.html -- +{{ define "main" }} +Single. +{{ end }} +-- layouts/_default/list.html -- +{{ define "main" }} +List. +{{ .Content }} +{{ range .Pages }}{{ .Title }}{{ end }}| +{{ end }} +-- content/mybundle1/index.md -- +--- +title: "My Bundle 1" +--- +-- content/mybundle2/index.md -- +--- +title: "My Bundle 2" +--- +-- content/_index.md -- +--- +title: "Home" +--- +Home! +` + + b := hugolib.TestRunning(t, files) + b.AssertFileContent("public/index.html", "Home!") + b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof!").Build() + b.BuildPartial("/") + b.AssertFileContent("public/index.html", "Baseof!!") + b.BuildPartial("/mybundle1/") + b.AssertFileContent("public/mybundle1/index.html", "Baseof!!") +} -- cgit v1.2.3