-
Notifications
You must be signed in to change notification settings - Fork 66
/
compiler.go
431 lines (391 loc) · 14.7 KB
/
compiler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
package gojaPlugin
import (
"fmt"
"github.com/dop251/goja"
"github.com/dop251/goja/parser"
"github.com/go-sourcemap/sourcemap"
"github.com/sirupsen/logrus"
"os"
"strconv"
"sync"
"time"
_ "embed" // we need this for embedding Babel
"encoding/json"
"errors"
)
//go:embed babel.min.js
var babelSrc string //nolint:gochecknoglobals
var (
DefaultOpts = map[string]interface{}{
// "presets": []string{"latest"},
"plugins": []interface{}{
// es2015 https://github.com/babel/babel/blob/v6.26.0/packages/babel-preset-es2015/src/index.js
// in goja
// []interface{}{"transform-es2015-template-literals", map[string]interface{}{"loose": false, "spec": false}},
// "transform-es2015-literals", // in goja
// "transform-es2015-function-name", // in goja
// []interface{}{"transform-es2015-arrow-functions", map[string]interface{}{"spec": false}}, // in goja
// "transform-es2015-block-scoped-functions", // in goja
// []interface{}{"transform-es2015-classes", map[string]interface{}{"loose": false}}, // in goja
// "transform-es2015-object-super", // in goja
// "transform-es2015-shorthand-properties", // in goja
// "transform-es2015-duplicate-keys", // in goja
// []interface{}{"transform-es2015-computed-properties", map[string]interface{}{"loose": false}}, // in goja
// "transform-es2015-for-of", // in goja
// "transform-es2015-sticky-regex", // in goja
// "transform-es2015-unicode-regex", // in goja
// "check-es2015-constants", // in goja
// []interface{}{"transform-es2015-spread", map[string]interface{}{"loose": false}}, // in goja
// "transform-es2015-parameters", // in goja
// []interface{}{"transform-es2015-destructuring", map[string]interface{}{"loose": false}}, // in goja
// "transform-es2015-block-scoping", // in goja
// "transform-es2015-typeof-symbol", // in goja
// all the other module plugins are just dropped
[]interface{}{"transform-es2015-modules-commonjs", map[string]interface{}{"loose": false}},
// "transform-regenerator", // Doesn't really work unless regeneratorRuntime is also added
// es2016 https://github.com/babel/babel/blob/v6.26.0/packages/babel-preset-es2016/src/index.js
// "transform-exponentiation-operator",
// es2017 https://github.com/babel/babel/blob/v6.26.0/packages/babel-preset-es2017/src/index.js
// "syntax-trailing-function-commas", // in goja
// "transform-async-to-generator", // Doesn't really work unless regeneratorRuntime is also added
},
"ast": false,
"sourceMaps": false,
"babelrc": false,
"compact": false,
"retainLines": true,
"highlightCode": false,
}
maxSrcLenForBabelSourceMap = 250 * 1024 //nolint:gochecknoglobals
maxSrcLenForBabelSourceMapOnce sync.Once //nolint:gochecknoglobals
onceBabelCode sync.Once //nolint:gochecknoglobals
globalBabelCode *goja.Program //nolint:gochecknoglobals
errGlobalBabelCode error //nolint:gochecknoglobals
onceBabel sync.Once //nolint:gochecknoglobals
globalBabel *babel //nolint:gochecknoglobals
)
const (
maxSrcLenForBabelSourceMapVarName = "K6_DEBUG_SOURCEMAP_FILESIZE_LIMIT"
sourceMapURLFromBabel = "k6://internal-should-not-leak/file.map"
)
// A Compiler compiles JavaScript source code (ES5.1 or ES6) into a goja.Program
type Compiler struct {
logger logrus.FieldLogger
babel *babel
Options Options
}
// New returns a new Compiler
func NewCompiler(logger logrus.FieldLogger) *Compiler {
return &Compiler{logger: logger}
}
// initializeBabel initializes a separate (non-global) instance of babel specifically for this Compiler.
// An error is returned only if babel itself couldn't be parsed/run which should never be possible.
func (c *Compiler) initializeBabel() error {
var err error
if c.babel == nil {
c.babel, err = newBabel()
}
return err
}
// Transform the given code into ES5
func (c *Compiler) Transform(src, filename string, inputSrcMap []byte) (code string, srcMap []byte, err error) {
if c.babel == nil {
onceBabel.Do(func() {
globalBabel, err = newBabel()
})
c.babel = globalBabel
}
if err != nil {
return
}
sourceMapEnabled := c.Options.SourceMapLoader != nil
maxSrcLenForBabelSourceMapOnce.Do(func() {
// TODO: drop this code and everything it's connected to when babel is dropped
v := os.Getenv(maxSrcLenForBabelSourceMapVarName)
if len(v) > 0 {
i, err := strconv.Atoi(v) //nolint:govet // we shadow err on purpose
if err != nil {
c.logger.Warnf("Tried to parse %q from %s as integer but couldn't %s\n",
v, maxSrcLenForBabelSourceMapVarName, err)
return
}
maxSrcLenForBabelSourceMap = i
}
})
if sourceMapEnabled && len(src) > maxSrcLenForBabelSourceMap {
sourceMapEnabled = false
c.logger.Warnf("The source for `%s` needs to go through babel but is over %d bytes. "+
"For performance reasons source map support will be disabled for this particular file.",
filename, maxSrcLenForBabelSourceMap)
}
// check that babel will likely be able to parse the inputSrcMap
if sourceMapEnabled && len(inputSrcMap) != 0 {
if err = verifySourceMapForBabel(inputSrcMap); err != nil {
sourceMapEnabled = false
inputSrcMap = nil
c.logger.WithError(err).Warnf(
"The source for `%s` needs to be transpiled by Babel, but its source map will"+
" not be accepted by Babel, so it was disabled", filename)
}
}
code, srcMap, err = c.babel.transformImpl(c.logger, src, filename, sourceMapEnabled, inputSrcMap)
return
}
// Options are options to the compiler
type Options struct {
CompatibilityMode CompatibilityMode
SourceMapLoader func(string) ([]byte, error)
Strict bool
}
// compilationState is helper struct to keep the state of a compilation
type compilationState struct {
// set when we couldn't load external source map so we can try parsing without loading it
couldntLoadSourceMap bool
// srcMap is the current full sourceMap that has been generated read so far
srcMap []byte
srcMapError error
wrapped bool // whether the original source is wrapped in a function to make it a commonjs module
compiler *Compiler
}
// Compile the program in the given CompatibilityMode, wrapping it between pre and post code
// TODO isESM will be used once goja support ESM modules natively
func (c *Compiler) Compile(src, filename string, isESM bool) (*goja.Program, string, error) {
return c.compileImpl(src, filename, !isESM, c.Options.CompatibilityMode, nil)
}
// sourceMapLoader is to be used with goja's WithSourceMapLoader
// it not only gets the file from disk in the simple case, but also returns it if the map was generated from babel
// additioanlly it fixes off by one error in commonjs dependencies due to having to wrap them in a function.
func (c *compilationState) sourceMapLoader(path string) ([]byte, error) {
if path == sourceMapURLFromBabel {
if c.wrapped {
return c.increaseMappingsByOne(c.srcMap)
}
return c.srcMap, nil
}
c.srcMap, c.srcMapError = c.compiler.Options.SourceMapLoader(path)
if c.srcMapError != nil {
c.couldntLoadSourceMap = true
return nil, c.srcMapError
}
_, c.srcMapError = sourcemap.Parse(path, c.srcMap)
if c.srcMapError != nil {
c.couldntLoadSourceMap = true
c.srcMap = nil
return nil, c.srcMapError
}
if c.wrapped {
return c.increaseMappingsByOne(c.srcMap)
}
return c.srcMap, nil
}
func (c *Compiler) compileImpl(
src, filename string, wrap bool, compatibilityMode CompatibilityMode, srcMap []byte,
) (*goja.Program, string, error) {
code := src
state := compilationState{srcMap: srcMap, compiler: c, wrapped: wrap}
if wrap { // the lines in the sourcemap (if available) will be fixed by increaseMappingsByOne
code = "(function(module, exports){\n" + code + "\n})\n"
}
opts := parser.WithDisableSourceMaps
if c.Options.SourceMapLoader != nil {
opts = parser.WithSourceMapLoader(state.sourceMapLoader)
}
ast, err := parser.ParseFile(nil, filename, code, 0, opts)
if state.couldntLoadSourceMap {
state.couldntLoadSourceMap = false // reset
// we probably don't want to abort scripts which have source maps but they can't be found,
// this also will be a breaking change, so if we couldn't we retry with it disabled
c.logger.WithError(state.srcMapError).Warnf("Couldn't load source map for %s", filename)
ast, err = parser.ParseFile(nil, filename, code, 0, parser.WithDisableSourceMaps)
}
if err != nil {
if compatibilityMode == CompatibilityModeExtended {
code, state.srcMap, err = c.Transform(src, filename, state.srcMap)
if err != nil {
return nil, code, err
}
// the compatibility mode "decreases" here as we shouldn't transform twice
return c.compileImpl(code, filename, wrap, CompatibilityModeBase, state.srcMap)
}
return nil, code, err
}
pgm, err := goja.CompileAST(ast, c.Options.Strict)
return pgm, code, err
}
type babel struct {
vm *goja.Runtime
this goja.Value
transform goja.Callable
m sync.Mutex
}
func newBabel() (*babel, error) {
onceBabelCode.Do(func() {
globalBabelCode, errGlobalBabelCode = goja.Compile("<internal/k6/compiler/lib/babel.min.js>", babelSrc, false)
})
if errGlobalBabelCode != nil {
return nil, errGlobalBabelCode
}
vm := goja.New()
_, err := vm.RunProgram(globalBabelCode)
if err != nil {
return nil, err
}
this := vm.Get("Babel")
bObj := this.ToObject(vm)
result := &babel{vm: vm, this: this}
if err = vm.ExportTo(bObj.Get("transform"), &result.transform); err != nil {
return nil, err
}
return result, err
}
// increaseMappingsByOne increases the lines in the sourcemap by line so that it fixes the case where we need to wrap a
// required file in a function to support/emulate commonjs
func (c *compilationState) increaseMappingsByOne(sourceMap []byte) ([]byte, error) {
var err error
m := make(map[string]interface{})
if err = json.Unmarshal(sourceMap, &m); err != nil {
return nil, err
}
mappings, ok := m["mappings"]
if !ok {
// no mappings, no idea what this will do, but just return it as technically we can have sourcemap with sections
// TODO implement incrementing of `offset` in the sections? to support that case as well
// see https://sourcemaps.info/spec.html#h.n05z8dfyl3yh
//
// TODO (kind of alternatively) drop the newline in the "commonjs" wrapping and have only the first line wrong
// and drop this whole function
return sourceMap, nil
}
if str, ok := mappings.(string); ok {
// ';' is the separator between lines so just adding 1 will make all mappings be for the line after which they were
// originally
m["mappings"] = ";" + str
} else {
// we have mappings but it's not a string - this is some kind of error
// we still won't abort the test but just not load the sourcemap
c.couldntLoadSourceMap = true
return nil, errors.New(`missing "mappings" in sourcemap`)
}
return json.Marshal(m)
}
// transformImpl the given code into ES5, while synchronizing to ensure only a single
// bundle instance / Goja VM is in use at a time.
func (b *babel) transformImpl(
logger logrus.FieldLogger, src, filename string, sourceMapsEnabled bool, inputSrcMap []byte,
) (string, []byte, error) {
b.m.Lock()
defer b.m.Unlock()
opts := make(map[string]interface{})
for k, v := range DefaultOpts {
opts[k] = v
}
if sourceMapsEnabled {
// given that the source map should provide accurate lines(and columns), this option isn't needed
// it also happens to make very long and awkward lines, especially around import/exports and definitely a lot
// less readable overall. Hopefully it also has some performance improvement not trying to keep the same lines
opts["retainLines"] = false
opts["sourceMaps"] = true
if inputSrcMap != nil {
srcMap := new(map[string]interface{})
if err := json.Unmarshal(inputSrcMap, &srcMap); err != nil {
return "", nil, err
}
opts["inputSourceMap"] = srcMap
}
}
opts["filename"] = filename
startTime := time.Now()
v, err := b.transform(b.this, b.vm.ToValue(src), b.vm.ToValue(opts))
if err != nil {
return "", nil, err
}
logger.WithField("t", time.Since(startTime)).Debug("Babel: Transformed")
vO := v.ToObject(b.vm)
var code string
if err = b.vm.ExportTo(vO.Get("code"), &code); err != nil {
return code, nil, err
}
if !sourceMapsEnabled {
return code, nil, nil
}
// this is to make goja try to load a sourcemap.
// it is a special url as it should never leak outside of this code
// additionally the alternative support from babel is to embed *the whole* sourcemap at the end
code += "\n//# sourceMappingURL=" + sourceMapURLFromBabel
stringify, err := b.vm.RunString("(function(m) { return JSON.stringify(m)})")
if err != nil {
return code, nil, err
}
c, _ := goja.AssertFunction(stringify)
mapAsJSON, err := c(goja.Undefined(), vO.Get("map"))
if err != nil {
return code, nil, err
}
return code, []byte(mapAsJSON.String()), nil
}
// Pool is a pool of compilers so it can be used easier in parallel tests as they have their own babel.
type Pool struct {
c chan *Compiler
}
// NewPool creates a Pool that will be using the provided logger and will preallocate (in parallel)
// the count of compilers each with their own babel.
//func NewPool(logger logrus.FieldLogger, count int) *Pool {
// c := &Pool{
// c: make(chan *Compiler, count),
// }
// go func() {
// for i := 0; i < count; i++ {
// go func() {
// co := New(logger)
// err := co.initializeBabel()
// if err != nil {
// panic(err)
// }
// c.Put(co)
// }()
// }
// }()
//
// return c
//}
// Get a compiler from the pool.
func (c *Pool) Get() *Compiler {
return <-c.c
}
// Put a compiler back in the pool.
func (c *Pool) Put(co *Compiler) {
c.c <- co
}
func verifySourceMapForBabel(srcMap []byte) error {
// this function exists to do what babel checks in sourcemap before we give it to it.
m := make(map[string]json.RawMessage)
err := json.Unmarshal(srcMap, &m)
if err != nil {
return fmt.Errorf("source map is not valid json: %w", err)
}
// there are no checks on it's value in babel
// we technically only support v3 though
if _, ok := m["version"]; !ok {
return fmt.Errorf("source map missing required 'version' field")
}
// This actually gets checked by the go implementation so it's not really necessary
if _, ok := m["mappings"]; !ok {
return fmt.Errorf("source map missing required 'mappings' field")
}
// the go implementation checks the value even if it doesn't require it exists
if _, ok := m["sources"]; !ok {
return fmt.Errorf("source map missing required 'sources' field")
}
return nil
}
// CompatibilityMode specifies the JS compatibility mode
//
//nolint:lll
//go:generate enumer -type=CompatibilityMode -transform=snake -trimprefix CompatibilityMode -output compatibility_mode_gen.go
type CompatibilityMode uint8
const (
// CompatibilityModeExtended achieves ES6+ compatibility with Babel
CompatibilityModeExtended CompatibilityMode = iota + 1
// CompatibilityModeBase is standard goja ES5.1+
CompatibilityModeBase
)