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) }