resources/js: Add option for setting bundle format
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 21 Jul 2020 15:59:03 +0000 (17:59 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 22 Jul 2020 08:13:30 +0000 (10:13 +0200)
Fixes #7503

docs/content/en/hugo-pipes/js.md
media/mediaType.go
resources/postpub/fields_test.go
resources/resource_transformers/js/build.go
resources/resource_transformers/js/build_test.go

index f361adc45c5c58fbdedc813fb141ea3a97b60e62..465854f3a19af4164a7f2e803ec7aa405e5ff69f 100644 (file)
@@ -45,6 +45,11 @@ defines [map]
 {{ $defines := dict "process.env.NODE_ENV" `"development"` }}
 ```
 
+format [string] {{< new-in "0.75.0" >}}
+: The output format.
+  One of: `iife`, `cjs`, `esm`.
+  Default is `iife`, a self-executing function, suitable for inclusion as a <script> tag. 
+
 ### Examples
 
 ```go-html-template
index 8a2efc4a454bda00147a47bc0d627482cbad22ca..21d4ddca572c99c3b7de7edbb8205b1ec24194d2 100644 (file)
@@ -378,6 +378,11 @@ func DecodeTypes(mms ...map[string]interface{}) (Types, error) {
        return m, nil
 }
 
+// IsZero reports whether this Type represents a zero value.
+func (m Type) IsZero() bool {
+       return m.SubType == ""
+}
+
 // MarshalJSON returns the JSON encoding of m.
 func (m Type) MarshalJSON() ([]byte, error) {
        type Alias Type
index fa0c9190ab5926437969564bba0dec90ae16c1ff..d67c7c9a94ffdc437fd66457d305a9a77bc561d0 100644 (file)
@@ -32,6 +32,7 @@ func TestCreatePlaceholders(t *testing.T) {
 
        c.Assert(m, qt.DeepEquals, map[string]interface{}{
                "FullSuffix":  "pre_foo.FullSuffix_post",
+               "IsZero":      "pre_foo.IsZero_post",
                "Type":        "pre_foo.Type_post",
                "MainType":    "pre_foo.MainType_post",
                "Delimiter":   "pre_foo.Delimiter_post",
index 488c6d1a49da6f3354f452c8df5902b0ca9ee3f5..8e0c7c13094ce2a322cd329272effd9dde0ff287 100644 (file)
@@ -33,8 +33,6 @@ 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
@@ -49,6 +47,11 @@ type Options struct {
        // Default is esnext.
        Target string
 
+       // The output format.
+       // One of: iife, cjs, esm
+       // Default is to esm.
+       Format string
+
        // External dependencies, e.g. "react".
        Externals []string `hash:"set"`
 
@@ -60,25 +63,29 @@ type Options struct {
 
        // What to use instead of React.Fragment.
        JSXFragment string
+
+       mediaType  media.Type
+       outDir     string
+       contents   string
+       sourcefile string
+       resolveDir string
 }
 
-func decodeOptions(m map[string]interface{}) (opts Options, err error) {
-       err = mapstructure.WeakDecode(m, &opts)
-       if err != nil {
-               return
+func decodeOptions(m map[string]interface{}) (Options, error) {
+       var opts Options
+
+       if err := mapstructure.WeakDecode(m, &opts); err != nil {
+               return opts, err
        }
 
        if opts.TargetPath != "" {
                opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
        }
 
-       if opts.Target == "" {
-               opts.Target = defaultTarget
-       }
-
        opts.Target = strings.ToLower(opts.Target)
+       opts.Format = strings.ToLower(opts.Format)
 
-       return
+       return opts, nil
 }
 
 type Client struct {
@@ -114,9 +121,40 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
                ctx.ReplaceOutPathExtension(".js")
        }
 
+       src, err := ioutil.ReadAll(ctx.From)
+       if err != nil {
+               return err
+       }
+
+       sdir, sfile := path.Split(ctx.SourcePath)
+       opts.sourcefile = sfile
+       opts.resolveDir = t.sfs.RealFilename(sdir)
+       opts.contents = string(src)
+       opts.mediaType = ctx.InMediaType
+
+       buildOptions, err := toBuildOptions(opts)
+       if err != nil {
+               return err
+       }
+
+       result := api.Build(buildOptions)
+       if len(result.Errors) > 0 {
+               return fmt.Errorf("%s", result.Errors[0].Text)
+       }
+       ctx.To.Write(result.OutputFiles[0].Contents)
+       return nil
+}
+
+func (c *Client) Process(res resources.ResourceTransformer, opts map[string]interface{}) (resource.Resource, error) {
+       return res.Transform(
+               &buildTransformation{rs: c.rs, sfs: c.sfs, optsm: opts},
+       )
+}
+
+func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
        var target api.Target
        switch opts.Target {
-       case defaultTarget:
+       case "", "esnext":
                target = api.ESNext
        case "es5":
                target = api.ES5
@@ -133,11 +171,17 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
        case "es2020":
                target = api.ES2020
        default:
-               return fmt.Errorf("invalid target: %q", opts.Target)
+               err = fmt.Errorf("invalid target: %q", opts.Target)
+               return
+       }
+
+       mediaType := opts.mediaType
+       if mediaType.IsZero() {
+               mediaType = media.JavascriptType
        }
 
        var loader api.Loader
-       switch ctx.InMediaType.SubType {
+       switch mediaType.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:
@@ -149,29 +193,43 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
        case media.JSXType.SubType:
                loader = api.LoaderJSX
        default:
-               return fmt.Errorf("unsupported Media Type: %q", ctx.InMediaType)
-
+               err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
+               return
        }
 
-       src, err := ioutil.ReadAll(ctx.From)
-       if err != nil {
-               return err
+       var format api.Format
+       // One of: iife, cjs, esm
+       switch opts.Format {
+       case "", "iife":
+               format = api.FormatIIFE
+       case "esm":
+               format = api.FormatESModule
+       case "cjs":
+               format = api.FormatCommonJS
+       default:
+               err = fmt.Errorf("unsupported script output format: %q", opts.Format)
+               return
+
        }
 
-       sdir, sfile := path.Split(ctx.SourcePath)
-       sdir = t.sfs.RealFilename(sdir)
+       var defines map[string]string
+       if opts.Defines != nil {
+               defines = cast.ToStringMapString(opts.Defines)
+       }
 
-       buildOptions := api.BuildOptions{
+       buildOptions = api.BuildOptions{
                Outfile: "",
                Bundle:  true,
 
                Target: target,
+               Format: format,
 
                MinifyWhitespace:  opts.Minify,
                MinifyIdentifiers: opts.Minify,
                MinifySyntax:      opts.Minify,
 
-               Defines: cast.ToStringMapString(opts.Defines),
+               Outdir:  opts.outDir,
+               Defines: defines,
 
                Externals: opts.Externals,
 
@@ -181,26 +239,12 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
                //Tsconfig: opts.TSConfig,
 
                Stdin: &api.StdinOptions{
-                       Contents:   string(src),
-                       Sourcefile: sfile,
-                       ResolveDir: sdir,
+                       Contents:   opts.contents,
+                       Sourcefile: opts.sourcefile,
+                       ResolveDir: opts.resolveDir,
                        Loader:     loader,
                },
        }
-       result := api.Build(buildOptions)
-       if len(result.Errors) > 0 {
-               return fmt.Errorf("%s", result.Errors[0].Text)
-       }
-       if len(result.OutputFiles) != 1 {
-               return fmt.Errorf("unexpected output count: %d", len(result.OutputFiles))
-       }
-
-       ctx.To.Write(result.OutputFiles[0].Contents)
-       return nil
-}
+       return
 
-func (c *Client) Process(res resources.ResourceTransformer, opts map[string]interface{}) (resource.Resource, error) {
-       return res.Transform(
-               &buildTransformation{rs: c.rs, sfs: c.sfs, optsm: opts},
-       )
 }
index 2e4c174f7a9b6d666a3acdb4da81f59daac3bf4e..ee97dede502d9487b13c4eeb0fa93d3745da3941 100644 (file)
@@ -16,6 +16,10 @@ package js
 import (
        "testing"
 
+       "github.com/gohugoio/hugo/media"
+
+       "github.com/evanw/esbuild/pkg/api"
+
        qt "github.com/frankban/quicktest"
 )
 
@@ -26,9 +30,37 @@ func TestOptionKey(t *testing.T) {
 
        opts := map[string]interface{}{
                "TargetPath": "foo",
+               "Target":     "es2018",
        }
 
        key := (&buildTransformation{optsm: opts}).Key()
 
-       c.Assert(key.Value(), qt.Equals, "jsbuild_15565843046704064284")
+       c.Assert(key.Value(), qt.Equals, "jsbuild_7891849149754191852")
+}
+
+func TestToBuildOptions(t *testing.T) {
+       c := qt.New(t)
+
+       opts, err := toBuildOptions(Options{mediaType: media.JavascriptType})
+       c.Assert(err, qt.IsNil)
+       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+               Bundle: true,
+               Target: api.ESNext,
+               Format: api.FormatIIFE,
+               Stdin:  &api.StdinOptions{},
+       })
+
+       opts, err = toBuildOptions(Options{
+               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType})
+       c.Assert(err, qt.IsNil)
+       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+               Bundle:            true,
+               Target:            api.ES2018,
+               Format:            api.FormatCommonJS,
+               MinifyIdentifiers: true,
+               MinifySyntax:      true,
+               MinifyWhitespace:  true,
+               Stdin:             &api.StdinOptions{},
+       })
+
 }