Repository: gohugoio/hugo (language: go)

Issue:
Module configuration prevent initializing modules (`hugo mod init`)
<!--
Please do not use the issue queue for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the forum:

https://discourse.gohugo.io
-->

<!-- Please answer these questions before submitting your issue. Thanks! -->

```sh
$ cat config/_default/hugo.yaml
module:
  imports:
    - path: github.com/hbstack/theme-start

$ hugo mod init example.com/user/repo
Error: failed to load modules: module "github.com/hbstack/theme-start" not found in "/tmp/start-page/themes/github.com/hbstack/theme-start"; either add it as a Hugo Module or store it in "/tmp/start-page/themes".: module does not exist
```

Hi, I encountered an strange issue that couldn't `hugo mod init` with existing module configurations, have to remove/comment those imports first.

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.118.2-da7983ac4b94d97d776d7c2405040de97e95c03d+extended linux/amd64 BuildDate=2023-08-31T11:23:51Z VendorInfo=gohugoio
</pre>

### Does this issue reproduce with the latest release?

Yes



Discussion / hints:
Same problem here with:
hugo v0.123.8+extended linux/amd64
docsy theme
Example: https://gitlab.com/Marzal/mundolibre/-/jobs/6357113230
same issue for me with any mod I found so far (themes like hugo-xmin, gethugothemes-modules,..).
Same on MacOS with Hugo v0.125.6+extended darwin/arm64 and WSL hugo v0.125.6 linux/amd64
Experiencing same on `hugo v0.127.0-74e0f3bd63c51f3c7a0f07a7c779eec9e922957e+extended linux/amd64 BuildDate=2024-06-05T10:27:59Z VendorInfo=snap:0.127.0`
For me the workaround is to append `--config fake-config.toml` to the `hugo mod init`

`hugo mod init gitlab.com/Marzal/mundolibre.gitlab.io --config fake-config.toml`


Fix patch:
diff --git a/commands/commandeer.go b/commands/commandeer.go
index f18a95bb997..e8cde2114f3 100644
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -88,6 +88,11 @@ type commonConfig struct {
 	fs      *hugofs.Fs
 }
 
+type configKey struct {
+	counter                    int32
+	ignoreModulesDoesNotExists bool
+}
+
 // This is the root command.
 type rootCommand struct {
 	Printf  func(format string, v ...interface{})
@@ -101,8 +106,8 @@ type rootCommand struct {
 
 	// Some, but not all commands need access to these.
 	// Some needs more than one, so keep them in a small cache.
-	commonConfigs *lazycache.Cache[int32, *commonConfig]
-	hugoSites     *lazycache.Cache[int32, *hugolib.HugoSites]
+	commonConfigs *lazycache.Cache[configKey, *commonConfig]
+	hugoSites     *lazycache.Cache[configKey, *hugolib.HugoSites]
 
 	// changesFromBuild received from Hugo in watch mode.
 	changesFromBuild chan []identity.Identity
@@ -160,17 +165,18 @@ func (r *rootCommand) Commands() []simplecobra.Commander {
 	return r.commands
 }
 
-func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commonConfig, error) {
-	cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) {
+func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*commonConfig, error) {
+	cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
 		fs := oldConf.fs
 		configs, err := allconfig.LoadConfig(
 			allconfig.ConfigSourceDescriptor{
-				Flags:       oldConf.cfg,
-				Fs:          fs.Source,
-				Filename:    r.cfgFile,
-				ConfigDir:   r.cfgDir,
-				Logger:      r.logger,
-				Environment: r.environment,
+				Flags:                    oldConf.cfg,
+				Fs:                       fs.Source,
+				Filename:                 r.cfgFile,
+				ConfigDir:                r.cfgDir,
+				Logger:                   r.logger,
+				Environment:              r.environment,
+				IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
 			},
 		)
 		if err != nil {
@@ -193,11 +199,11 @@ func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commo
 	return cc, err
 }
 
-func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commonConfig, error) {
+func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*commonConfig, error) {
 	if cfg == nil {
 		panic("cfg must be set")
 	}
-	cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) {
+	cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) {
 		var dir string
 		if r.source != "" {
 			dir, _ = filepath.Abs(r.source)
@@ -220,12 +226,13 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
 		// Load the config first to allow publishDir to be configured in config file.
 		configs, err := allconfig.LoadConfig(
 			allconfig.ConfigSourceDescriptor{
-				Flags:       cfg,
-				Fs:          hugofs.Os,
-				Filename:    r.cfgFile,
-				ConfigDir:   r.cfgDir,
-				Environment: r.environment,
-				Logger:      r.logger,
+				Flags:                    cfg,
+				Fs:                       hugofs.Os,
+				Filename:                 r.cfgFile,
+				ConfigDir:                r.cfgDir,
+				Environment:              r.environment,
+				Logger:                   r.logger,
+				IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
 			},
 		)
 		if err != nil {
@@ -307,7 +314,8 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
 }
 
 func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
-	h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) {
+	k := configKey{counter: r.configVersionID.Load()}
+	h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
 		depsCfg := r.newDepsConfig(conf)
 		return hugolib.NewHugoSites(depsCfg)
 	})
@@ -315,7 +323,12 @@ func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, err
 }
 
 func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) {
-	h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) {
+	return r.getOrCreateHugo(cfg, false)
+}
+
+func (r *rootCommand) getOrCreateHugo(cfg config.Provider, ignoreModuleDoesNotExist bool) (*hugolib.HugoSites, error) {
+	k := configKey{counter: r.configVersionID.Load(), ignoreModulesDoesNotExists: ignoreModuleDoesNotExist}
+	h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
 		conf, err := r.ConfigFromProvider(key, cfg)
 		if err != nil {
 			return nil, err
@@ -418,11 +431,11 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
 
 	r.changesFromBuild = make(chan []identity.Identity, 10)
 
-	r.commonConfigs = lazycache.New(lazycache.Options[int32, *commonConfig]{MaxEntries: 5})
+	r.commonConfigs = lazycache.New(lazycache.Options[configKey, *commonConfig]{MaxEntries: 5})
 	// We don't want to keep stale HugoSites in memory longer than needed.
-	r.hugoSites = lazycache.New(lazycache.Options[int32, *hugolib.HugoSites]{
+	r.hugoSites = lazycache.New(lazycache.Options[configKey, *hugolib.HugoSites]{
 		MaxEntries: 1,
-		OnEvict: func(key int32, value *hugolib.HugoSites) {
+		OnEvict: func(key configKey, value *hugolib.HugoSites) {
 			value.Close()
 			runtime.GC()
 		},
diff --git a/commands/config.go b/commands/config.go
index adf6bbe2bbd..c3d08ae22d4 100644
--- a/commands/config.go
+++ b/commands/config.go
@@ -58,7 +58,7 @@ func (c *configCommand) Name() string {
 }
 
 func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
-	conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
+	conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil))
 	if err != nil {
 		return err
 	}
@@ -209,7 +209,7 @@ func (c *configMountsCommand) Name() string {
 
 func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
 	r := c.configCmd.r
-	conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
+	conf, err := r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil))
 	if err != nil {
 		return err
 	}
diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go
index d15d47326dd..42bf68a3726 100644
--- a/commands/hugobuilder.go
+++ b/commands/hugobuilder.go
@@ -1047,7 +1047,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
 		"fastRenderMode": c.fastRenderMode,
 	})
 
-	conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg))
+	conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, cfg))
 	if err != nil {
 		return err
 	}
@@ -1116,7 +1116,7 @@ func (c *hugoBuilder) reloadConfig() error {
 
 	if err := c.withConfE(func(conf *commonConfig) error {
 		oldConf := conf
-		newConf, err := c.r.ConfigFromConfig(c.r.configVersionID.Load(), conf)
+		newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf)
 		if err != nil {
 			return err
 		}
diff --git a/commands/mod.go b/commands/mod.go
index 0fa83e5c005..a9c8c459d00 100644
--- a/commands/mod.go
+++ b/commands/mod.go
@@ -94,7 +94,7 @@ so this may/will change in future versions of Hugo.
 					applyLocalFlagsBuildConfig(cmd, r)
 				},
 				run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
-					h, err := r.Hugo(flagsToCfg(cd, nil))
+					h, err := r.getOrCreateHugo(flagsToCfg(cd, nil), true)
 					if err != nil {
 						return err
 					}
@@ -102,7 +102,11 @@ so this may/will change in future versions of Hugo.
 					if len(args) >= 1 {
 						initPath = args[0]
 					}
-					return h.Configs.ModulesClient.Init(initPath)
+					c := h.Configs.ModulesClient
+					if err := c.Init(initPath); err != nil {
+						return err
+					}
+					return nil
 				},
 			},
 			&simpleCommand{
@@ -115,7 +119,7 @@ so this may/will change in future versions of Hugo.
 					cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
 				},
 				run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
-					conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
+					conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
 					if err != nil {
 						return err
 					}
@@ -135,7 +139,7 @@ Note that for vendored modules, that is the version listed and not the one from
 					cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
 				},
 				run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
-					conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
+					conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
 					if err != nil {
 						return err
 					}
@@ -271,7 +275,7 @@ Run "go help get" for more information. All flags available for "go get" is also
 
 								cfg := config.New()
 								cfg.Set("workingDir", dir)
-								conf, err := r.ConfigFromProvider(r.configVersionID.Add(1), flagsToCfg(cd, cfg))
+								conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Add(1)}, flagsToCfg(cd, cfg))
 								if err != nil {
 									return err
 								}
@@ -284,7 +288,7 @@ Run "go help get" for more information. All flags available for "go get" is also
 						})
 						return nil
 					} else {
-						conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
+						conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil))
 						if err != nil {
 							return err
 						}
@@ -313,7 +317,7 @@ func (c *modCommands) Name() string {
 }
 
 func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
-	_, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), nil)
+	_, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, nil)
 	if err != nil {
 		return err
 	}
diff --git a/commands/new.go b/commands/new.go
index 4bad0059ff5..f6bb09e0023 100644
--- a/commands/new.go
+++ b/commands/new.go
@@ -93,7 +93,7 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
 					cfg.Set("workingDir", createpath)
 					cfg.Set("publishDir", "public")
 
-					conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
+					conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg))
 					if err != nil {
 						return err
 					}
@@ -136,7 +136,7 @@ according to your needs.`,
 					cfg := config.New()
 					cfg.Set("publishDir", "public")
 
-					conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
+					conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg))
 					if err != nil {
 						return err
 					}
diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go
index f6dfa7260fc..35517ece2db 100644
--- a/config/allconfig/allconfig.go
+++ b/config/allconfig/allconfig.go
@@ -809,7 +809,7 @@ func (c *Configs) Init() error {
 	}
 
 	if len(c.Modules) == 0 {
-		return errors.New("no modules loaded (ned at least the main module)")
+		return errors.New("no modules loaded (need at least the main module)")
 	}
 
 	// Apply default project mounts.
diff --git a/config/allconfig/load.go b/config/allconfig/load.go
index 84419cb2ef5..16e2019cfa9 100644
--- a/config/allconfig/load.go
+++ b/config/allconfig/load.go
@@ -64,7 +64,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
 		return nil, fmt.Errorf("failed to create config from result: %w", err)
 	}
 
-	moduleConfig, modulesClient, err := l.loadModules(configs)
+	moduleConfig, modulesClient, err := l.loadModules(configs, d.IgnoreModuleDoesNotExist)
 	if err != nil {
 		return nil, fmt.Errorf("failed to load modules: %w", err)
 	}
@@ -116,6 +116,9 @@ type ConfigSourceDescriptor struct {
 
 	// Defaults to os.Environ if not set.
 	Environ []string
+
+	// If set, this will be used to ignore the module does not exist error.
+	IgnoreModuleDoesNotExist bool
 }
 
 func (d ConfigSourceDescriptor) configFilenames() []string {
@@ -453,7 +456,7 @@ func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConf
 	return res, l.ModulesConfig, err
 }
 
-func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *modules.Client, error) {
+func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bool) (modules.ModulesConfig, *modules.Client, error) {
 	bcfg := configs.LoadingInfo.BaseConfig
 	conf := configs.Base
 	workingDir := bcfg.WorkingDir
@@ -487,17 +490,18 @@ func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *mo
 	}
 
 	modulesClient := modules.NewClient(modules.ClientConfig{
-		Fs:                 l.Fs,
-		Logger:             l.Logger,
-		Exec:               ex,
-		HookBeforeFinalize: hook,
-		WorkingDir:         workingDir,
-		ThemesDir:          themesDir,
-		PublishDir:         publishDir,
-		Environment:        l.Environment,
-		CacheDir:           conf.Caches.CacheDirModules(),
-		ModuleConfig:       conf.Module,
-		IgnoreVendor:       ignoreVendor,
+		Fs:                       l.Fs,
+		Logger:                   l.Logger,
+		Exec:                     ex,
+		HookBeforeFinalize:       hook,
+		WorkingDir:               workingDir,
+		ThemesDir:                themesDir,
+		PublishDir:               publishDir,
+		Environment:              l.Environment,
+		CacheDir:                 conf.Caches.CacheDirModules(),
+		ModuleConfig:             conf.Module,
+		IgnoreVendor:             ignoreVendor,
+		IgnoreModuleDoesNotExist: ignoreModuleDoesNotExist,
 	})
 
 	moduleConfig, err := modulesClient.Collect()
diff --git a/modules/client.go b/modules/client.go
index dce40d2db9b..404605c8c16 100644
--- a/modules/client.go
+++ b/modules/client.go
@@ -633,7 +633,7 @@ func (c *Client) runGo(
 
 	argsv := collections.StringSliceToInterfaceSlice(args)
 	argsv = append(argsv, hexec.WithEnviron(c.environ))
-	argsv = append(argsv, hexec.WithStderr(io.MultiWriter(stderr, os.Stderr)))
+	argsv = append(argsv, hexec.WithStderr(goOutputReplacerWriter{w: io.MultiWriter(stderr, os.Stderr)}))
 	argsv = append(argsv, hexec.WithStdout(stdout))
 	argsv = append(argsv, hexec.WithDir(c.ccfg.WorkingDir))
 	argsv = append(argsv, hexec.WithContext(ctx))
@@ -679,6 +679,24 @@ If you then run 'hugo mod graph' it should resolve itself to the most recent ver
 	return nil
 }
 
+var goOutputReplacer = strings.NewReplacer(
+	"go: to add module requirements and sums:", "hugo: to add module requirements and sums:",
+	"go mod tidy", "hugo mod tidy",
+)
+
+type goOutputReplacerWriter struct {
+	w io.Writer
+}
+
+func (w goOutputReplacerWriter) Write(p []byte) (n int, err error) {
+	s := goOutputReplacer.Replace(string(p))
+	_, err = w.w.Write([]byte(s))
+	if err != nil {
+		return 0, err
+	}
+	return len(p), nil
+}
+
 func (c *Client) tidy(mods Modules, goModOnly bool) error {
 	isGoMod := make(map[string]bool)
 	for _, m := range mods {
@@ -742,6 +760,9 @@ type ClientConfig struct {
 	// This can be nil.
 	IgnoreVendor glob.Glob
 
+	// Ignore any module not found errors.
+	IgnoreModuleDoesNotExist bool
+
 	// Absolute path to the project dir.
 	WorkingDir string
 
diff --git a/modules/collect.go b/modules/collect.go
index cfb6d924942..4eb12a1a6a8 100644
--- a/modules/collect.go
+++ b/modules/collect.go
@@ -699,6 +699,9 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
 }
 
 func (c *collector) wrapModuleNotFound(err error) error {
+	if c.Client.ccfg.IgnoreModuleDoesNotExist {
+		return nil
+	}
 	err = fmt.Errorf(err.Error()+": %w", ErrNotExist)
 	if c.GoModulesFilename == "" {
 		return err


Test patch:
diff --git a/testscripts/commands/mod_init.txt b/testscripts/commands/mod_init.txt
new file mode 100644
index 00000000000..c09e71a23c9
--- /dev/null
+++ b/testscripts/commands/mod_init.txt
@@ -0,0 +1,15 @@
+# Test the hugo init command.
+dostounix golden/go.mod.testsubmod
+
+hugo mod init testsubmod
+cmpenv go.mod $WORK/golden/go.mod.testsubmod
+
+-- hugo.toml --
+title = "Hugo Modules Test"
+[module]
+[[module.imports]]
+path="github.com/bep/empty-hugo-module"
+-- golden/go.mod.testsubmod --
+module testsubmod
+
+go ${GOVERSION}
\ No newline at end of file