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 }