hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

collect.go (16820B)

    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 	"bufio"
   18 	"fmt"
   19 	"os"
   20 	"path/filepath"
   21 	"regexp"
   22 	"strings"
   23 	"time"
   24 
   25 	"github.com/bep/debounce"
   26 	"github.com/gohugoio/hugo/common/loggers"
   27 
   28 	"github.com/spf13/cast"
   29 
   30 	"github.com/gohugoio/hugo/common/maps"
   31 
   32 	"github.com/gohugoio/hugo/common/hugo"
   33 	"github.com/gohugoio/hugo/parser/metadecoders"
   34 
   35 	"github.com/gohugoio/hugo/hugofs/files"
   36 
   37 	"github.com/rogpeppe/go-internal/module"
   38 
   39 	"errors"
   40 
   41 	"github.com/gohugoio/hugo/config"
   42 	"github.com/spf13/afero"
   43 )
   44 
   45 var ErrNotExist = errors.New("module does not exist")
   46 
   47 const vendorModulesFilename = "modules.txt"
   48 
   49 // IsNotExist returns whether an error means that a module could not be found.
   50 func IsNotExist(err error) bool {
   51 	return errors.Is(err, os.ErrNotExist)
   52 }
   53 
   54 // CreateProjectModule creates modules from the given config.
   55 // This is used in tests only.
   56 func CreateProjectModule(cfg config.Provider) (Module, error) {
   57 	workingDir := cfg.GetString("workingDir")
   58 	var modConfig Config
   59 
   60 	mod := createProjectModule(nil, workingDir, modConfig)
   61 	if err := ApplyProjectConfigDefaults(cfg, mod); err != nil {
   62 		return nil, err
   63 	}
   64 
   65 	return mod, nil
   66 }
   67 
   68 func (h *Client) Collect() (ModulesConfig, error) {
   69 	mc, coll := h.collect(true)
   70 	if coll.err != nil {
   71 		return mc, coll.err
   72 	}
   73 
   74 	if err := (&mc).setActiveMods(h.logger); err != nil {
   75 		return mc, err
   76 	}
   77 
   78 	if h.ccfg.HookBeforeFinalize != nil {
   79 		if err := h.ccfg.HookBeforeFinalize(&mc); err != nil {
   80 			return mc, err
   81 		}
   82 	}
   83 
   84 	if err := (&mc).finalize(h.logger); err != nil {
   85 		return mc, err
   86 	}
   87 
   88 	return mc, nil
   89 }
   90 
   91 func (h *Client) collect(tidy bool) (ModulesConfig, *collector) {
   92 	c := &collector{
   93 		Client: h,
   94 	}
   95 
   96 	c.collect()
   97 	if c.err != nil {
   98 		return ModulesConfig{}, c
   99 	}
  100 
  101 	// https://github.com/gohugoio/hugo/issues/6115
  102 	/*if !c.skipTidy && tidy {
  103 		if err := h.tidy(c.modules, true); err != nil {
  104 			c.err = err
  105 			return ModulesConfig{}, c
  106 		}
  107 	}*/
  108 
  109 	return ModulesConfig{
  110 		AllModules:        c.modules,
  111 		GoModulesFilename: c.GoModulesFilename,
  112 	}, c
  113 }
  114 
  115 type ModulesConfig struct {
  116 	// All modules, including any disabled.
  117 	AllModules Modules
  118 
  119 	// All active modules.
  120 	ActiveModules Modules
  121 
  122 	// Set if this is a Go modules enabled project.
  123 	GoModulesFilename string
  124 }
  125 
  126 func (m *ModulesConfig) setActiveMods(logger loggers.Logger) error {
  127 	var activeMods Modules
  128 	for _, mod := range m.AllModules {
  129 		if !mod.Config().HugoVersion.IsValid() {
  130 			logger.Warnf(`Module %q is not compatible with this Hugo version; run "hugo mod graph" for more information.`, mod.Path())
  131 		}
  132 		if !mod.Disabled() {
  133 			activeMods = append(activeMods, mod)
  134 		}
  135 	}
  136 
  137 	m.ActiveModules = activeMods
  138 
  139 	return nil
  140 }
  141 
  142 func (m *ModulesConfig) finalize(logger loggers.Logger) error {
  143 	for _, mod := range m.AllModules {
  144 		m := mod.(*moduleAdapter)
  145 		m.mounts = filterUnwantedMounts(m.mounts)
  146 	}
  147 	return nil
  148 }
  149 
  150 func filterUnwantedMounts(mounts []Mount) []Mount {
  151 	// Remove duplicates
  152 	seen := make(map[string]bool)
  153 	tmp := mounts[:0]
  154 	for _, m := range mounts {
  155 		if !seen[m.key()] {
  156 			tmp = append(tmp, m)
  157 		}
  158 		seen[m.key()] = true
  159 	}
  160 	return tmp
  161 }
  162 
  163 type collected struct {
  164 	// Pick the first and prevent circular loops.
  165 	seen map[string]bool
  166 
  167 	// Maps module path to a _vendor dir. These values are fetched from
  168 	// _vendor/modules.txt, and the first (top-most) will win.
  169 	vendored map[string]vendoredModule
  170 
  171 	// Set if a Go modules enabled project.
  172 	gomods goModules
  173 
  174 	// Ordered list of collected modules, including Go Modules and theme
  175 	// components stored below /themes.
  176 	modules Modules
  177 }
  178 
  179 // Collects and creates a module tree.
  180 type collector struct {
  181 	*Client
  182 
  183 	// Store away any non-fatal error and return at the end.
  184 	err error
  185 
  186 	// Set to disable any Tidy operation in the end.
  187 	skipTidy bool
  188 
  189 	*collected
  190 }
  191 
  192 func (c *collector) initModules() error {
  193 	c.collected = &collected{
  194 		seen:     make(map[string]bool),
  195 		vendored: make(map[string]vendoredModule),
  196 		gomods:   goModules{},
  197 	}
  198 
  199 	// If both these are true, we don't even need Go installed to build.
  200 	if c.ccfg.IgnoreVendor == nil && c.isVendored(c.ccfg.WorkingDir) {
  201 		return nil
  202 	}
  203 
  204 	// We may fail later if we don't find the mods.
  205 	return c.loadModules()
  206 }
  207 
  208 func (c *collector) isSeen(path string) bool {
  209 	key := pathKey(path)
  210 	if c.seen[key] {
  211 		return true
  212 	}
  213 	c.seen[key] = true
  214 	return false
  215 }
  216 
  217 func (c *collector) getVendoredDir(path string) (vendoredModule, bool) {
  218 	v, found := c.vendored[path]
  219 	return v, found
  220 }
  221 
  222 func (c *collector) add(owner *moduleAdapter, moduleImport Import, disabled bool) (*moduleAdapter, error) {
  223 	var (
  224 		mod       *goModule
  225 		moduleDir string
  226 		version   string
  227 		vendored  bool
  228 	)
  229 
  230 	modulePath := moduleImport.Path
  231 	var realOwner Module = owner
  232 
  233 	if !c.ccfg.shouldIgnoreVendor(modulePath) {
  234 		if err := c.collectModulesTXT(owner); err != nil {
  235 			return nil, err
  236 		}
  237 
  238 		// Try _vendor first.
  239 		var vm vendoredModule
  240 		vm, vendored = c.getVendoredDir(modulePath)
  241 		if vendored {
  242 			moduleDir = vm.Dir
  243 			realOwner = vm.Owner
  244 			version = vm.Version
  245 
  246 			if owner.projectMod {
  247 				// We want to keep the go.mod intact with the versions and all.
  248 				c.skipTidy = true
  249 			}
  250 
  251 		}
  252 	}
  253 
  254 	if moduleDir == "" {
  255 		var versionQuery string
  256 		mod = c.gomods.GetByPath(modulePath)
  257 		if mod != nil {
  258 			moduleDir = mod.Dir
  259 			versionQuery = mod.Version
  260 		}
  261 
  262 		if moduleDir == "" {
  263 			if c.GoModulesFilename != "" && isProbablyModule(modulePath) {
  264 				// Try to "go get" it and reload the module configuration.
  265 				if versionQuery == "" {
  266 					// See https://golang.org/ref/mod#version-queries
  267 					// This will select the latest release-version (not beta etc.).
  268 					versionQuery = "upgrade"
  269 				}
  270 				if err := c.Get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
  271 					return nil, err
  272 				}
  273 				if err := c.loadModules(); err != nil {
  274 					return nil, err
  275 				}
  276 
  277 				mod = c.gomods.GetByPath(modulePath)
  278 				if mod != nil {
  279 					moduleDir = mod.Dir
  280 				}
  281 			}
  282 
  283 			// Fall back to project/themes/<mymodule>
  284 			if moduleDir == "" {
  285 				var err error
  286 				moduleDir, err = c.createThemeDirname(modulePath, owner.projectMod || moduleImport.pathProjectReplaced)
  287 				if err != nil {
  288 					c.err = err
  289 					return nil, nil
  290 				}
  291 				if found, _ := afero.Exists(c.fs, moduleDir); !found {
  292 					c.err = c.wrapModuleNotFound(fmt.Errorf(`module %q not found; either add it as a Hugo Module or store it in %q.`, modulePath, c.ccfg.ThemesDir))
  293 					return nil, nil
  294 				}
  295 			}
  296 		}
  297 	}
  298 
  299 	if found, _ := afero.Exists(c.fs, moduleDir); !found {
  300 		c.err = c.wrapModuleNotFound(fmt.Errorf("%q not found", moduleDir))
  301 		return nil, nil
  302 	}
  303 
  304 	if !strings.HasSuffix(moduleDir, fileSeparator) {
  305 		moduleDir += fileSeparator
  306 	}
  307 
  308 	ma := &moduleAdapter{
  309 		dir:      moduleDir,
  310 		vendor:   vendored,
  311 		disabled: disabled,
  312 		gomod:    mod,
  313 		version:  version,
  314 		// This may be the owner of the _vendor dir
  315 		owner: realOwner,
  316 	}
  317 
  318 	if mod == nil {
  319 		ma.path = modulePath
  320 	}
  321 
  322 	if !moduleImport.IgnoreConfig {
  323 		if err := c.applyThemeConfig(ma); err != nil {
  324 			return nil, err
  325 		}
  326 	}
  327 
  328 	if err := c.applyMounts(moduleImport, ma); err != nil {
  329 		return nil, err
  330 	}
  331 
  332 	c.modules = append(c.modules, ma)
  333 	return ma, nil
  334 }
  335 
  336 func (c *collector) addAndRecurse(owner *moduleAdapter, disabled bool) error {
  337 	moduleConfig := owner.Config()
  338 	if owner.projectMod {
  339 		if err := c.applyMounts(Import{}, owner); err != nil {
  340 			return err
  341 		}
  342 	}
  343 
  344 	for _, moduleImport := range moduleConfig.Imports {
  345 		disabled := disabled || moduleImport.Disable
  346 
  347 		if !c.isSeen(moduleImport.Path) {
  348 			tc, err := c.add(owner, moduleImport, disabled)
  349 			if err != nil {
  350 				return err
  351 			}
  352 			if tc == nil || moduleImport.IgnoreImports {
  353 				continue
  354 			}
  355 			if err := c.addAndRecurse(tc, disabled); err != nil {
  356 				return err
  357 			}
  358 		}
  359 	}
  360 	return nil
  361 }
  362 
  363 func (c *collector) applyMounts(moduleImport Import, mod *moduleAdapter) error {
  364 	if moduleImport.NoMounts {
  365 		mod.mounts = nil
  366 		return nil
  367 	}
  368 
  369 	mounts := moduleImport.Mounts
  370 
  371 	modConfig := mod.Config()
  372 
  373 	if len(mounts) == 0 {
  374 		// Mounts not defined by the import.
  375 		mounts = modConfig.Mounts
  376 	}
  377 
  378 	if !mod.projectMod && len(mounts) == 0 {
  379 		// Create default mount points for every component folder that
  380 		// exists in the module.
  381 		for _, componentFolder := range files.ComponentFolders {
  382 			sourceDir := filepath.Join(mod.Dir(), componentFolder)
  383 			_, err := c.fs.Stat(sourceDir)
  384 			if err == nil {
  385 				mounts = append(mounts, Mount{
  386 					Source: componentFolder,
  387 					Target: componentFolder,
  388 				})
  389 			}
  390 		}
  391 	}
  392 
  393 	var err error
  394 	mounts, err = c.normalizeMounts(mod, mounts)
  395 	if err != nil {
  396 		return err
  397 	}
  398 
  399 	mounts, err = c.mountCommonJSConfig(mod, mounts)
  400 	if err != nil {
  401 		return err
  402 	}
  403 
  404 	mod.mounts = mounts
  405 	return nil
  406 }
  407 
  408 func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
  409 	var (
  410 		configFilename string
  411 		themeCfg       map[string]any
  412 		hasConfigFile  bool
  413 		err            error
  414 	)
  415 
  416 	// Viper supports more, but this is the sub-set supported by Hugo.
  417 	for _, configFormats := range config.ValidConfigFileExtensions {
  418 		configFilename = filepath.Join(tc.Dir(), "config."+configFormats)
  419 		hasConfigFile, _ = afero.Exists(c.fs, configFilename)
  420 		if hasConfigFile {
  421 			break
  422 		}
  423 	}
  424 
  425 	// The old theme information file.
  426 	themeTOML := filepath.Join(tc.Dir(), "theme.toml")
  427 
  428 	hasThemeTOML, _ := afero.Exists(c.fs, themeTOML)
  429 	if hasThemeTOML {
  430 		data, err := afero.ReadFile(c.fs, themeTOML)
  431 		if err != nil {
  432 			return err
  433 		}
  434 		themeCfg, err = metadecoders.Default.UnmarshalToMap(data, metadecoders.TOML)
  435 		if err != nil {
  436 			c.logger.Warnf("Failed to read module config for %q in %q: %s", tc.Path(), themeTOML, err)
  437 		} else {
  438 			maps.PrepareParams(themeCfg)
  439 		}
  440 	}
  441 
  442 	if hasConfigFile {
  443 		if configFilename != "" {
  444 			var err error
  445 			tc.cfg, err = config.FromFile(c.fs, configFilename)
  446 			if err != nil {
  447 				return err
  448 			}
  449 		}
  450 
  451 		tc.configFilenames = append(tc.configFilenames, configFilename)
  452 
  453 	}
  454 
  455 	// Also check for a config dir, which we overlay on top of the file configuration.
  456 	configDir := filepath.Join(tc.Dir(), "config")
  457 	dcfg, dirnames, err := config.LoadConfigFromDir(c.fs, configDir, c.ccfg.Environment)
  458 	if err != nil {
  459 		return err
  460 	}
  461 
  462 	if len(dirnames) > 0 {
  463 		tc.configFilenames = append(tc.configFilenames, dirnames...)
  464 
  465 		if hasConfigFile {
  466 			// Set will overwrite existing keys.
  467 			tc.cfg.Set("", dcfg.Get(""))
  468 		} else {
  469 			tc.cfg = dcfg
  470 		}
  471 	}
  472 
  473 	config, err := decodeConfig(tc.cfg, c.moduleConfig.replacementsMap)
  474 	if err != nil {
  475 		return err
  476 	}
  477 
  478 	const oldVersionKey = "min_version"
  479 
  480 	if hasThemeTOML {
  481 
  482 		// Merge old with new
  483 		if minVersion, found := themeCfg[oldVersionKey]; found {
  484 			if config.HugoVersion.Min == "" {
  485 				config.HugoVersion.Min = hugo.VersionString(cast.ToString(minVersion))
  486 			}
  487 		}
  488 
  489 		if config.Params == nil {
  490 			config.Params = make(map[string]any)
  491 		}
  492 
  493 		for k, v := range themeCfg {
  494 			if k == oldVersionKey {
  495 				continue
  496 			}
  497 			config.Params[k] = v
  498 		}
  499 
  500 	}
  501 
  502 	tc.config = config
  503 
  504 	return nil
  505 }
  506 
  507 func (c *collector) collect() {
  508 	defer c.logger.PrintTimerIfDelayed(time.Now(), "hugo: collected modules")
  509 	d := debounce.New(2 * time.Second)
  510 	d(func() {
  511 		c.logger.Println("hugo: downloading modules …")
  512 	})
  513 	defer d(func() {})
  514 
  515 	if err := c.initModules(); err != nil {
  516 		c.err = err
  517 		return
  518 	}
  519 
  520 	projectMod := createProjectModule(c.gomods.GetMain(), c.ccfg.WorkingDir, c.moduleConfig)
  521 
  522 	if err := c.addAndRecurse(projectMod, false); err != nil {
  523 		c.err = err
  524 		return
  525 	}
  526 
  527 	// Add the project mod on top.
  528 	c.modules = append(Modules{projectMod}, c.modules...)
  529 }
  530 
  531 func (c *collector) isVendored(dir string) bool {
  532 	_, err := c.fs.Stat(filepath.Join(dir, vendord, vendorModulesFilename))
  533 	return err == nil
  534 }
  535 
  536 func (c *collector) collectModulesTXT(owner Module) error {
  537 	vendorDir := filepath.Join(owner.Dir(), vendord)
  538 	filename := filepath.Join(vendorDir, vendorModulesFilename)
  539 
  540 	f, err := c.fs.Open(filename)
  541 	if err != nil {
  542 		if os.IsNotExist(err) {
  543 			return nil
  544 		}
  545 
  546 		return err
  547 	}
  548 
  549 	defer f.Close()
  550 
  551 	scanner := bufio.NewScanner(f)
  552 
  553 	for scanner.Scan() {
  554 		// # github.com/alecthomas/chroma v0.6.3
  555 		line := scanner.Text()
  556 		line = strings.Trim(line, "# ")
  557 		line = strings.TrimSpace(line)
  558 		parts := strings.Fields(line)
  559 		if len(parts) != 2 {
  560 			return fmt.Errorf("invalid modules list: %q", filename)
  561 		}
  562 		path := parts[0]
  563 
  564 		shouldAdd := c.Client.moduleConfig.VendorClosest
  565 
  566 		if !shouldAdd {
  567 			if _, found := c.vendored[path]; !found {
  568 				shouldAdd = true
  569 			}
  570 		}
  571 
  572 		if shouldAdd {
  573 			c.vendored[path] = vendoredModule{
  574 				Owner:   owner,
  575 				Dir:     filepath.Join(vendorDir, path),
  576 				Version: parts[1],
  577 			}
  578 		}
  579 
  580 	}
  581 	return nil
  582 }
  583 
  584 func (c *collector) loadModules() error {
  585 	modules, err := c.listGoMods()
  586 	if err != nil {
  587 		return err
  588 	}
  589 	c.gomods = modules
  590 	return nil
  591 }
  592 
  593 // Matches postcss.config.js etc.
  594 var commonJSConfigs = regexp.MustCompile(`(babel|postcss|tailwind)\.config\.js`)
  595 
  596 func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
  597 	for _, m := range mounts {
  598 		if strings.HasPrefix(m.Target, files.JsConfigFolderMountPrefix) {
  599 			// This follows the convention of the other component types (assets, content, etc.),
  600 			// if one or more is specified by the user, we skip the defaults.
  601 			// These mounts were added to Hugo in 0.75.
  602 			return mounts, nil
  603 		}
  604 	}
  605 
  606 	// Mount the common JS config files.
  607 	fis, err := afero.ReadDir(c.fs, owner.Dir())
  608 	if err != nil {
  609 		return mounts, err
  610 	}
  611 
  612 	for _, fi := range fis {
  613 		n := fi.Name()
  614 
  615 		should := n == files.FilenamePackageHugoJSON || n == files.FilenamePackageJSON
  616 		should = should || commonJSConfigs.MatchString(n)
  617 
  618 		if should {
  619 			mounts = append(mounts, Mount{
  620 				Source: n,
  621 				Target: filepath.Join(files.ComponentFolderAssets, files.FolderJSConfig, n),
  622 			})
  623 		}
  624 
  625 	}
  626 
  627 	return mounts, nil
  628 }
  629 
  630 func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
  631 	var out []Mount
  632 	dir := owner.Dir()
  633 
  634 	for _, mnt := range mounts {
  635 		errMsg := fmt.Sprintf("invalid module config for %q", owner.Path())
  636 
  637 		if mnt.Source == "" || mnt.Target == "" {
  638 			return nil, errors.New(errMsg + ": both source and target must be set")
  639 		}
  640 
  641 		mnt.Source = filepath.Clean(mnt.Source)
  642 		mnt.Target = filepath.Clean(mnt.Target)
  643 		var sourceDir string
  644 
  645 		if owner.projectMod && filepath.IsAbs(mnt.Source) {
  646 			// Abs paths in the main project is allowed.
  647 			sourceDir = mnt.Source
  648 		} else {
  649 			sourceDir = filepath.Join(dir, mnt.Source)
  650 		}
  651 
  652 		// Verify that Source exists
  653 		_, err := c.fs.Stat(sourceDir)
  654 		if err != nil {
  655 			continue
  656 		}
  657 
  658 		// Verify that target points to one of the predefined component dirs
  659 		targetBase := mnt.Target
  660 		idxPathSep := strings.Index(mnt.Target, string(os.PathSeparator))
  661 		if idxPathSep != -1 {
  662 			targetBase = mnt.Target[0:idxPathSep]
  663 		}
  664 		if !files.IsComponentFolder(targetBase) {
  665 			return nil, fmt.Errorf("%s: mount target must be one of: %v", errMsg, files.ComponentFolders)
  666 		}
  667 
  668 		out = append(out, mnt)
  669 	}
  670 
  671 	return out, nil
  672 }
  673 
  674 func (c *collector) wrapModuleNotFound(err error) error {
  675 	err = fmt.Errorf(err.Error()+": %w", ErrNotExist)
  676 	if c.GoModulesFilename == "" {
  677 		return err
  678 	}
  679 
  680 	baseMsg := "we found a go.mod file in your project, but"
  681 
  682 	switch c.goBinaryStatus {
  683 	case goBinaryStatusNotFound:
  684 		return fmt.Errorf(baseMsg+" you need to install Go to use it. See https://golang.org/dl/ : %q", err)
  685 	case goBinaryStatusTooOld:
  686 		return fmt.Errorf(baseMsg+" you need to a newer version of Go to use it. See https://golang.org/dl/ : %w", err)
  687 	}
  688 
  689 	return err
  690 }
  691 
  692 type vendoredModule struct {
  693 	Owner   Module
  694 	Dir     string
  695 	Version string
  696 }
  697 
  698 func createProjectModule(gomod *goModule, workingDir string, conf Config) *moduleAdapter {
  699 	// Create a pseudo module for the main project.
  700 	var path string
  701 	if gomod == nil {
  702 		path = "project"
  703 	}
  704 
  705 	return &moduleAdapter{
  706 		path:       path,
  707 		dir:        workingDir,
  708 		gomod:      gomod,
  709 		projectMod: true,
  710 		config:     conf,
  711 	}
  712 }
  713 
  714 // In the first iteration of Hugo Modules, we do not support multiple
  715 // major versions running at the same time, so we pick the first (upper most).
  716 // We will investigate namespaces in future versions.
  717 // TODO(bep) add a warning when the above happens.
  718 func pathKey(p string) string {
  719 	prefix, _, _ := module.SplitPathVersion(p)
  720 
  721 	return strings.ToLower(prefix)
  722 }