shithub: hugo

Download patch

ref: bf2837a314eaf70135791984a423b0b09f58741d
parent: cf6131dc18e5e833b021724a0d9bcdaa6827b8dd
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Wed Nov 4 11:13:37 EST 2020

js: Misc fixes

* Fix resolve of package.json deps in submodules
* Fix directory logic for writing assets/jsconfig.json

Fixes #7924
Fixes #7923

--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@
 	github.com/bep/tmc v0.5.1
 	github.com/disintegration/gift v1.2.1
 	github.com/dustin/go-humanize v1.0.0
-	github.com/evanw/esbuild v0.8.2
+	github.com/evanw/esbuild v0.8.3
 	github.com/fortytw2/leaktest v1.3.0
 	github.com/frankban/quicktest v1.11.1
 	github.com/fsnotify/fsnotify v1.4.9
--- a/go.sum
+++ b/go.sum
@@ -170,6 +170,8 @@
 github.com/evanw/esbuild v0.8.1/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
 github.com/evanw/esbuild v0.8.2 h1:pwvPPsU8dqwBLdPwBmETdp1ccpefC1l+8RKZD1PafcA=
 github.com/evanw/esbuild v0.8.2/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.8.3 h1:uPgAFhcGcNyMDrBnfUDcimt0N9AC9UsxeROkC8C27os=
+github.com/evanw/esbuild v0.8.3/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q=
 github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -354,26 +354,31 @@
 	// Write a jsconfig.json file to the project's /asset directory
 	// to help JS intellisense in VS Code etc.
 	if !h.ResourceSpec.BuildConfig.NoJSConfigInAssets && h.BaseFs.Assets.Dirs != nil {
-		m := h.BaseFs.Assets.Dirs[0].Meta()
-		assetsDir := m.Filename()
-		if strings.HasPrefix(assetsDir, h.ResourceSpec.WorkingDir) {
-			if jsConfig := h.ResourceSpec.JSConfigBuilder.Build(assetsDir); jsConfig != nil {
+		fi, err := h.BaseFs.Assets.Fs.Stat("")
+		if err != nil {
+			h.Log.Warnf("Failed to resolve jsconfig.json dir: %s", err)
+		} else {
+			m := fi.(hugofs.FileMetaInfo).Meta()
+			assetsDir := m.SourceRoot()
+			if strings.HasPrefix(assetsDir, h.ResourceSpec.WorkingDir) {
+				if jsConfig := h.ResourceSpec.JSConfigBuilder.Build(assetsDir); jsConfig != nil {
 
-				b, err := json.MarshalIndent(jsConfig, "", " ")
-				if err != nil {
-					h.Log.Warnf("Failed to create jsconfig.json: %s", err)
+					b, err := json.MarshalIndent(jsConfig, "", " ")
+					if err != nil {
+						h.Log.Warnf("Failed to create jsconfig.json: %s", err)
 
-				} else {
-					filename := filepath.Join(assetsDir, "jsconfig.json")
-					if h.running {
-						h.skipRebuildForFilenamesMu.Lock()
-						h.skipRebuildForFilenames[filename] = true
-						h.skipRebuildForFilenamesMu.Unlock()
-					}
-					// Make sure it's  written to the OS fs as this is used by
-					// editors.
-					if err := afero.WriteFile(hugofs.Os, filename, b, 0666); err != nil {
-						h.Log.Warnf("Failed to write jsconfig.json: %s", err)
+					} else {
+						filename := filepath.Join(assetsDir, "jsconfig.json")
+						if h.running {
+							h.skipRebuildForFilenamesMu.Lock()
+							h.skipRebuildForFilenames[filename] = true
+							h.skipRebuildForFilenamesMu.Unlock()
+						}
+						// Make sure it's  written to the OS fs as this is used by
+						// editors.
+						if err := afero.WriteFile(hugofs.Os, filename, b, 0666); err != nil {
+							h.Log.Warnf("Failed to write jsconfig.json: %s", err)
+						}
 					}
 				}
 			}
--- a/hugolib/js_test.go
+++ b/hugolib/js_test.go
@@ -176,12 +176,22 @@
         
 go 1.15
         
-require github.com/gohugoio/hugoTestProjectJSModImports v0.3.0 // indirect
+require github.com/gohugoio/hugoTestProjectJSModImports v0.5.0 // indirect
 
 `)
 
 	b.WithContent("p1.md", "").WithNothingAdded()
 
+	b.WithSourceFile("package.json", `{
+ "dependencies": {
+  "date-fns": "^2.16.1"
+ }
+}`)
+
+	b.Assert(os.Chdir(workDir), qt.IsNil)
+	_, err = exec.Command("npm", "install").CombinedOutput()
+	b.Assert(err, qt.IsNil)
+
 	b.Build(BuildCfg{})
 
 	b.AssertFileContent("public/js/main.js", `
@@ -189,8 +199,9 @@
 Hello1 from mod1: $
 return "Hello2 from mod1";
 var Hugo = "Rocks!";
-return "Hello3 from mod2";
-return "Hello from lib in the main project";
+Hello3 from mod2. Date from date-fns: ${today}
+Hello from lib in the main project
+Hello5 from mod2.
 var myparam = "Hugo Rocks!";`)
 
 }
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -18,14 +18,12 @@
 	"fmt"
 	"io/ioutil"
 	"os"
-	"path"
-	"path/filepath"
 	"strings"
 
-	"github.com/gohugoio/hugo/hugofs"
-
 	"github.com/spf13/afero"
 
+	"github.com/gohugoio/hugo/hugofs"
+
 	"github.com/gohugoio/hugo/common/herrors"
 
 	"github.com/gohugoio/hugo/hugolib/filesystems"
@@ -79,10 +77,9 @@
 		return err
 	}
 
-	sdir, _ := path.Split(ctx.SourcePath)
 	opts.sourcefile = ctx.SourcePath
-	opts.resolveDir = t.c.sfs.RealFilename(sdir)
 	opts.workDir = t.c.rs.WorkingDir
+	opts.resolveDir = opts.workDir
 	opts.contents = string(src)
 	opts.mediaType = ctx.InMediaType
 
@@ -99,39 +96,54 @@
 	result := api.Build(buildOptions)
 
 	if len(result.Errors) > 0 {
-		first := result.Errors[0]
-		loc := first.Location
-		path := loc.File
 
-		var err error
-		var f afero.File
-		var filename string
+		createErr := func(msg api.Message) error {
+			loc := msg.Location
+			path := loc.File
 
-		if !strings.HasPrefix(path, "..") {
-			// Try first in the assets fs
-			var fi os.FileInfo
-			fi, err = t.c.rs.BaseFs.Assets.Fs.Stat(path)
+			var (
+				f   afero.File
+				err error
+			)
+
+			if strings.HasPrefix(path, nsImportHugo) {
+				path = strings.TrimPrefix(path, nsImportHugo+":")
+				f, err = hugofs.Os.Open(path)
+			} else {
+				var fi os.FileInfo
+				fi, err = t.c.sfs.Fs.Stat(path)
+				if err == nil {
+					m := fi.(hugofs.FileMetaInfo).Meta()
+					path = m.Filename()
+					f, err = m.Open()
+				}
+
+			}
+
 			if err == nil {
-				m := fi.(hugofs.FileMetaInfo).Meta()
-				filename = m.Filename()
-				f, err = m.Open()
+				fe := herrors.NewFileError("js", 0, loc.Line, loc.Column, errors.New(msg.Text))
+				err, _ := herrors.WithFileContext(fe, path, f, herrors.SimpleLineMatcher)
+				f.Close()
+				return err
 			}
+
+			return fmt.Errorf("%s", msg.Text)
 		}
 
-		if f == nil {
-			path = filepath.Join(t.c.rs.WorkingDir, path)
-			filename = path
-			f, err = t.c.rs.Fs.Os.Open(path)
+		var errors []error
+
+		for _, msg := range result.Errors {
+			errors = append(errors, createErr(msg))
 		}
 
-		if err == nil {
-			fe := herrors.NewFileError("js", 0, loc.Line, loc.Column, errors.New(first.Text))
-			err, _ := herrors.WithFileContext(fe, filename, f, herrors.SimpleLineMatcher)
-			f.Close()
-			return err
+		// Return 1, log the rest.
+		for i, err := range errors {
+			if i > 0 {
+				t.c.rs.Logger.Errorf("js.Build failed: %s", err)
+			}
 		}
 
-		return fmt.Errorf("%s", result.Errors[0].Text)
+		return errors[0]
 	}
 
 	ctx.To.Write(result.OutputFiles[0].Contents)
--- a/resources/resource_transformers/js/options.go
+++ b/resources/resource_transformers/js/options.go
@@ -16,6 +16,7 @@
 import (
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"path/filepath"
 	"strings"
 	"sync"
@@ -31,6 +32,13 @@
 	"github.com/spf13/cast"
 )
 
+const (
+	nsImportHugo = "ns-hugo"
+	nsParams     = "ns-params"
+
+	stdinImporter = "<stdin>"
+)
+
 // Options esbuild configuration
 type Options struct {
 	// If not set, the source path will be used as the base target path.
@@ -111,6 +119,26 @@
 	m map[string]api.OnResolveResult
 }
 
+var extensionToLoaderMap = map[string]api.Loader{
+	".js":   api.LoaderJS,
+	".mjs":  api.LoaderJS,
+	".cjs":  api.LoaderJS,
+	".jsx":  api.LoaderJSX,
+	".ts":   api.LoaderTS,
+	".tsx":  api.LoaderTSX,
+	".css":  api.LoaderCSS,
+	".json": api.LoaderJSON,
+	".txt":  api.LoaderText,
+}
+
+func loaderFromFilename(filename string) api.Loader {
+	l, found := extensionToLoaderMap[filepath.Ext(filename)]
+	if found {
+		return l
+	}
+	return api.LoaderJS
+}
+
 func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
 	fs := c.rs.Assets
 
@@ -119,20 +147,21 @@
 	}
 
 	resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
-		relDir := fs.MakePathRelative(args.ResolveDir)
 
-		if relDir == "" {
-			// Not in a Hugo Module, probably in node_modules.
-			return api.OnResolveResult{}, nil
+		isStdin := args.Importer == stdinImporter
+		var relDir string
+		if !isStdin {
+			relDir = filepath.Dir(fs.MakePathRelative(args.Importer))
+		} else {
+			relDir = filepath.Dir(opts.sourcefile)
 		}
 
 		impPath := args.Path
 
-		// stdin is the main entry file which already is at the relative root.
 		// Imports not starting with a "." is assumed to live relative to /assets.
 		// Hugo makes no assumptions about the directory structure below /assets.
-		if args.Importer != "<stdin>" && strings.HasPrefix(impPath, ".") {
-			impPath = filepath.Join(relDir, args.Path)
+		if relDir != "" && strings.HasPrefix(impPath, ".") {
+			impPath = filepath.Join(relDir, impPath)
 		}
 
 		findFirst := func(base string) hugofs.FileMeta {
@@ -164,6 +193,7 @@
 			// It may be a regular file imported without an extension.
 			m = findFirst(impPath)
 		}
+		//
 
 		if m != nil {
 			// Store the source root so we can create a jsconfig.json
@@ -172,9 +202,11 @@
 			// in server mode, we may get stale entries on renames etc.,
 			// but that shouldn't matter too much.
 			c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot())
-			return api.OnResolveResult{Path: m.Filename(), Namespace: ""}, nil
+			return api.OnResolveResult{Path: m.Filename(), Namespace: nsImportHugo}, nil
 		}
 
+		// Not found in /assets. Probably in node_modules. ESBuild will handle that
+		// rather complex logic.
 		return api.OnResolveResult{}, nil
 	}
 
@@ -205,6 +237,23 @@
 					return imp, nil
 
 				})
+			build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsImportHugo},
+				func(args api.OnLoadArgs) (api.OnLoadResult, error) {
+					b, err := ioutil.ReadFile(args.Path)
+
+					if err != nil {
+						return api.OnLoadResult{}, errors.Wrapf(err, "failed to read %q", args.Path)
+					}
+					c := string(b)
+					return api.OnLoadResult{
+						// See https://github.com/evanw/esbuild/issues/502
+						// This allows all modules to resolve dependencies
+						// in the main project's node_modules.
+						ResolveDir: opts.resolveDir,
+						Contents:   &c,
+						Loader:     loaderFromFilename(args.Path),
+					}, nil
+				})
 		},
 	}
 
@@ -226,10 +275,10 @@
 				func(args api.OnResolveArgs) (api.OnResolveResult, error) {
 					return api.OnResolveResult{
 						Path:      args.Path,
-						Namespace: "params",
+						Namespace: nsParams,
 					}, nil
 				})
-			build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "params"},
+			build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsParams},
 				func(args api.OnLoadArgs) (api.OnLoadResult, error) {
 					return api.OnLoadResult{
 						Contents: &bs,