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 }