166 lines
3.9 KiB
Go
166 lines
3.9 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/pkg/api"
|
|
"modernc.org/quickjs"
|
|
|
|
"reichard.io/poiesis/internal/functions"
|
|
_ "reichard.io/poiesis/internal/stdlib"
|
|
)
|
|
|
|
type Runtime struct {
|
|
vm *quickjs.VM
|
|
ctx context.Context
|
|
|
|
stdout io.Writer
|
|
stderr io.Writer
|
|
}
|
|
|
|
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
|
|
// Create VM
|
|
vm, err := quickjs.NewVM()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vm.SetCanBlock(true)
|
|
|
|
// Create Runtime
|
|
r := &Runtime{vm: vm, ctx: ctx, stdout: os.Stdout, stderr: os.Stderr}
|
|
if err := r.populateGlobals(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply Options
|
|
for _, opt := range opts {
|
|
opt(r)
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (r *Runtime) populateGlobals() error {
|
|
// Add Helpers
|
|
if err := r.vm.StdAddHelpers(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add Log Hook
|
|
if err := r.vm.RegisterFunc("customLog", func(args ...any) {
|
|
for i, arg := range args {
|
|
if i > 0 {
|
|
_, _ = fmt.Fprint(r.stdout, " ")
|
|
}
|
|
_, _ = fmt.Fprint(r.stdout, arg)
|
|
}
|
|
_, _ = fmt.Fprintln(r.stdout)
|
|
}, false); err != nil {
|
|
return err
|
|
}
|
|
if _, err := r.vm.Eval("console.log = customLog;", quickjs.EvalGlobal); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Register Custom Functions
|
|
for name, fn := range functions.GetRegisteredFunctions() {
|
|
// Register Main Function
|
|
if err := r.vm.RegisterFunc(name, func(allArgs ...any) (any, error) {
|
|
return fn.Call(r.ctx, allArgs)
|
|
}, false); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wrap Exception - The QuickJS library does not allow us to throw exceptions, so we
|
|
// wrap the function with native JS to appropriately throw on error.
|
|
wrappedFunc := wrapFunc(fn.Name(), fn.IsAsync())
|
|
if _, err := r.vm.Eval(wrappedFunc, quickjs.EvalGlobal); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
_, err = r.vm.Eval(string(transformedCode), quickjs.EvalGlobal)
|
|
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 wrapFunc(funcName string, isAsync bool) string {
|
|
if isAsync {
|
|
return fmt.Sprintf(`
|
|
(function() {
|
|
const original = globalThis[%q];
|
|
|
|
globalThis[%q] = function(...args) {
|
|
console.log("calling")
|
|
return new Promise((resolve, reject) => {
|
|
const [result, error] = original.apply(this, args);
|
|
console.log("result", result)
|
|
if (error) {
|
|
console.log("reject")
|
|
reject(new Error(error));
|
|
} else {
|
|
console.log("resolve")
|
|
resolve(result);
|
|
}
|
|
});
|
|
};
|
|
})();
|
|
`, funcName, funcName)
|
|
}
|
|
|
|
return fmt.Sprintf(`
|
|
(function() {
|
|
const original = globalThis[%q];
|
|
|
|
globalThis[%q] = function(...args) {
|
|
const [result, error] = original.apply(this, args);
|
|
if (error) {
|
|
throw new Error(error);
|
|
}
|
|
return result;
|
|
};
|
|
})();
|
|
`, funcName, funcName)
|
|
}
|