Skip to content

Commit

Permalink
Fix panic on server rebuilds when using both base templates and templ…
Browse files Browse the repository at this point in the history
…ate.Defer

Fixes gohugoio#12963
  • Loading branch information
bep committed Dec 16, 2024
1 parent 565c30e commit a5e5be2
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 65 deletions.
3 changes: 3 additions & 0 deletions common/types/evictingqueue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
27 changes: 23 additions & 4 deletions hugolib/integrationtest_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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...)
Expand Down
40 changes: 0 additions & 40 deletions internal/js/esbuild/batch_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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..")
}
}
56 changes: 35 additions & 21 deletions tpl/tplimpl/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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),
},
Expand Down Expand Up @@ -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
)

Expand All @@ -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
)

Expand Down Expand Up @@ -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) {
Expand All @@ -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
}

Expand Down Expand Up @@ -1021,15 +1035,15 @@ 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)
}
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)
Expand Down
49 changes: 49 additions & 0 deletions tpl/tplimpl/tplimpl_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,52 @@ E: An _emphasized_ word.
"<details>\n <summary>Details</summary>\n <p>D: An <em>emphasized</em> word.</p>\n</details>",
)
}

// 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!!")
}

0 comments on commit a5e5be2

Please sign in to comment.