package runtime import ( "context" "fmt" "io" "os" "github.com/evanw/esbuild/pkg/api" "modernc.org/quickjs" "reichard.io/poiesis/internal/functions" ) type Runtime struct { vm *quickjs.VM ctx context.Context stdout io.Writer stderr io.Writer } func New(ctx context.Context) (*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 } 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, builtin := range functions.GetRegisteredFunctions() { // Register Main Function if err := r.vm.RegisterFunc(name, builtin.WrapFn(r.ctx), 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. if _, err := r.vm.Eval(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; }; })(); `, name, name), quickjs.EvalGlobal); err != nil { return err } } return nil } func (r *Runtime) RunFile(filePath string, stdout, stderr io.Writer) error { r.stdout = stdout r.stderr = stderr content, err := r.transformFile(filePath) if err != nil { _, _ = fmt.Fprintf(stderr, "Error: %v\n", err) return err } if len(content.errors) > 0 { _, _ = fmt.Fprintf(stderr, "Transpilation errors:\n") for _, err := range content.errors { _, _ = fmt.Fprintf(stderr, " %s\n", err.Text) } return fmt.Errorf("transpilation failed") } _, err = r.vm.Eval(content.code, quickjs.EvalGlobal) if err != nil { _, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err) return err } return nil } func (r *Runtime) RunCode(tsCode string, stdout, stderr io.Writer) error { r.stdout = stdout r.stderr = stderr content := r.transformCode(tsCode) if len(content.errors) > 0 { _, _ = fmt.Fprintf(stderr, "Transpilation errors:\n") for _, err := range content.errors { _, _ = fmt.Fprintf(stderr, " %s\n", err.Text) } return fmt.Errorf("transpilation failed") } _, err := r.vm.Eval(content.code, quickjs.EvalGlobal) if err != nil { _, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err) return err } return nil } type transformResult struct { code string errors []api.Message } func (r *Runtime) transformFile(filePath string) (*transformResult, error) { tsFileContent, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("error reading file: %w", err) } return r.transformCode(string(tsFileContent)), nil } func (r *Runtime) transformCode(tsCode string) *transformResult { // wrappedCode := `(async () => { // try { // ` + tsCode + ` // } catch (err) { // console.error(err); // } // })()` result := api.Transform(tsCode, api.TransformOptions{ Loader: api.LoaderTS, Target: api.ES2022, Format: api.FormatIIFE, Sourcemap: api.SourceMapNone, TreeShaking: api.TreeShakingFalse, }) return &transformResult{ code: string(result.Code), errors: result.Errors, } }