Skip to content

Commit

Permalink
feat: TypeScript and ES6+ support using esbuild.
Browse files Browse the repository at this point in the history
With the introduction of the "enhanced" compatibility mode, the test source code is transformed
using esbuild instead of Babel.

Source files with the extension ".ts" are loaded by esbuild's TypeScript loader, which results in partial
TypeScript support. This esbuild removes exactly the type information, but does not provide type safety.

Source files other than ".ts" are loaded by esbuild's JavaScript loader, which results in the support of a
more modern JavaScript dialect than goja.
  • Loading branch information
szkiba committed May 13, 2024
1 parent 633d338 commit b0486e4
Show file tree
Hide file tree
Showing 121 changed files with 88,121 additions and 18 deletions.
3 changes: 2 additions & 1 deletion cmd/runtime_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ func runtimeOptionFlagSet(includeSysEnv bool) *pflag.FlagSet {
flags.SortFlags = false
flags.Bool("include-system-env-vars", includeSysEnv, "pass the real system environment variables to the runtime")
flags.String("compatibility-mode", "extended",
`JavaScript compiler compatibility mode, "extended" or "base"
`JavaScript compiler compatibility mode, "extended" or "base" or "enhanced"
base: pure goja - Golang JS VM supporting ES5.1+
extended: base + Babel with parts of ES2015 preset
slower to compile in case the script uses syntax unsupported by base
enhanced: esbuild-based transpiling for TypeScript and ES6+ support
`)
flags.StringP("type", "t", "", "override test type, \"js\" or \"archive\"")
flags.StringArrayP("env", "e", nil, "add/override environment variable with `VAR=value`")
Expand Down
11 changes: 11 additions & 0 deletions cmd/runtime_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func TestRuntimeOptions(t *testing.T) {
defaultCompatMode = null.NewString("extended", false)
baseCompatMode = null.NewString("base", true)
extendedCompatMode = null.NewString("extended", true)
enhancedCompatMode = null.NewString("enhanced", true)
defaultTracesOutput = null.NewString("none", false)
)

Expand Down Expand Up @@ -143,6 +144,16 @@ func TestRuntimeOptions(t *testing.T) {
TracesOutput: defaultTracesOutput,
},
},
"disabled sys env by default with enhanced compat mode": {
useSysEnv: false,
systemEnv: map[string]string{"test1": "val1", "K6_COMPATIBILITY_MODE": "enhanced"},
expRTOpts: lib.RuntimeOptions{
IncludeSystemEnvVars: null.NewBool(false, false),
CompatibilityMode: enhancedCompatMode,
Env: map[string]string{},
TracesOutput: defaultTracesOutput,
},
},
"disabled sys env by cli 1": {
useSysEnv: true,
systemEnv: map[string]string{"test1": "val1", "K6_COMPATIBILITY_MODE": "base"},
Expand Down
6 changes: 6 additions & 0 deletions examples/enhanced/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { User, newUser } from "./user.ts";

export default () => {
const user: User = newUser("John");
console.log(user);
};
20 changes: 20 additions & 0 deletions examples/enhanced/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
interface User {
name: string;
id: number;
}

class UserAccount implements User {
name: string;
id: number;

constructor(name: string) {
this.name = name;
this.id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
}
}

function newUser(name: string): User {
return new UserAccount(name);
}

export { User, newUser };
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5
github.com/andybalholm/brotli v1.1.0
github.com/dop251/goja v0.0.0-20240220182346-e401ed450204
github.com/evanw/esbuild v0.21.2
github.com/fatih/color v1.16.0
github.com/go-sourcemap/sourcemap v2.1.4+incompatible
github.com/golang/protobuf v1.5.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanw/esbuild v0.21.2 h1:CLplcGi794CfHLVmUbvVfTMKkykm+nyIHU8SU60KUTA=
github.com/evanw/esbuild v0.21.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
Expand Down
2 changes: 1 addition & 1 deletion js/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func TestNewBundle(t *testing.T) {
}{
{
"InvalidCompat", "es1", `export default function() {};`,
`invalid compatibility mode "es1". Use: "extended", "base"`,
`invalid compatibility mode "es1". Use: "extended", "base", "enhanced"`,
},
// ES2015 modules are not supported
{
Expand Down
32 changes: 21 additions & 11 deletions js/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,19 +239,29 @@ func (c *Compiler) compileImpl(
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 == lib.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, lib.CompatibilityModeBase, state.srcMap)

if err == nil {
pgm, err := goja.CompileAST(ast, c.Options.Strict)
return pgm, code, err
}

if compatibilityMode == lib.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, lib.CompatibilityModeBase, state.srcMap)
}

if compatibilityMode == lib.CompatibilityModeEnhanced {
code, state.srcMap, err = esbuildTransform(src, filename)
if err != nil {
return nil, code, err
}
return nil, code, err
return c.compileImpl(code, filename, wrap, lib.CompatibilityModeBase, state.srcMap)
}
pgm, err := goja.CompileAST(ast, c.Options.Strict)
return pgm, code, err
return nil, code, err
}

type babel struct {
Expand Down
55 changes: 55 additions & 0 deletions js/compiler/enhanced.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package compiler

import (
"path/filepath"

"github.com/dop251/goja/file"
"github.com/dop251/goja/parser"
"github.com/evanw/esbuild/pkg/api"
)

func esbuildTransform(src, filename string) (code string, srcMap []byte, err error) {
opts := api.TransformOptions{
Sourcefile: filename,
Loader: api.LoaderJS,
Target: api.ES2017,
Format: api.FormatCommonJS,
Sourcemap: api.SourceMapExternal,
SourcesContent: api.SourcesContentExclude,
LegalComments: api.LegalCommentsNone,
Platform: api.PlatformNeutral,
LogLevel: api.LogLevelSilent,
Charset: api.CharsetUTF8,
}

if filepath.Ext(filename) == ".ts" {
opts.Loader = api.LoaderTS
}

result := api.Transform(src, opts)

if hasError, err := esbuildCheckError(&result); hasError {
return "", nil, err
}

return string(result.Code), result.Map, nil
}

func esbuildCheckError(result *api.TransformResult) (bool, error) {
if len(result.Errors) == 0 {
return false, nil
}

msg := result.Errors[0]
err := &parser.Error{Message: msg.Text}

if msg.Location != nil {
err.Position = file.Position{
Filename: msg.Location.File,
Line: msg.Location.Line,
Column: msg.Location.Column,
}
}

return true, err
}
50 changes: 50 additions & 0 deletions js/compiler/enhanced_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package compiler

import (
"errors"
"testing"

"github.com/dop251/goja/parser"
"github.com/stretchr/testify/require"
)

func Test_esbuildTransform_js(t *testing.T) {
t.Parallel()

code, srcMap, err := esbuildTransform(`export default function(name) { return "Hello, " + name }`, "script.js")

require.NoError(t, err)
require.NotNil(t, srcMap)
require.NotEmpty(t, code)
}

func Test_esbuildTransform_ts(t *testing.T) {
t.Parallel()

script := `export function hello(name:string) : string { return "Hello, " + name}`

code, srcMap, err := esbuildTransform(script, "script.ts")

require.NoError(t, err)
require.NotNil(t, srcMap)
require.NotEmpty(t, code)
}

func Test_esbuildTransform_error(t *testing.T) {
t.Parallel()

script := `export function hello(name:string) : string { return "Hello, " + name}`

_, _, err := esbuildTransform(script, "script.js")

require.Error(t, err)

var perr *parser.Error

require.True(t, errors.As(err, &perr))
require.NotNil(t, perr.Position)
require.Equal(t, "script.js", perr.Position.Filename)
require.Equal(t, 1, perr.Position.Line)
require.Equal(t, 26, perr.Position.Column)
require.Equal(t, "Expected \")\" but found \":\"", perr.Message)
}
11 changes: 6 additions & 5 deletions lib/compatibility_mode_gen.go

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

2 changes: 2 additions & 0 deletions lib/runtime_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
CompatibilityModeExtended CompatibilityMode = iota + 1
// CompatibilityModeBase is standard goja ES5.1+
CompatibilityModeBase
// CompatibilityModeEnhanced achieves TypeScript and ES6+ compatibility with esbuild
CompatibilityModeEnhanced
)

// RuntimeOptions are settings passed onto the goja JS runtime
Expand Down
21 changes: 21 additions & 0 deletions vendor/github.com/evanw/esbuild/LICENSE.md

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

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

Loading

0 comments on commit b0486e4

Please sign in to comment.