Skip to content

Commit

Permalink
Use goja instead of otto
Browse files Browse the repository at this point in the history
Using Otto, we aren’t able to use full regular expressions that
JavaScript supports, since it uses the default regular expression
engine in Go. Goja supports regular expressions from JavaScript
  • Loading branch information
enmand committed May 13, 2018
1 parent 34a429d commit d2b2356
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 53 deletions.
3 changes: 3 additions & 0 deletions pkg/engines/engines.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ type Engine interface {
Run(name string, args ...interface{}) (string, error)

Type() Type

// Return the runtime for the engine, if one is available
Runtime() interface{}
}
101 changes: 78 additions & 23 deletions pkg/engines/js/init.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,67 @@
// go:generated ./vendor/UnnoTed/fileb0x
package js

import (
"fmt"

"github.com/dop251/goja"
"github.com/enmand/quarid-go/generated/langsupport"
"github.com/enmand/quarid-go/pkg/engines"
"github.com/robertkrimen/otto"
_ "github.com/robertkrimen/otto/underscore" // underscorejs for otto
"github.com/pkg/errors"
)

const _modpath = "__modpath_%s___jsvm"
const (
_compilerScript = "./js/compiler.js"
)

var _compiler = &goja.Program{}

type jsvm struct {
vm *otto.Otto
modules map[string]interface{}
vm *goja.Runtime
compiler goja.Callable
modules map[string]*goja.Program
}

func init() {
_compilerSrc, err := langsupport.ReadFile(_compilerScript)
if err != nil {
panic(fmt.Sprintf("unable to load JavaScript compiler (%s): %s", _compilerScript, err))
}
_compiler, err = goja.Compile(_compilerScript, string(_compilerSrc), false)
if err != nil {
panic(fmt.Sprintf("unable to compile JavaScript copmiler (%s): %s", _compilerScript, err))
}
}

// New returns a new Otto-based JavaScript virtual machine
func New() engines.Engine {
func New() (engines.Engine, error) {
v := &jsvm{
vm: otto.New(),
vm: goja.New(),
}

if err := v.initialize(); err != nil {
return nil, err
}

out, err := v.vm.RunProgram(_compiler)
if err != nil {
return nil, errors.Wrap(err, "unable to evaluate compiler")
}

call, ok := goja.AssertFunction(out)
if !ok {
return nil, errors.Wrap(err, "unable to find compiler")
}

exports := v.vm.NewObject()
call(out, nil, exports)

v.compiler, ok = goja.AssertFunction(exports.Get("_compile"))
if !ok {
return nil, errors.Wrap(err, "unable to find compiler function")
}

return v
return v, nil
}

// Load the JavaScript engine
Expand All @@ -33,14 +73,30 @@ func (v *jsvm) Type() engines.Type {
return engines.JS
}

func (v *jsvm) transpile(source string) (string, error) {
val, err := v.compiler(nil, v.vm.ToValue(source))
if err != nil {
return "", errors.Wrap(err, "unable to compile src")
}

return val.String(), nil
}

func (v *jsvm) Runtime() interface{} {
return v.vm
}

func (v *jsvm) Compile(path string, source string) (interface{}, error) {
if _, ok := v.modules[path]; ok {
return nil, fmt.Errorf("Plugin named %s already exists", path)
return v.modules[path], nil
}

source = fmt.Sprintf("var exports = {}; %s;", source)
ts, err := v.transpile(source)
if err != nil {
return nil, err
}

s, err := v.vm.Run(source)
s, err := goja.Compile(path, ts, true)
if err != nil {
return nil, fmt.Errorf("Could not compile %s: %s", path, err)
}
Expand All @@ -52,25 +108,24 @@ func (v *jsvm) Compile(path string, source string) (interface{}, error) {
func (v *jsvm) Run(path string, args ...interface{}) (string, error) {
module := v.modules[path]

v.vm.Set(_modpath, path)

val, err := v.vm.Run(module.(*otto.Script))
val, err := v.vm.RunProgram(module)
if err != nil {
return "", fmt.Errorf("Could not run plugin %s: %s", val, err)
}

ret, err := val.ToString()
if err != nil {
return "", fmt.Errorf("Could not convert return to response: %s", err)
}

return ret, nil
return val.String(), nil
}

func (v *jsvm) initialize() error {
v.modules = make(map[string]interface{})

v.vm.Set("require", RequireFunc)
v.modules = make(map[string]*goja.Program)

v.vm.Set("require", NewRequire(v.vm).Require)
NewWeakMap(v.vm).Enable()
NewMap(v.vm).Enable()
//v.vm.Set("WeakSet", v.WeakSet)
NewSet(v.vm).Enable("WeakSet")
v.vm.Set("Symbol", NewSymbol)
NewSet(v.vm).Enable()

return nil
}
107 changes: 77 additions & 30 deletions pkg/engines/js/require.go
Original file line number Diff line number Diff line change
@@ -1,56 +1,103 @@
package js

import (
"errors"
"fmt"
"io/ioutil"
"strings"
"sync"

"github.com/robertkrimen/otto"
Dbg "github.com/robertkrimen/otto/dbg"
"github.com/dop251/goja"
"github.com/enmand/quarid-go/generated/langsupport"
)

// Implements require() in the JavaScript VM.
func RequireFunc(call otto.FunctionCall) otto.Value {
// Require implements the 'require(module)' pattern from NodeJS
type Require struct {
modules map[string]*goja.Program
runtime *goja.Runtime

v, _ := call.Otto.Get(_modpath)
l sync.Mutex
}

// NewRequire returns a new Require object
func NewRequire(runtime *goja.Runtime) *Require {
return &Require{
modules: make(map[string]*goja.Program),
runtime: runtime,
}
}

path, _ := call.Argument(0).ToString()
fullPath := fmt.Sprintf("%s/%s", v.String(), path)
// Require implements require() in the JavaScript VM.
func (r *Require) Require(call goja.FunctionCall) goja.Value {
pathVal := call.Argument(0)
if goja.IsNull(pathVal) && goja.IsUndefined(pathVal) {
return r.runtime.NewGoError(errors.New("no path given"))
}

if !strings.Contains(path, ".") {
return _internalRequire(call, fullPath)
} else {
return _externalRequire(call, fullPath)
path := pathVal.String()
if strings.HasPrefix(path, "./") {
return r._externalRequire(call, path)
}

return r._internalRequire(call, path)

}

func _internalRequire(call otto.FunctionCall, path string) otto.Value {
requireError(path, "internal")
return otto.UndefinedValue()
func (r *Require) _externalRequire(call goja.FunctionCall, path string) goja.Value {
r.requireError(path, "external", errors.New("not implemented"))
return goja.Undefined()
}

func _externalRequire(call otto.FunctionCall, path string) otto.Value {
d, err := ioutil.ReadFile(path)
func (r *Require) _internalRequire(call goja.FunctionCall, path string) goja.Value {
script, err := readBoxedFile(path)
if err != nil || script == nil {
// No external module found, let's search our internal path
r.requireError(path, "internal", err)
return r._internalRequire(call, path)

}
source := "(function(module, exports) {" + string(*script) + "\n})"
p, err := goja.Compile(path, source, false)
if err != nil {
// No external module found, let's search our internal path
return _internalRequire(call, path)
r.requireError(path, "internal", fmt.Errorf("unable to compile %s", path))
return nil
}

_, v, err := otto.Run(d)
out, err := r.runtime.RunProgram(p)
if err != nil {
requireError(path, "external")
return otto.UndefinedValue()
r.requireError(path, "internal", errors.New("unable to run program"))
}

reqCall, ok := goja.AssertFunction(out)
if ok != true {
r.requireError(path, "internal", errors.New("unable to get transpiled function"))
return nil
}

return v
exports := r.runtime.NewObject()
_, err = reqCall(out, nil, exports)

return exports
}

func (r *Require) requireError(path, requireType string, err error) {
fmt.Printf(" warn/Module '%s' (%s module search) module not loaded: %s\n", path, requireType, err)
}

func requireError(path, requireType string) {
_, dbf := Dbg.New()
dbf(
"%/warn//Module '%s' (%s module search) not found, module not loaded",
path,
requireType,
)
func readBoxedFile(path string) (*string, error) {
var fullPath string
if strings.HasPrefix(path, "!") {
fullPath = fmt.Sprintf("js/%s", path[1:])
}

if strings.HasPrefix(path, "@") {
fullPath = fmt.Sprintf("js/node_modules/%s", path)
}

d, err := langsupport.ReadFile(fullPath)
if err != nil {
return nil, err
}

script := string(d)
return &script, nil
}

0 comments on commit d2b2356

Please sign in to comment.