diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/README.md b/README.md index b82beae..9d1ce02 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,3 @@ -# gode +# imply -A test project to implement a Node like javascript environment based on goja - -# Example - -```golang -package main - -import ( - "fmt" - - "github.com/81120/gode/core" -) - -func main() { - gode := core.New() - gode.RegisterBuildInModule() - - r := gode.GetRts() - v, err := r.RunString(` - var t = require('./test.js'); - t.test(); - `) - if err != nil { - fmt.Print(err) - } else { - fmt.Println(v) - } -} -``` +mostly a failed attempt to continue the work of [this repo](github.com/81120/gode/core), which as it turns out I don't even think is necessary - however this may be a good place to start for putting together embedded scripting for some project. diff --git a/console.go b/console.go new file mode 100644 index 0000000..2cd7bff --- /dev/null +++ b/console.go @@ -0,0 +1,24 @@ +package imply + +import ( + "fmt" + + js "github.com/dop251/goja" +) + +func basicLog(call js.FunctionCall) js.Value { + str := call.Argument(0) + fmt.Println(str.String()) + return str +} + +// RegisterConsole register a console.basicLog to runtime +func (c *Core) RegisterConsole(log func(call js.FunctionCall) js.Value) error { + o := c.NewObject() + err := o.Set("log", log) + if err != nil { + return err + } + err = c.Set("console", o) + return err +} diff --git a/core/console.go b/core/console.go deleted file mode 100644 index ad3c65f..0000000 --- a/core/console.go +++ /dev/null @@ -1,21 +0,0 @@ -package core - -import ( - "fmt" - - js "github.com/dop251/goja" -) - -func log(call js.FunctionCall) js.Value { - str := call.Argument(0) - fmt.Print(str.String()) - return str -} - -// RegisterConsole register a console.log to runtime -func RegisterConsole(c *Core) { - r := c.GetRts() - o := r.NewObject() - o.Set("log", log) - r.Set("console", o) -} diff --git a/core/event-loop.go b/core/event-loop.go deleted file mode 100644 index 9a8bc95..0000000 --- a/core/event-loop.go +++ /dev/null @@ -1 +0,0 @@ -package core diff --git a/core/loader.go b/core/loader.go deleted file mode 100644 index 265f1ad..0000000 --- a/core/loader.go +++ /dev/null @@ -1,59 +0,0 @@ -package core - -import ( - "io/ioutil" - "path/filepath" - - js "github.com/dop251/goja" -) - -func moduleTemplate(c string) string { - return "(function(module, exports) {" + c + "\n})" -} - -func createModule(c *Core) *js.Object { - r := c.GetRts() - m := r.NewObject() - e := r.NewObject() - m.Set("exports", e) - - return m -} - -func compileModule(p string) *js.Program { - code, _ := ioutil.ReadFile(p) - text := moduleTemplate(string(code)) - prg, _ := js.Compile(p, text, false) - - return prg -} - -func loadModule(c *Core, p string) js.Value { - p = filepath.Clean(p) - pkg := c.Pkg[p] - if pkg != nil { - return pkg - } - - prg := compileModule(p) - - r := c.GetRts() - f, _ := r.RunProgram(prg) - g, _ := js.AssertFunction(f) - - m := createModule(c) - jsExports := m.Get("exports") - g(jsExports, m, jsExports) - - return m.Get("exports") -} - -// RegisterLoader register a simple commonjs style loader to runtime -func RegisterLoader(c *Core) { - r := c.GetRts() - - r.Set("require", func(call js.FunctionCall) js.Value { - p := call.Argument(0).String() - return loadModule(c, p) - }) -} diff --git a/core/rts.go b/core/rts.go deleted file mode 100644 index a65809e..0000000 --- a/core/rts.go +++ /dev/null @@ -1,33 +0,0 @@ -package core - -import ( - js "github.com/dop251/goja" -) - -// Core is the basic struct of gode -type Core struct { - Rts *js.Runtime - Pkg map[string]js.Value -} - -// New create a *Core -func New() *Core { - vm := js.New() - pkg := make(map[string]js.Value) - - return &Core{ - Rts: vm, - Pkg: pkg, - } -} - -// GetRts get the object of javascript runtime -func (c *Core) GetRts() *js.Runtime { - return c.Rts -} - -// RegisterBuildInModule register some build in modules to the runtime -func (c *Core) RegisterBuildInModule() { - RegisterConsole(c) - RegisterLoader(c) -} diff --git a/go.mod b/go.mod index f4c8eba..a2d2a51 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ -module github.com/81120/gode +module git.tcp.direct/kayos/imply -go 1.13 +go 1.19 + +require github.com/dop251/goja v0.0.0-20220915101355-d79e1b125a30 require ( - github.com/dlclark/regexp2 v1.2.0 // indirect - github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 - github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect - golang.org/x/text v0.3.2 // indirect + github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index 1a516af..f3d07c4 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,28 @@ -github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= -github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733 h1:cyNc40Dx5YNEO94idePU8rhVd3dn+sd04Arh0kDBAaw= -github.com/dop251/goja v0.0.0-20190912223329-aa89e6a4c733/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= -github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug= -github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220915101355-d79e1b125a30 h1:ygMJa3f5Uw4JHQo9n52aSFHYxdRvZWPOoihpDK8hCPs= +github.com/dop251/goja v0.0.0-20220915101355-d79e1b125a30/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/imply.go b/imply.go new file mode 100644 index 0000000..059e46b --- /dev/null +++ b/imply.go @@ -0,0 +1,46 @@ +package imply + +import ( + "sync" + + js "github.com/dop251/goja" +) + +// Core is the basic struct of gode +type Core struct { + *js.Runtime + *sync.RWMutex + Pkg map[string]js.Value +} + +// NewCore creates a pointer to a new Core. +// A Core is a type that is used to organize a collection of runtime javascript modules. +func NewCore() *Core { + return &Core{ + Runtime: js.New(), + RWMutex: &sync.RWMutex{}, + Pkg: make(map[string]js.Value), + } +} + +// RegisterBuiltins register some build in modules to the runtime +func (c *Core) RegisterBuiltins() error { + if err := c.RegisterConsole(basicLog); err != nil { + return err + } + return c.RegisterLoader() +} + +// RegisterLoader registers a simple commonjs style loader to runtime. +func (c *Core) RegisterLoader() error { + err := c.Set("require", func(call js.FunctionCall) js.Value { + var val js.Value + var err error + val, err = c.LoadJSPackageFile(call.Argument(0).String()) + if err != nil { + return c.NewGoError(err) + } + return val + }) + return err +} diff --git a/loader.go b/loader.go new file mode 100644 index 0000000..095a419 --- /dev/null +++ b/loader.go @@ -0,0 +1,100 @@ +package imply + +import ( + "fmt" + "os" + + js "github.com/dop251/goja" +) + +func moduleTemplate(c string) string { + return "(function(module, exports) {\n" + c + "\n})" +} + +func (c *Core) createPack() (*js.Object, error) { + m := c.NewObject() + e := c.NewObject() + err := m.Set("exports", e) + return m, err +} + +func load(name string, code []byte) (*js.Program, error) { + text := moduleTemplate(string(code)) + return js.Compile(name, text, false) +} + +func (c *Core) validate(prg *js.Program) (jsExports js.Value, err error) { + var f js.Value + f, err = c.RunProgram(prg) + if err != nil { + return + } + g, ok := js.AssertFunction(f) + if !ok { + err = fmt.Errorf("[%T] %v is not a function", f, f) + return + } + pkg, err := c.createPack() + if err != nil { + return + } + jsExports = pkg.Get("exports") + _, err = g(jsExports, pkg, jsExports) + if err != nil { + return + } + // fmt.Printf("VALIDATED: [%T] %v\n", jsExports, spew.Sprint(jsExports)) + return +} + +func (c *Core) checkExisting(p string) (js.Value, bool) { + c.RLock() + defer c.RUnlock() + val, ok := c.Pkg[p] + return val, ok +} + +func (c *Core) registerModule(p string, jsExports js.Value) { + c.Lock() + c.Pkg[p] = jsExports + c.Unlock() +} + +func (c *Core) validateAndRegister(name string, prg *js.Program) (jsExports js.Value, err error) { + if jsExports, err = c.validate(prg); err != nil { + return nil, err + } + if jsExports == nil { + return nil, fmt.Errorf("module %s does not export anything", name) + } + c.registerModule(name, jsExports) + return jsExports, nil +} + +// LoadJSPackageFile loads a javascript package from a file, validates it, and registers it. +// The given path will be used as the name of the module. +// +// Warning: If name is already registered, the existing module will be returned. +func (c *Core) LoadJSPackageFile(path string) (js.Value, error) { + code, err := os.ReadFile(path) + if err != nil { + // println("!!ERR!! " + err.Error()) + return nil, err + } + // println("new: " + path) + return c.LoadJSPackage(path, code) +} + +// LoadJSPackage loads a javascript package from code, validates it, and registers it by name. +// +// Warning: If name is already registered, the existing module will be returned. +func (c *Core) LoadJSPackage(name string, code []byte) (js.Value, error) { + if mod, ok := c.checkExisting(name); ok { + return mod, nil + } + prg, err := load(name, code) + if err != nil { + return nil, err + } + return c.validateAndRegister(name, prg) +} diff --git a/loader_test.go b/loader_test.go new file mode 100644 index 0000000..67a46ec --- /dev/null +++ b/loader_test.go @@ -0,0 +1,99 @@ +package imply + +import ( + "strings" + "testing" + + js "github.com/dop251/goja" +) + +type testConsole struct { + t *testing.T +} + +func (tc *testConsole) log(call js.FunctionCall) js.Value { + tc.t.Helper() + ret := call.Argument(0) + tc.t.Logf("[JSLOG]%v: [%T] %v", call.This, ret, ret) + return ret +} + +func newTestConsole(t *testing.T) *testConsole { + t.Helper() + return &testConsole{t} +} + +func testCore(t *testing.T) *Core { + t.Helper() + core := NewCore() + tc := newTestConsole(t) + if err := core.RegisterConsole(tc.log); err != nil { + t.Fatalf("RegisterConsole failed: %s", err) + } + if err := core.RegisterLoader(); err != nil { + t.Fatalf("RegisterLoader failed: %s", err) + } + _, err := core.RunString("console.log('init');") + if err != nil { + t.Fatalf("failed sanity check: %v", err) + } + return core +} + +func ExampleNewCore() { + core := NewCore() + if err := core.RegisterBuiltins(); err != nil { + panic(err) + } + +} + +func TestImply(t *testing.T) { + const ( + one = "let t = require('tests/" + two = "test.js');\n" + three = "test-mustfail.js');\n" + ) + + var base *strings.Builder + var want = "Hello Golang~" + + res := func(base *strings.Builder, js string, shouldFail bool) { + core := testCore(t) + jscode := base.String() + js + t.Logf("Running:\n%s", jscode) + v, err := core.RunString(jscode) + switch { + case err != nil && !shouldFail: + t.Errorf("RunString failed: %s", err) + case v == nil && !shouldFail: + t.Errorf("RunString failed: got nil") + case v != nil && v.String() != want && !shouldFail: + t.Errorf("RunString failed.\nwanted: %s\ngot: %s", want, v.String()) + case err == nil && shouldFail: + t.Errorf("RunString should have failed, got nil error") + case v != nil && shouldFail: + t.Errorf("RunString result should have been nil!\nGot: [%T] %v", v, v) + case err != nil && shouldFail: + // t.Logf("success, wanted err: [%T] %v", err, err) + } + } + + t.Run("Simple", func(t *testing.T) { + base = &strings.Builder{} + base.WriteString(one) + base.WriteString(two) + res(base, "t.test();", false) + res(base, "t.foo();", false) + res(base, "t.bar();", true) + base.Reset() + }) + + t.Run("InvalidSyntax", func(t *testing.T) { + base = &strings.Builder{} + base.WriteString(one) + base.WriteString(three) + res(base, "t.test();", true) + base.Reset() + }) +} diff --git a/main.go b/main.go deleted file mode 100644 index 0e1ef84..0000000 --- a/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/81120/gode/core" -) - -func main() { - gode := core.New() - gode.RegisterBuildInModule() - - r := gode.GetRts() - v, err := r.RunString(` - var t = require('./test.js'); - t.test(); - `) - if err != nil { - fmt.Print(err) - } else { - fmt.Println(v) - } -} diff --git a/test.js b/test.js deleted file mode 100644 index 998c259..0000000 --- a/test.js +++ /dev/null @@ -1,8 +0,0 @@ -function test() { - console.log('hello world\n'); - return 'Hello Golang~'; -} - -module.exports = { - test: test, -}; diff --git a/tests/test-mustfail.js b/tests/test-mustfail.js new file mode 100644 index 0000000..3fa7e9d --- /dev/null +++ b/tests/test-mustfail.js @@ -0,0 +1,11 @@ +function test() { + console.log('hello world\n'); + return 'Hello Golang~';;;;;; + fasd;lkfgaderogkdrfoa + ;l!#$!@1 +} + +module.exports = { + bad: test, + nope: test, +}; diff --git a/tests/test.js b/tests/test.js new file mode 100644 index 0000000..6e9f7f3 --- /dev/null +++ b/tests/test.js @@ -0,0 +1,13 @@ +function test() { + console.log('hello world\n'); + return 'Hello Golang~'; +} + +function bar() { + return self.foo(); +} + +module.exports = { + test: test, + foo: test, +};