github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/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/cli/safeexec"
    27  	"github.com/gohugoio/hugo/common/hexec"
    28  	"github.com/gohugoio/hugo/common/loggers"
    29  
    30  	"github.com/gohugoio/hugo/common/hugo"
    31  	"github.com/gohugoio/hugo/resources/internal"
    32  
    33  	"github.com/mitchellh/mapstructure"
    34  
    35  	"github.com/gohugoio/hugo/common/herrors"
    36  	"github.com/gohugoio/hugo/resources"
    37  	"github.com/gohugoio/hugo/resources/resource"
    38  	"github.com/pkg/errors"
    39  )
    40  
    41  // Options from https://babeljs.io/docs/en/options
    42  type Options struct {
    43  	Config string // Custom path to config file
    44  
    45  	Minified   bool
    46  	NoComments bool
    47  	Compact    *bool
    48  	Verbose    bool
    49  	NoBabelrc  bool
    50  	SourceMap  string
    51  }
    52  
    53  // DecodeOptions decodes options to and generates command flags
    54  func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
    55  	if m == nil {
    56  		return
    57  	}
    58  	err = mapstructure.WeakDecode(m, &opts)
    59  	return
    60  }
    61  
    62  func (opts Options) toArgs() []string {
    63  	var args []string
    64  
    65  	// external is not a known constant on the babel command line
    66  	// .sourceMaps must be a boolean, "inline", "both", or undefined
    67  	switch opts.SourceMap {
    68  	case "external":
    69  		args = append(args, "--source-maps")
    70  	case "inline":
    71  		args = append(args, "--source-maps=inline")
    72  	}
    73  	if opts.Minified {
    74  		args = append(args, "--minified")
    75  	}
    76  	if opts.NoComments {
    77  		args = append(args, "--no-comments")
    78  	}
    79  	if opts.Compact != nil {
    80  		args = append(args, "--compact="+strconv.FormatBool(*opts.Compact))
    81  	}
    82  	if opts.Verbose {
    83  		args = append(args, "--verbose")
    84  	}
    85  	if opts.NoBabelrc {
    86  		args = append(args, "--no-babelrc")
    87  	}
    88  	return args
    89  }
    90  
    91  // Client is the client used to do Babel transformations.
    92  type Client struct {
    93  	rs *resources.Spec
    94  }
    95  
    96  // New creates a new Client with the given specification.
    97  func New(rs *resources.Spec) *Client {
    98  	return &Client{rs: rs}
    99  }
   100  
   101  type babelTransformation struct {
   102  	options Options
   103  	rs      *resources.Spec
   104  }
   105  
   106  func (t *babelTransformation) Key() internal.ResourceTransformationKey {
   107  	return internal.NewResourceTransformationKey("babel", t.options)
   108  }
   109  
   110  // Transform shells out to babel-cli to do the heavy lifting.
   111  // For this to work, you need some additional tools. To install them globally:
   112  // npm install -g @babel/core @babel/cli
   113  // If you want to use presets or plugins such as @babel/preset-env
   114  // Then you should install those globally as well. e.g:
   115  // npm install -g @babel/preset-env
   116  // Instead of installing globally, you can also install everything as a dev-dependency (--save-dev instead of -g)
   117  func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
   118  	const localBabelPath = "node_modules/.bin/"
   119  	const binaryName = "babel"
   120  
   121  	// Try first in the project's node_modules.
   122  	csiBinPath := filepath.Join(t.rs.WorkingDir, localBabelPath, binaryName)
   123  
   124  	binary := csiBinPath
   125  
   126  	if _, err := safeexec.LookPath(binary); err != nil {
   127  		// Try PATH
   128  		binary = binaryName
   129  		if _, err := safeexec.LookPath(binary); err != nil {
   130  			// This may be on a CI server etc. Will fall back to pre-built assets.
   131  			return herrors.ErrFeatureNotAvailable
   132  		}
   133  	}
   134  
   135  	var configFile string
   136  	logger := t.rs.Logger
   137  
   138  	var errBuf bytes.Buffer
   139  	infoW := loggers.LoggerToWriterWithPrefix(logger.Info(), "babel")
   140  
   141  	if t.options.Config != "" {
   142  		configFile = t.options.Config
   143  	} else {
   144  		configFile = "babel.config.js"
   145  	}
   146  
   147  	configFile = filepath.Clean(configFile)
   148  
   149  	// We need an absolute filename to the config file.
   150  	if !filepath.IsAbs(configFile) {
   151  		configFile = t.rs.BaseFs.ResolveJSConfigFile(configFile)
   152  		if configFile == "" && t.options.Config != "" {
   153  			// Only fail if the user specified config file is not found.
   154  			return errors.Errorf("babel config %q not found:", configFile)
   155  		}
   156  	}
   157  
   158  	ctx.ReplaceOutPathExtension(".js")
   159  
   160  	var cmdArgs []string
   161  
   162  	if configFile != "" {
   163  		logger.Infoln("babel: use config file", configFile)
   164  		cmdArgs = []string{"--config-file", configFile}
   165  	}
   166  
   167  	if optArgs := t.options.toArgs(); len(optArgs) > 0 {
   168  		cmdArgs = append(cmdArgs, optArgs...)
   169  	}
   170  	cmdArgs = append(cmdArgs, "--filename="+ctx.SourcePath)
   171  
   172  	// Create compile into a real temp file:
   173  	// 1. separate stdout/stderr messages from babel (https://github.com/gohugoio/hugo/issues/8136)
   174  	// 2. allow generation and retrieval of external source map.
   175  	compileOutput, err := ioutil.TempFile("", "compileOut-*.js")
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	cmdArgs = append(cmdArgs, "--out-file="+compileOutput.Name())
   181  	defer os.Remove(compileOutput.Name())
   182  
   183  	cmd, err := hexec.SafeCommand(binary, cmdArgs...)
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	cmd.Stderr = io.MultiWriter(infoW, &errBuf)
   189  	cmd.Stdout = cmd.Stderr
   190  	cmd.Env = hugo.GetExecEnviron(t.rs.WorkingDir, t.rs.Cfg, t.rs.BaseFs.Assets.Fs)
   191  
   192  	stdin, err := cmd.StdinPipe()
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	go func() {
   198  		defer stdin.Close()
   199  		io.Copy(stdin, ctx.From)
   200  	}()
   201  
   202  	err = cmd.Run()
   203  	if err != nil {
   204  		return errors.Wrap(err, errBuf.String())
   205  	}
   206  
   207  	content, err := ioutil.ReadAll(compileOutput)
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	mapFile := compileOutput.Name() + ".map"
   213  	if _, err := os.Stat(mapFile); err == nil {
   214  		defer os.Remove(mapFile)
   215  		sourceMap, err := ioutil.ReadFile(mapFile)
   216  		if err != nil {
   217  			return err
   218  		}
   219  		if err = ctx.PublishSourceMap(string(sourceMap)); err != nil {
   220  			return err
   221  		}
   222  		targetPath := path.Base(ctx.OutPath) + ".map"
   223  		re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
   224  		content = []byte(re.ReplaceAllString(string(content), "//# sourceMappingURL="+targetPath+"\n"))
   225  	}
   226  
   227  	ctx.To.Write(content)
   228  
   229  	return nil
   230  }
   231  
   232  // Process transforms the given Resource with the Babel processor.
   233  func (c *Client) Process(res resources.ResourceTransformer, options Options) (resource.Resource, error) {
   234  	return res.Transform(
   235  		&babelTransformation{rs: c.rs, options: options},
   236  	)
   237  }