Files
goa/engine_goja.go
Hanzo Dev 54a40e19c1 goa: pure-Go polyglot embedding (Go, Python, TS/JS, Rust/WASM)
One Module/Engine interface with a JSON-in/JSON-out calling convention runs
four pure-Go engines behind a Pool that makes single-threaded interpreters
serve requests as parallel goroutines:

  native               plain Go handlers, zero overhead
  goja + esbuild       JavaScript and TypeScript (TS transpiled to ES2020)
  gpython              Python, no cgo and no GIL -> true goroutine parallelism
  wazero               Rust/WASM via a trivial string ABI

Services mount from a manifest (lang + source + routes), so any Go, Python,
Rust or JS backend collapses into a single static CGO_ENABLED=0 binary served
on the zip/gofiber app. A Go-backed json stdlib shim lets Python backends keep
importing the real module names while running at Go speed.

Examples and tests cover every engine; the Rust guest is a real wasm32 fixture
exercised end to end.
2026-06-28 19:14:19 -07:00

109 lines
2.9 KiB
Go

package goa
import (
"context"
"fmt"
"sync"
esbuild "github.com/evanw/esbuild/pkg/api"
"github.com/dop251/goja"
)
// gojaEngine runs JavaScript and TypeScript. TS/JSX is transpiled to JS with
// esbuild first (pure Go), then executed on goja (pure Go). The module is
// loaded as CommonJS: handlers are read from module.exports or the global
// scope. A handler is `function name(jsonString) { return jsonString }`.
type gojaEngine struct{}
func init() { Register(gojaEngine{}) }
func (gojaEngine) Lang() string { return "javascript" }
func (gojaEngine) Aliases() []string { return []string{"js", "ts", "typescript", "jsx", "tsx"} }
func (gojaEngine) Load(_ context.Context, src []byte, opt LoadOptions) (Module, error) {
res := esbuild.Transform(string(src), esbuild.TransformOptions{
Loader: esbuild.LoaderTS, // TS is a superset of JS, so this also accepts plain JS
Format: esbuild.FormatCommonJS,
Target: esbuild.ES2020,
})
if len(res.Errors) > 0 {
return nil, fmt.Errorf("goa/js: esbuild: %s", res.Errors[0].Text)
}
vm := goja.New()
// Minimal CommonJS surface + injected env.
module := vm.NewObject()
exports := vm.NewObject()
_ = module.Set("exports", exports)
_ = vm.Set("module", module)
_ = vm.Set("exports", exports)
procEnv := vm.NewObject()
for k, v := range opt.Env {
_ = procEnv.Set(k, v)
}
proc := vm.NewObject()
_ = proc.Set("env", procEnv)
_ = vm.Set("process", proc)
if _, err := vm.RunString(string(res.Code)); err != nil {
return nil, fmt.Errorf("goa/js: load %q: %w", opt.Name, err)
}
exp := module.Get("exports")
expObj, _ := exp.(*goja.Object) // module.exports may have been reassigned
if expObj == nil {
expObj = exports
}
return &gojaModule{vm: vm, exports: expObj}, nil
}
type gojaModule struct {
mu sync.Mutex
vm *goja.Runtime
exports *goja.Object
}
func (m *gojaModule) resolve(fn string) (goja.Callable, bool) {
if v := m.exports.Get(fn); v != nil {
if cb, ok := goja.AssertFunction(v); ok {
return cb, true
}
}
if v := m.vm.Get(fn); v != nil {
if cb, ok := goja.AssertFunction(v); ok {
return cb, true
}
}
return nil, false
}
func (m *gojaModule) Invoke(_ context.Context, fn string, payload []byte) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
cb, ok := m.resolve(fn)
if !ok {
return nil, fmt.Errorf("goa/js: no exported function %q", fn)
}
res, err := cb(goja.Undefined(), m.vm.ToValue(string(payload)))
if err != nil {
return nil, fmt.Errorf("goa/js: %s: %w", fn, err)
}
if res == nil || goja.IsUndefined(res) || goja.IsNull(res) {
return []byte("null"), nil
}
return []byte(res.String()), nil
}
func (m *gojaModule) Funcs() []string {
m.mu.Lock()
defer m.mu.Unlock()
var out []string
for _, k := range m.exports.Keys() {
if _, ok := goja.AssertFunction(m.exports.Get(k)); ok {
out = append(out, k)
}
}
return out
}
func (m *gojaModule) Close() error { return nil }