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  }