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