diff --git a/go.mod b/go.mod index f48a2619c..828107086 100644 --- 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 8470f4778..589f6c84b 100644 --- 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= diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 603772afd..bd5c2b661 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -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) + } } } } diff --git a/hugolib/js_test.go b/hugolib/js_test.go index 6c27219f3..25617b168 100644 --- a/hugolib/js_test.go +++ b/hugolib/js_test.go @@ -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!";`) } diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go index 8a7c21592..3a7065e0d 100644 --- a/resources/resource_transformers/js/build.go +++ b/resources/resource_transformers/js/build.go @@ -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 + 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 !strings.HasPrefix(path, "..") { - // Try first in the assets fs - var fi os.FileInfo - fi, err = t.c.rs.BaseFs.Assets.Fs.Stat(path) 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) + } + + var errors []error + + for _, msg := range result.Errors { + errors = append(errors, createErr(msg)) + } + + // Return 1, log the rest. + for i, err := range errors { + if i > 0 { + t.c.rs.Logger.Errorf("js.Build failed: %s", err) } } - if f == nil { - path = filepath.Join(t.c.rs.WorkingDir, path) - filename = path - f, err = t.c.rs.Fs.Os.Open(path) - } - - 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 fmt.Errorf("%s", result.Errors[0].Text) + return errors[0] } ctx.To.Write(result.OutputFiles[0].Contents) diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go index 84a6c1a78..654dbbab9 100644 --- a/resources/resource_transformers/js/options.go +++ b/resources/resource_transformers/js/options.go @@ -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 = "" +) + // 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 != "" && 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,