Skip to content

Commit 9f5a920

Browse files
committed
Add Hugo Modules
This commit implements Hugo Modules. This is a broad subject, but some keywords include: * A new `module` configuration section where you can import almost anything. You can configure both your own file mounts nd the file mounts of the modules you import. This is the new recommended way of configuring what you earlier put in `configDir`, `staticDir` etc. And it also allows you to mount folders in non-Hugo-projects, e.g. the `SCSS` folder in the Bootstrap GitHub project. * A module consists of a set of mounts to the standard 7 component types in Hugo: `static`, `content`, `layouts`, `data`, `assets`, `i18n`, and `archetypes`. Yes, Theme Components can now include content, which should be very useful, especially in bigger multilingual projects. * Modules not in your local file cache will be downloaded automatically and even "hot replaced" while the server is running. * Hugo Modules supports and encourages semver versioned modules, and uses the minimal version selection algorithm to resolve versions. * A new set of CLI commands are provided to manage all of this: `hugo mod init`, `hugo mod get`, `hugo mod graph`, `hugo mod tidy`, and `hugo mod vendor`. All of the above is backed by Go Modules. Fixes gohugoio#5973 Fixes gohugoio#5996 Fixes gohugoio#6010 Fixes gohugoio#5911 Fixes gohugoio#5940 Fixes gohugoio#6074 Fixes gohugoio#6082 Fixes gohugoio#6092
1 parent 4795314 commit 9f5a920

File tree

158 files changed

+9809
-5347
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

158 files changed

+9809
-5347
lines changed

benchbep.sh

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
gobench -package=./hugolib -bench="BenchmarkSiteBuilding/YAML,num_langs=3,num_pages=5000,tags_per_page=5,shortcodes,render" -count=3 > 1.bench
2-
benchcmp -best 0.bench 1.bench
1+
gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree"

cache/filecache/filecache.go

+27-22
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ type Cache struct {
4444
// 0 is effectively turning this cache off.
4545
maxAge time.Duration
4646

47+
// When set, we just remove this entire root directory on expiration.
48+
pruneAllRootDir string
49+
4750
nlocker *lockTracker
4851
}
4952

@@ -77,11 +80,12 @@ type ItemInfo struct {
7780
}
7881

7982
// NewCache creates a new file cache with the given filesystem and max age.
80-
func NewCache(fs afero.Fs, maxAge time.Duration) *Cache {
83+
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
8184
return &Cache{
82-
Fs: fs,
83-
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
84-
maxAge: maxAge,
85+
Fs: fs,
86+
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
87+
maxAge: maxAge,
88+
pruneAllRootDir: pruneAllRootDir,
8589
}
8690
}
8791

@@ -307,9 +311,15 @@ func (f Caches) Get(name string) *Cache {
307311
// NewCaches creates a new set of file caches from the given
308312
// configuration.
309313
func NewCaches(p *helpers.PathSpec) (Caches, error) {
310-
dcfg, err := decodeConfig(p)
311-
if err != nil {
312-
return nil, err
314+
var dcfg Configs
315+
if c, ok := p.Cfg.Get("filecacheConfigs").(Configs); ok {
316+
dcfg = c
317+
} else {
318+
var err error
319+
dcfg, err = DecodeConfig(p.Fs.Source, p.Cfg)
320+
if err != nil {
321+
return nil, err
322+
}
313323
}
314324

315325
fs := p.Fs.Source
@@ -319,30 +329,25 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) {
319329
var cfs afero.Fs
320330

321331
if v.isResourceDir {
322-
cfs = p.BaseFs.Resources.Fs
332+
cfs = p.BaseFs.ResourcesCache
323333
} else {
324334
cfs = fs
325335
}
326336

327-
var baseDir string
328-
if !strings.HasPrefix(v.Dir, "_gen") {
329-
// We do cache eviction (file removes) and since the user can set
330-
// his/hers own cache directory, we really want to make sure
331-
// we do not delete any files that do not belong to this cache.
332-
// We do add the cache name as the root, but this is an extra safe
333-
// guard. We skip the files inside /resources/_gen/ because
334-
// that would be breaking.
335-
baseDir = filepath.Join(v.Dir, filecacheRootDirname, k)
336-
} else {
337-
baseDir = filepath.Join(v.Dir, k)
338-
}
339-
if err = cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
337+
baseDir := v.Dir
338+
339+
if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
340340
return nil, err
341341
}
342342

343343
bfs := afero.NewBasePathFs(cfs, baseDir)
344344

345-
m[k] = NewCache(bfs, v.MaxAge)
345+
var pruneAllRootDir string
346+
if k == cacheKeyModules {
347+
pruneAllRootDir = "pkg"
348+
}
349+
350+
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
346351
}
347352

348353
return m, nil

cache/filecache/filecache_config.go

+42-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"strings"
2020
"time"
2121

22+
"github.com/gohugoio/hugo/config"
23+
2224
"github.com/gohugoio/hugo/helpers"
2325

2426
"github.com/mitchellh/mapstructure"
@@ -32,7 +34,7 @@ const (
3234
resourcesGenDir = ":resourceDir/_gen"
3335
)
3436

35-
var defaultCacheConfig = cacheConfig{
37+
var defaultCacheConfig = Config{
3638
MaxAge: -1, // Never expire
3739
Dir: ":cacheDir/:project",
3840
}
@@ -42,9 +44,20 @@ const (
4244
cacheKeyGetCSV = "getcsv"
4345
cacheKeyImages = "images"
4446
cacheKeyAssets = "assets"
47+
cacheKeyModules = "modules"
4548
)
4649

47-
var defaultCacheConfigs = map[string]cacheConfig{
50+
type Configs map[string]Config
51+
52+
func (c Configs) CacheDirModules() string {
53+
return c[cacheKeyModules].Dir
54+
}
55+
56+
var defaultCacheConfigs = Configs{
57+
cacheKeyModules: {
58+
MaxAge: -1,
59+
Dir: ":cacheDir/modules",
60+
},
4861
cacheKeyGetJSON: defaultCacheConfig,
4962
cacheKeyGetCSV: defaultCacheConfig,
5063
cacheKeyImages: {
@@ -57,9 +70,7 @@ var defaultCacheConfigs = map[string]cacheConfig{
5770
},
5871
}
5972

60-
type cachesConfig map[string]cacheConfig
61-
62-
type cacheConfig struct {
73+
type Config struct {
6374
// Max age of cache entries in this cache. Any items older than this will
6475
// be removed and not returned from the cache.
6576
// a negative value means forever, 0 means cache is disabled.
@@ -88,25 +99,28 @@ func (f Caches) ImageCache() *Cache {
8899
return f[cacheKeyImages]
89100
}
90101

102+
// ModulesCache gets the file cache for Hugo Modules.
103+
func (f Caches) ModulesCache() *Cache {
104+
return f[cacheKeyModules]
105+
}
106+
91107
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
92108
func (f Caches) AssetsCache() *Cache {
93109
return f[cacheKeyAssets]
94110
}
95111

96-
func decodeConfig(p *helpers.PathSpec) (cachesConfig, error) {
97-
c := make(cachesConfig)
112+
func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
113+
c := make(Configs)
98114
valid := make(map[string]bool)
99115
// Add defaults
100116
for k, v := range defaultCacheConfigs {
101117
c[k] = v
102118
valid[k] = true
103119
}
104120

105-
cfg := p.Cfg
106-
107121
m := cfg.GetStringMap(cachesConfigKey)
108122

109-
_, isOsFs := p.Fs.Source.(*afero.OsFs)
123+
_, isOsFs := fs.(*afero.OsFs)
110124

111125
for k, v := range m {
112126
cc := defaultCacheConfig
@@ -148,7 +162,7 @@ func decodeConfig(p *helpers.PathSpec) (cachesConfig, error) {
148162

149163
for i, part := range parts {
150164
if strings.HasPrefix(part, ":") {
151-
resolved, isResource, err := resolveDirPlaceholder(p, part)
165+
resolved, isResource, err := resolveDirPlaceholder(fs, cfg, part)
152166
if err != nil {
153167
return c, err
154168
}
@@ -176,6 +190,18 @@ func decodeConfig(p *helpers.PathSpec) (cachesConfig, error) {
176190
}
177191
}
178192

193+
if !strings.HasPrefix(v.Dir, "_gen") {
194+
// We do cache eviction (file removes) and since the user can set
195+
// his/hers own cache directory, we really want to make sure
196+
// we do not delete any files that do not belong to this cache.
197+
// We do add the cache name as the root, but this is an extra safe
198+
// guard. We skip the files inside /resources/_gen/ because
199+
// that would be breaking.
200+
v.Dir = filepath.Join(v.Dir, filecacheRootDirname, k)
201+
} else {
202+
v.Dir = filepath.Join(v.Dir, k)
203+
}
204+
179205
if disabled {
180206
v.MaxAge = 0
181207
}
@@ -187,15 +213,17 @@ func decodeConfig(p *helpers.PathSpec) (cachesConfig, error) {
187213
}
188214

189215
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
190-
func resolveDirPlaceholder(p *helpers.PathSpec, placeholder string) (cacheDir string, isResource bool, err error) {
216+
func resolveDirPlaceholder(fs afero.Fs, cfg config.Provider, placeholder string) (cacheDir string, isResource bool, err error) {
217+
workingDir := cfg.GetString("workingDir")
218+
191219
switch strings.ToLower(placeholder) {
192220
case ":resourcedir":
193221
return "", true, nil
194222
case ":cachedir":
195-
d, err := helpers.GetCacheDir(p.Fs.Source, p.Cfg)
223+
d, err := helpers.GetCacheDir(fs, cfg)
196224
return d, false, err
197225
case ":project":
198-
return filepath.Base(p.WorkingDir), false, nil
226+
return filepath.Base(workingDir), false, nil
199227
}
200228

201229
return "", false, errors.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)

cache/filecache/filecache_config_test.go

+17-28
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import (
2020
"testing"
2121
"time"
2222

23-
"github.com/gohugoio/hugo/helpers"
23+
"github.com/spf13/afero"
2424

2525
"github.com/gohugoio/hugo/config"
26-
"github.com/gohugoio/hugo/hugofs"
2726

2827
"github.com/spf13/viper"
2928
"github.com/stretchr/testify/require"
@@ -57,22 +56,19 @@ dir = "/path/to/c3"
5756

5857
cfg, err := config.FromConfigString(configStr, "toml")
5958
assert.NoError(err)
60-
fs := hugofs.NewMem(cfg)
61-
p, err := helpers.NewPathSpec(fs, cfg)
59+
fs := afero.NewMemMapFs()
60+
decoded, err := DecodeConfig(fs, cfg)
6261
assert.NoError(err)
6362

64-
decoded, err := decodeConfig(p)
65-
assert.NoError(err)
66-
67-
assert.Equal(4, len(decoded))
63+
assert.Equal(5, len(decoded))
6864

6965
c2 := decoded["getcsv"]
7066
assert.Equal("11h0m0s", c2.MaxAge.String())
71-
assert.Equal(filepath.FromSlash("/path/to/c2"), c2.Dir)
67+
assert.Equal(filepath.FromSlash("/path/to/c2/filecache/getcsv"), c2.Dir)
7268

7369
c3 := decoded["images"]
7470
assert.Equal(time.Duration(-1), c3.MaxAge)
75-
assert.Equal(filepath.FromSlash("/path/to/c3"), c3.Dir)
71+
assert.Equal(filepath.FromSlash("/path/to/c3/filecache/images"), c3.Dir)
7672

7773
}
7874

@@ -105,14 +101,11 @@ dir = "/path/to/c3"
105101

106102
cfg, err := config.FromConfigString(configStr, "toml")
107103
assert.NoError(err)
108-
fs := hugofs.NewMem(cfg)
109-
p, err := helpers.NewPathSpec(fs, cfg)
110-
assert.NoError(err)
111-
112-
decoded, err := decodeConfig(p)
104+
fs := afero.NewMemMapFs()
105+
decoded, err := DecodeConfig(fs, cfg)
113106
assert.NoError(err)
114107

115-
assert.Equal(4, len(decoded))
108+
assert.Equal(5, len(decoded))
116109

117110
for _, v := range decoded {
118111
assert.Equal(time.Duration(0), v.MaxAge)
@@ -133,24 +126,22 @@ func TestDecodeConfigDefault(t *testing.T) {
133126
cfg.Set("cacheDir", "/cache/thecache")
134127
}
135128

136-
fs := hugofs.NewMem(cfg)
137-
p, err := helpers.NewPathSpec(fs, cfg)
138-
assert.NoError(err)
129+
fs := afero.NewMemMapFs()
139130

140-
decoded, err := decodeConfig(p)
131+
decoded, err := DecodeConfig(fs, cfg)
141132

142133
assert.NoError(err)
143134

144-
assert.Equal(4, len(decoded))
135+
assert.Equal(5, len(decoded))
145136

146137
imgConfig := decoded[cacheKeyImages]
147138
jsonConfig := decoded[cacheKeyGetJSON]
148139

149140
if runtime.GOOS == "windows" {
150-
assert.Equal("_gen", imgConfig.Dir)
141+
assert.Equal(filepath.FromSlash("_gen/images"), imgConfig.Dir)
151142
} else {
152-
assert.Equal("_gen", imgConfig.Dir)
153-
assert.Equal("/cache/thecache/hugoproject", jsonConfig.Dir)
143+
assert.Equal("_gen/images", imgConfig.Dir)
144+
assert.Equal("/cache/thecache/hugoproject/filecache/getjson", jsonConfig.Dir)
154145
}
155146

156147
assert.True(imgConfig.isResourceDir)
@@ -183,11 +174,9 @@ dir = "/"
183174

184175
cfg, err := config.FromConfigString(configStr, "toml")
185176
assert.NoError(err)
186-
fs := hugofs.NewMem(cfg)
187-
p, err := helpers.NewPathSpec(fs, cfg)
188-
assert.NoError(err)
177+
fs := afero.NewMemMapFs()
189178

190-
_, err = decodeConfig(p)
179+
_, err = DecodeConfig(fs, cfg)
191180
assert.Error(err)
192181

193182
}

0 commit comments

Comments
 (0)