Fix panic on server rebuilds when using both base templates and template.Defer
Fixes #12963
This commit is contained in:
parent
565c30eac9
commit
a5e5be234c
5 changed files with 110 additions and 65 deletions
|
@ -65,6 +65,9 @@ func (q *EvictingStringQueue) Len() int {
|
||||||
|
|
||||||
// Contains returns whether the queue contains v.
|
// Contains returns whether the queue contains v.
|
||||||
func (q *EvictingStringQueue) Contains(v string) bool {
|
func (q *EvictingStringQueue) Contains(v string) bool {
|
||||||
|
if q == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
q.mu.Lock()
|
q.mu.Lock()
|
||||||
defer q.mu.Unlock()
|
defer q.mu.Unlock()
|
||||||
return q.set[v]
|
return q.set[v]
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/gohugoio/hugo/common/hexec"
|
"github.com/gohugoio/hugo/common/hexec"
|
||||||
"github.com/gohugoio/hugo/common/loggers"
|
"github.com/gohugoio/hugo/common/loggers"
|
||||||
"github.com/gohugoio/hugo/common/maps"
|
"github.com/gohugoio/hugo/common/maps"
|
||||||
|
"github.com/gohugoio/hugo/common/types"
|
||||||
"github.com/gohugoio/hugo/config"
|
"github.com/gohugoio/hugo/config"
|
||||||
"github.com/gohugoio/hugo/config/allconfig"
|
"github.com/gohugoio/hugo/config/allconfig"
|
||||||
"github.com/gohugoio/hugo/config/security"
|
"github.com/gohugoio/hugo/config/security"
|
||||||
|
@ -466,6 +467,28 @@ func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
|
||||||
return s
|
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() {
|
func (s *IntegrationTestBuilder) Close() {
|
||||||
s.Helper()
|
s.Helper()
|
||||||
s.Assert(s.H.Close(), qt.IsNil)
|
s.Assert(s.H.Close(), qt.IsNil)
|
||||||
|
@ -747,10 +770,6 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
|
||||||
s.counters = &buildCounters{}
|
s.counters = &buildCounters{}
|
||||||
cfg.testCounters = s.counters
|
cfg.testCounters = s.counters
|
||||||
|
|
||||||
if s.buildCount > 0 && (len(changeEvents) == 0) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s.buildCount++
|
s.buildCount++
|
||||||
|
|
||||||
err := s.H.Build(cfg, changeEvents...)
|
err := s.H.Build(cfg, changeEvents...)
|
||||||
|
|
|
@ -721,43 +721,3 @@ console.log("config.params.id", id3);
|
||||||
b.EditFileReplaceAll("assets/other/bar.css", ".bar-edit {", ".bar-edit2 {").Build()
|
b.EditFileReplaceAll("assets/other/bar.css", ".bar-edit {", ".bar-edit2 {").Build()
|
||||||
b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit2 {")
|
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..")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/common/maps"
|
||||||
"github.com/gohugoio/hugo/common/types"
|
"github.com/gohugoio/hugo/common/types"
|
||||||
"github.com/gohugoio/hugo/output/layouts"
|
"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 {
|
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
|
||||||
return &templateNamespace{
|
return &templateNamespace{
|
||||||
prototypeHTML: htmltemplate.New("").Funcs(funcs),
|
prototypeHTML: htmltemplate.New("").Funcs(funcs),
|
||||||
prototypeText: texttemplate.New("").Funcs(funcs),
|
prototypeText: texttemplate.New("").Funcs(funcs),
|
||||||
|
prototypeHTMLCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](),
|
||||||
|
prototypeTextCloneCache: maps.NewCache[prototypeCloneID, *texttemplate.Template](),
|
||||||
templateStateMap: &templateStateMap{
|
templateStateMap: &templateStateMap{
|
||||||
templates: make(map[string]*templateState),
|
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) {
|
func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
|
||||||
if overlay.isText {
|
if overlay.isText {
|
||||||
var (
|
var (
|
||||||
templ = t.main.prototypeTextClone.New(overlay.name)
|
templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -713,7 +716,7 @@ func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Tem
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
templ = t.main.prototypeHTMLClone.New(overlay.name)
|
templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -953,27 +956,37 @@ func (t *templateHandler) postTransform() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type prototypeCloneID uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
prototypeCloneIDBaseof prototypeCloneID = iota + 1
|
||||||
|
prototypeCloneIDDefer
|
||||||
|
)
|
||||||
|
|
||||||
type templateNamespace struct {
|
type templateNamespace struct {
|
||||||
prototypeText *texttemplate.Template
|
prototypeText *texttemplate.Template
|
||||||
prototypeHTML *htmltemplate.Template
|
prototypeHTML *htmltemplate.Template
|
||||||
prototypeTextClone *texttemplate.Template
|
|
||||||
prototypeHTMLClone *htmltemplate.Template
|
prototypeHTMLCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template]
|
||||||
|
prototypeTextCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template]
|
||||||
|
|
||||||
*templateStateMap
|
*templateStateMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateNamespace) getPrototypeText() *texttemplate.Template {
|
func (t *templateNamespace) getPrototypeText(id prototypeCloneID) *texttemplate.Template {
|
||||||
if t.prototypeTextClone != nil {
|
v, ok := t.prototypeTextCloneCache.Get(id)
|
||||||
return t.prototypeTextClone
|
if !ok {
|
||||||
|
return t.prototypeText
|
||||||
}
|
}
|
||||||
return t.prototypeText
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateNamespace) getPrototypeHTML() *htmltemplate.Template {
|
func (t *templateNamespace) getPrototypeHTML(id prototypeCloneID) *htmltemplate.Template {
|
||||||
if t.prototypeHTMLClone != nil {
|
v, ok := t.prototypeHTMLCloneCache.Get(id)
|
||||||
return t.prototypeHTMLClone
|
if !ok {
|
||||||
|
return t.prototypeHTML
|
||||||
}
|
}
|
||||||
return t.prototypeHTML
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
|
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 {
|
func (t *templateNamespace) createPrototypes() error {
|
||||||
t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone())
|
for _, id := range []prototypeCloneID{prototypeCloneIDBaseof, prototypeCloneIDDefer} {
|
||||||
t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone())
|
t.prototypeHTMLCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone()))
|
||||||
|
t.prototypeTextCloneCache.Set(id, texttemplate.Must(t.prototypeText.Clone()))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1021,7 +1035,7 @@ func (t *templateNamespace) addDeferredTemplate(owner *templateState, name strin
|
||||||
var templ tpl.Template
|
var templ tpl.Template
|
||||||
|
|
||||||
if owner.isText() {
|
if owner.isText() {
|
||||||
prototype := t.getPrototypeText()
|
prototype := t.getPrototypeText(prototypeCloneIDDefer)
|
||||||
tt, err := prototype.New(name).Parse("")
|
tt, err := prototype.New(name).Parse("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
|
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
|
tt.Tree.Root = n
|
||||||
templ = tt
|
templ = tt
|
||||||
} else {
|
} else {
|
||||||
prototype := t.getPrototypeHTML()
|
prototype := t.getPrototypeHTML(prototypeCloneIDDefer)
|
||||||
tt, err := prototype.New(name).Parse("")
|
tt, err := prototype.New(name).Parse("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
|
return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
|
||||||
|
|
|
@ -649,3 +649,52 @@ E: An _emphasized_ word.
|
||||||
"<details>\n <summary>Details</summary>\n <p>D: An <em>emphasized</em> word.</p>\n</details>",
|
"<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!!")
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue