Skip to content

Commit

Permalink
feat(js): add compatibility mode option to JS compiler
Browse files Browse the repository at this point in the history
Partially implements #1049.

This required a substantial refactor of `compiler.go`, mostly in order
to be able to test a compiler with different compatibility mode values
during the same test run, which was problematic since `compiler.Compiler`
was a singleton. So the solution is to instead make a new `babel` struct
the singleton, which encapsulates everything needed for Babel to run,
while allowing several `Compiler` instances to exist. In the author's
humble opinion, this has the added benefit of being a cleaner design
(which was further experimentally expanded in 4e52c78).
  • Loading branch information
Ivan Mirić committed Oct 25, 2019
1 parent bbe7794 commit c463026
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 177 deletions.
30 changes: 18 additions & 12 deletions js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
192 changes: 138 additions & 54 deletions js/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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", `
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
})
}
})
}
9 changes: 3 additions & 6 deletions js/common/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
51 changes: 51 additions & 0 deletions js/compiler/compatibility_mode_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c463026

Please sign in to comment.