Repository: gohugoio/hugo (language: go)

Issue:
Add hugo.Deps
There are situations where it would be useful to have the Hugo Module dependency information available in the templates (mostly, for JS libraries built from /assets, to know: What version of this library is currently running?).

So I propose we add `hugo.Deps` that lists the dependencies with some basic info:

* Path (e.g. `github.com/bep/mylib`)
* Version (e.g. `v1.0.0`)
* Hash (source hash)

~~And then a lookup method so you can do `$mylib := hugo.Deps.ByPath "foo"`~~





Discussion / hints:
Do we need a lookup method? Can't `where` do the job?

Fix patch:
diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go
index d8f92e2983f..c8db18afee8 100644
--- a/common/hugo/hugo.go
+++ b/common/hugo/hugo.go
@@ -21,6 +21,7 @@ import (
 	"runtime/debug"
 	"sort"
 	"strings"
+	"time"
 
 	"github.com/gohugoio/hugo/hugofs/files"
 
@@ -57,6 +58,8 @@ type Info struct {
 	// This can also be set by the user.
 	// It can be any string, but it will be all lower case.
 	Environment string
+
+	deps []*Dependency
 }
 
 // Version returns the current version as a comparable version string.
@@ -77,8 +80,13 @@ func (i Info) IsExtended() bool {
 	return IsExtended
 }
 
+// Deps gets a list of dependencies for this Hugo build.
+func (i Info) Deps() []*Dependency {
+	return i.deps
+}
+
 // NewInfo creates a new Hugo Info object.
-func NewInfo(environment string) Info {
+func NewInfo(environment string, deps []*Dependency) Info {
 	if environment == "" {
 		environment = EnvironmentProduction
 	}
@@ -86,6 +94,7 @@ func NewInfo(environment string) Info {
 		CommitHash:  commitHash,
 		BuildDate:   buildDate,
 		Environment: environment,
+		deps:        deps,
 	}
 }
 
@@ -156,3 +165,27 @@ func IsRunningAsTest() bool {
 	}
 	return false
 }
+
+// Dependency is a single dependency, which can be either a Hugo Module or a local theme.
+type Dependency struct {
+	// Returns the path to this module.
+	// This will either be the module path, e.g. "github.com/gohugoio/myshortcodes",
+	// or the path below your /theme folder, e.g. "mytheme".
+	Path string
+
+	// The module version.
+	Version string
+
+	// Whether this dependency is vendored.
+	Vendor bool
+
+	// Time version was created.
+	Time time.Time
+
+	// In the dependency tree, this is the first module that defines this module
+	// as a dependency.
+	Owner *Dependency
+
+	// Replaced by this dependency.
+	Replace *Dependency
+}
diff --git a/docs/content/en/functions/hugo.md b/docs/content/en/functions/hugo.md
index 6cbb36019ca..fb20d2717a8 100644
--- a/docs/content/en/functions/hugo.md
+++ b/docs/content/en/functions/hugo.md
@@ -49,3 +49,67 @@ hugo.IsProduction
 {{% note "Use the Hugo Generator Tag" %}}
 We highly recommend using `hugo.Generator` in your website's `<head>`. `hugo.Generator` is included by default in all themes hosted on [themes.gohugo.io](https://themes.gohugo.io). The generator tag allows the Hugo team to track the usage and popularity of Hugo.
 {{% /note %}}
+
+hugo.Deps
+: See [hugo.Deps](#hugodeps)
+
+
+## hugo.Deps
+
+{{< new-in "0.92.0" >}}
+
+`hugo.Deps` returns a list of dependencies for a project (either Hugo Modules or local theme components).
+
+Eeach dependency contains:
+
+Path (string)
+: Returns the path to this module. This will either be the module path, e.g. "github.com/gohugoio/myshortcodes", or the path below your /theme folder, e.g. "mytheme".
+
+Version (string)
+:  The module version.
+	
+Vendor (bool)
+: Whether this dependency is vendored.
+ 
+Time (time.Time)
+: Time version was created.
+
+Owner
+: In the dependency tree, this is the first module that defines this module as a dependency.
+
+Replace (*Dependency)
+: Replaced by this dependency.
+
+An example table listing the dependencies:
+
+```html
+ <h2>Dependencies</h2>
+<table class="table table-dark">
+  <thead>
+    <tr>
+      <th scope="col">#</th>
+      <th scope="col">Path</th>
+      <th scope="col">Version</th>
+      <th scope="col">Time</th>
+      <th scope="col">Vendor</th>
+    </tr>
+  </thead>
+  <tbody>
+    {{ range $index, $element := hugo.Deps }}
+    <tr>
+      <th scope="row">{{ add $index 1 }}</th>
+      <td>{{ with $element.Owner }}{{.Path }}{{ else }}PROJECT{{ end }}</td>
+      <td>
+        {{ $element.Path }}
+        {{ with $element.Replace}}
+        => {{ .Path }}
+        {{ end }}
+      </td>
+      <td>{{ $element.Version }}</td>
+      <td>{{ with $element.Time }}{{ . }}{{ end }}</td>
+      <td>{{ $element.Vendor }}</td>
+    </tr>
+    {{ end }}
+  </tbody>
+</table>
+```
\ No newline at end of file
diff --git a/hugolib/site.go b/hugolib/site.go
index 624630d8019..13d5482b1ef 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -31,6 +31,7 @@ import (
 	"time"
 
 	"github.com/gohugoio/hugo/common/types"
+	"github.com/gohugoio/hugo/modules"
 	"golang.org/x/text/unicode/norm"
 
 	"github.com/gohugoio/hugo/common/paths"
@@ -1333,6 +1334,32 @@ func (s *Site) initializeSiteInfo() error {
 		}
 	}
 
+	// Assemble dependencies to be used in hugo.Deps.
+	// TODO(bep) another reminder: We need to clean up this Site vs HugoSites construct.
+	var deps []*hugo.Dependency
+	var depFromMod func(m modules.Module) *hugo.Dependency
+	depFromMod = func(m modules.Module) *hugo.Dependency {
+		dep := &hugo.Dependency{
+			Path:    m.Path(),
+			Version: m.Version(),
+			Time:    m.Time(),
+			Vendor:  m.Vendor(),
+		}
+
+		// These are pointers, but this all came from JSON so there's no recursive navigation,
+		// so just create new values.
+		if m.Replace() != nil {
+			dep.Replace = depFromMod(m.Replace())
+		}
+		if m.Owner() != nil {
+			dep.Owner = depFromMod(m.Owner())
+		}
+		return dep
+	}
+	for _, m := range s.Paths.AllModules {
+		deps = append(deps, depFromMod(m))
+	}
+
 	s.Info = &SiteInfo{
 		title:                          lang.GetString("title"),
 		Author:                         lang.GetStringMap("author"),
@@ -1351,7 +1378,7 @@ func (s *Site) initializeSiteInfo() error {
 		permalinks:                     permalinks,
 		owner:                          s.h,
 		s:                              s,
-		hugoInfo:                       hugo.NewInfo(s.Cfg.GetString("environment")),
+		hugoInfo:                       hugo.NewInfo(s.Cfg.GetString("environment"), deps),
 	}
 
 	rssOutputFormat, found := s.outputFormats[page.KindHome].GetByName(output.RSSFormat.Name)
diff --git a/resources/page/site.go b/resources/page/site.go
index 31058637b76..9728df691d6 100644
--- a/resources/page/site.go
+++ b/resources/page/site.go
@@ -120,7 +120,7 @@ func (t testSite) Data() map[string]interface{} {
 // NewDummyHugoSite creates a new minimal test site.
 func NewDummyHugoSite(cfg config.Provider) Site {
 	return testSite{
-		h: hugo.NewInfo(hugo.EnvironmentProduction),
+		h: hugo.NewInfo(hugo.EnvironmentProduction, nil),
 		l: langs.NewLanguage("en", cfg),
 	}
 }


Test patch:
diff --git a/common/hugo/hugo_test.go b/common/hugo/hugo_test.go
index 0631be62c06..ff36cab7cef 100644
--- a/common/hugo/hugo_test.go
+++ b/common/hugo/hugo_test.go
@@ -23,7 +23,7 @@ import (
 func TestHugoInfo(t *testing.T) {
 	c := qt.New(t)
 
-	hugoInfo := NewInfo("")
+	hugoInfo := NewInfo("", nil)
 
 	c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
 	c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
@@ -34,6 +34,6 @@ func TestHugoInfo(t *testing.T) {
 	c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
 	c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
 
-	devHugoInfo := NewInfo("development")
+	devHugoInfo := NewInfo("development", nil)
 	c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
 }
diff --git a/hugolib/hugo_smoke_test.go b/hugolib/hugo_smoke_test.go
index 798504f0d14..46aecf9cc17 100644
--- a/hugolib/hugo_smoke_test.go
+++ b/hugolib/hugo_smoke_test.go
@@ -162,7 +162,7 @@ Some **Markdown** in JSON shortcode.
 	b.WithContent("blog/mybundle/mydata.csv", "Bundled CSV")
 
 	const (
-		commonPageTemplate            = `|{{ .Kind }}|{{ .Title }}|{{ .Path }}|{{ .Summary }}|{{ .Content }}|RelPermalink: {{ .RelPermalink }}|WordCount: {{ .WordCount }}|Pages: {{ .Pages }}|Data Pages: Pages({{ len .Data.Pages }})|Resources: {{ len .Resources }}|Summary: {{ .Summary }}`
+		commonPageTemplate            = `|{{ .Kind }}|{{ .Title }}|{{ .File.Path }}|{{ .Summary }}|{{ .Content }}|RelPermalink: {{ .RelPermalink }}|WordCount: {{ .WordCount }}|Pages: {{ .Pages }}|Data Pages: Pages({{ len .Data.Pages }})|Resources: {{ len .Resources }}|Summary: {{ .Summary }}`
 		commonPaginatorTemplate       = `|Paginator: {{ with .Paginator }}{{ .PageNumber }}{{ else }}NIL{{ end }}`
 		commonListTemplateNoPaginator = `|{{ $pages := .Pages }}{{ if .IsHome }}{{ $pages = .Site.RegularPages }}{{ end }}{{ range $i, $e := ($pages | first 1) }}|Render {{ $i }}: {{ .Kind }}|{{ .Render "li" }}|{{ end }}|Site params: {{ $.Site.Params.hugo }}|RelPermalink: {{ .RelPermalink }}`
 		commonListTemplate            = commonPaginatorTemplate + `|{{ $pages := .Pages }}{{ if .IsHome }}{{ $pages = .Site.RegularPages }}{{ end }}{{ range $i, $e := ($pages | first 1) }}|Render {{ $i }}: {{ .Kind }}|{{ .Render "li" }}|{{ end }}|Site params: {{ $.Site.Params.hugo }}|RelPermalink: {{ .RelPermalink }}`