shithub: hugo

Download patch

ref: 2a171ff1c5d9b1603fe78c67d2d894bb2efccc8b
parent: 67f920419a53c7ff11e01c4286dca23e92110a12
author: Niek de Wit <niekdewit@live.nl>
date: Fri Mar 22 13:07:37 EDT 2019

resources: Add JavaScript transpiling solution

Add a new pipe called TranspileJS which uses the Babel cli. This makes it possible for users to write ES6 JavaScript code and transpile it to ES5 during website generation so that the code still works with older browser versions.

Fixes #5764

diff: cannot open b/resources/resource_transformers/transpilejs//null: file does not exist: 'b/resources/resource_transformers/transpilejs//null'
--- /dev/null
+++ b/docs/content/en/hugo-pipes/transformjs.md
@@ -1,0 +1,69 @@
+---
+title: TransformJS
+description: Hugo Pipes can process JS files with Babel.
+date: 2019-03-21
+publishdate: 2019-03-21
+lastmod: 2019-03-21
+categories: [asset management]
+keywords: []
+menu:
+  docs:
+    parent: "pipes"
+    weight: 75
+weight: 75
+sections_weight: 75
+draft: false
+---
+
+Any JavaScript resource file can be transpiled to another JavaScript version using `resources.TransformJS` which takes for argument the resource object and a slice of options listed below. TransformJS uses the [babel cli](https://babeljs.io/docs/en/babel-cli).
+
+
+{{% note %}}
+Hugo Pipe's TranspileJS requires the `@babel/cli` and `@babel/core` JavaScript packages to be installed in the environment (`npm install -g @babel/cli @babel/core`) along with any Babel plugin(s) or preset(s) used (e.g., `npm install -g @babel/preset-env`).
+
+If you are using the Hugo Snap package, Babel and plugin(s) need to be installed locally within your Hugo site directory, e.g., `npm install @babel/cli @babel/core` without the `-g` flag.
+{{% /note %}}
+### Options
+
+config [string]
+: Path to the Babel configuration file
+
+_If no configuration file is used:_
+
+plugins [string]
+: Comma seperated string of Babel plugins to use
+
+presets [string]
+: Comma seperated string of Babel presets to use
+
+minified [bool]
+: Save as much bytes as possible when printing
+
+noComments [bool]
+: Write comments to generated output (true by default)
+
+compact [string]
+: Do not include superfluous whitespace characters and line terminators (true/false/auto)
+
+verbose [bool]
+: Log everything
+
+### Examples
+Without a `.babelrc` file, you can simply pass the options like so:
+```go-html-template
+{{- $transpileOpts := (dict "presets" "@babel/preset-env" "minified" true "noComments" true "compact" "true" ) -}}
+{{- $transpiled := resources.Get "scripts/main.js" | transpileJS $transpileOpts -}}
+```
+
+If you rather want to use a config file, you can leave out the options in the template.
+```go-html-template
+{{- $transpiled := resources.Get "scripts/main.js" | transpileJS $transpileOpts -}}
+```
+Then, you can either create a `.babelrc` in the root of your project, or your can create a `.babel.config.js`.
+More information on these configuration files can be found here: [babel configuration](https://babeljs.io/docs/en/configuration)
+
+Finally, you can also pass a custom file path to a config file like so:
+```go-html-template
+{{- $transpileOpts := (dict "config" "config/my-babel-config.js" ) -}}
+{{- $transpiled := resources.Get "scripts/main.js" | transpileJS $transpileOpts -}}
+```
\ No newline at end of file
--- /dev/null
+++ b/resources/resource_transformers/transpilejs/transpilejs.go
@@ -1,0 +1,191 @@
+// Copyright 2018 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 transpilejs
+
+import (
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+
+	"github.com/gohugoio/hugo/resources/internal"
+
+	"github.com/mitchellh/mapstructure"
+
+	"github.com/gohugoio/hugo/common/herrors"
+	"github.com/gohugoio/hugo/hugofs"
+	"github.com/gohugoio/hugo/resources"
+	"github.com/gohugoio/hugo/resources/resource"
+	"github.com/pkg/errors"
+)
+
+// Options from https://babeljs.io/docs/en/options
+type Options struct {
+	Config     string //Custom path to config file
+	Plugins    string //Comma seperated string of plugins
+	Presets    string //Comma seperated string of presets
+	Minified   bool   //true/false
+	NoComments bool   //true/false
+	Compact    string //true/false/auto
+	Verbose    bool   //true/false
+	NoBabelrc  bool   //true/false
+}
+
+func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
+	if m == nil {
+		return
+	}
+	err = mapstructure.WeakDecode(m, &opts)
+	return
+}
+func (opts Options) toArgs() []string {
+	var args []string
+
+	if opts.Plugins != "" {
+		args = append(args, "--plugins="+opts.Plugins)
+	}
+	if opts.Presets != "" {
+		args = append(args, "--presets="+opts.Presets)
+	}
+	if opts.Minified {
+		args = append(args, "--minified")
+	}
+	if opts.NoComments {
+		args = append(args, "--no-comments")
+	}
+	if opts.Compact != "" {
+		args = append(args, "--compact="+opts.Compact)
+	}
+	if opts.Verbose {
+		args = append(args, "--verbose")
+	}
+	if opts.NoBabelrc {
+		args = append(args, "--no-babelrc")
+	}
+	return args
+}
+
+// Client is the client used to do Babel transformations.
+type Client struct {
+	rs *resources.Spec
+}
+
+// New creates a new Client with the given specification.
+func New(rs *resources.Spec) *Client {
+	return &Client{rs: rs}
+}
+
+type babelTransformation struct {
+	options Options
+	rs      *resources.Spec
+}
+
+func (t *babelTransformation) Key() internal.ResourceTransformationKey {
+	return internal.NewResourceTransformationKey("babel", t.options)
+}
+
+// Transform shells out to babel-cli to do the heavy lifting.
+// For this to work, you need some additional tools. To install them globally:
+// npm install -g @babel/core @babel/cli
+// If you want to use presets or plugins such as @babel/preset-env
+// Then you should install those globally as well. e.g:
+// npm install -g @babel/preset-env
+// Instead of installing globally, you can also install everything as a dev-dependency (--save-dev instead of -g)
+func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
+
+	const localBabelPath = "node_modules/@babel/cli/bin/"
+	const binaryName = "babel.js"
+
+	// Try first in the project's node_modules.
+	csiBinPath := filepath.Join(t.rs.WorkingDir, localBabelPath, binaryName)
+
+	binary := csiBinPath
+
+	if _, err := exec.LookPath(binary); err != nil {
+		// Try PATH
+		binary = binaryName
+		if _, err := exec.LookPath(binary); err != nil {
+
+			// This may be on a CI server etc. Will fall back to pre-built assets.
+			return herrors.ErrFeatureNotAvailable
+		}
+	}
+
+	var configFile string
+	logger := t.rs.Logger
+
+	if t.options.Config != "" {
+		configFile = t.options.Config
+	} else {
+		configFile = "babel.config.js"
+	}
+
+	configFile = filepath.Clean(configFile)
+
+	// We need an abolute filename to the config file.
+	if !filepath.IsAbs(configFile) {
+		// We resolve this against the virtual Work filesystem, to allow
+		// this config file to live in one of the themes if needed.
+		fi, err := t.rs.BaseFs.Work.Stat(configFile)
+		if err != nil {
+			if t.options.Config != "" {
+				// Only fail if the user specificed config file is not found.
+				return errors.Wrapf(err, "babel config %q not found:", configFile)
+			}
+		} else {
+			configFile = fi.(hugofs.FileMetaInfo).Meta().Filename()
+		}
+	}
+
+	var cmdArgs []string
+
+	if configFile != "" {
+		logger.INFO.Println("babel: use config file", configFile)
+		cmdArgs = []string{"--config-file", configFile}
+	}
+
+	if optArgs := t.options.toArgs(); len(optArgs) > 0 {
+		cmdArgs = append(cmdArgs, optArgs...)
+	}
+	cmdArgs = append(cmdArgs, "--filename="+ctx.SourcePath)
+
+	cmd := exec.Command(binary, cmdArgs...)
+
+	cmd.Stdout = ctx.To
+	cmd.Stderr = os.Stderr
+
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return err
+	}
+
+	go func() {
+		defer stdin.Close()
+		io.Copy(stdin, ctx.From)
+	}()
+
+	err = cmd.Run()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Process transforms the given Resource with the Babel processor.
+func (c *Client) Process(res resources.ResourceTransformer, options Options) (resource.Resource, error) {
+	return res.Transform(
+		&babelTransformation{rs: c.rs, options: options},
+	)
+}
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -67,7 +67,7 @@
 
   node:
     plugin: x-nodejs
-    node-packages: [postcss-cli]
+    node-packages: [postcss-cli, @babel/cli]
     filesets:
       node:
         - bin/node
@@ -74,9 +74,13 @@
       postcss:
         - bin/postcss
         - lib/node_modules/postcss-cli/*
+      babel:
+        - bin/babel.js
+        - lib/node_modules/@babel/cli/*
     prime:
       - $node
       - $postcss
+      - $babel
 
   pygments:
     plugin: python
--- a/tpl/resources/init.go
+++ b/tpl/resources/init.go
@@ -60,6 +60,11 @@
 			[][2]string{},
 		)
 
+		ns.AddMethodMapping(ctx.TranspileJS,
+			[]string{"transpileJS"},
+			[][2]string{},
+		)
+
 		return ns
 
 	}
--- a/tpl/resources/resources.go
+++ b/tpl/resources/resources.go
@@ -34,6 +34,7 @@
 	"github.com/gohugoio/hugo/resources/resource_transformers/postcss"
 	"github.com/gohugoio/hugo/resources/resource_transformers/templates"
 	"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
+	"github.com/gohugoio/hugo/resources/resource_transformers/transpilejs"
 	"github.com/spf13/cast"
 )
 
@@ -54,14 +55,15 @@
 	}
 
 	return &Namespace{
-		deps:            deps,
-		scssClient:      scssClient,
-		createClient:    create.New(deps.ResourceSpec),
-		bundlerClient:   bundler.New(deps.ResourceSpec),
-		integrityClient: integrity.New(deps.ResourceSpec),
-		minifyClient:    minifyClient,
-		postcssClient:   postcss.New(deps.ResourceSpec),
-		templatesClient: templates.New(deps.ResourceSpec, deps),
+		deps:              deps,
+		scssClient:        scssClient,
+		createClient:      create.New(deps.ResourceSpec),
+		bundlerClient:     bundler.New(deps.ResourceSpec),
+		integrityClient:   integrity.New(deps.ResourceSpec),
+		minifyClient:      minifyClient,
+		postcssClient:     postcss.New(deps.ResourceSpec),
+		templatesClient:   templates.New(deps.ResourceSpec, deps),
+		transpileJSClient: transpilejs.New(deps.ResourceSpec),
 	}, nil
 }
 
@@ -69,13 +71,14 @@
 type Namespace struct {
 	deps *deps.Deps
 
-	createClient    *create.Client
-	bundlerClient   *bundler.Client
-	scssClient      *scss.Client
-	integrityClient *integrity.Client
-	minifyClient    *minifier.Client
-	postcssClient   *postcss.Client
-	templatesClient *templates.Client
+	createClient      *create.Client
+	bundlerClient     *bundler.Client
+	scssClient        *scss.Client
+	integrityClient   *integrity.Client
+	minifyClient      *minifier.Client
+	postcssClient     *postcss.Client
+	transpileJSClient *transpilejs.Client
+	templatesClient   *templates.Client
 }
 
 // Get locates the filename given in Hugo's assets filesystem
@@ -277,6 +280,26 @@
 
 func (ns *Namespace) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) {
 	return ns.deps.ResourceSpec.PostProcess(r)
+
+}
+
+// TranspileJS processes the given Resource with Babel.
+func (ns *Namespace) TranspileJS(args ...interface{}) (resource.Resource, error) {
+	r, m, err := ns.resolveArgs(args)
+	if err != nil {
+		return nil, err
+	}
+	var options transpilejs.Options
+	if m != nil {
+		options, err = transpilejs.DecodeOptions(m)
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return ns.transpileJSClient.Process(r, options)
+
 }
 
 // We allow string or a map as the first argument in some cases.