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
This commit is contained in:
Niek de Wit
2019-03-22 17:07:37 +01:00
committed by Bjørn Erik Pedersen
parent 67f920419a
commit 2a171ff1c5
5 changed files with 308 additions and 16 deletions

View File

@@ -0,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},
)
}