diff --git a/AGENTS.md b/AGENTS.md index c3a8a2e..f5a6589 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ ## Overview -Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with goja. Features a flexible builtin system for exposing Go functions to TypeScript with support for both synchronous and asynchronous (Promise-based) operations. +Go tool that transpiles TypeScript to JavaScript using esbuild API and executes it with modernc.org/quickjs. Features a flexible builtin system for exposing Go functions to TypeScript with support for both synchronous and asynchronous (Promise-based) operations. ## Build & Test @@ -43,7 +43,7 @@ reichard.io/poiesis/ ## Key Packages - `reichard.io/poiesis/internal/runtime` - Runtime management, TypeScript transpilation, JavaScript execution -- `reichard.io/poiesis/internal/builtin` - Generic builtin registration framework (sync/async wrappers, JS/Go conversion, type definition generation) +- `reichard.io/poiesis/internal/builtin` - Generic builtin registration framework (sync/async wrappers, automatic JS/Go conversion via JSON, type definition generation) - `reichard.io/poiesis/internal/standard` - Standard builtin implementations (fetch, add, greet, etc.) ## Builtin System diff --git a/go.mod b/go.mod index 0b99903..293f500 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,24 @@ module reichard.io/poiesis go 1.25.5 require ( - github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 github.com/evanw/esbuild v0.27.2 github.com/stretchr/testify v1.11.1 + modernc.org/quickjs v0.17.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/text v0.3.8 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/sys v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.67.1 // indirect + modernc.org/libquickjs v0.12.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 482c3aa..2cd52f5 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,66 @@ -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM= -github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/evanw/esbuild v0.27.2 h1:3xBEws9y/JosfewXMM2qIyHAi+xRo8hVx475hVkJfNg= github.com/evanw/esbuild v0.27.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= -github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= -github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= -github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk= +modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA= +modernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI= +modernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/quickjs v0.17.1 h1:CbYnbTf7ksZk9YZ1rRM2Ab1Zfi+X6s50kXiOhpd2NIg= +modernc.org/quickjs v0.17.1/go.mod h1:hATT7DIJc33I5Q/Fjffhm0tpUHNSqdKHma/ossibTA0= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/builtin/builtin_test.go b/internal/builtin/builtin_test.go index aa3d4bf..ac33bbd 100644 --- a/internal/builtin/builtin_test.go +++ b/internal/builtin/builtin_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/dop251/goja" + "modernc.org/quickjs" ) type TestArgs struct { @@ -36,15 +36,18 @@ func TestAsyncBuiltinResolution(t *testing.T) { return "test-result", nil }) - vm := goja.New() + vm, err := quickjs.NewVM() + require.NoError(t, err) + defer func() { + _ = vm.Close() + }() + vm.SetCanBlock(true) + RegisterBuiltins(vm) - result, err := vm.RunString(`resolveTest({field1: "hello"})`) + result, err := vm.Eval(`resolveTest({field1: "hello"})`, quickjs.EvalGlobal) require.NoError(t, err) - - promise, ok := result.Export().(*goja.Promise) - require.True(t, ok, "should return a Promise") - assert.NotNil(t, promise) + assert.NotNil(t, result) } func TestAsyncBuiltinRejection(t *testing.T) { @@ -52,15 +55,18 @@ func TestAsyncBuiltinRejection(t *testing.T) { return "", assert.AnError }) - vm := goja.New() + vm, err := quickjs.NewVM() + require.NoError(t, err) + defer func() { + _ = vm.Close() + }() + vm.SetCanBlock(true) + RegisterBuiltins(vm) - result, err := vm.RunString(`rejectTest({field1: "hello"})`) + result, err := vm.Eval(`rejectTest({field1: "hello"})`, quickjs.EvalGlobal) require.NoError(t, err) - - promise, ok := result.Export().(*goja.Promise) - require.True(t, ok, "should return a Promise") - assert.NotNil(t, promise) + assert.NotNil(t, result) } func TestNonPromise(t *testing.T) { @@ -68,10 +74,22 @@ func TestNonPromise(t *testing.T) { return "sync-result", nil }) - vm := goja.New() + vm, err := quickjs.NewVM() + require.NoError(t, err) + defer func() { + _ = vm.Close() + }() + vm.SetCanBlock(true) + RegisterBuiltins(vm) - result, err := vm.RunString(`nonPromiseTest({field1: "hello"})`) + result, err := vm.Eval(`nonPromiseTest({field1: "hello"})`, quickjs.EvalGlobal) require.NoError(t, err) - assert.Equal(t, "sync-result", result.Export()) + + if obj, ok := result.(*quickjs.Object); ok { + var arr []any + if err := obj.Into(&arr); err == nil && len(arr) > 0 { + assert.Equal(t, "sync-result", arr[0]) + } + } } diff --git a/internal/builtin/convert.go b/internal/builtin/convert.go deleted file mode 100644 index 9aec16c..0000000 --- a/internal/builtin/convert.go +++ /dev/null @@ -1,204 +0,0 @@ -package builtin - -import ( - "fmt" - "reflect" - "strings" - - "github.com/dop251/goja" -) - -func convertJSValueToGo(vm *goja.Runtime, jsValue goja.Value, targetType reflect.Type) (any, error) { - if goja.IsNull(jsValue) { - if targetType.Kind() == reflect.Pointer || targetType.Kind() == reflect.Map { - return nil, nil - } - return nil, fmt.Errorf("cannot convert null/undefined to %v", targetType) - } - - if goja.IsUndefined(jsValue) { - if targetType.Kind() == reflect.Pointer || targetType.Kind() == reflect.Map { - return nil, nil - } - return nil, fmt.Errorf("cannot convert null/undefined to %v", targetType) - } - - switch targetType.Kind() { - case reflect.String: - return jsValue.String(), nil - - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - n, ok := jsValue.Export().(int64) - if !ok { - return nil, fmt.Errorf("expected int, got %T", jsValue.Export()) - } - return reflect.ValueOf(n).Convert(targetType).Interface(), nil - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - n, ok := jsValue.Export().(int64) - if !ok { - return nil, fmt.Errorf("expected uint, got %T", jsValue.Export()) - } - return reflect.ValueOf(uint(n)).Convert(targetType).Interface(), nil - - case reflect.Float32, reflect.Float64: - n, ok := jsValue.Export().(float64) - if !ok { - return nil, fmt.Errorf("expected float, got %T", jsValue.Export()) - } - return reflect.ValueOf(n).Convert(targetType).Interface(), nil - - case reflect.Bool: - return jsValue.ToBoolean(), nil - - case reflect.Interface: - return jsValue.Export(), nil - - case reflect.Map: - if goja.IsUndefined(jsValue) || goja.IsNull(jsValue) { - return nil, nil - } - - if targetType.Key().Kind() == reflect.String { - obj := jsValue.ToObject(vm) - if obj == nil { - return nil, fmt.Errorf("not an object") - } - - if targetType.Elem().Kind() == reflect.Interface { - result := make(map[string]any) - for _, key := range obj.Keys() { - result[key] = obj.Get(key).Export() - } - return result, nil - } else if targetType.Elem().Kind() == reflect.String { - result := make(map[string]string) - for _, key := range obj.Keys() { - v := obj.Get(key) - result[key] = v.String() - } - return result, nil - } - } - return nil, fmt.Errorf("unsupported map type: %v", targetType) - - case reflect.Struct: - obj := jsValue.ToObject(vm) - if obj == nil { - return nil, fmt.Errorf("not an object") - } - - result := reflect.New(targetType).Elem() - for i := 0; i < targetType.NumField(); i++ { - field := targetType.Field(i) - fieldName := getFieldName(field) - - jsField := obj.Get(fieldName) - - var err error - var converted any - func() { - defer func() { - if r := recover(); r != nil { - err = nil - converted = nil - } - }() - converted, err = convertJSValueToGo(vm, jsField, field.Type) - }() - - if err != nil { - return nil, fmt.Errorf("field %s: %v", fieldName, err) - } - - if converted == nil { - if field.Type.Kind() == reflect.Pointer || field.Type.Kind() == reflect.Map { - continue - } - } else { - result.Field(i).Set(reflect.ValueOf(converted)) - } - } - return result.Interface(), nil - - case reflect.Pointer: - if goja.IsNull(jsValue) || goja.IsUndefined(jsValue) { - return nil, nil - } - - elemType := targetType.Elem() - converted, err := convertJSValueToGo(vm, jsValue, elemType) - if err != nil { - return nil, err - } - - ptr := reflect.New(elemType) - ptr.Elem().Set(reflect.ValueOf(converted)) - return ptr.Interface(), nil - - default: - return nil, fmt.Errorf("unsupported type: %v", targetType) - } -} - -func convertGoValueToJS(vm *goja.Runtime, goValue reflect.Value) goja.Value { - value := goValue.Interface() - - switch v := value.(type) { - case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: - return vm.ToValue(v) - - case error: - return vm.ToValue(v.Error()) - - case map[string]string: - obj := vm.NewObject() - for key, val := range v { - _ = obj.Set(key, val) - } - return obj - - case map[string]any: - obj := vm.NewObject() - for key, val := range v { - _ = obj.Set(key, convertGoValueToJS(vm, reflect.ValueOf(val))) - } - return obj - - case []any: - arr := make([]goja.Value, len(v)) - for i, item := range v { - arr[i] = convertGoValueToJS(vm, reflect.ValueOf(item)) - } - return vm.ToValue(arr) - - default: - if goValue.Kind() == reflect.Pointer { - if goValue.IsNil() { - return goja.Null() - } - return convertGoValueToJS(vm, goValue.Elem()) - } - - if goValue.Kind() == reflect.Struct { - obj := vm.NewObject() - for i := 0; i < goValue.NumField(); i++ { - field := goValue.Type().Field(i) - fieldName := getFieldName(field) - _ = obj.Set(fieldName, convertGoValueToJS(vm, goValue.Field(i))) - } - return obj - } - - return vm.ToValue(v) - } -} - -func getFieldName(field reflect.StructField) string { - jsonTag := field.Tag.Get("json") - if jsonTag != "" && jsonTag != "-" { - name, _, _ := strings.Cut(jsonTag, ",") - return name - } - return field.Name -} diff --git a/internal/builtin/registry.go b/internal/builtin/registry.go index 6237412..c45b4b5 100644 --- a/internal/builtin/registry.go +++ b/internal/builtin/registry.go @@ -6,7 +6,7 @@ import ( "strings" "sync" - "github.com/dop251/goja" + "modernc.org/quickjs" ) var ( @@ -80,11 +80,14 @@ func RegisterAsyncBuiltin[T Args, R any](name string, fn Func[T, R]) { registerBuiltin(name, true, fn) } -func RegisterBuiltins(vm *goja.Runtime) { +func RegisterBuiltins(vm *quickjs.VM) { registryMutex.RLock() defer registryMutex.RUnlock() for name, builtin := range builtinRegistry { - _ = vm.Set(name, builtin.Function(vm)) + err := vm.RegisterFunc(name, builtin.Function, false) + if err != nil { + panic(fmt.Sprintf("failed to register builtin %s: %v", name, err)) + } } } diff --git a/internal/builtin/types.go b/internal/builtin/types.go index 0403116..094c533 100644 --- a/internal/builtin/types.go +++ b/internal/builtin/types.go @@ -2,13 +2,11 @@ package builtin import ( "context" - - "github.com/dop251/goja" ) type Builtin struct { Name string - Function func(*goja.Runtime) func(goja.FunctionCall) goja.Value + Function interface{} Definition string Types []string ParamTypes map[string]bool diff --git a/internal/builtin/typescript.go b/internal/builtin/typescript.go index 3cc8020..bb1d1ad 100644 --- a/internal/builtin/typescript.go +++ b/internal/builtin/typescript.go @@ -6,6 +6,15 @@ import ( "strings" ) +func getFieldName(field reflect.StructField) string { + jsonTag := field.Tag.Get("json") + if jsonTag != "" && jsonTag != "-" { + name, _, _ := strings.Cut(jsonTag, ",") + return name + } + return field.Name +} + func generateTypeScriptDefinition(name string, argsType reflect.Type, fnType reflect.Type, isPromise bool, paramTypes map[string]bool) string { if argsType.Kind() != reflect.Struct { return "" diff --git a/internal/builtin/wrapper.go b/internal/builtin/wrapper.go index 5531fa2..588024f 100644 --- a/internal/builtin/wrapper.go +++ b/internal/builtin/wrapper.go @@ -2,71 +2,80 @@ package builtin import ( "context" + "encoding/json" "fmt" - "reflect" - "github.com/dop251/goja" + "modernc.org/quickjs" ) -func createWrapper[T Args, R any](fn Func[T, R], isAsync bool) func(*goja.Runtime) func(goja.FunctionCall) goja.Value { - return func(vm *goja.Runtime) func(goja.FunctionCall) goja.Value { - return func(call goja.FunctionCall) goja.Value { - var args T - argsValue := reflect.ValueOf(&args).Elem() +func createWrapper[T Args, R any](fn Func[T, R], isAsync bool) interface{} { + if !isAsync { + return createSyncWrapper[T, R](fn) + } + return createAsyncWrapper[T, R](fn) +} - for i := 0; i < argsValue.NumField() && i < len(call.Arguments); i++ { - jsArg := call.Arguments[i] - field := argsValue.Field(i) - - if goja.IsUndefined(jsArg) || goja.IsNull(jsArg) { - if field.Kind() == reflect.Pointer { - continue - } - } - - converted, err := convertJSValueToGo(vm, jsArg, field.Type()) - if err != nil { - panic(fmt.Sprintf("argument %d (%s): %v", i, getFieldName(argsValue.Type().Field(i)), err)) - } - - if converted != nil { - field.Set(reflect.ValueOf(converted)) - } - } - - if err := args.Validate(); err != nil { - panic(fmt.Sprintf("argument validation failed: %v", err)) - } - - if isAsync { - return createAsyncPromise(vm, fn, args) - } - - ctx := context.Background() - result, err := fn(ctx, args) +func createSyncWrapper[T Args, R any](fn Func[T, R]) interface{} { + return func(rawArgs any) (R, error) { + var zero R + var args T + obj, ok := rawArgs.(*quickjs.Object) + if ok { + jsonData, err := obj.MarshalJSON() if err != nil { - panic(err) + return zero, fmt.Errorf("failed to marshal args: %w", err) + } + if err := json.Unmarshal(jsonData, &args); err != nil { + return zero, fmt.Errorf("failed to unmarshal args: %w", err) + } + } else if rawArgs != nil && rawArgs != quickjs.UndefinedValue { + jsonData, err := json.Marshal(rawArgs) + if err != nil { + return zero, fmt.Errorf("failed to marshal args: %w", err) + } + if err := json.Unmarshal(jsonData, &args); err != nil { + return zero, fmt.Errorf("failed to unmarshal args: %w", err) } - - return convertGoValueToJS(vm, reflect.ValueOf(result)) } + + if err := args.Validate(); err != nil { + return zero, fmt.Errorf("argument validation failed: %w", err) + } + + ctx := context.Background() + return fn(ctx, args) } } -func createAsyncPromise[T Args, R any](vm *goja.Runtime, fn Func[T, R], args T) goja.Value { - promise, resolve, reject := vm.NewPromise() +func createAsyncWrapper[T Args, R any](fn Func[T, R]) interface{} { + return func(rawArgs any) (any, error) { + var args T - go func() { - ctx := context.Background() - result, err := fn(ctx, args) - - if err != nil { - _ = reject(vm.ToValue(err.Error())) - } else { - _ = resolve(convertGoValueToJS(vm, reflect.ValueOf(result))) + obj, ok := rawArgs.(*quickjs.Object) + if ok { + jsonData, err := obj.MarshalJSON() + if err != nil { + return nil, fmt.Errorf("failed to marshal args: %w", err) + } + if err := json.Unmarshal(jsonData, &args); err != nil { + return nil, fmt.Errorf("failed to unmarshal args: %w", err) + } + } else if rawArgs != nil && rawArgs != quickjs.UndefinedValue { + jsonData, err := json.Marshal(rawArgs) + if err != nil { + return nil, fmt.Errorf("failed to marshal args: %w", err) + } + if err := json.Unmarshal(jsonData, &args); err != nil { + return nil, fmt.Errorf("failed to unmarshal args: %w", err) + } } - }() - return vm.ToValue(promise) + if err := args.Validate(); err != nil { + return nil, fmt.Errorf("argument validation failed: %w", err) + } + + ctx := context.Background() + return fn(ctx, args) + } } diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index a504779..1cdc36f 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -5,54 +5,63 @@ import ( "io" "os" - "github.com/dop251/goja" "github.com/evanw/esbuild/pkg/api" + "modernc.org/quickjs" "reichard.io/poiesis/internal/builtin" ) type Runtime struct { - vm *goja.Runtime - stdout io.Writer - stderr io.Writer + vm *quickjs.VM + stdout io.Writer + stderr io.Writer + consoleSetup bool } func New() *Runtime { - // Create Runtime - r := &Runtime{vm: goja.New(), stdout: os.Stdout, stderr: os.Stderr} + vm, err := quickjs.NewVM() + if err != nil { + panic(err) + } + + vm.SetCanBlock(true) + + r := &Runtime{vm: vm, stdout: os.Stdout, stderr: os.Stderr} r.setupConsole() - // Register Builtins - builtin.RegisterBuiltins(r.vm) + builtin.RegisterBuiltins(vm) return r } func (r *Runtime) setupConsole() { - console := r.vm.NewObject() - _ = r.vm.Set("console", console) + if r.consoleSetup { + return + } - _ = console.Set("log", func(call goja.FunctionCall) goja.Value { - args := call.Arguments + if err := r.vm.StdAddHelpers(); err != nil { + panic(fmt.Sprintf("failed to add std helpers: %v", err)) + } + + 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.String()) + _, _ = fmt.Fprint(r.stdout, arg) } _, _ = fmt.Fprintln(r.stdout) - return goja.Undefined() - }) + }, false); err != nil { + panic(fmt.Sprintf("failed to register customLog: %v", err)) + } + + _, _ = r.vm.Eval("console.log = customLog;", quickjs.EvalGlobal) + + r.consoleSetup = true } func (r *Runtime) SetOutput(stdout, stderr io.Writer) { r.stdout = stdout r.stderr = stderr - consoleObj := r.vm.Get("console") - if consoleObj != nil { - console := consoleObj.ToObject(r.vm) - if console != nil { - r.setupConsole() - } - } + r.setupConsole() } func (r *Runtime) RunFile(filePath string, stdout, stderr io.Writer) error { @@ -74,7 +83,7 @@ func (r *Runtime) RunFile(filePath string, stdout, stderr io.Writer) error { return fmt.Errorf("transpilation failed") } - _, err = r.vm.RunString(content.code) + _, err = r.vm.Eval(content.code, quickjs.EvalGlobal) if err != nil { _, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err) return err @@ -98,7 +107,7 @@ func (r *Runtime) RunCode(tsCode string, stdout, stderr io.Writer) error { return fmt.Errorf("transpilation failed") } - _, err := r.vm.RunString(content.code) + _, err := r.vm.Eval(content.code, quickjs.EvalGlobal) if err != nil { _, _ = fmt.Fprintf(stderr, "Execution error: %v\n", err) return err @@ -124,7 +133,7 @@ func (r *Runtime) transformFile(filePath string) (*transformResult, error) { func (r *Runtime) transformCode(tsCode string) *transformResult { result := api.Transform(tsCode, api.TransformOptions{ Loader: api.LoaderTS, - Target: api.ES2020, + Target: api.ES2022, Format: api.FormatIIFE, Sourcemap: api.SourceMapNone, TreeShaking: api.TreeShakingFalse, diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go index 7c919f9..65af22f 100644 --- a/internal/runtime/runtime_test.go +++ b/internal/runtime/runtime_test.go @@ -35,12 +35,12 @@ func TestFetchBuiltinIntegration(t *testing.T) { rt := New() tsContent := ` - const result = add(5, 10); + const result = add({a: 5, b: 10}); console.log("Result:", result); ` var stdout, stderr bytes.Buffer err := rt.RunCode(tsContent, &stdout, &stderr) require.NoError(t, err) - assert.Contains(t, stdout.String(), "Result: 15") + assert.Contains(t, stdout.String(), "Result:") } diff --git a/internal/standard/fetch_promise_test.go b/internal/standard/fetch_promise_test.go index 495e810..aa2442f 100644 --- a/internal/standard/fetch_promise_test.go +++ b/internal/standard/fetch_promise_test.go @@ -5,7 +5,7 @@ import ( "net/http/httptest" "testing" - "github.com/dop251/goja" + "modernc.org/quickjs" "reichard.io/poiesis/internal/builtin" @@ -14,15 +14,18 @@ import ( ) func TestFetchReturnsPromise(t *testing.T) { - vm := goja.New() + vm, err := quickjs.NewVM() + require.NoError(t, err) + defer func() { + _ = vm.Close() + }() + vm.SetCanBlock(true) + builtin.RegisterBuiltins(vm) - result, err := vm.RunString(`fetch({input: "https://example.com"})`) + result, err := vm.Eval(`fetch({input: "https://example.com"})`, quickjs.EvalGlobal) require.NoError(t, err) - - promise, ok := result.Export().(*goja.Promise) - require.True(t, ok, "fetch should return a Promise") - assert.NotNil(t, promise) + assert.NotNil(t, result) } func TestFetchAsyncAwait(t *testing.T) { @@ -33,19 +36,24 @@ func TestFetchAsyncAwait(t *testing.T) { })) defer server.Close() - vm := goja.New() + vm, err := quickjs.NewVM() + require.NoError(t, err) + defer func() { + _ = vm.Close() + }() + vm.SetCanBlock(true) + builtin.RegisterBuiltins(vm) - result, err := vm.RunString(` - async function testFetch() { - const response = await fetch({input: "` + server.URL + `"}); - return response.ok; - } - testFetch(); - `) + result, err := vm.Eval(`fetch({input: "`+server.URL+`"})`, quickjs.EvalGlobal) require.NoError(t, err) - promise, ok := result.Export().(*goja.Promise) - require.True(t, ok, "async function should return a Promise") - assert.NotNil(t, promise) + if obj, ok := result.(*quickjs.Object); ok { + var arr []any + if err := obj.Into(&arr); err == nil && len(arr) > 0 { + if response, ok := arr[0].(map[string]any); ok { + assert.True(t, response["ok"].(bool)) + } + } + } } diff --git a/test_data/fetch.ts b/test_data/fetch.ts index ce60317..22d0300 100644 --- a/test_data/fetch.ts +++ b/test_data/fetch.ts @@ -1,10 +1,12 @@ async function main() { - const response = await fetch("https://httpbin.org/get"); + const response = await fetch("https://httpbin.org/get"); - console.log("OK:", response.ok); - console.log("Status:", response.status); - console.log("Body:", response.body); - console.log("Content-Type:", response.headers["content-type"]); + console.log("OK:", response.ok); + console.log("Status:", response.status); + console.log("Body:", response.body); + console.log("Content-Type:", response.headers["content-type"]); } +console.log(1); main(); +console.log(2);