diff --git a/js/bundle.go b/js/bundle.go index c0eac23aca2d..f9c268e1ff8c 100644 --- a/js/bundle.go +++ b/js/bundle.go @@ -60,25 +60,27 @@ type BundleInstance struct { // NewBundle creates a new bundle from a source file and a filesystem. func NewBundle(src *loader.SourceData, filesystems map[string]afero.Fs, rtOpts lib.RuntimeOptions) (*Bundle, error) { - compiler, err := compiler.New() + compatMode, err := lib.ValidateCompatibilityMode(rtOpts.CompatibilityMode.String) if err != nil { return nil, err } // Compile sources, both ES5 and ES6 are supported. code := string(src.Data) - pgm, _, err := compiler.Compile(code, src.URL.String(), "", "", true) + c := compiler.New() + pgm, _, err := c.Compile(code, src.URL.String(), "", "", true, compatMode) if err != nil { return nil, err } // Make a bundle, instantiate it into a throwaway VM to populate caches. rt := goja.New() bundle := Bundle{ - Filename: src.URL, - Source: code, - Program: pgm, - BaseInitContext: NewInitContext(rt, compiler, new(context.Context), filesystems, loader.Dir(src.URL)), - Env: rtOpts.Env, + Filename: src.URL, + Source: code, + Program: pgm, + BaseInitContext: NewInitContext(rt, c, compatMode, new(context.Context), + filesystems, loader.Dir(src.URL)), + Env: rtOpts.Env, } if err := bundle.instantiate(rt, bundle.BaseInitContext); err != nil { return nil, err @@ -129,7 +131,7 @@ func NewBundle(src *loader.SourceData, filesystems map[string]afero.Fs, rtOpts l // NewBundleFromArchive creates a new bundle from an lib.Archive. func NewBundleFromArchive(arc *lib.Archive, rtOpts lib.RuntimeOptions) (*Bundle, error) { - compiler, err := compiler.New() + compatMode, err := lib.ValidateCompatibilityMode(rtOpts.CompatibilityMode.String) if err != nil { return nil, err } @@ -138,12 +140,14 @@ func NewBundleFromArchive(arc *lib.Archive, rtOpts lib.RuntimeOptions) (*Bundle, return nil, errors.Errorf("expected bundle type 'js', got '%s'", arc.Type) } - pgm, _, err := compiler.Compile(string(arc.Data), arc.FilenameURL.String(), "", "", true) + c := compiler.New() + pgm, _, err := c.Compile(string(arc.Data), arc.FilenameURL.String(), "", "", true, compatMode) if err != nil { return nil, err } - initctx := NewInitContext(goja.New(), compiler, new(context.Context), arc.Filesystems, arc.PwdURL) + initctx := NewInitContext(goja.New(), c, compatMode, + new(context.Context), arc.Filesystems, arc.PwdURL) env := arc.Env if env == nil { @@ -235,8 +239,10 @@ func (b *Bundle) instantiate(rt *goja.Runtime, init *InitContext) error { rt.SetFieldNameMapper(common.FieldNameMapper{}) rt.SetRandSource(common.NewRandSource()) - if _, err := rt.RunProgram(jslib.GetCoreJS()); err != nil { - return err + if init.compatibilityMode == compiler.CompatibilityModeExtended { + if _, err := rt.RunProgram(jslib.GetCoreJS()); err != nil { + return err + } } exports := rt.NewObject() diff --git a/js/bundle_test.go b/js/bundle_test.go index 30ee9187ddcf..07944fcb0be3 100644 --- a/js/bundle_test.go +++ b/js/bundle_test.go @@ -33,6 +33,7 @@ import ( "time" "github.com/dop251/goja" + "github.com/loadimpact/k6/js/compiler" "github.com/loadimpact/k6/lib" "github.com/loadimpact/k6/lib/consts" "github.com/loadimpact/k6/lib/fsext" @@ -113,6 +114,40 @@ func TestNewBundle(t *testing.T) { assert.Equal(t, "file:///", b.BaseInitContext.pwd.String()) } }) + t.Run("CompatibilityModeBase", func(t *testing.T) { + t.Run("ok/Minimal", func(t *testing.T) { + rtOpts := lib.RuntimeOptions{CompatibilityMode: null.StringFrom(compiler.CompatibilityModeBase.String())} + _, err := getSimpleBundleWithOptions("/script.js", `module.exports.default = function() {};`, rtOpts) + assert.NoError(t, err) + }) + t.Run("err", func(t *testing.T) { + testCases := []struct { + name string + compatMode string + code string + expErr string + }{ + {"InvalidCompat", "es1", `export default function() {};`, + `invalid compatibility mode "es1". Use: "extended", "base"`}, + // ES6 modules are not supported + {"Modules", "base", `export default function() {};`, + "file:///script.js: Line 1:1 Unexpected reserved word"}, + // Arrow functions are not supported + {"ArrowFuncs", "base", + `module.exports.default = function() {}; () => {};`, + "file:///script.js: Line 1:42 Unexpected token ) (and 1 more errors)"}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + rtOpts := lib.RuntimeOptions{CompatibilityMode: null.StringFrom(tc.compatMode)} + _, err := getSimpleBundleWithOptions("/script.js", tc.code, rtOpts) + assert.EqualError(t, err, tc.expErr) + }) + } + }) + }) t.Run("Options", func(t *testing.T) { t.Run("Empty", func(t *testing.T) { _, err := getSimpleBundle("/script.js", ` @@ -373,65 +408,53 @@ func TestNewBundle(t *testing.T) { }) } -func TestNewBundleFromArchive(t *testing.T) { - fs := afero.NewMemMapFs() - assert.NoError(t, fs.MkdirAll("/path/to", 0755)) - assert.NoError(t, afero.WriteFile(fs, "/path/to/file.txt", []byte(`hi`), 0644)) - assert.NoError(t, afero.WriteFile(fs, "/path/to/exclaim.js", []byte(`export default function(s) { return s + "!" };`), 0644)) - - data := ` - import exclaim from "./exclaim.js"; - export let options = { vus: 12345 }; - export let file = open("./file.txt"); - export default function() { return exclaim(file); }; - ` - b, err := getSimpleBundleWithFs("/path/to/script.js", data, fs) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, lib.Options{VUs: null.IntFrom(12345)}, b.Options) - - bi, err := b.Instantiate() - if !assert.NoError(t, err) { - return +func getArchive(data string, rtOpts lib.RuntimeOptions) (*lib.Archive, error) { + b, err := getSimpleBundleWithOptions("script.js", data, rtOpts) + if err != nil { + return nil, err } - v, err := bi.Default(goja.Undefined()) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "hi!", v.Export()) - - arc := b.makeArchive() - assert.Equal(t, "js", arc.Type) - assert.Equal(t, lib.Options{VUs: null.IntFrom(12345)}, arc.Options) - assert.Equal(t, "file:///path/to/script.js", arc.FilenameURL.String()) - assert.Equal(t, data, string(arc.Data)) - assert.Equal(t, "file:///path/to/", arc.PwdURL.String()) + return b.makeArchive(), nil +} - exclaimData, err := afero.ReadFile(arc.Filesystems["file"], "/path/to/exclaim.js") - assert.NoError(t, err) - assert.Equal(t, `export default function(s) { return s + "!" };`, string(exclaimData)) +func TestNewBundleFromArchive(t *testing.T) { + t.Run("ok", func(t *testing.T) { + testCases := []struct { + name string + code string + rtOpts lib.RuntimeOptions + }{ + {"MinimalExtended", `export let options = { vus: 12345 }; + export default function() { return "hi!"; };`, + lib.RuntimeOptions{}}, + {"MinimalBase", `module.exports.options = { vus: 12345 }; + module.exports.default = function() { return "hi!" };`, + lib.RuntimeOptions{CompatibilityMode: null.StringFrom(compiler.CompatibilityModeBase.String())}}, + } - fileData, err := afero.ReadFile(arc.Filesystems["file"], "/path/to/file.txt") - assert.NoError(t, err) - assert.Equal(t, `hi`, string(fileData)) - assert.Equal(t, consts.Version, arc.K6Version) + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + arc, err := getArchive(tc.code, tc.rtOpts) + assert.NoError(t, err) + b, err := NewBundleFromArchive(arc, tc.rtOpts) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, lib.Options{VUs: null.IntFrom(12345)}, b.Options) - b2, err := NewBundleFromArchive(arc, lib.RuntimeOptions{}) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, lib.Options{VUs: null.IntFrom(12345)}, b2.Options) + bi, err := b.Instantiate() + if !assert.NoError(t, err) { + return + } + val, err := bi.Default(goja.Undefined()) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, "hi!", val.Export()) + }) + } + }) - bi2, err := b.Instantiate() - if !assert.NoError(t, err) { - return - } - v2, err := bi2.Default(goja.Undefined()) - if !assert.NoError(t, err) { - return - } - assert.Equal(t, "hi!", v2.Export()) } func TestOpen(t *testing.T) { @@ -675,3 +698,64 @@ func TestBundleEnv(t *testing.T) { }) } } + +func TestBundleMakeArchive(t *testing.T) { + t.Run("ok", func(t *testing.T) { + fs := afero.NewMemMapFs() + _ = fs.MkdirAll("/path/to", 0755) + _ = afero.WriteFile(fs, "/path/to/file.txt", []byte(`hi`), 0644) + _ = afero.WriteFile(fs, "/path/to/exclaim.js", []byte(`export default function(s) { return s + "!" };`), 0644) + data := ` + import exclaim from "./exclaim.js"; + export let options = { vus: 12345 }; + export let file = open("./file.txt"); + export default function() { return exclaim(file); }; + ` + b, err := getSimpleBundleWithFs("/path/to/script.js", data, fs) + assert.NoError(t, err) + + arc := b.makeArchive() + + assert.Equal(t, "js", arc.Type) + assert.Equal(t, lib.Options{VUs: null.IntFrom(12345)}, arc.Options) + assert.Equal(t, "file:///path/to/script.js", arc.FilenameURL.String()) + assert.Equal(t, data, string(arc.Data)) + assert.Equal(t, "file:///path/to/", arc.PwdURL.String()) + + exclaimData, err := afero.ReadFile(arc.Filesystems["file"], "/path/to/exclaim.js") + assert.NoError(t, err) + assert.Equal(t, `export default function(s) { return s + "!" };`, string(exclaimData)) + + fileData, err := afero.ReadFile(arc.Filesystems["file"], "/path/to/file.txt") + assert.NoError(t, err) + assert.Equal(t, `hi`, string(fileData)) + assert.Equal(t, consts.Version, arc.K6Version) + }) + t.Run("err", func(t *testing.T) { + testCases := []struct { + name string + compatMode string + code string + expErr string + }{ + {"InvalidCompat", "es1", `export default function() {};`, + `invalid compatibility mode "es1". Use: "extended", "base"`}, + // ES6 modules are not supported + {"Modules", "base", `export default function() {};`, + "file://script.js: Line 1:1 Unexpected reserved word"}, + // Arrow functions are not supported + {"ArrowFuncs", "base", + `module.exports.default = function() {}; () => {};`, + "file://script.js: Line 1:42 Unexpected token ) (and 1 more errors)"}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + rtOpts := lib.RuntimeOptions{CompatibilityMode: null.StringFrom(tc.compatMode)} + _, err := getArchive(tc.code, rtOpts) + assert.EqualError(t, err, tc.expErr) + }) + } + }) +} diff --git a/js/common/util.go b/js/common/util.go index 658ee58ccdd9..397633bed457 100644 --- a/js/common/util.go +++ b/js/common/util.go @@ -27,12 +27,9 @@ import ( // Runs an ES6 string in the given runtime. Use this rather than writing ES5 in tests. func RunString(rt *goja.Runtime, src string) (goja.Value, error) { - compiler, err := compiler.New() - if err != nil { - panic(err) - } - - src, _, err = compiler.Transform(src, "__string__") + var err error + c := compiler.New() + src, _, err = c.Transform(src, "__string__") if err != nil { return goja.Undefined(), err } diff --git a/js/compiler/compatibility_mode_gen.go b/js/compiler/compatibility_mode_gen.go new file mode 100644 index 000000000000..19d46d1876f9 --- /dev/null +++ b/js/compiler/compatibility_mode_gen.go @@ -0,0 +1,51 @@ +// Code generated by "enumer -type=CompatibilityMode -transform=snake -trimprefix CompatibilityMode -output compatibility_mode_gen.go"; DO NOT EDIT. + +// +package compiler + +import ( + "fmt" +) + +const _CompatibilityModeName = "extendedbase" + +var _CompatibilityModeIndex = [...]uint8{0, 8, 12} + +func (i CompatibilityMode) String() string { + i -= 1 + if i >= CompatibilityMode(len(_CompatibilityModeIndex)-1) { + return fmt.Sprintf("CompatibilityMode(%d)", i+1) + } + return _CompatibilityModeName[_CompatibilityModeIndex[i]:_CompatibilityModeIndex[i+1]] +} + +var _CompatibilityModeValues = []CompatibilityMode{1, 2} + +var _CompatibilityModeNameToValueMap = map[string]CompatibilityMode{ + _CompatibilityModeName[0:8]: 1, + _CompatibilityModeName[8:12]: 2, +} + +// CompatibilityModeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func CompatibilityModeString(s string) (CompatibilityMode, error) { + if val, ok := _CompatibilityModeNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to CompatibilityMode values", s) +} + +// CompatibilityModeValues returns all values of the enum +func CompatibilityModeValues() []CompatibilityMode { + return _CompatibilityModeValues +} + +// IsACompatibilityMode returns "true" if the value is listed in the enum definition. "false" otherwise +func (i CompatibilityMode) IsACompatibilityMode() bool { + for _, v := range _CompatibilityModeValues { + if i == v { + return true + } + } + return false +} diff --git a/js/compiler/compiler.go b/js/compiler/compiler.go index ff8e74de617c..8de753ccca95 100644 --- a/js/compiler/compiler.go +++ b/js/compiler/compiler.go @@ -44,103 +44,119 @@ var ( "highlightCode": false, } - compilerInstance *Compiler - once sync.Once + once sync.Once // nolint:gochecknoglobals + babl *babel // nolint:gochecknoglobals ) -// A Compiler uses Babel to compile ES6 code into something ES5-compatible. -type Compiler struct { - vm *goja.Runtime +// 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 - // JS pointers. - this goja.Value - transform goja.Callable - mutex sync.Mutex //TODO: cache goja.CompileAST() in an init() function? -} - -// Constructs a new compiler. -func New() (*Compiler, error) { - var err error - once.Do(func() { - compilerInstance, err = new() - }) - - return compilerInstance, err -} - -func new() (*Compiler, error) { - conf := rice.Config{ - LocateOrder: []rice.LocateMethod{rice.LocateEmbedded}, - } - - babelSrc := conf.MustFindBox("lib").MustString("babel.min.js") - - c := &Compiler{vm: goja.New()} - if _, err := c.vm.RunString(babelSrc); err != nil { - return nil, err - } +const ( + // CompatibilityModeExtended achieves ES6+ compatibility with Babel and core.js + CompatibilityModeExtended CompatibilityMode = iota + 1 + // CompatibilityModeBase is standard goja ES5.1+ + CompatibilityModeBase +) - c.this = c.vm.Get("Babel") - thisObj := c.this.ToObject(c.vm) - if err := c.vm.ExportTo(thisObj.Get("transform"), &c.transform); err != nil { - return nil, err - } +// A Compiler compiles JavaScript source code (ES5.1 or ES6) into a goja.Program +type Compiler struct{} - return c, nil +// New returns a new Compiler +func New() *Compiler { + return &Compiler{} } -// Transform the given code into ES5. +// Transform the given code into ES5, while synchronizing to ensure only a single +// babel instance is in use. func (c *Compiler) Transform(src, filename string) (code string, srcmap SourceMap, err error) { - opts := make(map[string]interface{}) - for k, v := range DefaultOpts { - opts[k] = v - } - opts["filename"] = filename - - c.mutex.Lock() - defer c.mutex.Unlock() - - startTime := time.Now() - v, err := c.transform(c.this, c.vm.ToValue(src), c.vm.ToValue(opts)) - if err != nil { - return code, srcmap, err - } - logrus.WithField("t", time.Since(startTime)).Debug("Babel: Transformed") - vO := v.ToObject(c.vm) - - if err := c.vm.ExportTo(vO.Get("code"), &code); err != nil { - return code, srcmap, err - } - - var rawmap map[string]interface{} - if err := c.vm.ExportTo(vO.Get("map"), &rawmap); err != nil { - return code, srcmap, err - } - if err := mapstructure.Decode(rawmap, &srcmap); err != nil { - return code, srcmap, err + var b *babel + if b, err = newBabel(); err != nil { + return } - return code, srcmap, nil + b.mutex.Lock() + defer b.mutex.Unlock() + return b.Transform(src, filename) } -// Compiles the program, first trying ES5, then ES6. -func (c *Compiler) Compile(src, filename string, pre, post string, strict bool) (*goja.Program, string, error) { - return c.compile(src, filename, pre, post, strict, true) +// Compile the program +func (c *Compiler) Compile(src, filename string, pre, post string, + strict bool, compatMode CompatibilityMode) (*goja.Program, string, error) { + return c.compile(src, filename, pre, post, strict, compatMode) } -func (c *Compiler) compile(src, filename string, pre, post string, strict, tryBabel bool) (*goja.Program, string, error) { +func (c *Compiler) compile(src, filename string, pre, post string, + strict bool, compatMode CompatibilityMode) (*goja.Program, string, error) { code := pre + src + post ast, err := parser.ParseFile(nil, filename, code, 0) if err != nil { - if tryBabel { + if compatMode == CompatibilityModeExtended { code, _, err := c.Transform(src, filename) if err != nil { return nil, code, err } - return c.compile(code, filename, pre, post, strict, false) + return c.compile(code, filename, pre, post, strict, compatMode) } return nil, src, err } pgm, err := goja.CompileAST(ast, strict) return pgm, code, err } + +type babel struct { + vm *goja.Runtime + this goja.Value + transform goja.Callable + mutex sync.Mutex //TODO: cache goja.CompileAST() in an init() function? +} + +func newBabel() (*babel, error) { + var err error + + once.Do(func() { + conf := rice.Config{ + LocateOrder: []rice.LocateMethod{rice.LocateEmbedded}, + } + babelSrc := conf.MustFindBox("lib").MustString("babel.min.js") + vm := goja.New() + if _, err = vm.RunString(babelSrc); err != nil { + return + } + + this := vm.Get("Babel") + bObj := this.ToObject(vm) + babl = &babel{vm: vm, this: this} + if err = vm.ExportTo(bObj.Get("transform"), &babl.transform); err != nil { + return + } + }) + + return babl, err +} + +func (b *babel) Transform(src, filename string) (code string, srcmap SourceMap, err error) { + opts := DefaultOpts + opts["filename"] = filename + + startTime := time.Now() + v, err := b.transform(b.this, b.vm.ToValue(src), b.vm.ToValue(opts)) + if err != nil { + return + } + logrus.WithField("t", time.Since(startTime)).Debug("Babel: Transformed") + + vO := v.ToObject(b.vm) + if err = b.vm.ExportTo(vO.Get("code"), &code); err != nil { + return + } + var rawmap map[string]interface{} + if err = b.vm.ExportTo(vO.Get("map"), &rawmap); err != nil { + return + } + if err = mapstructure.Decode(rawmap, &srcmap); err != nil { + return + } + return +} diff --git a/js/compiler/compiler_test.go b/js/compiler/compiler_test.go index 79cbec0a220c..add1625cbeab 100644 --- a/js/compiler/compiler_test.go +++ b/js/compiler/compiler_test.go @@ -27,18 +27,8 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNew(t *testing.T) { - c, err := New() - assert.NotNil(t, c) - assert.NoError(t, err) -} - func TestTransform(t *testing.T) { - c, err := New() - if !assert.NoError(t, err) { - return - } - + c := New() t.Run("blank", func(t *testing.T) { src, _, err := c.Transform("", "test.js") assert.NoError(t, err) @@ -78,13 +68,10 @@ func TestTransform(t *testing.T) { } func TestCompile(t *testing.T) { - c, err := New() - if !assert.NoError(t, err) { - return - } + c := New() t.Run("ES5", func(t *testing.T) { src := `1+(function() { return 2; })()` - pgm, code, err := c.Compile(src, "script.js", "", "", true) + pgm, code, err := c.Compile(src, "script.js", "", "", true, CompatibilityModeBase) if !assert.NoError(t, err) { return } @@ -95,7 +82,8 @@ func TestCompile(t *testing.T) { } t.Run("Wrap", func(t *testing.T) { - pgm, code, err := c.Compile(src, "script.js", "(function(){return ", "})", true) + pgm, code, err := c.Compile(src, "script.js", + "(function(){return ", "})", true, CompatibilityModeBase) if !assert.NoError(t, err) { return } @@ -114,14 +102,14 @@ func TestCompile(t *testing.T) { t.Run("Invalid", func(t *testing.T) { src := `1+(function() { return 2; )()` - _, _, err := c.Compile(src, "script.js", "", "", true) + _, _, err := c.Compile(src, "script.js", "", "", true, CompatibilityModeExtended) assert.IsType(t, &goja.Exception{}, err) assert.Contains(t, err.Error(), `SyntaxError: script.js: Unexpected token (1:26) > 1 | 1+(function() { return 2; )()`) }) }) t.Run("ES6", func(t *testing.T) { - pgm, code, err := c.Compile(`1+(()=>2)()`, "script.js", "", "", true) + pgm, code, err := c.Compile(`1+(()=>2)()`, "script.js", "", "", true, CompatibilityModeExtended) if !assert.NoError(t, err) { return } @@ -132,7 +120,7 @@ func TestCompile(t *testing.T) { } t.Run("Wrap", func(t *testing.T) { - pgm, code, err := c.Compile(`fn(1+(()=>2)())`, "script.js", "(function(fn){", "})", true) + pgm, code, err := c.Compile(`fn(1+(()=>2)())`, "script.js", "(function(fn){", "})", true, CompatibilityModeExtended) if !assert.NoError(t, err) { return } @@ -153,7 +141,7 @@ func TestCompile(t *testing.T) { }) t.Run("Invalid", func(t *testing.T) { - _, _, err := c.Compile(`1+(=>2)()`, "script.js", "", "", true) + _, _, err := c.Compile(`1+(=>2)()`, "script.js", "", "", true, CompatibilityModeExtended) assert.IsType(t, &goja.Exception{}, err) assert.Contains(t, err.Error(), `SyntaxError: script.js: Unexpected token (1:3) > 1 | 1+(=>2)()`) diff --git a/js/initcontext.go b/js/initcontext.go index ecd3008a1f3e..2941b28f1dfa 100644 --- a/js/initcontext.go +++ b/js/initcontext.go @@ -56,20 +56,23 @@ type InitContext struct { // Cache of loaded programs and files. programs map[string]programWithSource + + compatibilityMode compiler.CompatibilityMode } // NewInitContext creates a new initcontext with the provided arguments func NewInitContext( - rt *goja.Runtime, compiler *compiler.Compiler, ctxPtr *context.Context, filesystems map[string]afero.Fs, pwd *url.URL, + rt *goja.Runtime, c *compiler.Compiler, compatMode compiler.CompatibilityMode, + ctxPtr *context.Context, filesystems map[string]afero.Fs, pwd *url.URL, ) *InitContext { return &InitContext{ - runtime: rt, - compiler: compiler, - ctxPtr: ctxPtr, - filesystems: filesystems, - pwd: pwd, - - programs: make(map[string]programWithSource), + runtime: rt, + compiler: c, + ctxPtr: ctxPtr, + filesystems: filesystems, + pwd: pwd, + programs: make(map[string]programWithSource), + compatibilityMode: compatMode, } } @@ -92,7 +95,8 @@ func newBoundInitContext(base *InitContext, ctxPtr *context.Context, rt *goja.Ru pwd: base.pwd, compiler: base.compiler, - programs: programs, + programs: programs, + compatibilityMode: base.compatibilityMode, } } @@ -177,7 +181,8 @@ func (i *InitContext) requireFile(name string) (goja.Value, error) { } func (i *InitContext) compileImport(src, filename string) (*goja.Program, error) { - pgm, _, err := i.compiler.Compile(src, filename, "(function(module, exports){\n", "\n})\n", true) + pgm, _, err := i.compiler.Compile(src, filename, + "(function(module, exports){\n", "\n})\n", true, i.compatibilityMode) return pgm, err } diff --git a/lib/runtime_options.go b/lib/runtime_options.go index 124ba7274d6d..3544857904b6 100644 --- a/lib/runtime_options.go +++ b/lib/runtime_options.go @@ -20,13 +20,22 @@ package lib -import null "gopkg.in/guregu/null.v3" +import ( + "fmt" + "strings" + + "github.com/loadimpact/k6/js/compiler" + null "gopkg.in/guregu/null.v3" +) // RuntimeOptions are settings passed onto the goja JS runtime type RuntimeOptions struct { // Whether to pass the actual system environment variables to the JS runtime IncludeSystemEnvVars null.Bool `json:"includeSystemEnvVars" envconfig:"K6_INCLUDE_SYSTEM_ENV_VARS"` + // JS compatibility mode: "latest" (Goja+Babel+core.js) or "es5" (plain Goja) + CompatibilityMode null.String `json:"compatibilityMode"` + // Environment variables passed onto the runner Env map[string]string `json:"env" envconfig:"K6_ENV"` } @@ -37,8 +46,27 @@ func (o RuntimeOptions) Apply(opts RuntimeOptions) RuntimeOptions { if opts.IncludeSystemEnvVars.Valid { o.IncludeSystemEnvVars = opts.IncludeSystemEnvVars } + if opts.CompatibilityMode.Valid { + o.CompatibilityMode = opts.CompatibilityMode + } if opts.Env != nil { o.Env = opts.Env } return o } + +// ValidateCompatibilityMode checks if the provided val is a valid compatibility mode +func ValidateCompatibilityMode(val string) (cm compiler.CompatibilityMode, err error) { + if val == "" { + return compiler.CompatibilityModeExtended, nil + } + if cm, err = compiler.CompatibilityModeString(val); err != nil { + var compatValues []string + for _, v := range compiler.CompatibilityModeValues() { + compatValues = append(compatValues, v.String()) + } + err = fmt.Errorf(`invalid compatibility mode "%s". Use: "%s"`, + val, strings.Join(compatValues, `", "`)) + } + return +}