diff --git a/docs/content/en/hugo-pipes/js.md b/docs/content/en/hugo-pipes/js.md index f361adc45c5..aa12af4dfe3 100644 --- a/docs/content/en/hugo-pipes/js.md +++ b/docs/content/en/hugo-pipes/js.md @@ -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 `esm`. + ### Examples ```go-html-template diff --git a/media/mediaType.go b/media/mediaType.go index 8a2efc4a454..21d4ddca572 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -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 diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go index 488c6d1a49d..b4725aa35a8 100644 --- a/resources/resource_transformers/js/build.go +++ b/resources/resource_transformers/js/build.go @@ -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,39 @@ 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 "", "esm": + format = api.FormatESModule + case "iife": + format = api.FormatIIFE + case "cjs": + format = api.FormatCommonJS } - 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 +235,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}, - ) } diff --git a/resources/resource_transformers/js/build_test.go b/resources/resource_transformers/js/build_test.go index 2e4c174f7a9..1e951bb459e 100644 --- a/resources/resource_transformers/js/build_test.go +++ b/resources/resource_transformers/js/build_test.go @@ -16,6 +16,10 @@ package js import ( "testing" + "github.com/gohugoio/hugo/media" + + "github.com/evanw/esbuild/pkg/api" + qt "github.com/frankban/quicktest" ) @@ -32,3 +36,30 @@ func TestOptionKey(t *testing.T) { c.Assert(key.Value(), qt.Equals, "jsbuild_15565843046704064284") } + +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.FormatESModule, + 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{}, + }) + +}