mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-21 21:35:28 +02:00
Allow themes to define output formats, media types and params
This allows a `config.toml` (or `yaml`, ´yml`, or `json`) in the theme to set: 1) `params` (but cannot override params in project. Will also get its own "namespace", i.e. `{{ .Site.Params.mytheme.my_param }}` will be the same as `{{ .Site.Params.my_param }}` providing that the main project does not define a param with that key. 2) `menu` -- but cannot redefine/add menus in the project. Must create its own menus with its own identifiers. 3) `languages` -- only `params` and `menu`. Same rules as above. 4) **new** `outputFormats` 5) **new** `mediaTypes` This should help with the "theme portability" issue and people having to copy and paste lots of setting into their projects. Fixes #4490
This commit is contained in:
@@ -14,6 +14,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gohugoio/hugo/utils"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
@@ -23,11 +35,22 @@ import (
|
||||
|
||||
type commandeer struct {
|
||||
*deps.DepsCfg
|
||||
|
||||
subCmdVs []*cobra.Command
|
||||
|
||||
pathSpec *helpers.PathSpec
|
||||
visitedURLs *types.EvictingStringQueue
|
||||
|
||||
staticDirsConfig []*src.Dirs
|
||||
|
||||
// We watch these for changes.
|
||||
configFiles []string
|
||||
|
||||
doWithCommandeer func(c *commandeer) error
|
||||
|
||||
// We can do this only once.
|
||||
fsCreate sync.Once
|
||||
|
||||
serverPorts []int
|
||||
languages helpers.Languages
|
||||
|
||||
@@ -65,16 +88,158 @@ func (c *commandeer) initFs(fs *hugofs.Fs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCommandeer(cfg *deps.DepsCfg, running bool) (*commandeer, error) {
|
||||
cfg.Running = running
|
||||
func newCommandeer(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
|
||||
var languages helpers.Languages
|
||||
c := &commandeer{
|
||||
doWithCommandeer: doWithCommandeer,
|
||||
subCmdVs: append([]*cobra.Command{hugoCmdV}, subCmdVs...),
|
||||
visitedURLs: types.NewEvictingStringQueue(10)}
|
||||
|
||||
if l, ok := cfg.Cfg.Get("languagesSorted").(helpers.Languages); ok {
|
||||
languages = l
|
||||
return c, c.loadConfig(running)
|
||||
}
|
||||
|
||||
func (c *commandeer) loadConfig(running bool) error {
|
||||
|
||||
if c.DepsCfg == nil {
|
||||
c.DepsCfg = &deps.DepsCfg{}
|
||||
}
|
||||
|
||||
c := &commandeer{DepsCfg: cfg, languages: languages, visitedURLs: types.NewEvictingStringQueue(10)}
|
||||
cfg := c.DepsCfg
|
||||
c.configured = false
|
||||
cfg.Running = running
|
||||
|
||||
var dir string
|
||||
if source != "" {
|
||||
dir, _ = filepath.Abs(source)
|
||||
} else {
|
||||
dir, _ = os.Getwd()
|
||||
}
|
||||
|
||||
var sourceFs afero.Fs = hugofs.Os
|
||||
if c.DepsCfg.Fs != nil {
|
||||
sourceFs = c.DepsCfg.Fs.Source
|
||||
}
|
||||
|
||||
config, configFiles, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: source, WorkingDir: dir, Filename: cfgFile})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Cfg = config
|
||||
c.configFiles = configFiles
|
||||
|
||||
for _, cmdV := range c.subCmdVs {
|
||||
c.initializeFlags(cmdV)
|
||||
}
|
||||
|
||||
if l, ok := c.Cfg.Get("languagesSorted").(helpers.Languages); ok {
|
||||
c.languages = l
|
||||
}
|
||||
|
||||
if baseURL != "" {
|
||||
config.Set("baseURL", baseURL)
|
||||
}
|
||||
|
||||
if c.doWithCommandeer != nil {
|
||||
err = c.doWithCommandeer(c)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(disableKinds) > 0 {
|
||||
c.Set("disableKinds", disableKinds)
|
||||
}
|
||||
|
||||
logger, err := createLogger(cfg.Cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg.Logger = logger
|
||||
|
||||
config.Set("logI18nWarnings", logI18nWarnings)
|
||||
|
||||
if theme != "" {
|
||||
config.Set("theme", theme)
|
||||
}
|
||||
|
||||
if themesDir != "" {
|
||||
config.Set("themesDir", themesDir)
|
||||
}
|
||||
|
||||
if destination != "" {
|
||||
config.Set("publishDir", destination)
|
||||
}
|
||||
|
||||
config.Set("workingDir", dir)
|
||||
|
||||
if contentDir != "" {
|
||||
config.Set("contentDir", contentDir)
|
||||
}
|
||||
|
||||
if layoutDir != "" {
|
||||
config.Set("layoutDir", layoutDir)
|
||||
}
|
||||
|
||||
if cacheDir != "" {
|
||||
config.Set("cacheDir", cacheDir)
|
||||
}
|
||||
|
||||
createMemFs := config.GetBool("renderToMemory")
|
||||
|
||||
if createMemFs {
|
||||
// Rendering to memoryFS, publish to Root regardless of publishDir.
|
||||
config.Set("publishDir", "/")
|
||||
}
|
||||
|
||||
c.fsCreate.Do(func() {
|
||||
fs := hugofs.NewFrom(sourceFs, config)
|
||||
|
||||
// Hugo writes the output to memory instead of the disk.
|
||||
if createMemFs {
|
||||
fs.Destination = new(afero.MemMapFs)
|
||||
}
|
||||
|
||||
err = c.initFs(fs)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheDir = config.GetString("cacheDir")
|
||||
if cacheDir != "" {
|
||||
if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
|
||||
cacheDir = cacheDir + helpers.FilePathSeparator
|
||||
}
|
||||
isDir, err := helpers.DirExists(cacheDir, sourceFs)
|
||||
utils.CheckErr(cfg.Logger, err)
|
||||
if !isDir {
|
||||
mkdir(cacheDir)
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
} else {
|
||||
config.Set("cacheDir", helpers.GetTempDir("hugo_cache", sourceFs))
|
||||
}
|
||||
|
||||
cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
|
||||
|
||||
themeDir := c.PathSpec().GetThemeDir()
|
||||
if themeDir != "" {
|
||||
if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
|
||||
return newSystemError("Unable to find theme Directory:", themeDir)
|
||||
}
|
||||
}
|
||||
|
||||
themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
|
||||
|
||||
if themeVersionMismatch {
|
||||
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
|
||||
helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
173
commands/hugo.go
173
commands/hugo.go
@@ -25,8 +25,6 @@ import (
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -44,7 +42,6 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
@@ -55,7 +52,6 @@ import (
|
||||
"github.com/spf13/fsync"
|
||||
jww "github.com/spf13/jwalterweatherman"
|
||||
"github.com/spf13/nitro"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Hugo represents the Hugo sites to build. This variable is exported as it
|
||||
@@ -142,10 +138,6 @@ Complete documentation is available at http://gohugo.io/.`,
|
||||
return err
|
||||
}
|
||||
|
||||
if buildWatch {
|
||||
c.watchConfig()
|
||||
}
|
||||
|
||||
return c.build()
|
||||
},
|
||||
}
|
||||
@@ -301,129 +293,11 @@ func init() {
|
||||
// InitializeConfig initializes a config file with sensible default configuration flags.
|
||||
func InitializeConfig(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
|
||||
|
||||
var cfg *deps.DepsCfg = &deps.DepsCfg{}
|
||||
|
||||
// Init file systems. This may be changed at a later point.
|
||||
osFs := hugofs.Os
|
||||
|
||||
config, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: osFs, Src: source, Name: cfgFile})
|
||||
c, err := newCommandeer(running, doWithCommandeer, subCmdVs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Init file systems. This may be changed at a later point.
|
||||
cfg.Cfg = config
|
||||
|
||||
c, err := newCommandeer(cfg, running)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, cmdV := range append([]*cobra.Command{hugoCmdV}, subCmdVs...) {
|
||||
c.initializeFlags(cmdV)
|
||||
}
|
||||
|
||||
if baseURL != "" {
|
||||
config.Set("baseURL", baseURL)
|
||||
}
|
||||
|
||||
if doWithCommandeer != nil {
|
||||
if err := doWithCommandeer(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(disableKinds) > 0 {
|
||||
c.Set("disableKinds", disableKinds)
|
||||
}
|
||||
|
||||
logger, err := createLogger(cfg.Cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.Logger = logger
|
||||
|
||||
config.Set("logI18nWarnings", logI18nWarnings)
|
||||
|
||||
if theme != "" {
|
||||
config.Set("theme", theme)
|
||||
}
|
||||
|
||||
if themesDir != "" {
|
||||
config.Set("themesDir", themesDir)
|
||||
}
|
||||
|
||||
if destination != "" {
|
||||
config.Set("publishDir", destination)
|
||||
}
|
||||
|
||||
var dir string
|
||||
if source != "" {
|
||||
dir, _ = filepath.Abs(source)
|
||||
} else {
|
||||
dir, _ = os.Getwd()
|
||||
}
|
||||
config.Set("workingDir", dir)
|
||||
|
||||
if contentDir != "" {
|
||||
config.Set("contentDir", contentDir)
|
||||
}
|
||||
|
||||
if layoutDir != "" {
|
||||
config.Set("layoutDir", layoutDir)
|
||||
}
|
||||
|
||||
if cacheDir != "" {
|
||||
config.Set("cacheDir", cacheDir)
|
||||
}
|
||||
|
||||
fs := hugofs.NewFrom(osFs, config)
|
||||
|
||||
// Hugo writes the output to memory instead of the disk.
|
||||
// This is only used for benchmark testing. Cause the content is only visible
|
||||
// in memory.
|
||||
if config.GetBool("renderToMemory") {
|
||||
fs.Destination = new(afero.MemMapFs)
|
||||
// Rendering to memoryFS, publish to Root regardless of publishDir.
|
||||
config.Set("publishDir", "/")
|
||||
}
|
||||
|
||||
cacheDir = config.GetString("cacheDir")
|
||||
if cacheDir != "" {
|
||||
if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
|
||||
cacheDir = cacheDir + helpers.FilePathSeparator
|
||||
}
|
||||
isDir, err := helpers.DirExists(cacheDir, fs.Source)
|
||||
utils.CheckErr(cfg.Logger, err)
|
||||
if !isDir {
|
||||
mkdir(cacheDir)
|
||||
}
|
||||
config.Set("cacheDir", cacheDir)
|
||||
} else {
|
||||
config.Set("cacheDir", helpers.GetTempDir("hugo_cache", fs.Source))
|
||||
}
|
||||
|
||||
if err := c.initFs(fs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
|
||||
|
||||
themeDir := c.PathSpec().GetThemeDir()
|
||||
if themeDir != "" {
|
||||
if _, err := cfg.Fs.Source.Stat(themeDir); os.IsNotExist(err) {
|
||||
return nil, newSystemError("Unable to find theme Directory:", themeDir)
|
||||
}
|
||||
}
|
||||
|
||||
themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch()
|
||||
|
||||
if themeVersionMismatch {
|
||||
cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n",
|
||||
helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
|
||||
}
|
||||
@@ -524,20 +398,6 @@ If you need to set this configuration value from the command line, set it via an
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commandeer) watchConfig() {
|
||||
v := c.Cfg.(*viper.Viper)
|
||||
v.WatchConfig()
|
||||
v.OnConfigChange(func(e fsnotify.Event) {
|
||||
c.Logger.FEEDBACK.Println("Config file changed:", e.Name)
|
||||
// Force a full rebuild
|
||||
utils.CheckErr(c.Logger, c.recreateAndBuildSites(true))
|
||||
if !c.Cfg.GetBool("disableLiveReload") {
|
||||
// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *commandeer) fullBuild() error {
|
||||
var (
|
||||
g errgroup.Group
|
||||
@@ -942,6 +802,7 @@ func (c *commandeer) resetAndBuildSites() (err error) {
|
||||
|
||||
func (c *commandeer) initSites() error {
|
||||
if Hugo != nil {
|
||||
Hugo.Cfg = c.Cfg
|
||||
Hugo.Log.ResetLogCounters()
|
||||
return nil
|
||||
}
|
||||
@@ -1009,6 +870,15 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Identifies changes to config (config.toml) files.
|
||||
configSet := make(map[string]bool)
|
||||
|
||||
for _, configFile := range c.configFiles {
|
||||
c.Logger.FEEDBACK.Println("Watching for config changes in", configFile)
|
||||
watcher.Add(configFile)
|
||||
configSet[configFile] = true
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
@@ -1021,6 +891,21 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
|
||||
// Special handling for symbolic links inside /content.
|
||||
filtered := []fsnotify.Event{}
|
||||
for _, ev := range evs {
|
||||
if configSet[ev.Name] {
|
||||
if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
|
||||
continue
|
||||
}
|
||||
// Config file changed. Need full rebuild.
|
||||
if err := c.loadConfig(true); err != nil {
|
||||
jww.ERROR.Println("Failed to reload config:", err)
|
||||
} else if err := c.recreateAndBuildSites(true); err != nil {
|
||||
jww.ERROR.Println(err)
|
||||
} else if !buildWatch && !c.Cfg.GetBool("disableLiveReload") {
|
||||
livereload.ForceRefresh()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Check the most specific first, i.e. files.
|
||||
contentMapped := Hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
|
||||
if len(contentMapped) > 0 {
|
||||
@@ -1212,7 +1097,7 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
|
||||
|
||||
// isThemeVsHugoVersionMismatch returns whether the current Hugo version is
|
||||
// less than the theme's min_version.
|
||||
func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) {
|
||||
func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (mismatch bool, requiredMinVersion string) {
|
||||
if !c.PathSpec().ThemeSet() {
|
||||
return
|
||||
}
|
||||
@@ -1221,13 +1106,13 @@ func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinV
|
||||
|
||||
path := filepath.Join(themeDir, "theme.toml")
|
||||
|
||||
exists, err := helpers.Exists(path, c.Fs.Source)
|
||||
exists, err := helpers.Exists(path, fs)
|
||||
|
||||
if err != nil || !exists {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := afero.ReadFile(c.Fs.Source, path)
|
||||
b, err := afero.ReadFile(fs, path)
|
||||
|
||||
tomlMeta, err := parser.HandleTOMLMetaData(b)
|
||||
|
||||
|
@@ -24,6 +24,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -111,12 +112,16 @@ func init() {
|
||||
|
||||
}
|
||||
|
||||
var serverPorts []int
|
||||
|
||||
func server(cmd *cobra.Command, args []string) error {
|
||||
// If a Destination is provided via flag write to disk
|
||||
if destination != "" {
|
||||
renderToDisk = true
|
||||
}
|
||||
|
||||
var serverCfgInit sync.Once
|
||||
|
||||
cfgInit := func(c *commandeer) error {
|
||||
c.Set("renderToMemory", !renderToDisk)
|
||||
if cmd.Flags().Changed("navigateToChanged") {
|
||||
@@ -132,37 +137,42 @@ func server(cmd *cobra.Command, args []string) error {
|
||||
c.Set("watch", true)
|
||||
}
|
||||
|
||||
serverPorts := make([]int, 1)
|
||||
var err error
|
||||
|
||||
if c.languages.IsMultihost() {
|
||||
if !serverAppend {
|
||||
return newSystemError("--appendPort=false not supported when in multihost mode")
|
||||
}
|
||||
serverPorts = make([]int, len(c.languages))
|
||||
}
|
||||
// We can only do this once.
|
||||
serverCfgInit.Do(func() {
|
||||
serverPorts = make([]int, 1)
|
||||
|
||||
currentServerPort := serverPort
|
||||
|
||||
for i := 0; i < len(serverPorts); i++ {
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort)))
|
||||
if err == nil {
|
||||
l.Close()
|
||||
serverPorts[i] = currentServerPort
|
||||
} else {
|
||||
if i == 0 && serverCmd.Flags().Changed("port") {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
return newSystemErrorF("Server startup failed: %s", err)
|
||||
if c.languages.IsMultihost() {
|
||||
if !serverAppend {
|
||||
err = newSystemError("--appendPort=false not supported when in multihost mode")
|
||||
}
|
||||
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
if err != nil {
|
||||
return newSystemError("Unable to find alternative port to use:", err)
|
||||
}
|
||||
serverPorts[i] = sp.Port
|
||||
serverPorts = make([]int, len(c.languages))
|
||||
}
|
||||
|
||||
currentServerPort = serverPorts[i] + 1
|
||||
}
|
||||
currentServerPort := serverPort
|
||||
|
||||
for i := 0; i < len(serverPorts); i++ {
|
||||
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort)))
|
||||
if err == nil {
|
||||
l.Close()
|
||||
serverPorts[i] = currentServerPort
|
||||
} else {
|
||||
if i == 0 && serverCmd.Flags().Changed("port") {
|
||||
// port set explicitly by user -- he/she probably meant it!
|
||||
err = newSystemErrorF("Server startup failed: %s", err)
|
||||
}
|
||||
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
|
||||
sp, err := helpers.FindAvailablePort()
|
||||
if err != nil {
|
||||
err = newSystemError("Unable to find alternative port to use:", err)
|
||||
}
|
||||
serverPorts[i] = sp.Port
|
||||
}
|
||||
|
||||
currentServerPort = serverPorts[i] + 1
|
||||
}
|
||||
})
|
||||
|
||||
c.serverPorts = serverPorts
|
||||
|
||||
@@ -184,7 +194,7 @@ func server(cmd *cobra.Command, args []string) error {
|
||||
|
||||
baseURL, err := fixURL(language, baseURL, serverPort)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
if isMultiHost {
|
||||
language.Set("baseURL", baseURL)
|
||||
@@ -194,7 +204,7 @@ func server(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
@@ -215,10 +225,6 @@ func server(cmd *cobra.Command, args []string) error {
|
||||
s.RegisterMediaTypes()
|
||||
}
|
||||
|
||||
if serverWatch {
|
||||
c.watchConfig()
|
||||
}
|
||||
|
||||
// Watch runs its own server as part of the routine
|
||||
if serverWatch {
|
||||
|
||||
|
Reference in New Issue
Block a user