This post was created at 16 days, 2 hours and 41 minutes ago.
It has the following tags:
Easily Embedding Javascript into Go.
If you need to embed a scripting language into a go program, here is a little snippet you can drop in as a subpackage that will hopefully help. I've not used this too extensively just yet, but compared to the setups I was having to do with lua before this is such a nice experience. I may come back and update this at a later point if I make any major changes to it. I'm also not the worlds best programmer, essentially being a somewhat advanced hobbyist, so there could be major problems with this that I'm not seeing.
package js_serv
import (
"bytes"
"errors"
"fmt"
"io"
"sync"
"github.com/dop251/goja"
"github.com/evanw/esbuild/pkg/api"
)
func NewJSService() JSService {
return JSService{}
}
type JSService struct {
lock sync.Mutex
libraries map[string]map[string]any
}
func (jss *JSService) RegisterLibraryValue(libName, funcName string, f any) {
jss.lock.Lock()
defer jss.lock.Unlock()
if jss.libraries == nil {
jss.libraries = map[string]map[string]any{}
}
if jss.libraries[libName] == nil {
jss.libraries[libName] = map[string]any{}
}
jss.libraries[libName][funcName] = f
}
func (jss *JSService) GetVM(src io.Reader) (*goja.Runtime, error) {
jss.lock.Lock()
defer jss.lock.Unlock()
script, err := io.ReadAll(src)
if err != nil {
return nil, fmt.Errorf("error reading program source: %w", err)
}
vm := goja.New()
for libName, lib := range jss.libraries {
libObj := vm.NewObject()
for valueName, value := range lib {
if err := libObj.Set(valueName, value); err != nil {
return nil, fmt.Errorf("error injecting value '%v' for library '%v': %w", libName, valueName, err)
}
}
if err := vm.Set(libName, libObj); err != nil {
return nil, fmt.Errorf("error creating library object for library '%v': %w", libName, err)
}
}
_, err = vm.RunString(string(script))
if err != nil {
return nil, fmt.Errorf("error running javascript script: %w", err)
}
return vm, nil
}
func GetValueFromJSVM[T any](jss *JSService, valueName string, src io.Reader, typescript bool) (T, error) {
if typescript {
jsSrc, err := TranspileTS(src)
if err != nil {
return *new(T), err
}
src = jsSrc
}
vm, err := jss.GetVM(src)
if err != nil {
return *new(T), fmt.Errorf("error getting value from JS script: %w", err)
}
item := *new(T)
if err := vm.ExportTo(vm.Get(valueName), &item); err != nil {
return *new(T), fmt.Errorf("error exporting value from javascript VM: %w", err)
}
return item, nil
}
func TranspileTS(src io.Reader) (io.Reader, error) {
s, err := io.ReadAll(src)
if err != nil {
return nil, fmt.Errorf("error reading typescript source: %w", err)
}
result := api.Build(api.BuildOptions{
Stdin: &api.StdinOptions{
Contents: string(s),
Loader: api.LoaderTS,
},
Platform: api.PlatformNeutral,
Sourcemap: api.SourceMapInline,
Target: api.ES2015,
TreeShaking: api.TreeShakingFalse,
SourcesContent: api.SourcesContentInclude,
})
if len(result.Errors) > 0 {
err := errors.New("error transpiling typescript source")
for _, errMsg := range result.Errors {
text := fmt.Sprintf("%v: @ Line %v:%v", errMsg.Text, errMsg.Location.Line, errMsg.Location.Column)
err = errors.Join(err, errors.New(text))
}
return nil, err
}
return bytes.NewReader(result.OutputFiles[0].Contents), nil
}