Skip to content

Commit 8f823b5

Browse files
refactor: move to using goja instead of otto
1 parent cc94bc3 commit 8f823b5

File tree

15 files changed

+254
-141
lines changed

15 files changed

+254
-141
lines changed

.golangci.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,12 @@ linters-settings:
6868
checks: ["all", "-ST1000", "-ST1003"]
6969
cyclop:
7070
max-complexity: 15
71+
ireturn:
72+
allow:
73+
- anon
74+
- error
75+
- empty
76+
- stdlib
77+
- "github.com/dop251/goja.Value"
7178
run:
7279
go: "1.19"

engine.go

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,33 @@
55
package easytemplate
66

77
import (
8+
"errors"
89
"fmt"
910
"io/fs"
1011
"os"
1112
"path"
1213

13-
"github.com/robertkrimen/otto"
1414
// provides underscore support for js interpreted by the engine.
15-
_ "github.com/robertkrimen/otto/underscore"
15+
"github.com/dop251/goja"
1616
"github.com/speakeasy-api/easytemplate/internal/template"
17+
"github.com/speakeasy-api/easytemplate/internal/underscore"
1718
)
1819

1920
var (
2021
// ErrAlreadyRan is returned when the engine has already been ran, and can't be ran again. In order to run the engine again, a new engine must be created.
21-
ErrAlreadyRan = fmt.Errorf("engine has already been ran")
22+
ErrAlreadyRan = errors.New("engine has already been ran")
2223
// ErrReserved is returned when a template or js function is reserved and can't be overridden.
23-
ErrReserved = fmt.Errorf("function is a reserved function and can't be overridden")
24+
ErrReserved = errors.New("function is a reserved function and can't be overridden")
25+
// ErrInvalidArg is returned when an invalid argument is passed to a function.
26+
ErrInvalidArg = errors.New("invalid argument")
2427
)
2528

29+
// CallContext is the context that is passed to go functions when called from js.
30+
type CallContext struct {
31+
goja.FunctionCall
32+
VM *jsVM
33+
}
34+
2635
// Opt is a function that configures the Engine.
2736
type Opt func(*Engine)
2837

@@ -61,7 +70,7 @@ func WithTemplateFuncs(funcs map[string]any) Opt {
6170
}
6271

6372
// WithJSFuncs allows for providing additional functions available to javascript in the engine.
64-
func WithJSFuncs(funcs map[string]func(call otto.FunctionCall) otto.Value) Opt {
73+
func WithJSFuncs(funcs map[string]func(call CallContext) goja.Value) Opt {
6574
return func(e *Engine) {
6675
for k, v := range funcs {
6776
if _, ok := e.jsFuncs[k]; ok {
@@ -81,7 +90,21 @@ type Engine struct {
8190
templator *template.Templator
8291

8392
ran bool
84-
jsFuncs map[string]func(call otto.FunctionCall) otto.Value
93+
jsFuncs map[string]func(call CallContext) goja.Value
94+
}
95+
96+
type jsVM struct {
97+
*goja.Runtime
98+
}
99+
100+
var _ template.VM = &jsVM{}
101+
102+
func (v *jsVM) GetObject(val goja.Value) *goja.Object {
103+
return val.ToObject(v.Runtime)
104+
}
105+
106+
func (v *jsVM) Compile(name string, src string, strict bool) (*goja.Program, error) {
107+
return goja.Compile(name, src, strict)
85108
}
86109

87110
// New creates a new Engine with the provided options.
@@ -99,12 +122,12 @@ func New(opts ...Opt) *Engine {
99122

100123
e := &Engine{
101124
templator: t,
102-
jsFuncs: map[string]func(call otto.FunctionCall) otto.Value{},
125+
jsFuncs: map[string]func(call CallContext) goja.Value{},
103126
}
104127

105128
t.ReadFunc = e.readFile
106129

107-
e.jsFuncs = map[string]func(call otto.FunctionCall) otto.Value{
130+
e.jsFuncs = map[string]func(call CallContext) goja.Value{
108131
"require": e.require,
109132
"templateFile": e.templateFileJS,
110133
"templateString": e.templateStringJS,
@@ -130,12 +153,12 @@ func (e *Engine) RunScript(scriptFile string, data any) error {
130153
return fmt.Errorf("failed to read script file: %w", err)
131154
}
132155

133-
s, err := vm.Compile("", script)
156+
s, err := vm.Compile(scriptFile, string(script), false)
134157
if err != nil {
135158
return fmt.Errorf("failed to compile script: %w", err)
136159
}
137160

138-
if _, err := vm.Run(s); err != nil {
161+
if _, err := vm.RunProgram(s); err != nil {
139162
return fmt.Errorf("failed to run script: %w", err)
140163
}
141164

@@ -162,22 +185,37 @@ func (e *Engine) RunTemplateString(templateFile string, data any) (string, error
162185
return e.templator.TemplateString(vm, templateFile, data)
163186
}
164187

165-
func (e *Engine) init(data any) (*otto.Otto, error) {
188+
func (e *Engine) init(data any) (*jsVM, error) {
166189
if e.ran {
167190
return nil, ErrAlreadyRan
168191
}
169192
e.ran = true
170193

171-
vm := otto.New()
194+
g := goja.New()
195+
_, err := g.RunString(underscore.JS)
196+
if err != nil {
197+
return nil, fmt.Errorf("failed to init underscore: %w", err)
198+
}
199+
200+
vm := &jsVM{g}
172201

173202
for k, v := range e.jsFuncs {
174-
if err := vm.Set(k, v); err != nil {
203+
wrappedFn := func(v func(call CallContext) goja.Value) func(call goja.FunctionCall) goja.Value {
204+
return func(call goja.FunctionCall) goja.Value {
205+
return v(CallContext{
206+
FunctionCall: call,
207+
VM: vm,
208+
})
209+
}
210+
}(v)
211+
212+
if err := vm.Set(k, wrappedFn); err != nil {
175213
return nil, fmt.Errorf("failed to set js function %s: %w", k, err)
176214
}
177215
}
178216

179217
// This need to have the vm passed in so that the functions can be called
180-
e.templator.TmplFuncs["templateFile"] = func(vm *otto.Otto) func(string, string, any) (string, error) {
218+
e.templator.TmplFuncs["templateFile"] = func(vm *jsVM) func(string, string, any) (string, error) {
181219
return func(templateFile, outFile string, data any) (string, error) {
182220
err := e.templator.TemplateFile(vm, templateFile, outFile, data)
183221
if err != nil {
@@ -187,7 +225,7 @@ func (e *Engine) init(data any) (*otto.Otto, error) {
187225
return "", nil
188226
}
189227
}(vm)
190-
e.templator.TmplFuncs["templateString"] = func(vm *otto.Otto) func(string, any) (string, error) {
228+
e.templator.TmplFuncs["templateString"] = func(vm *jsVM) func(string, any) (string, error) {
191229
return func(templateFile string, data any) (string, error) {
192230
templated, err := e.templator.TemplateString(vm, templateFile, data)
193231
if err != nil {
@@ -209,53 +247,56 @@ func (e *Engine) init(data any) (*otto.Otto, error) {
209247
return vm, nil
210248
}
211249

212-
func (e *Engine) require(call otto.FunctionCall) otto.Value {
213-
vm := call.Otto
250+
func (e *Engine) require(call CallContext) goja.Value {
251+
vm := call.VM
214252

215253
scriptPath := call.Argument(0).String()
216254

217255
script, err := e.readFile(scriptPath)
218256
if err != nil {
219-
panic(vm.MakeCustomError("requireScript", err.Error()))
257+
panic(vm.NewGoError(err))
220258
}
221259

222-
s, err := vm.Compile("", script)
260+
s, err := vm.Compile(scriptPath, string(script), false)
223261
if err != nil {
224-
panic(vm.MakeCustomError("requireScript", err.Error()))
262+
panic(vm.NewGoError(err))
225263
}
226264

227-
if _, err := vm.Run(s); err != nil {
228-
panic(vm.MakeCustomError("requireScript", err.Error()))
265+
if _, err := vm.RunProgram(s); err != nil {
266+
panic(vm.NewGoError(err))
229267
}
230268

231-
return otto.Value{}
269+
return goja.Undefined()
232270
}
233271

234-
func (e *Engine) registerTemplateFunc(call otto.FunctionCall) otto.Value {
272+
func (e *Engine) registerTemplateFunc(call CallContext) goja.Value {
235273
name := call.Argument(0).String()
236-
fn := call.Argument(1)
274+
fn, ok := goja.AssertFunction(call.Argument(1))
275+
if !ok {
276+
panic(call.VM.NewGoError(fmt.Errorf("%w: second argument must be a function", ErrInvalidArg)))
277+
}
237278

238279
if _, ok := e.templator.TmplFuncs[name]; ok {
239-
panic(call.Otto.MakeCustomError("registerTemplateFunc", fmt.Sprintf("template function %s already exists", name)))
280+
panic(call.VM.NewGoError(fmt.Errorf("%w: template function %s already exists", ErrReserved, name)))
240281
}
241282

242-
e.templator.TmplFuncs[name] = func(fn otto.Value) func(args ...interface{}) any {
283+
e.templator.TmplFuncs[name] = func(fn goja.Callable) func(args ...interface{}) any {
243284
return func(args ...interface{}) any {
244-
val, err := fn.Call(fn, args...)
245-
if err != nil {
246-
panic(err)
285+
callableArgs := make([]goja.Value, len(args))
286+
for i, arg := range args {
287+
callableArgs[i] = call.VM.ToValue(arg)
247288
}
248289

249-
v, err := val.Export()
290+
val, err := fn(goja.Undefined(), callableArgs...)
250291
if err != nil {
251292
panic(err)
252293
}
253294

254-
return v
295+
return val.Export()
255296
}
256297
}(fn)
257298

258-
return otto.Value{}
299+
return goja.Undefined()
259300
}
260301

261302
func (e *Engine) readFile(file string) ([]byte, error) {

engine_integration_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package easytemplate_test
22

33
import (
4+
"fmt"
45
"os"
56
"testing"
67

7-
_ "github.com/robertkrimen/otto/underscore"
8+
"github.com/dop251/goja"
89
"github.com/speakeasy-api/easytemplate"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
@@ -34,6 +35,19 @@ func TestEngine_RunScript_Success(t *testing.T) {
3435

3536
return nil
3637
}),
38+
easytemplate.WithJSFuncs(map[string]func(call easytemplate.CallContext) goja.Value{
39+
"multiply": func(call easytemplate.CallContext) goja.Value {
40+
a := call.Argument(0).ToInteger()
41+
b := call.Argument(1).ToInteger()
42+
43+
return call.VM.ToValue(a * b)
44+
},
45+
}),
46+
easytemplate.WithTemplateFuncs(map[string]any{
47+
"toFloatWithPrecision": func(i int64, precision int) string {
48+
return fmt.Sprintf("%.*f", precision, float64(i))
49+
},
50+
}),
3751
)
3852
err = e.RunScript("scripts/test.js", map[string]interface{}{
3953
"Test": "global",

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ module github.com/speakeasy-api/easytemplate
33
go 1.19
44

55
require (
6+
github.com/dop251/goja v0.0.0-20230119130012-17fd568758fe
67
github.com/golang/mock v1.6.0
7-
github.com/robertkrimen/otto v0.2.1
88
github.com/stretchr/testify v1.8.1
99
)
1010

1111
require (
1212
github.com/davecgh/go-spew v1.1.1 // indirect
13+
github.com/dlclark/regexp2 v1.7.0 // indirect
14+
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
1315
github.com/pmezard/go-difflib v1.0.0 // indirect
14-
golang.org/x/text v0.4.0 // indirect
15-
gopkg.in/sourcemap.v1 v1.0.5 // indirect
16+
golang.org/x/text v0.5.0 // indirect
1617
gopkg.in/yaml.v3 v3.0.1 // indirect
1718
)

go.sum

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
12
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
34
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
6+
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
7+
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
8+
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
9+
github.com/dop251/goja v0.0.0-20230119130012-17fd568758fe h1:PsLX9tWRxptk4COd23DJ0bnbXRKPDvBP+A6740Ndxp0=
10+
github.com/dop251/goja v0.0.0-20230119130012-17fd568758fe/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
11+
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
12+
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
13+
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
14+
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
415
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
516
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
17+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
18+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
19+
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
20+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
21+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
22+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
23+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
625
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
726
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8-
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
9-
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
27+
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
28+
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
1029
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1130
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
1231
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -31,18 +50,23 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
3150
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
3251
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
3352
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
34-
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
35-
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
53+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
54+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
55+
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
56+
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
3657
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
3758
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
3859
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
3960
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
4061
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
4162
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
42-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
4363
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
44-
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
45-
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
64+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
65+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
66+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
67+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
68+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
69+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
4670
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4771
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4872
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)