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