55package easytemplate
66
77import (
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
1920var (
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.
2736type 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
261302func (e * Engine ) readFile (file string ) ([]byte , error ) {
0 commit comments