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 }