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 }