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