ref: 173187e2633f3fc037c83e1e3de2902ae3c93b92
parent: 8a1c637c4494751046142e0ef345fce38fc1431b
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Thu Oct 29 13:14:04 EDT 2020
Add module.replacements Fixes #7904 Fixes #7908
--- a/modules/client.go
+++ b/modules/client.go
@@ -613,6 +613,15 @@
return c.noVendor == nil || !c.noVendor.Match(path)
}
+func (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) {
+ modulePath = filepath.Clean(modulePath)
+ moduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath)
+ if !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) {
+ return "", errors.Errorf("invalid module path %q; must be relative to themesDir when defined outside of the project", modulePath)
+ }
+ return moduleDir, nil
+}
+
// ClientConfig configures the module Client.
type ClientConfig struct {
Fs afero.Fs
--- a/modules/client_test.go
+++ b/modules/client_test.go
@@ -15,6 +15,8 @@
import (
"bytes"
+ "os"
+ "path/filepath"
"testing"
"github.com/gohugoio/hugo/hugofs/glob"
@@ -41,10 +43,14 @@
workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, modName)
c.Assert(err, qt.IsNil)
+ themesDir := filepath.Join(workingDir, "themes")
+ err = os.Mkdir(themesDir, 0777)
+ c.Assert(err, qt.IsNil)
ccfg := ClientConfig{
Fs: hugofs.Os,
WorkingDir: workingDir,
+ ThemesDir: themesDir,
}
withConfig(&ccfg)
@@ -129,6 +135,28 @@
var graphb bytes.Buffer
c.Assert(client.Graph(&graphb), qt.IsNil)
c.Assert(graphb.String(), qt.Equals, expect)
+ })
+
+ // https://github.com/gohugoio/hugo/issues/7908
+ c.Run("createThemeDirname", func(c *qt.C) {
+ mcfg := DefaultModuleConfig
+ client, clean := newClient(
+ c, func(cfg *ClientConfig) {
+ cfg.ModuleConfig = mcfg
+ })
+ defer clean()
+
+ dirname, err := client.createThemeDirname("foo", false)
+ c.Assert(err, qt.IsNil)
+ c.Assert(dirname, qt.Equals, filepath.Join(client.ccfg.ThemesDir, "foo"))
+
+ dirname, err = client.createThemeDirname("../../foo", true)
+ c.Assert(err, qt.IsNil)
+ c.Assert(dirname, qt.Equals, filepath.Join(client.ccfg.ThemesDir, "../../foo"))
+
+ dirname, err = client.createThemeDirname("../../foo", false)
+ c.Assert(err, qt.Not(qt.IsNil))
+
})
}
--- a/modules/collect.go
+++ b/modules/collect.go
@@ -274,10 +274,14 @@
}
}
- // Fall back to /themes/<mymodule>
+ // Fall back to project/themes/<mymodule>
if moduleDir == "" {
- moduleDir = filepath.Join(c.ccfg.ThemesDir, modulePath)
-
+ var err error
+ moduleDir, err = c.createThemeDirname(modulePath, owner.projectMod)
+ if err != nil {
+ c.err = err
+ return nil, nil
+ }
if found, _ := afero.Exists(c.fs, moduleDir); !found {
c.err = c.wrapModuleNotFound(errors.Errorf(`module %q not found; either add it as a Hugo Module or store it in %q.`, modulePath, c.ccfg.ThemesDir))
return nil, nil
@@ -441,7 +445,7 @@
tc.cfg = cfg
}
- config, err := DecodeConfig(cfg)
+ config, err := decodeConfig(cfg, c.moduleConfig.replacementsMap)
if err != nil {
return err
}
@@ -605,7 +609,6 @@
mnt.Source = filepath.Clean(mnt.Source)
mnt.Target = filepath.Clean(mnt.Target)
-
var sourceDir string
if owner.projectMod && filepath.IsAbs(mnt.Source) {
--- a/modules/config.go
+++ b/modules/config.go
@@ -18,6 +18,8 @@
"path/filepath"
"strings"
+ "github.com/pkg/errors"
+
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/config"
@@ -40,6 +42,14 @@
// Comma separated glob list matching paths that should be
// treated as private.
Private: "*.*",
+
+ // A list of replacement directives mapping a module path to a directory
+ // or a theme component in the themes folder.
+ // Note that this will turn the component into a traditional theme component
+ // that does not partake in vendoring etc.
+ // The syntax is the similar to the replacement directives used in go.mod, e.g:
+ // github.com/mod1 -> ../mod1,github.com/mod2 -> ../mod2
+ Replacements: nil,
}
// ApplyProjectConfigDefaults applies default/missing module configuration for
@@ -182,7 +192,12 @@
// DecodeConfig creates a modules Config from a given Hugo configuration.
func DecodeConfig(cfg config.Provider) (Config, error) {
+ return decodeConfig(cfg, nil)
+}
+
+func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Config, error) {
c := DefaultModuleConfig
+ c.replacementsMap = pathReplacements
if cfg == nil {
return c, nil
@@ -197,6 +212,37 @@
return c, err
}
+ if c.replacementsMap == nil {
+
+ if len(c.Replacements) == 1 {
+ c.Replacements = strings.Split(c.Replacements[0], ",")
+ }
+
+ for i, repl := range c.Replacements {
+ c.Replacements[i] = strings.TrimSpace(repl)
+ }
+
+ c.replacementsMap = make(map[string]string)
+ for _, repl := range c.Replacements {
+ parts := strings.Split(repl, "->")
+ if len(parts) != 2 {
+ return c, errors.Errorf(`invalid module.replacements: %q; configure replacement pairs on the form "oldpath->newpath" `, repl)
+ }
+
+ c.replacementsMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
+ }
+ }
+
+ if c.replacementsMap != nil && c.Imports != nil {
+ for i, imp := range c.Imports {
+ if newImp, found := c.replacementsMap[imp.Path]; found {
+ imp.Path = newImp
+ c.Imports[i] = imp
+ }
+ }
+
+ }
+
for i, mnt := range c.Mounts {
mnt.Source = filepath.Clean(mnt.Source)
mnt.Target = filepath.Clean(mnt.Target)
@@ -232,6 +278,9 @@
// A optional Glob pattern matching module paths to skip when vendoring, e.g.
// "github.com/**".
NoVendor string
+
+ Replacements []string
+ replacementsMap map[string]string
// Configures GOPROXY.
Proxy string
--- a/modules/config_test.go
+++ b/modules/config_test.go
@@ -41,7 +41,9 @@
func TestDecodeConfig(t *testing.T) {
c := qt.New(t)
- tomlConfig := `
+
+ c.Run("Basic", func(c *qt.C) {
+ tomlConfig := `
[module]
[module.hugoVersion]
@@ -63,31 +65,61 @@
target="content/blog"
lang="en"
`
- cfg, err := config.FromConfigString(tomlConfig, "toml")
- c.Assert(err, qt.IsNil)
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ c.Assert(err, qt.IsNil)
- mcfg, err := DecodeConfig(cfg)
- c.Assert(err, qt.IsNil)
+ mcfg, err := DecodeConfig(cfg)
+ c.Assert(err, qt.IsNil)
- v056 := hugo.VersionString("0.56.0")
+ v056 := hugo.VersionString("0.56.0")
- hv := mcfg.HugoVersion
+ hv := mcfg.HugoVersion
- c.Assert(v056.Compare(hv.Min), qt.Equals, -1)
- c.Assert(v056.Compare(hv.Max), qt.Equals, 1)
- c.Assert(hv.Extended, qt.Equals, true)
+ c.Assert(v056.Compare(hv.Min), qt.Equals, -1)
+ c.Assert(v056.Compare(hv.Max), qt.Equals, 1)
+ c.Assert(hv.Extended, qt.Equals, true)
- if hugo.IsExtended {
- c.Assert(hv.IsValid(), qt.Equals, true)
- }
+ if hugo.IsExtended {
+ c.Assert(hv.IsValid(), qt.Equals, true)
+ }
- c.Assert(len(mcfg.Mounts), qt.Equals, 1)
- c.Assert(len(mcfg.Imports), qt.Equals, 1)
- imp := mcfg.Imports[0]
- imp.Path = "github.com/bep/mycomponent"
- c.Assert(imp.Mounts[1].Source, qt.Equals, "src/markdown/blog")
- c.Assert(imp.Mounts[1].Target, qt.Equals, "content/blog")
- c.Assert(imp.Mounts[1].Lang, qt.Equals, "en")
+ c.Assert(len(mcfg.Mounts), qt.Equals, 1)
+ c.Assert(len(mcfg.Imports), qt.Equals, 1)
+ imp := mcfg.Imports[0]
+ imp.Path = "github.com/bep/mycomponent"
+ c.Assert(imp.Mounts[1].Source, qt.Equals, "src/markdown/blog")
+ c.Assert(imp.Mounts[1].Target, qt.Equals, "content/blog")
+ c.Assert(imp.Mounts[1].Lang, qt.Equals, "en")
+ })
+
+ c.Run("Replacements", func(c *qt.C) {
+ for _, tomlConfig := range []string{`
+[module]
+replacements="a->b,github.com/bep/mycomponent->c"
+[[module.imports]]
+path="github.com/bep/mycomponent"
+`, `
+[module]
+replacements=["a->b","github.com/bep/mycomponent->c"]
+[[module.imports]]
+path="github.com/bep/mycomponent"
+`} {
+
+ cfg, err := config.FromConfigString(tomlConfig, "toml")
+ c.Assert(err, qt.IsNil)
+
+ mcfg, err := DecodeConfig(cfg)
+ c.Assert(err, qt.IsNil)
+ c.Assert(mcfg.Replacements, qt.DeepEquals, []string{"a->b", "github.com/bep/mycomponent->c"})
+ c.Assert(mcfg.replacementsMap, qt.DeepEquals, map[string]string{
+ "a": "b",
+ "github.com/bep/mycomponent": "c",
+ })
+
+ c.Assert(mcfg.Imports[0].Path, qt.Equals, "c")
+
+ }
+ })
}