mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-30 22:39:58 +02:00
@@ -27,6 +27,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/cache/filecache"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/urls"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
@@ -184,7 +185,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
type configCompiler interface {
|
||||
CompileConfig() error
|
||||
CompileConfig(logger loggers.Logger) error
|
||||
}
|
||||
|
||||
func (c Config) cloneForLang() *Config {
|
||||
@@ -209,7 +210,7 @@ func (c Config) cloneForLang() *Config {
|
||||
return &x
|
||||
}
|
||||
|
||||
func (c *Config) CompileConfig() error {
|
||||
func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
var transientErr error
|
||||
s := c.Timeout
|
||||
if _, err := strconv.Atoi(s); err == nil {
|
||||
@@ -328,7 +329,7 @@ func (c *Config) CompileConfig() error {
|
||||
|
||||
for _, s := range allDecoderSetups {
|
||||
if getCompiler := s.getCompiler; getCompiler != nil {
|
||||
if err := getCompiler(c).CompileConfig(); err != nil {
|
||||
if err := getCompiler(c).CompileConfig(logger); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -668,8 +669,8 @@ func (c Configs) GetByLang(lang string) config.AllProvider {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromLoadConfigResult creates a new Config from res.
|
||||
func FromLoadConfigResult(fs afero.Fs, res config.LoadConfigResult) (*Configs, error) {
|
||||
// fromLoadConfigResult creates a new Config from res.
|
||||
func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadConfigResult) (*Configs, error) {
|
||||
if !res.Cfg.IsSet("languages") {
|
||||
// We need at least one
|
||||
lang := res.Cfg.GetString("defaultContentLanguage")
|
||||
@@ -690,7 +691,7 @@ func FromLoadConfigResult(fs afero.Fs, res config.LoadConfigResult) (*Configs, e
|
||||
languagesConfig := cfg.GetStringMap("languages")
|
||||
var isMultiHost bool
|
||||
|
||||
if err := all.CompileConfig(); err != nil {
|
||||
if err := all.CompileConfig(logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -769,7 +770,7 @@ func FromLoadConfigResult(fs afero.Fs, res config.LoadConfigResult) (*Configs, e
|
||||
if err := decodeConfigFromParams(fs, bcfg, mergedConfig, clone, differentRootKeys); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode config for language %q: %w", k, err)
|
||||
}
|
||||
if err := clone.CompileConfig(); err != nil {
|
||||
if err := clone.CompileConfig(logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
langConfigMap[k] = clone
|
||||
|
@@ -92,6 +92,9 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
p.c.Build = config.DecodeBuildConfig(p.p)
|
||||
return nil
|
||||
},
|
||||
getCompiler: func(c *Config) configCompiler {
|
||||
return &c.Build
|
||||
},
|
||||
},
|
||||
"frontmatter": {
|
||||
key: "frontmatter",
|
||||
|
@@ -44,6 +44,10 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
d.Environ = os.Environ()
|
||||
}
|
||||
|
||||
if d.Logger == nil {
|
||||
d.Logger = loggers.NewErrorLogger()
|
||||
}
|
||||
|
||||
l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()}
|
||||
// Make sure we always do this, even in error situations,
|
||||
// as we have commands (e.g. "hugo mod init") that will
|
||||
@@ -54,7 +58,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
configs, err := FromLoadConfigResult(d.Fs, res)
|
||||
configs, err := fromLoadConfigResult(d.Fs, d.Logger, res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create config from result: %w", err)
|
||||
}
|
||||
@@ -67,7 +71,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
|
||||
if len(l.ModulesConfigFiles) > 0 {
|
||||
// Config merged in from modules.
|
||||
// Re-read the config.
|
||||
configs, err = FromLoadConfigResult(d.Fs, res)
|
||||
configs, err = fromLoadConfigResult(d.Fs, d.Logger, res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create config from modules config: %w", err)
|
||||
}
|
||||
|
@@ -15,12 +15,14 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
@@ -77,9 +79,29 @@ type LoadConfigResult struct {
|
||||
BaseConfig BaseConfig
|
||||
}
|
||||
|
||||
var DefaultBuild = BuildConfig{
|
||||
var defaultBuild = BuildConfig{
|
||||
UseResourceCacheWhen: "fallback",
|
||||
WriteStats: false,
|
||||
|
||||
CacheBusters: []CacheBuster{
|
||||
{
|
||||
Source: `assets/.*\.(js|ts|jsx|tsx)`,
|
||||
Target: `(js|scripts|javascript)`,
|
||||
},
|
||||
{
|
||||
Source: `assets/.*\.(css|sass|scss)$`,
|
||||
Target: cssTargetCachebusterRe,
|
||||
},
|
||||
{
|
||||
Source: `(postcss|tailwind)\.config\.js`,
|
||||
Target: cssTargetCachebusterRe,
|
||||
},
|
||||
// This is deliberatly coarse grained; it will cache bust resources with "json" in the cache key when js files changes, which is good.
|
||||
{
|
||||
Source: `assets/.*\.(.*)$`,
|
||||
Target: `$1`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// BuildConfig holds some build related configuration.
|
||||
@@ -93,6 +115,14 @@ type BuildConfig struct {
|
||||
// Can be used to toggle off writing of the intellinsense /assets/jsconfig.js
|
||||
// file.
|
||||
NoJSConfigInAssets bool
|
||||
|
||||
// Can used to control how the resource cache gets evicted on rebuilds.
|
||||
CacheBusters []CacheBuster
|
||||
}
|
||||
|
||||
func (b BuildConfig) clone() BuildConfig {
|
||||
b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b BuildConfig) UseResourceCache(err error) bool {
|
||||
@@ -107,16 +137,47 @@ func (b BuildConfig) UseResourceCache(err error) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MatchCacheBuster returns the cache buster for the given path p, nil if none.
|
||||
func (s BuildConfig) MatchCacheBuster(logger loggers.Logger, p string) (func(string) bool, error) {
|
||||
var matchers []func(string) bool
|
||||
for _, cb := range s.CacheBusters {
|
||||
if matcher := cb.compiledSource(p); matcher != nil {
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
}
|
||||
if len(matchers) > 0 {
|
||||
return (func(cacheKey string) bool {
|
||||
for _, m := range matchers {
|
||||
if m(cacheKey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *BuildConfig) CompileConfig(logger loggers.Logger) error {
|
||||
for i, cb := range b.CacheBusters {
|
||||
if err := cb.CompileConfig(logger); err != nil {
|
||||
return fmt.Errorf("failed to compile cache buster %q: %w", cb.Source, err)
|
||||
}
|
||||
b.CacheBusters[i] = cb
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DecodeBuildConfig(cfg Provider) BuildConfig {
|
||||
m := cfg.GetStringMap("build")
|
||||
b := DefaultBuild
|
||||
b := defaultBuild.clone()
|
||||
if m == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
err := mapstructure.WeakDecode(m, &b)
|
||||
if err != nil {
|
||||
return DefaultBuild
|
||||
return defaultBuild
|
||||
}
|
||||
|
||||
b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen)
|
||||
@@ -152,7 +213,7 @@ type Server struct {
|
||||
compiledRedirects []glob.Glob
|
||||
}
|
||||
|
||||
func (s *Server) CompileConfig() error {
|
||||
func (s *Server) CompileConfig(logger loggers.Logger) error {
|
||||
if s.compiledHeaders != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -162,6 +223,7 @@ func (s *Server) CompileConfig() error {
|
||||
for _, r := range s.Redirects {
|
||||
s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -228,10 +290,75 @@ type Redirect struct {
|
||||
Force bool
|
||||
}
|
||||
|
||||
// CacheBuster configures cache busting for assets.
|
||||
type CacheBuster struct {
|
||||
// Trigger for files matching this regexp.
|
||||
Source string
|
||||
|
||||
// Cache bust targets matching this regexp.
|
||||
// This regexp can contain group matches (e.g. $1) from the source regexp.
|
||||
Target string
|
||||
|
||||
compiledSource func(string) func(string) bool
|
||||
}
|
||||
|
||||
func (c *CacheBuster) CompileConfig(logger loggers.Logger) error {
|
||||
if c.compiledSource != nil {
|
||||
return nil
|
||||
}
|
||||
source := c.Source
|
||||
target := c.Target
|
||||
sourceRe, err := regexp.Compile(source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile cache buster source %q: %w", c.Source, err)
|
||||
}
|
||||
var compileErr error
|
||||
c.compiledSource = func(s string) func(string) bool {
|
||||
m := sourceRe.FindStringSubmatch(s)
|
||||
matchString := "no match"
|
||||
match := m != nil
|
||||
if match {
|
||||
matchString = "match!"
|
||||
}
|
||||
logger.Debugf("cachebuster: Matching %q with source %q: %s\n", s, source, matchString)
|
||||
if !match {
|
||||
return nil
|
||||
}
|
||||
groups := m[1:]
|
||||
// Replace $1, $2 etc. in target.
|
||||
|
||||
for i, g := range groups {
|
||||
target = strings.ReplaceAll(target, fmt.Sprintf("$%d", i+1), g)
|
||||
}
|
||||
targetRe, err := regexp.Compile(target)
|
||||
if err != nil {
|
||||
compileErr = fmt.Errorf("failed to compile cache buster target %q: %w", target, err)
|
||||
return nil
|
||||
}
|
||||
return func(s string) bool {
|
||||
match = targetRe.MatchString(s)
|
||||
matchString := "no match"
|
||||
if match {
|
||||
matchString = "match!"
|
||||
}
|
||||
logger.Debugf("cachebuster: Matching %q with target %q: %s\n", s, target, matchString)
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
}
|
||||
return compileErr
|
||||
}
|
||||
|
||||
func (r Redirect) IsZero() bool {
|
||||
return r.From == ""
|
||||
}
|
||||
|
||||
const (
|
||||
// Keep this a little coarse grained, some false positives are OK.
|
||||
cssTargetCachebusterRe = `(css|styles|scss|sass)`
|
||||
)
|
||||
|
||||
func DecodeServer(cfg Provider) (Server, error) {
|
||||
s := &Server{}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -91,7 +92,7 @@ status = 301
|
||||
|
||||
s, err := DecodeServer(cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(s.CompileConfig(), qt.IsNil)
|
||||
c.Assert(s.CompileConfig(loggers.NewErrorLogger()), qt.IsNil)
|
||||
|
||||
c.Assert(s.MatchHeaders("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{
|
||||
{Key: "X-Content-Type-Options", Value: "nosniff"},
|
||||
@@ -139,3 +140,27 @@ status = 301`,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigCacheBusters(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
cfg := New()
|
||||
conf := DecodeBuildConfig(cfg)
|
||||
l := loggers.NewInfoLogger()
|
||||
c.Assert(conf.CompileConfig(l), qt.IsNil)
|
||||
|
||||
m, err := conf.MatchCacheBuster(l, "assets/foo/main.js")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(m, qt.IsNotNil)
|
||||
c.Assert(m("scripts"), qt.IsTrue)
|
||||
c.Assert(m("asdf"), qt.IsFalse)
|
||||
|
||||
m, _ = conf.MatchCacheBuster(l, "tailwind.config.js")
|
||||
c.Assert(m("css"), qt.IsTrue)
|
||||
c.Assert(m("js"), qt.IsFalse)
|
||||
|
||||
m, err = conf.MatchCacheBuster(l, "assets/foo.json")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(m, qt.IsNotNil)
|
||||
c.Assert(m("json"), qt.IsTrue)
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user