config.go (11056B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package modules
15
16 import (
17 "fmt"
18 "path/filepath"
19 "strings"
20
21 "github.com/gohugoio/hugo/common/hugo"
22
23 "github.com/gohugoio/hugo/config"
24 "github.com/gohugoio/hugo/hugofs/files"
25 "github.com/gohugoio/hugo/langs"
26 "github.com/mitchellh/mapstructure"
27 )
28
29 var DefaultModuleConfig = Config{
30
31 // Default to direct, which means "git clone" and similar. We
32 // will investigate proxy settings in more depth later.
33 // See https://github.com/golang/go/issues/26334
34 Proxy: "direct",
35
36 // Comma separated glob list matching paths that should not use the
37 // proxy configured above.
38 NoProxy: "none",
39
40 // Comma separated glob list matching paths that should be
41 // treated as private.
42 Private: "*.*",
43
44 // A list of replacement directives mapping a module path to a directory
45 // or a theme component in the themes folder.
46 // Note that this will turn the component into a traditional theme component
47 // that does not partake in vendoring etc.
48 // The syntax is the similar to the replacement directives used in go.mod, e.g:
49 // github.com/mod1 -> ../mod1,github.com/mod2 -> ../mod2
50 Replacements: nil,
51 }
52
53 // ApplyProjectConfigDefaults applies default/missing module configuration for
54 // the main project.
55 func ApplyProjectConfigDefaults(cfg config.Provider, mod Module) error {
56 moda := mod.(*moduleAdapter)
57
58 // Map legacy directory config into the new module.
59 languages := cfg.Get("languagesSortedDefaultFirst").(langs.Languages)
60 isMultiHost := languages.IsMultihost()
61
62 // To bridge between old and new configuration format we need
63 // a way to make sure all of the core components are configured on
64 // the basic level.
65 componentsConfigured := make(map[string]bool)
66 for _, mnt := range moda.mounts {
67 if !strings.HasPrefix(mnt.Target, files.JsConfigFolderMountPrefix) {
68 componentsConfigured[mnt.Component()] = true
69 }
70 }
71
72 type dirKeyComponent struct {
73 key string
74 component string
75 multilingual bool
76 }
77
78 dirKeys := []dirKeyComponent{
79 {"contentDir", files.ComponentFolderContent, true},
80 {"dataDir", files.ComponentFolderData, false},
81 {"layoutDir", files.ComponentFolderLayouts, false},
82 {"i18nDir", files.ComponentFolderI18n, false},
83 {"archetypeDir", files.ComponentFolderArchetypes, false},
84 {"assetDir", files.ComponentFolderAssets, false},
85 {"", files.ComponentFolderStatic, isMultiHost},
86 }
87
88 createMountsFor := func(d dirKeyComponent, cfg config.Provider) []Mount {
89 var lang string
90 if language, ok := cfg.(*langs.Language); ok {
91 lang = language.Lang
92 }
93
94 // Static mounts are a little special.
95 if d.component == files.ComponentFolderStatic {
96 var mounts []Mount
97 staticDirs := getStaticDirs(cfg)
98 if len(staticDirs) > 0 {
99 componentsConfigured[d.component] = true
100 }
101
102 for _, dir := range staticDirs {
103 mounts = append(mounts, Mount{Lang: lang, Source: dir, Target: d.component})
104 }
105
106 return mounts
107
108 }
109
110 if cfg.IsSet(d.key) {
111 source := cfg.GetString(d.key)
112 componentsConfigured[d.component] = true
113
114 return []Mount{{
115 // No lang set for layouts etc.
116 Source: source,
117 Target: d.component,
118 }}
119 }
120
121 return nil
122 }
123
124 createMounts := func(d dirKeyComponent) []Mount {
125 var mounts []Mount
126 if d.multilingual {
127 if d.component == files.ComponentFolderContent {
128 seen := make(map[string]bool)
129 hasContentDir := false
130 for _, language := range languages {
131 if language.ContentDir != "" {
132 hasContentDir = true
133 break
134 }
135 }
136
137 if hasContentDir {
138 for _, language := range languages {
139 contentDir := language.ContentDir
140 if contentDir == "" {
141 contentDir = files.ComponentFolderContent
142 }
143 if contentDir == "" || seen[contentDir] {
144 continue
145 }
146 seen[contentDir] = true
147 mounts = append(mounts, Mount{Lang: language.Lang, Source: contentDir, Target: d.component})
148 }
149 }
150
151 componentsConfigured[d.component] = len(seen) > 0
152
153 } else {
154 for _, language := range languages {
155 mounts = append(mounts, createMountsFor(d, language)...)
156 }
157 }
158 } else {
159 mounts = append(mounts, createMountsFor(d, cfg)...)
160 }
161
162 return mounts
163 }
164
165 var mounts []Mount
166 for _, dirKey := range dirKeys {
167 if componentsConfigured[dirKey.component] {
168 continue
169 }
170
171 mounts = append(mounts, createMounts(dirKey)...)
172
173 }
174
175 // Add default configuration
176 for _, dirKey := range dirKeys {
177 if componentsConfigured[dirKey.component] {
178 continue
179 }
180 mounts = append(mounts, Mount{Source: dirKey.component, Target: dirKey.component})
181 }
182
183 // Prepend the mounts from configuration.
184 mounts = append(moda.mounts, mounts...)
185
186 moda.mounts = mounts
187
188 return nil
189 }
190
191 // DecodeConfig creates a modules Config from a given Hugo configuration.
192 func DecodeConfig(cfg config.Provider) (Config, error) {
193 return decodeConfig(cfg, nil)
194 }
195
196 func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Config, error) {
197 c := DefaultModuleConfig
198 c.replacementsMap = pathReplacements
199
200 if cfg == nil {
201 return c, nil
202 }
203
204 themeSet := cfg.IsSet("theme")
205 moduleSet := cfg.IsSet("module")
206
207 if moduleSet {
208 m := cfg.GetStringMap("module")
209 if err := mapstructure.WeakDecode(m, &c); err != nil {
210 return c, err
211 }
212
213 if c.replacementsMap == nil {
214
215 if len(c.Replacements) == 1 {
216 c.Replacements = strings.Split(c.Replacements[0], ",")
217 }
218
219 for i, repl := range c.Replacements {
220 c.Replacements[i] = strings.TrimSpace(repl)
221 }
222
223 c.replacementsMap = make(map[string]string)
224 for _, repl := range c.Replacements {
225 parts := strings.Split(repl, "->")
226 if len(parts) != 2 {
227 return c, fmt.Errorf(`invalid module.replacements: %q; configure replacement pairs on the form "oldpath->newpath" `, repl)
228 }
229
230 c.replacementsMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
231 }
232 }
233
234 if c.replacementsMap != nil && c.Imports != nil {
235 for i, imp := range c.Imports {
236 if newImp, found := c.replacementsMap[imp.Path]; found {
237 imp.Path = newImp
238 imp.pathProjectReplaced = true
239 c.Imports[i] = imp
240 }
241 }
242 }
243
244 for i, mnt := range c.Mounts {
245 mnt.Source = filepath.Clean(mnt.Source)
246 mnt.Target = filepath.Clean(mnt.Target)
247 c.Mounts[i] = mnt
248 }
249
250 }
251
252 if themeSet {
253 imports := config.GetStringSlicePreserveString(cfg, "theme")
254 for _, imp := range imports {
255 c.Imports = append(c.Imports, Import{
256 Path: imp,
257 })
258 }
259
260 }
261
262 return c, nil
263 }
264
265 // Config holds a module config.
266 type Config struct {
267 Mounts []Mount
268 Imports []Import
269
270 // Meta info about this module (license information etc.).
271 Params map[string]any
272
273 // Will be validated against the running Hugo version.
274 HugoVersion HugoVersion
275
276 // A optional Glob pattern matching module paths to skip when vendoring, e.g.
277 // "github.com/**".
278 NoVendor string
279
280 // When enabled, we will pick the vendored module closest to the module
281 // using it.
282 // The default behaviour is to pick the first.
283 // Note that there can still be only one dependency of a given module path,
284 // so once it is in use it cannot be redefined.
285 VendorClosest bool
286
287 Replacements []string
288 replacementsMap map[string]string
289
290 // Configures GOPROXY.
291 Proxy string
292 // Configures GONOPROXY.
293 NoProxy string
294 // Configures GOPRIVATE.
295 Private string
296
297 // Set the workspace file to use, e.g. hugo.work.
298 // Enables Go "Workspace" mode.
299 // Requires Go 1.18+
300 // See https://tip.golang.org/doc/go1.18
301 Workspace string
302 }
303
304 // hasModuleImport reports whether the project config have one or more
305 // modules imports, e.g. github.com/bep/myshortcodes.
306 func (c Config) hasModuleImport() bool {
307 for _, imp := range c.Imports {
308 if isProbablyModule(imp.Path) {
309 return true
310 }
311 }
312 return false
313 }
314
315 // HugoVersion holds Hugo binary version requirements for a module.
316 type HugoVersion struct {
317 // The minimum Hugo version that this module works with.
318 Min hugo.VersionString
319
320 // The maximum Hugo version that this module works with.
321 Max hugo.VersionString
322
323 // Set if the extended version is needed.
324 Extended bool
325 }
326
327 func (v HugoVersion) String() string {
328 extended := ""
329 if v.Extended {
330 extended = " extended"
331 }
332
333 if v.Min != "" && v.Max != "" {
334 return fmt.Sprintf("%s/%s%s", v.Min, v.Max, extended)
335 }
336
337 if v.Min != "" {
338 return fmt.Sprintf("Min %s%s", v.Min, extended)
339 }
340
341 if v.Max != "" {
342 return fmt.Sprintf("Max %s%s", v.Max, extended)
343 }
344
345 return extended
346 }
347
348 // IsValid reports whether this version is valid compared to the running
349 // Hugo binary.
350 func (v HugoVersion) IsValid() bool {
351 current := hugo.CurrentVersion.Version()
352 if v.Extended && !hugo.IsExtended {
353 return false
354 }
355
356 isValid := true
357
358 if v.Min != "" && current.Compare(v.Min) > 0 {
359 isValid = false
360 }
361
362 if v.Max != "" && current.Compare(v.Max) < 0 {
363 isValid = false
364 }
365
366 return isValid
367 }
368
369 type Import struct {
370 Path string // Module path
371 pathProjectReplaced bool // Set when Path is replaced in project config.
372 IgnoreConfig bool // Ignore any config in config.toml (will still follow imports).
373 IgnoreImports bool // Do not follow any configured imports.
374 NoMounts bool // Do not mount any folder in this import.
375 NoVendor bool // Never vendor this import (only allowed in main project).
376 Disable bool // Turn off this module.
377 Mounts []Mount
378 }
379
380 type Mount struct {
381 Source string // relative path in source repo, e.g. "scss"
382 Target string // relative target path, e.g. "assets/bootstrap/scss"
383
384 Lang string // any language code associated with this mount.
385
386 // Include only files matching the given Glob patterns (string or slice).
387 IncludeFiles any
388
389 // Exclude all files matching the given Glob patterns (string or slice).
390 ExcludeFiles any
391 }
392
393 // Used as key to remove duplicates.
394 func (m Mount) key() string {
395 return strings.Join([]string{m.Lang, m.Source, m.Target}, "/")
396 }
397
398 func (m Mount) Component() string {
399 return strings.Split(m.Target, fileSeparator)[0]
400 }
401
402 func (m Mount) ComponentAndName() (string, string) {
403 c, n, _ := strings.Cut(m.Target, fileSeparator)
404 return c, n
405 }
406
407 func getStaticDirs(cfg config.Provider) []string {
408 var staticDirs []string
409 for i := -1; i <= 10; i++ {
410 staticDirs = append(staticDirs, getStringOrStringSlice(cfg, "staticDir", i)...)
411 }
412 return staticDirs
413 }
414
415 func getStringOrStringSlice(cfg config.Provider, key string, id int) []string {
416 if id >= 0 {
417 key = fmt.Sprintf("%s%d", key, id)
418 }
419
420 return config.GetStringSlicePreserveString(cfg, key)
421 }