133 lines
2.7 KiB
Go
133 lines
2.7 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/pkg/api"
|
|
"github.com/fastschema/qjs"
|
|
|
|
"reichard.io/poiesis/internal/functions"
|
|
_ "reichard.io/poiesis/internal/stdlib"
|
|
)
|
|
|
|
type Runtime struct {
|
|
ctx *qjs.Context
|
|
opts qjs.Option
|
|
}
|
|
|
|
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
|
|
// Create Runtime
|
|
r := &Runtime{opts: qjs.Option{Context: ctx}}
|
|
for _, opt := range opts {
|
|
opt(r)
|
|
}
|
|
|
|
// Create QuickJS Context
|
|
rt, err := qjs.New(r.opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.ctx = rt.Context()
|
|
|
|
// Populate Globals
|
|
if err := r.populateGlobals(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (r *Runtime) populateGlobals() error {
|
|
// Register Custom Functions
|
|
for name, fn := range functions.GetRegisteredFunctions() {
|
|
// Register Main Function
|
|
if fn.IsAsync() {
|
|
r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
|
|
qjsVal, err := callFunc(this, fn)
|
|
if err != nil {
|
|
this.Promise().Reject(this.Context().NewError(err))
|
|
return
|
|
}
|
|
this.Promise().Resolve(qjsVal)
|
|
})
|
|
} else {
|
|
r.ctx.SetFunc(name, func(this *qjs.This) (*qjs.Value, error) {
|
|
return callFunc(this, fn)
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Runtime) RunFile(filePath string) error {
|
|
tsFileContent, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading file: %w", err)
|
|
}
|
|
|
|
return r.RunCode(string(tsFileContent))
|
|
}
|
|
|
|
func (r *Runtime) RunCode(tsCode string) error {
|
|
transformedCode, err := r.transformCode(tsCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := r.ctx.Eval("code.ts", qjs.Code(string(transformedCode)))
|
|
if result != nil {
|
|
result.Free()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *Runtime) transformCode(tsCode string) ([]byte, error) {
|
|
result := api.Transform(tsCode, api.TransformOptions{
|
|
Loader: api.LoaderTS,
|
|
Target: api.ES2022,
|
|
// Format: api.FormatIIFE,
|
|
Sourcemap: api.SourceMapNone,
|
|
TreeShaking: api.TreeShakingFalse,
|
|
})
|
|
|
|
if len(result.Errors) > 0 {
|
|
var allErrs []string
|
|
for _, e := range result.Errors {
|
|
allErrs = append(allErrs, e.Text)
|
|
}
|
|
return nil, fmt.Errorf("transpilation failed: %s", strings.Join(allErrs, ", "))
|
|
}
|
|
|
|
return result.Code, nil
|
|
}
|
|
|
|
func callFunc(this *qjs.This, fn functions.Function) (*qjs.Value, error) {
|
|
qjsArgs := this.Args()
|
|
fnArgs := fn.Arguments()
|
|
|
|
var allArgs []any
|
|
for i := range min(len(fnArgs), len(qjsArgs)) {
|
|
rVal, err := qjs.JsArgToGo(qjsArgs[i], fnArgs[i])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
allArgs = append(allArgs, rVal.Interface())
|
|
}
|
|
|
|
result, err := fn.Call(this.Context(), allArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
val, err := qjs.ToJsValue(this.Context(), result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return val, nil
|
|
}
|