Add proper Media Type handling in js.Build
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 12 Jul 2020 10:47:14 +0000 (12:47 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 13 Jul 2020 08:56:23 +0000 (10:56 +0200)
See #732

go.mod
go.sum
hugolib/js_test.go
media/mediaType.go
media/mediaType_test.go
resources/resource_transformers/js/build.go
resources/resource_transformers/js/build_test.go [new file with mode: 0644]
tpl/internal/resourcehelpers/helpers.go [new file with mode: 0644]
tpl/js/js.go
tpl/resources/resources.go

diff --git a/go.mod b/go.mod
index fc84e01395a341d894487aef3f384593a75b922b..70eafd52ba138f9fa103fc886bc84e42a55fed89 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.6.1
+       github.com/evanw/esbuild v0.6.2
        github.com/fortytw2/leaktest v1.3.0
        github.com/frankban/quicktest v1.7.2
        github.com/fsnotify/fsnotify v1.4.7
diff --git a/go.sum b/go.sum
index a4ffd884010deb9e3754c2753b7d59fe14119488..7b5474951e279472d2ab41a3fe604de5938a1aac 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -119,6 +119,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/evanw/esbuild v0.6.1 h1:XkoACQJCiqUmwySWssu0/iUj7J6IbNMR9dqbSbh1/vk=
 github.com/evanw/esbuild v0.6.1/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.6.2 h1:pp33TIPgiHCtKL/gMW/V/PFHWNx/5cDTqbJHqAiy0jg=
+github.com/evanw/esbuild v0.6.2/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
 github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q=
 github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
index 1e59927c79fbce8eae3023ded21ee99880c7becb..a421ed338f3b9fe600c314c0df826ff2c59d8417 100644 (file)
@@ -15,7 +15,9 @@ package hugolib
 
 import (
        "os"
+       "os/exec"
        "path/filepath"
+       "runtime"
        "testing"
 
        "github.com/gohugoio/hugo/htesting"
@@ -29,11 +31,15 @@ import (
        "github.com/gohugoio/hugo/common/loggers"
 )
 
-func TestJS_Build(t *testing.T) {
+func TestJSBuildWithNPM(t *testing.T) {
        if !isCI() {
                t.Skip("skip (relative) long running modules test when running locally")
        }
 
+       if runtime.GOOS == "windows" {
+               t.Skip("skip NPM test on Windows")
+       }
+
        wd, _ := os.Getwd()
        defer func() {
                os.Chdir(wd)
@@ -43,13 +49,44 @@ func TestJS_Build(t *testing.T) {
 
        mainJS := `
        import "./included";
+       import { toCamelCase } from "to-camel-case";
+       
        console.log("main");
-       `
+       console.log("To camel:", toCamelCase("space case"));
+`
        includedJS := `
        console.log("included");
+       
        `
 
-       workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-babel")
+       jsxContent := `
+import * as React from 'react'
+import * as ReactDOM from 'react-dom'
+
+ ReactDOM.render(
+   <h1>Hello, world!</h1>,
+   document.getElementById('root')
+ );
+`
+
+       tsContent := `function greeter(person: string) {
+    return "Hello, " + person;
+}
+
+let user = [0, 1, 2];
+
+document.body.textContent = greeter(user);`
+
+       packageJSON := `{
+  "scripts": {},
+
+  "dependencies": {
+    "to-camel-case": "1.0.0"
+  }
+}
+`
+
+       workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js-npm")
        c.Assert(err, qt.IsNil)
        defer clean()
 
@@ -65,23 +102,99 @@ func TestJS_Build(t *testing.T) {
        b.WithContent("p1.md", "")
 
        b.WithTemplates("index.html", `
-       {{ $options := dict "minify" true }}
-       {{ $transpiled := resources.Get "js/main.js" | js.Build $options }}
-       Built: {{ $transpiled.Content | safeJS }}
-       `)
+{{ $options := dict "minify" false "externals" (slice "react" "react-dom") }}
+{{ $js := resources.Get "js/main.js" | js.Build $options }}
+JS:  {{ template "print" $js }}
+{{ $jsx := resources.Get "js/myjsx.jsx" | js.Build $options }}
+JSX: {{ template "print" $jsx }}
+{{ $ts := resources.Get "js/myts.ts" | js.Build }}
+TS: {{ template "print" $ts }}
+
+{{ define "print" }}RelPermalink: {{.RelPermalink}}|MIME: {{ .MediaType }}|Content: {{ .Content | safeJS }}{{ end }}
+
+`)
+
+       jsDir := filepath.Join(workDir, "assets", "js")
+       b.Assert(os.MkdirAll(jsDir, 0777), qt.IsNil)
+       b.Assert(os.Chdir(workDir), qt.IsNil)
+       b.WithSourceFile("package.json", packageJSON)
+       b.WithSourceFile("assets/js/main.js", mainJS)
+       b.WithSourceFile("assets/js/myjsx.jsx", jsxContent)
+       b.WithSourceFile("assets/js/myts.ts", tsContent)
+
+       b.WithSourceFile("assets/js/included.js", includedJS)
+
+       out, err := exec.Command("npm", "install").CombinedOutput()
+       b.Assert(err, qt.IsNil, qt.Commentf(string(out)))
+
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent("public/index.html", `
+console.log(&#34;included&#34;);
+if (hasSpace.test(string))
+const React = __toModule(require(&#34;react&#34;));
+function greeter(person) {
+`)
+
+}
+
+func TestJSBuild(t *testing.T) {
+       if !isCI() {
+               t.Skip("skip (relative) long running modules test when running locally")
+       }
+
+       wd, _ := os.Getwd()
+       defer func() {
+               os.Chdir(wd)
+       }()
+
+       c := qt.New(t)
+
+       mainJS := `
+       import "./included";
+       
+       console.log("main");
+
+`
+       includedJS := `
+       console.log("included");
+       
+       `
+
+       workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js")
+       c.Assert(err, qt.IsNil)
+       defer clean()
+
+       v := viper.New()
+       v.Set("workingDir", workDir)
+       v.Set("disableKinds", []string{"taxonomy", "term", "page"})
+       b := newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger())
+
+       b.Fs = hugofs.NewDefault(v)
+       b.WithWorkingDir(workDir)
+       b.WithViper(v)
+       b.WithContent("p1.md", "")
+
+       b.WithTemplates("index.html", `
+{{ $js := resources.Get "js/main.js" | js.Build }}
+JS:  {{ template "print" $js }}
+
+
+{{ define "print" }}RelPermalink: {{.RelPermalink}}|MIME: {{ .MediaType }}|Content: {{ .Content | safeJS }}{{ end }}
+
+`)
 
        jsDir := filepath.Join(workDir, "assets", "js")
        b.Assert(os.MkdirAll(jsDir, 0777), qt.IsNil)
+       b.Assert(os.Chdir(workDir), qt.IsNil)
        b.WithSourceFile("assets/js/main.js", mainJS)
        b.WithSourceFile("assets/js/included.js", includedJS)
 
-       _, err = captureStdout(func() error {
-               return b.BuildE(BuildCfg{})
-       })
-       b.Assert(err, qt.IsNil)
+       b.Build(BuildCfg{})
 
        b.AssertFileContent("public/index.html", `
-  Built: (()=&gt;{console.log(&#34;included&#34;);console.log(&#34;main&#34;);})();
-       `)
+console.log(&#34;included&#34;);
+
+`)
 
 }
index e33583a0e45289b4345c707f94d34526747a6a64..8a2efc4a454bda00147a47bc0d627482cbad22ca 100644 (file)
@@ -45,7 +45,6 @@ type Type struct {
 
        Delimiter string `json:"delimiter"` // e.g. "."
 
-       // TODO(bep) make this a string to make it hashable + method
        Suffixes []string `json:"suffixes"`
 
        // Set when doing lookup by suffix.
@@ -130,13 +129,17 @@ var (
        CSVType        = Type{MainType: "text", SubType: "csv", Suffixes: []string{"csv"}, Delimiter: defaultDelimiter}
        HTMLType       = Type{MainType: "text", SubType: "html", Suffixes: []string{"html"}, Delimiter: defaultDelimiter}
        JavascriptType = Type{MainType: "application", SubType: "javascript", Suffixes: []string{"js"}, Delimiter: defaultDelimiter}
-       JSONType       = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter}
-       RSSType        = Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
-       XMLType        = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
-       SVGType        = Type{MainType: "image", SubType: "svg", mimeSuffix: "xml", Suffixes: []string{"svg"}, Delimiter: defaultDelimiter}
-       TextType       = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter}
-       TOMLType       = Type{MainType: "application", SubType: "toml", Suffixes: []string{"toml"}, Delimiter: defaultDelimiter}
-       YAMLType       = Type{MainType: "application", SubType: "yaml", Suffixes: []string{"yaml", "yml"}, Delimiter: defaultDelimiter}
+       TypeScriptType = Type{MainType: "application", SubType: "typescript", Suffixes: []string{"ts"}, Delimiter: defaultDelimiter}
+       TSXType        = Type{MainType: "text", SubType: "tsx", Suffixes: []string{"tsx"}, Delimiter: defaultDelimiter}
+       JSXType        = Type{MainType: "text", SubType: "jsx", Suffixes: []string{"jsx"}, Delimiter: defaultDelimiter}
+
+       JSONType = Type{MainType: "application", SubType: "json", Suffixes: []string{"json"}, Delimiter: defaultDelimiter}
+       RSSType  = Type{MainType: "application", SubType: "rss", mimeSuffix: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
+       XMLType  = Type{MainType: "application", SubType: "xml", Suffixes: []string{"xml"}, Delimiter: defaultDelimiter}
+       SVGType  = Type{MainType: "image", SubType: "svg", mimeSuffix: "xml", Suffixes: []string{"svg"}, Delimiter: defaultDelimiter}
+       TextType = Type{MainType: "text", SubType: "plain", Suffixes: []string{"txt"}, Delimiter: defaultDelimiter}
+       TOMLType = Type{MainType: "application", SubType: "toml", Suffixes: []string{"toml"}, Delimiter: defaultDelimiter}
+       YAMLType = Type{MainType: "application", SubType: "yaml", Suffixes: []string{"yaml", "yml"}, Delimiter: defaultDelimiter}
 
        // Common image types
        PNGType  = Type{MainType: "image", SubType: "png", Suffixes: []string{"png"}, Delimiter: defaultDelimiter}
@@ -165,6 +168,9 @@ var DefaultTypes = Types{
        SASSType,
        HTMLType,
        JavascriptType,
+       TypeScriptType,
+       TSXType,
+       JSXType,
        JSONType,
        RSSType,
        XMLType,
index f18fd90bb0a0aac6a5e5f1a2296fb6bc586c2ad0..ee7d4407afe5fdcfac516dfd0797d2960d108781 100644 (file)
@@ -40,6 +40,9 @@ func TestDefaultTypes(t *testing.T) {
                {CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
                {HTMLType, "text", "html", "html", "text/html", "text/html"},
                {JavascriptType, "application", "javascript", "js", "application/javascript", "application/javascript"},
+               {TypeScriptType, "application", "typescript", "ts", "application/typescript", "application/typescript"},
+               {TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"},
+               {JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"},
                {JSONType, "application", "json", "json", "application/json", "application/json"},
                {RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
                {SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
@@ -58,7 +61,7 @@ func TestDefaultTypes(t *testing.T) {
 
        }
 
-       c.Assert(len(DefaultTypes), qt.Equals, 23)
+       c.Assert(len(DefaultTypes), qt.Equals, 26)
 
 }
 
index 6224ee178ce73c698b3081d8a018b04731e0c158..c48778692e57e8c1f1328dfa60c2b1820e9b0820 100644 (file)
@@ -17,8 +17,11 @@ import (
        "fmt"
        "io/ioutil"
        "path"
+       "strings"
 
+       "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/hugolib/filesystems"
+       "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/resources/internal"
 
        "github.com/mitchellh/mapstructure"
@@ -28,15 +31,46 @@ import (
        "github.com/gohugoio/hugo/resources/resource"
 )
 
+const defaultTarget = "esnext"
+
 type Options struct {
+       // If not set, the source path will be used as the base target path.
+       // Note that the target path's extension may change if the target MIME type
+       // is different, e.g. when the source is TypeScript.
+       TargetPath string
+
+       // Whether to minify to output.
+       Minify bool
+
+       // The language target.
+       // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
+       // Default is esnext.
+       Target string
+
+       // External dependencies, e.g. "react".
+       Externals []string `hash:"set"`
+
+       // What to use instead of React.createElement.
+       JSXFactory string
+
+       // What to use instead of React.Fragment.
+       JSXFragment string
+}
+
+type internalOptions struct {
+       TargetPath  string
        Minify      bool
-       Externals   []string
        Target      string
-       Loader      string
-       Defines     map[string]string
        JSXFactory  string
        JSXFragment string
-       TSConfig    string
+
+       Externals []string `hash:"set"`
+
+       // These are currently not exposed in the public Options struct,
+       // but added here to make the options hash as stable as possible for
+       // whenever we do.
+       Defines  map[string]string
+       TSConfig string
 }
 
 func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
@@ -44,6 +78,13 @@ func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
                return
        }
        err = mapstructure.WeakDecode(m, &opts)
+
+       if opts.TargetPath != "" {
+               opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
+       }
+
+       opts.Target = strings.ToLower(opts.Target)
+
        return
 }
 
@@ -57,7 +98,7 @@ func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
 }
 
 type buildTransformation struct {
-       options Options
+       options internalOptions
        rs      *resources.Spec
        sfs     *filesystems.SourceFilesystem
 }
@@ -67,9 +108,17 @@ func (t *buildTransformation) Key() internal.ResourceTransformationKey {
 }
 
 func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+       ctx.OutMediaType = media.JavascriptType
+
+       if t.options.TargetPath != "" {
+               ctx.OutPath = t.options.TargetPath
+       } else {
+               ctx.ReplaceOutPathExtension(".js")
+       }
+
        var target api.Target
        switch t.options.Target {
-       case "", "esnext":
+       case defaultTarget:
                target = api.ESNext
        case "es6", "es2015":
                target = api.ES2015
@@ -88,29 +137,20 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
        }
 
        var loader api.Loader
-       switch t.options.Loader {
-       case "", "js":
+       switch ctx.InMediaType.SubType {
+       // TODO(bep) ESBuild support a set of other loaders, but I currently fail
+       // to see the relevance. That may change as we start using this.
+       case media.JavascriptType.SubType:
                loader = api.LoaderJS
-       case "jsx":
-               loader = api.LoaderJSX
-       case "ts":
+       case media.TypeScriptType.SubType:
                loader = api.LoaderTS
-       case "tsx":
+       case media.TSXType.SubType:
                loader = api.LoaderTSX
-       case "json":
-               loader = api.LoaderJSON
-       case "text":
-               loader = api.LoaderText
-       case "base64":
-               loader = api.LoaderBase64
-       case "dataURL":
-               loader = api.LoaderDataURL
-       case "file":
-               loader = api.LoaderFile
-       case "binary":
-               loader = api.LoaderBinary
+       case media.JSXType.SubType:
+               loader = api.LoaderJSX
        default:
-               return fmt.Errorf("invalid loader: %q", t.options.Loader)
+               return fmt.Errorf("unsupported Media Type: %q", ctx.InMediaType)
+
        }
 
        src, err := ioutil.ReadAll(ctx.From)
@@ -159,8 +199,23 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
        return nil
 }
 
-func (c *Client) Process(res resources.ResourceTransformer, options Options) (resource.Resource, error) {
+func (c *Client) Process(res resources.ResourceTransformer, opts Options) (resource.Resource, error) {
        return res.Transform(
-               &buildTransformation{rs: c.rs, sfs: c.sfs, options: options},
+               &buildTransformation{rs: c.rs, sfs: c.sfs, options: toInternalOptions(opts)},
        )
 }
+
+func toInternalOptions(opts Options) internalOptions {
+       target := opts.Target
+       if target == "" {
+               target = defaultTarget
+       }
+       return internalOptions{
+               TargetPath:  opts.TargetPath,
+               Minify:      opts.Minify,
+               Target:      target,
+               Externals:   opts.Externals,
+               JSXFactory:  opts.JSXFactory,
+               JSXFragment: opts.JSXFragment,
+       }
+}
diff --git a/resources/resource_transformers/js/build_test.go b/resources/resource_transformers/js/build_test.go
new file mode 100644 (file)
index 0000000..3f2a1e1
--- /dev/null
@@ -0,0 +1,69 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+       "testing"
+
+       qt "github.com/frankban/quicktest"
+)
+
+// This test is added to test/warn against breaking the "stability" of the
+// cache key. It's sometimes needed to break this, but should be avoided if possible.
+func TestOptionKey(t *testing.T) {
+       c := qt.New(t)
+
+       opts := internalOptions{
+               TargetPath: "foo",
+       }
+
+       key := (&buildTransformation{options: opts}).Key()
+
+       c.Assert(key.Value(), qt.Equals, "jsbuild_9405671309963492201")
+}
+
+func TestToInternalOptions(t *testing.T) {
+       c := qt.New(t)
+
+       o := Options{
+               TargetPath:  "v1",
+               Target:      "v2",
+               JSXFactory:  "v3",
+               JSXFragment: "v4",
+               Externals:   []string{"react"},
+               Minify:      true,
+       }
+
+       c.Assert(toInternalOptions(o), qt.DeepEquals, internalOptions{
+               TargetPath:  "v1",
+               Minify:      true,
+               Target:      "v2",
+               JSXFactory:  "v3",
+               JSXFragment: "v4",
+               Externals:   []string{"react"},
+               Defines:     nil,
+               TSConfig:    "",
+       })
+
+       c.Assert(toInternalOptions(Options{}), qt.DeepEquals, internalOptions{
+               TargetPath:  "",
+               Minify:      false,
+               Target:      "esnext",
+               JSXFactory:  "",
+               JSXFragment: "",
+               Externals:   nil,
+               Defines:     nil,
+               TSConfig:    "",
+       })
+}
diff --git a/tpl/internal/resourcehelpers/helpers.go b/tpl/internal/resourcehelpers/helpers.go
new file mode 100644 (file)
index 0000000..4f8b753
--- /dev/null
@@ -0,0 +1,71 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Portions Copyright The Go Authors.
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resourcehelpers
+
+import (
+       "errors"
+       "fmt"
+
+       _errors "github.com/pkg/errors"
+
+       "github.com/gohugoio/hugo/common/maps"
+       "github.com/gohugoio/hugo/resources"
+)
+
+// We allow string or a map as the first argument in some cases.
+func ResolveIfFirstArgIsString(args []interface{}) (resources.ResourceTransformer, string, bool) {
+       if len(args) != 2 {
+               return nil, "", false
+       }
+
+       v1, ok1 := args[0].(string)
+       if !ok1 {
+               return nil, "", false
+       }
+       v2, ok2 := args[1].(resources.ResourceTransformer)
+
+       return v2, v1, ok2
+}
+
+// This roundabout way of doing it is needed to get both pipeline behaviour and options as arguments.
+func ResolveArgs(args []interface{}) (resources.ResourceTransformer, map[string]interface{}, error) {
+       if len(args) == 0 {
+               return nil, nil, errors.New("no Resource provided in transformation")
+       }
+
+       if len(args) == 1 {
+               r, ok := args[0].(resources.ResourceTransformer)
+               if !ok {
+                       return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
+               }
+               return r, nil, nil
+       }
+
+       r, ok := args[1].(resources.ResourceTransformer)
+       if !ok {
+               if _, ok := args[1].(map[string]interface{}); !ok {
+                       return nil, nil, fmt.Errorf("no Resource provided in transformation")
+               }
+               return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
+       }
+
+       m, err := maps.ToStringMapE(args[0])
+       if err != nil {
+               return nil, nil, _errors.Wrap(err, "invalid options type")
+       }
+
+       return r, m, nil
+}
index d8ba35a76ee924ac972ab1aeecba530d9dd45074..4dc97a707655578c68fbd5e033a09c51cde66ee0 100644 (file)
 package js
 
 import (
-       "errors"
-       "fmt"
-
-       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/deps"
+       "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/resources"
        "github.com/gohugoio/hugo/resources/resource"
        "github.com/gohugoio/hugo/resources/resource_transformers/js"
-       _errors "github.com/pkg/errors"
+       "github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
 )
 
 // New returns a new instance of the js-namespaced template functions.
@@ -41,50 +38,33 @@ type Namespace struct {
 
 // Build processes the given Resource with ESBuild.
 func (ns *Namespace) Build(args ...interface{}) (resource.Resource, error) {
-       r, m, err := ns.resolveArgs(args)
-       if err != nil {
-               return nil, err
-       }
-       var options js.Options
-       if m != nil {
-               options, err = js.DecodeOptions(m)
+       var (
+               r          resources.ResourceTransformer
+               m          map[string]interface{}
+               targetPath string
+               err        error
+               ok         bool
+       )
+
+       r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)
 
+       if !ok {
+               r, m, err = resourcehelpers.ResolveArgs(args)
                if err != nil {
                        return nil, err
                }
        }
 
-       return ns.client.Process(r, options)
-
-}
-
-// This roundabout way of doing it is needed to get both pipeline behaviour and options as arguments.
-// This is a copy of tpl/resources/resolveArgs
-func (ns *Namespace) resolveArgs(args []interface{}) (resources.ResourceTransformer, map[string]interface{}, error) {
-       if len(args) == 0 {
-               return nil, nil, errors.New("no Resource provided in transformation")
-       }
-
-       if len(args) == 1 {
-               r, ok := args[0].(resources.ResourceTransformer)
-               if !ok {
-                       return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
-               }
-               return r, nil, nil
-       }
-
-       r, ok := args[1].(resources.ResourceTransformer)
-       if !ok {
-               if _, ok := args[1].(map[string]interface{}); !ok {
-                       return nil, nil, fmt.Errorf("no Resource provided in transformation")
+       var options js.Options
+       if targetPath != "" {
+               options.TargetPath = helpers.ToSlashTrimLeading(targetPath)
+       } else if m != nil {
+               options, err = js.DecodeOptions(m)
+               if err != nil {
+                       return nil, err
                }
-               return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
        }
 
-       m, err := maps.ToStringMapE(args[0])
-       if err != nil {
-               return nil, nil, _errors.Wrap(err, "invalid options type")
-       }
+       return ns.client.Process(r, options)
 
-       return r, m, nil
 }
index 6625702ab26093e9046bb25f282be502c17e9488..cdde6bd5d31efc84871bc930d658aa0048cf98b2 100644 (file)
@@ -19,13 +19,14 @@ import (
        "fmt"
        "path/filepath"
 
+       "github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
+
+       "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/resources/postpub"
 
-       "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/deps"
        "github.com/gohugoio/hugo/resources"
        "github.com/gohugoio/hugo/resources/resource"
-       _errors "github.com/pkg/errors"
 
        "github.com/gohugoio/hugo/resources/resource_factories/bundler"
        "github.com/gohugoio/hugo/resources/resource_factories/create"
@@ -239,10 +240,10 @@ func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) {
                ok         bool
        )
 
-       r, targetPath, ok = ns.resolveIfFirstArgIsString(args)
+       r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)
 
        if !ok {
-               r, m, err = ns.resolveArgs(args)
+               r, m, err = resourcehelpers.ResolveArgs(args)
                if err != nil {
                        return nil, err
                }
@@ -250,7 +251,7 @@ func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) {
 
        var options scss.Options
        if targetPath != "" {
-               options.TargetPath = targetPath
+               options.TargetPath = helpers.ToSlashTrimLeading(targetPath)
        } else if m != nil {
                options, err = scss.DecodeOptions(m)
                if err != nil {
@@ -263,7 +264,7 @@ func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) {
 
 // PostCSS processes the given Resource with PostCSS
 func (ns *Namespace) PostCSS(args ...interface{}) (resource.Resource, error) {
-       r, m, err := ns.resolveArgs(args)
+       r, m, err := resourcehelpers.ResolveArgs(args)
        if err != nil {
                return nil, err
        }
@@ -285,7 +286,7 @@ func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedReso
 
 // Babel processes the given Resource with Babel.
 func (ns *Namespace) Babel(args ...interface{}) (resource.Resource, error) {
-       r, m, err := ns.resolveArgs(args)
+       r, m, err := resourcehelpers.ResolveArgs(args)
        if err != nil {
                return nil, err
        }
@@ -301,48 +302,3 @@ func (ns *Namespace) Babel(args ...interface{}) (resource.Resource, error) {
        return ns.babelClient.Process(r, options)
 
 }
-
-// We allow string or a map as the first argument in some cases.
-func (ns *Namespace) resolveIfFirstArgIsString(args []interface{}) (resources.ResourceTransformer, string, bool) {
-       if len(args) != 2 {
-               return nil, "", false
-       }
-
-       v1, ok1 := args[0].(string)
-       if !ok1 {
-               return nil, "", false
-       }
-       v2, ok2 := args[1].(resources.ResourceTransformer)
-
-       return v2, v1, ok2
-}
-
-// This roundabout way of doing it is needed to get both pipeline behaviour and options as arguments.
-func (ns *Namespace) resolveArgs(args []interface{}) (resources.ResourceTransformer, map[string]interface{}, error) {
-       if len(args) == 0 {
-               return nil, nil, errors.New("no Resource provided in transformation")
-       }
-
-       if len(args) == 1 {
-               r, ok := args[0].(resources.ResourceTransformer)
-               if !ok {
-                       return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
-               }
-               return r, nil, nil
-       }
-
-       r, ok := args[1].(resources.ResourceTransformer)
-       if !ok {
-               if _, ok := args[1].(map[string]interface{}); !ok {
-                       return nil, nil, fmt.Errorf("no Resource provided in transformation")
-               }
-               return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0])
-       }
-
-       m, err := maps.ToStringMapE(args[0])
-       if err != nil {
-               return nil, nil, _errors.Wrap(err, "invalid options type")
-       }
-
-       return r, m, nil
-}