shithub: hugo

Download patch

ref: 0256959a358bb26b983c9d9496862b0fdf387621
parent: eded9ac2a05b9a7244c25c70ca8f761b69b33385
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Tue Jul 21 13:59:03 EDT 2020

resources/js: Add option for setting bundle format

Fixes #7503

--- a/docs/content/en/hugo-pipes/js.md
+++ b/docs/content/en/hugo-pipes/js.md
@@ -45,6 +45,11 @@
 {{ $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
--- a/media/mediaType.go
+++ b/media/mediaType.go
@@ -378,6 +378,11 @@
 	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
--- a/resources/postpub/fields_test.go
+++ b/resources/postpub/fields_test.go
@@ -32,6 +32,7 @@
 
 	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",
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -33,8 +33,6 @@
 	"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 @@
 	// 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,12 +63,19 @@
 
 	// 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 != "" {
@@ -72,13 +82,10 @@
 		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 @@
 		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 @@
 	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 @@
 	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
+	}
 
+	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
+
 	}
 
-	src, err := ioutil.ReadAll(ctx.From)
-	if err != nil {
-		return err
+	var defines map[string]string
+	if opts.Defines != nil {
+		defines = cast.ToStringMapString(opts.Defines)
 	}
 
-	sdir, sfile := path.Split(ctx.SourcePath)
-	sdir = t.sfs.RealFilename(sdir)
-
-	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 @@
 		//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))
-	}
+	return
 
-	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},
-	)
 }
--- a/resources/resource_transformers/js/build_test.go
+++ b/resources/resource_transformers/js/build_test.go
@@ -16,6 +16,10 @@
 import (
 	"testing"
 
+	"github.com/gohugoio/hugo/media"
+
+	"github.com/evanw/esbuild/pkg/api"
+
 	qt "github.com/frankban/quicktest"
 )
 
@@ -26,9 +30,37 @@
 
 	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{},
+	})
+
 }