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 }