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