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 #12963
  • Loading branch information
bep committed Dec 16, 2024
1 parent 565c30e commit 5e9821f
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),
prototypeHTMLTemplCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](),
prototypeTEXTTemplCloneCache: 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.getPrototypeCloneText(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.getPrototypeCloneHTML(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

prototypeHTMLTemplCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template]
prototypeTEXTTemplCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template]

*templateStateMap
}

func (t *templateNamespace) getPrototypeText() *texttemplate.Template {
if t.prototypeTextClone != nil {
return t.prototypeTextClone
func (t *templateNamespace) getPrototypeCloneText(id prototypeCloneID) *texttemplate.Template {
v, ok := t.prototypeTEXTTemplCloneCache.Get(id)
if !ok {
panic("no prototype clone found")
}
return t.prototypeText
return v
}

func (t *templateNamespace) getPrototypeHTML() *htmltemplate.Template {
if t.prototypeHTMLClone != nil {
return t.prototypeHTMLClone
func (t *templateNamespace) getPrototypeCloneHTML(id prototypeCloneID) *htmltemplate.Template {
v, ok := t.prototypeHTMLTemplCloneCache.Get(id)
if !ok {
panic("no prototype clone found")
}
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.prototypeHTMLTemplCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone()))
t.prototypeTEXTTemplCloneCache.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.getPrototypeCloneText(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.getPrototypeCloneHTML(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 5e9821f

Please sign in to comment.