js: Misc fixes
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 4 Nov 2020 15:13:37 +0000 (16:13 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 4 Nov 2020 18:21:43 +0000 (19:21 +0100)
* Fix resolve of package.json deps in submodules
* Fix directory logic for writing assets/jsconfig.json

Fixes #7924
Fixes #7923

go.mod
go.sum
hugolib/hugo_sites_build.go
hugolib/js_test.go
resources/resource_transformers/js/build.go
resources/resource_transformers/js/options.go

diff --git a/go.mod b/go.mod
index f48a2619ce4af51ef20579a74350be95a587144e..8281070868efc63a492390567e9b3fe7ded87fc7 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
        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
diff --git a/go.sum b/go.sum
index 8470f4778443905adf4d9dd993adcc2b5a129b9b..589f6c84b878af38fac6fd5f1703fed08f5ad0f0 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -170,6 +170,8 @@ github.com/evanw/esbuild v0.8.1 h1:AqGawd1vAh0l88ZzAyuG9/w4B3Hswt0wM5s05AYHYXo=
 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=
index 603772afd546d7ea6dc466766f699d96eed89acd..bd5c2b6619f4f73a2f10206c69859f7ff46fe4fe 100644 (file)
@@ -354,26 +354,31 @@ func (h *HugoSites) postProcess() error {
        // 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)
+                                               }
                                        }
                                }
                        }
index 6c27219f332eb11545faa0842a7d44f3ed421b06..25617b168765d08fe39714c7dc6d7b89972c4487 100644 (file)
@@ -176,12 +176,22 @@ path="github.com/gohugoio/hugoTestProjectJSModImports"
         
 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 @@ greeting: "greeting configured in mod2"
 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!";`)
 
 }
index 8a7c21592ef44a32e04ec7be67f0a03763836b01..3a7065e0d568eceb2f40371c857639d3d94cba28 100644 (file)
@@ -18,14 +18,12 @@ import (
        "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 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
                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 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
        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
-
-               if !strings.HasPrefix(path, "..") {
-                       // Try first in the assets fs
-                       var fi os.FileInfo
-                       fi, err = t.c.rs.BaseFs.Assets.Fs.Stat(path)
+
+               createErr := func(msg api.Message) error {
+                       loc := msg.Location
+                       path := loc.File
+
+                       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)
index 84a6c1a78452125c7c8437545a5ca680a12147e1..654dbbab9882b1275b7ae0265b65552c77d11049 100644 (file)
@@ -16,6 +16,7 @@ package js
 import (
        "encoding/json"
        "fmt"
+       "io/ioutil"
        "path/filepath"
        "strings"
        "sync"
@@ -31,6 +32,13 @@ import (
        "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 @@ type importCache struct {
        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 @@ func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
        }
 
        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 @@ func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
                        // 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 @@ func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
                        // 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 @@ func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
                                        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 createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
                                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,