Easily Embedding Javascript into Go. | j4d3­blooded­.­XYZ
This post was created at 16 days, 2 hours and 41 minutes ago.
It has the following tags:
javascript go

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
}