diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-05-21 14:25:16 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-05-22 14:14:35 +0200 |
commit | 2c3d4dfb745799b5de11f9ec0463a4ace19e97de (patch) | |
tree | 22f8dfe5b6f0bd39d66757119c2ea2ce5f83743d /config | |
parent | 1292d5a26af55ffd22512a01ae3a82c769e9bb01 (diff) |
Add cache busting config to support Tailwind 3
Fixes #10974
Diffstat (limited to 'config')
-rw-r--r-- | config/allconfig/allconfig.go | 15 | ||||
-rw-r--r-- | config/allconfig/alldecoders.go | 3 | ||||
-rw-r--r-- | config/allconfig/load.go | 8 | ||||
-rw-r--r-- | config/commonConfig.go | 137 | ||||
-rw-r--r-- | config/commonConfig_test.go | 27 |
5 files changed, 175 insertions, 15 deletions
diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 4886aa561..ade7ea1be 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -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 diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go index d7adb6e28..c6faf571d 100644 --- a/config/allconfig/alldecoders.go +++ b/config/allconfig/alldecoders.go @@ -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", diff --git a/config/allconfig/load.go b/config/allconfig/load.go index 6ae26d28e..ad090d60d 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -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) } diff --git a/config/commonConfig.go b/config/commonConfig.go index 8cac2e1e5..bd3e235bd 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -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{} diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go index f05664448..106069bdc 100644 --- a/config/commonConfig_test.go +++ b/config/commonConfig_test.go @@ -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) + +} |