package config
import (
+ "os"
"path/filepath"
"strings"
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/paths"
+
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
return m, nil
}
+func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provider, []string, error) {
+ defaultConfigDir := filepath.Join(configDir, "_default")
+ environmentConfigDir := filepath.Join(configDir, environment)
+ cfg := New()
+
+ var configDirs []string
+ // Merge from least to most specific.
+ for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
+ if _, err := sourceFs.Stat(dir); err == nil {
+ configDirs = append(configDirs, dir)
+ }
+ }
+
+ if len(configDirs) == 0 {
+ return nil, nil, nil
+ }
+
+ // Keep track of these so we can watch them for changes.
+ var dirnames []string
+
+ for _, configDir := range configDirs {
+ err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
+ if fi == nil || err != nil {
+ return nil
+ }
+
+ if fi.IsDir() {
+ dirnames = append(dirnames, path)
+ return nil
+ }
+
+ if !IsValidConfigFilename(path) {
+ return nil
+ }
+
+ name := paths.Filename(filepath.Base(path))
+
+ item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path)
+ if err != nil {
+ // This will be used in error reporting, use the most specific value.
+ dirnames = []string{path}
+ return errors.Wrapf(err, "failed to unmarshl config for path %q", path)
+ }
+
+ var keyPath []string
+
+ if name != "config" {
+ // Can be params.jp, menus.en etc.
+ name, lang := paths.FileAndExtNoDelimiter(name)
+
+ keyPath = []string{name}
+
+ if lang != "" {
+ keyPath = []string{"languages", lang}
+ switch name {
+ case "menu", "menus":
+ keyPath = append(keyPath, "menus")
+ case "params":
+ keyPath = append(keyPath, "params")
+ }
+ }
+ }
+
+ root := item
+ if len(keyPath) > 0 {
+ root = make(map[string]interface{})
+ m := root
+ for i, key := range keyPath {
+ if i >= len(keyPath)-1 {
+ m[key] = item
+ } else {
+ nm := make(map[string]interface{})
+ m[key] = nm
+ m = nm
+ }
+ }
+ }
+
+ // Migrate menu => menus etc.
+ RenameKeys(root)
+
+ // Set will overwrite keys with the same name, recursively.
+ cfg.Set("", root)
+
+ return nil
+ })
+ if err != nil {
+ return nil, dirnames, err
+ }
+
+ }
+
+ return cfg, dirnames, nil
+
+}
+
var keyAliases maps.KeyRenamer
func init() {
}
if d.AbsConfigDir != "" {
- dirnames, err := l.loadConfigFromConfigDir()
+ dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, d.AbsConfigDir, l.Environment)
if err == nil {
- configFiles = append(configFiles, dirnames...)
+ if len(dirnames) > 0 {
+ l.cfg.Set("", dcfg.Get(""))
+ configFiles = append(configFiles, dirnames...)
+ }
} else if err != ErrNoConfigFile {
+ if len(dirnames) > 0 {
+ return nil, nil, l.wrapFileError(err, dirnames[0])
+ }
return nil, nil, err
}
}
hook := func(m *modules.ModulesConfig) error {
for _, tc := range m.ActiveModules {
- if tc.ConfigFilename() != "" {
+ if len(tc.ConfigFilenames()) > 0 {
if tc.Watch() {
- configFilenames = append(configFilenames, tc.ConfigFilename())
+ configFilenames = append(configFilenames, tc.ConfigFilenames()...)
}
// Merge from theme config into v1 based on configured
HookBeforeFinalize: hook,
WorkingDir: workingDir,
ThemesDir: themesDir,
+ Environment: l.Environment,
CacheDir: filecacheConfigs.CacheDirModules(),
ModuleConfig: modConfig,
IgnoreVendor: ignoreVendor,
return filename, nil
}
-func (l configLoader) loadConfigFromConfigDir() ([]string, error) {
- sourceFs := l.Fs
- configDir := l.AbsConfigDir
-
- if _, err := sourceFs.Stat(configDir); err != nil {
- // Config dir does not exist.
- return nil, nil
- }
-
- defaultConfigDir := filepath.Join(configDir, "_default")
- environmentConfigDir := filepath.Join(configDir, l.Environment)
-
- var configDirs []string
- // Merge from least to most specific.
- for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
- if _, err := sourceFs.Stat(dir); err == nil {
- configDirs = append(configDirs, dir)
- }
- }
-
- if len(configDirs) == 0 {
- return nil, nil
- }
-
- // Keep track of these so we can watch them for changes.
- var dirnames []string
-
- for _, configDir := range configDirs {
- err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
- if fi == nil || err != nil {
- return nil
- }
-
- if fi.IsDir() {
- dirnames = append(dirnames, path)
- return nil
- }
-
- if !config.IsValidConfigFilename(path) {
- return nil
- }
-
- name := cpaths.Filename(filepath.Base(path))
-
- item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path)
- if err != nil {
- return l.wrapFileError(err, path)
- }
-
- var keyPath []string
-
- if name != "config" {
- // Can be params.jp, menus.en etc.
- name, lang := cpaths.FileAndExtNoDelimiter(name)
-
- keyPath = []string{name}
-
- if lang != "" {
- keyPath = []string{"languages", lang}
- switch name {
- case "menu", "menus":
- keyPath = append(keyPath, "menus")
- case "params":
- keyPath = append(keyPath, "params")
- }
- }
- }
-
- root := item
- if len(keyPath) > 0 {
- root = make(map[string]interface{})
- m := root
- for i, key := range keyPath {
- if i >= len(keyPath)-1 {
- m[key] = item
- } else {
- nm := make(map[string]interface{})
- m[key] = nm
- m = nm
- }
- }
- }
-
- // Migrate menu => menus etc.
- config.RenameKeys(root)
-
- // Set will overwrite keys with the same name, recursively.
- l.cfg.Set("", root)
-
- return nil
- })
- if err != nil {
- return nil, err
- }
-
- }
-
- return dirnames, nil
-}
-
func (l configLoader) loadLanguageSettings(oldLangs langs.Languages) error {
_, err := langs.LoadLanguageSettings(l.cfg, oldLangs)
return err
}
+func TestLoadConfigFromThemeDir(t *testing.T) {
+ t.Parallel()
+
+ mainConfig := `
+theme = "test-theme"
+
+[params]
+m1 = "mv1"
+`
+
+ themeConfig := `
+[params]
+t1 = "tv1"
+t2 = "tv2"
+`
+
+ themeConfigDir := filepath.Join("themes", "test-theme", "config")
+ themeConfigDirDefault := filepath.Join(themeConfigDir, "_default")
+ themeConfigDirProduction := filepath.Join(themeConfigDir, "production")
+
+ projectConfigDir := "config"
+
+ b := newTestSitesBuilder(t)
+ b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
+ b.Assert(b.Fs.Source.MkdirAll(themeConfigDirDefault, 0777), qt.IsNil)
+ b.Assert(b.Fs.Source.MkdirAll(themeConfigDirProduction, 0777), qt.IsNil)
+ b.Assert(b.Fs.Source.MkdirAll(projectConfigDir, 0777), qt.IsNil)
+
+ b.WithSourceFile(filepath.Join(projectConfigDir, "config.toml"), `[params]
+m2 = "mv2"
+`)
+ b.WithSourceFile(filepath.Join(themeConfigDirDefault, "config.toml"), `[params]
+t2 = "tv2d"
+t3 = "tv3d"
+`)
+
+ b.WithSourceFile(filepath.Join(themeConfigDirProduction, "config.toml"), `[params]
+t3 = "tv3p"
+`)
+
+ b.Build(BuildCfg{})
+
+ got := b.Cfg.Get("params").(maps.Params)
+
+ b.Assert(got, qt.DeepEquals, maps.Params{
+ "t3": "tv3p",
+ "m1": "mv1",
+ "t1": "tv1",
+ "t2": "tv2d",
+ })
+
+}
+
func TestPrivacyConfig(t *testing.T) {
t.Parallel()
// Absolute path to the project's themes dir.
ThemesDir string
+ // Eg. "production"
+ Environment string
+
CacheDir string // Module cache
ModuleConfig Config
}
func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
var (
configFilename string
- cfg config.Provider
themeCfg map[string]interface{}
- hasConfig bool
+ hasConfigFile bool
err error
)
// Viper supports more, but this is the sub-set supported by Hugo.
for _, configFormats := range config.ValidConfigFileExtensions {
configFilename = filepath.Join(tc.Dir(), "config."+configFormats)
- hasConfig, _ = afero.Exists(c.fs, configFilename)
- if hasConfig {
+ hasConfigFile, _ = afero.Exists(c.fs, configFilename)
+ if hasConfigFile {
break
}
}
}
}
- if hasConfig {
+ if hasConfigFile {
if configFilename != "" {
var err error
- cfg, err = config.FromFile(c.fs, configFilename)
+ tc.cfg, err = config.FromFile(c.fs, configFilename)
if err != nil {
return errors.Wrapf(err, "failed to read module config for %q in %q", tc.Path(), configFilename)
}
}
- tc.configFilename = configFilename
- tc.cfg = cfg
+ tc.configFilenames = append(tc.configFilenames, configFilename)
+
+ }
+
+ // Also check for a config dir, which we overlay on top of the file configuration.
+ configDir := filepath.Join(tc.Dir(), "config")
+ dcfg, dirnames, err := config.LoadConfigFromDir(c.fs, configDir, c.ccfg.Environment)
+ if err != nil {
+ return err
+ }
+
+ if len(dirnames) > 0 {
+ tc.configFilenames = append(tc.configFilenames, dirnames...)
+
+ if hasConfigFile {
+ // Set will overwrite existing keys.
+ tc.cfg.Set("", dcfg.Get(""))
+ } else {
+ tc.cfg = dcfg
+ }
}
- config, err := decodeConfig(cfg, c.moduleConfig.replacementsMap)
+ config, err := decodeConfig(tc.cfg, c.moduleConfig.replacementsMap)
if err != nil {
return err
}
// The decoded module config and mounts.
Config() Config
- // Optional configuration filename (e.g. "/themes/mytheme/config.json").
+ // Optional configuration filenames (e.g. "/themes/mytheme/config.json").
// This will be added to the special configuration watch list when in
// server mode.
- ConfigFilename() string
+ ConfigFilenames() []string
// Directory holding files for this module.
Dir() string
mounts []Mount
- configFilename string
- cfg config.Provider
- config Config
+ configFilenames []string
+ cfg config.Provider
+ config Config
// Set if a Go module.
gomod *goModule
return m.config
}
-func (m *moduleAdapter) ConfigFilename() string {
- return m.configFilename
+func (m *moduleAdapter) ConfigFilenames() []string {
+ return m.configFilenames
}
func (m *moduleAdapter) Dir() string {