github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/resources/resource_transformers/babel/babel.go (about)

     1  // Copyright 2020 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package babel
    15  
    16  import (
    17  	"bytes"
    18  	"io"
    19  	"io/ioutil"
    20  	"os"
    21  	"path"
    22  	"path/filepath"
    23  	"regexp"
    24  	"strconv"
    25  
    26  	"github.com/gohugoio/hugo/common/hexec"
    27  	"github.com/gohugoio/hugo/common/loggers"
    28  
    29  	"github.com/gohugoio/hugo/common/hugo"
    30  	"github.com/gohugoio/hugo/resources/internal"
    31  
    32  	"github.com/mitchellh/mapstructure"
    33  
    34  	"github.com/gohugoio/hugo/common/herrors"
    35  	"github.com/gohugoio/hugo/resources"
    36  	"github.com/gohugoio/hugo/resources/resource"
    37  	"github.com/pkg/errors"
    38  )
    39  
    40  // Options from https://babeljs.io/docs/en/options
    41  type Options struct {
    42  	Config string // Custom path to config file
    43  
    44  	Minified   bool
    45  	NoComments bool
    46  	Compact    *bool
    47  	Verbose    bool
    48  	NoBabelrc  bool
    49  	SourceMap  string
    50  }
    51  
    52  // DecodeOptions decodes options to and generates command flags
    53  func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
    54  	if m == nil {
    55  		return
    56  	}
    57  	err = mapstructure.WeakDecode(m, &opts)
    58  	return
    59  }
    60  
    61  func (opts Options) toArgs() []interface{} {
    62  	var args []interface{}
    63  
    64  	// external is not a known constant on the babel command line
    65  	// .sourceMaps must be a boolean, "inline", "both", or undefined
    66  	switch opts.SourceMap {
    67  	case "external":
    68  		args = append(args, "--source-maps")
    69  	case "inline":
    70  		args = append(args, "--source-maps=inline")
    71  	}
    72  	if opts.Minified {
    73  		args = append(args, "--minified")
    74  	}
    75  	if opts.NoComments {
    76  		args = append(args, "--no-comments")
    77  	}
    78  	if opts.Compact != nil {
    79  		args = append(args, "--compact="+strconv.FormatBool(*opts.Compact))
    80  	}
    81  	if opts.Verbose {
    82  		args = append(args, "--verbose")
    83  	}
    84  	if opts.NoBabelrc {
    85  		args = append(args, "--no-babelrc")
    86  	}
    87  	return args
    88  }
    89  
    90  // Client is the client used to do Babel transformations.
    91  type Client struct {
    92  	rs *resources.Spec
    93  }
    94  
    95  // New creates a new Client with the given specification.
    96  func New(rs *resources.Spec) *Client {
    97  	return &Client{rs: rs}
    98  }
    99  
   100  type babelTransformation struct {
   101  	options Options
   102  	rs      *resources.Spec
   103  }
   104  
   105  func (t *babelTransformation) Key() internal.ResourceTransformationKey {
   106  	return internal.NewResourceTransformationKey("babel", t.options)
   107  }
   108  
   109  // Transform shells out to babel-cli to do the heavy lifting.
   110  // For this to work, you need some additional tools. To install them globally:
   111  // npm install -g @babel/core @babel/cli
   112  // If you want to use presets or plugins such as @babel/preset-env
   113  // Then you should install those globally as well. e.g:
   114  // npm install -g @babel/preset-env
   115  // Instead of installing globally, you can also install everything as a dev-dependency (--save-dev instead of -g)
   116  func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
   117  	const binaryName = "babel"
   118  
   119  	ex := t.rs.ExecHelper
   120  
   121  	if err := ex.Sec().CheckAllowedExec(binaryName); err != nil {
   122  		return err
   123  	}
   124  
   125  	var configFile string
   126  	logger := t.rs.Logger
   127  
   128  	var errBuf bytes.Buffer
   129  	infoW := loggers.LoggerToWriterWithPrefix(logger.Info(), "babel")
   130  
   131  	if t.options.Config != "" {
   132  		configFile = t.options.Config
   133  	} else {
   134  		configFile = "babel.config.js"
   135  	}
   136  
   137  	configFile = filepath.Clean(configFile)
   138  
   139  	// We need an absolute filename to the config file.
   140  	if !filepath.IsAbs(configFile) {
   141  		configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
   142  		if configFile == "" && t.options.Config != "" {
   143  			// Only fail if the user specified config file is not found.
   144  			return errors.Errorf("babel config %q not found:", configFile)
   145  		}
   146  	}
   147  
   148  	ctx.ReplaceOutPathExtension(".js")
   149  
   150  	var cmdArgs []interface{}
   151  
   152  	if configFile != "" {
   153  		logger.Infoln("babel: use config file", configFile)
   154  		cmdArgs = []interface{}{"--config-file", configFile}
   155  	}
   156  
   157  	if optArgs := t.options.toArgs(); len(optArgs) > 0 {
   158  		cmdArgs = append(cmdArgs, optArgs...)
   159  	}
   160  	cmdArgs = append(cmdArgs, "--filename="+ctx.SourcePath)
   161  
   162  	// Create compile into a real temp file:
   163  	// 1. separate stdout/stderr messages from babel (https://github.com/gohugoio/hugo/issues/8136)
   164  	// 2. allow generation and retrieval of external source map.
   165  	compileOutput, err := ioutil.TempFile("", "compileOut-*.js")
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	cmdArgs = append(cmdArgs, "--out-file="+compileOutput.Name())
   171  	stderr := io.MultiWriter(infoW, &errBuf)
   172  	cmdArgs = append(cmdArgs, hexec.WithStderr(stderr))
   173  	cmdArgs = append(cmdArgs, hexec.WithStdout(stderr))
   174  	cmdArgs = append(cmdArgs, hexec.WithEnviron(hugo.GetExecEnviron(t.rs.WorkingDir, t.rs.Cfg, t.rs.BaseFs.Assets.Fs)))
   175  
   176  	defer os.Remove(compileOutput.Name())
   177  
   178  	// ARGA [--no-install babel --config-file /private/var/folders/_g/j3j21hts4fn7__h04w2x8gb40000gn/T/hugo-test-babel812882892/babel.config.js --source-maps --filename=js/main2.js --out-file=/var/folders/_g/j3j21hts4fn7__h04w2x8gb40000gn/T/compileOut-2237820197.js]
   179  	//      [--no-install babel --config-file /private/var/folders/_g/j3j21hts4fn7__h04w2x8gb40000gn/T/hugo-test-babel332846848/babel.config.js --filename=js/main.js --out-file=/var/folders/_g/j3j21hts4fn7__h04w2x8gb40000gn/T/compileOut-1451390834.js 0x10304ee60 0x10304ed60 0x10304f060]
   180  	cmd, err := ex.Npx(binaryName, cmdArgs...)
   181  
   182  	if err != nil {
   183  		if hexec.IsNotFound(err) {
   184  			// This may be on a CI server etc. Will fall back to pre-built assets.
   185  			return herrors.ErrFeatureNotAvailable
   186  		}
   187  		return err
   188  	}
   189  
   190  	stdin, err := cmd.StdinPipe()
   191  
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	go func() {
   197  		defer stdin.Close()
   198  		io.Copy(stdin, ctx.From)
   199  	}()
   200  
   201  	err = cmd.Run()
   202  	if err != nil {
   203  		if hexec.IsNotFound(err) {
   204  			return herrors.ErrFeatureNotAvailable
   205  		}
   206  		return errors.Wrap(err, errBuf.String())
   207  	}
   208  
   209  	content, err := ioutil.ReadAll(compileOutput)
   210  	if err != nil {
   211  		return err
   212  	}
   213  
   214  	mapFile := compileOutput.Name() + ".map"
   215  	if _, err := os.Stat(mapFile); err == nil {
   216  		defer os.Remove(mapFile)
   217  		sourceMap, err := ioutil.ReadFile(mapFile)
   218  		if err != nil {
   219  			return err
   220  		}
   221  		if err = ctx.PublishSourceMap(string(sourceMap)); err != nil {
   222  			return err
   223  		}
   224  		targetPath := path.Base(ctx.OutPath) + ".map"
   225  		re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
   226  		content = []byte(re.ReplaceAllString(string(content), "//# sourceMappingURL="+targetPath+"\n"))
   227  	}
   228  
   229  	ctx.To.Write(content)
   230  
   231  	return nil
   232  }
   233  
   234  // Process transforms the given Resource with the Babel processor.
   235  func (c *Client) Process(res resources.ResourceTransformer, options Options) (resource.Resource, error) {
   236  	return res.Transform(
   237  		&babelTransformation{rs: c.rs, options: options},
   238  	)
   239  }