hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

babel.go (6866B)

    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 	"fmt"
   19 	"io"
   20 	"io/ioutil"
   21 	"os"
   22 	"path"
   23 	"path/filepath"
   24 	"regexp"
   25 	"strconv"
   26 
   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 )
   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]any) (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() []any {
   62 	var args []any
   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 fmt.Errorf("babel config %q not found:", configFile)
  145 		}
  146 	}
  147 
  148 	ctx.ReplaceOutPathExtension(".js")
  149 
  150 	var cmdArgs []any
  151 
  152 	if configFile != "" {
  153 		logger.Infoln("babel: use config file", configFile)
  154 		cmdArgs = []any{"--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 fmt.Errorf(errBuf.String()+": %w", err)
  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 }