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.
109 lines
2.9 KiB
Go
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 }
|