Break it: forget to commit often
This commit is contained in:
parent
12006ca82c
commit
7f8fbec295
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.idea/
|
32
README.md
32
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.
|
||||
|
24
console.go
Normal file
24
console.go
Normal file
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -1 +0,0 @@
|
||||
package core
|
@ -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)
|
||||
})
|
||||
}
|
33
core/rts.go
33
core/rts.go
@ -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)
|
||||
}
|
13
go.mod
13
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
|
||||
)
|
||||
|
35
go.sum
35
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=
|
||||
|
46
imply.go
Normal file
46
imply.go
Normal file
@ -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
|
||||
}
|
100
loader.go
Normal file
100
loader.go
Normal file
@ -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)
|
||||
}
|
99
loader_test.go
Normal file
99
loader_test.go
Normal file
@ -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()
|
||||
})
|
||||
}
|
23
main.go
23
main.go
@ -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)
|
||||
}
|
||||
}
|
8
test.js
8
test.js
@ -1,8 +0,0 @@
|
||||
function test() {
|
||||
console.log('hello world\n');
|
||||
return 'Hello Golang~';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
test: test,
|
||||
};
|
11
tests/test-mustfail.js
Normal file
11
tests/test-mustfail.js
Normal file
@ -0,0 +1,11 @@
|
||||
function test() {
|
||||
console.log('hello world\n');
|
||||
return 'Hello Golang~';;;;;;
|
||||
fasd;lkfgaderogkdrfoa
|
||||
;l!#$!@1
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
bad: test,
|
||||
nope: test,
|
||||
};
|
13
tests/test.js
Normal file
13
tests/test.js
Normal file
@ -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,
|
||||
};
|
Loading…
Reference in New Issue
Block a user