modernc
This commit is contained in:
@@ -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
|
||||
|
||||
18
go.mod
18
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
|
||||
)
|
||||
|
||||
69
go.sum
69
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=
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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 {
|
||||
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)
|
||||
}
|
||||
|
||||
func createSyncWrapper[T Args, R any](fn Func[T, R]) interface{} {
|
||||
return func(rawArgs any) (R, error) {
|
||||
var zero R
|
||||
var args T
|
||||
argsValue := reflect.ValueOf(&args).Elem()
|
||||
|
||||
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())
|
||||
obj, ok := rawArgs.(*quickjs.Object)
|
||||
if ok {
|
||||
jsonData, err := obj.MarshalJSON()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("argument %d (%s): %v", i, getFieldName(argsValue.Type().Field(i)), err))
|
||||
return zero, fmt.Errorf("failed to marshal args: %w", err)
|
||||
}
|
||||
|
||||
if converted != nil {
|
||||
field.Set(reflect.ValueOf(converted))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := args.Validate(); err != nil {
|
||||
panic(fmt.Sprintf("argument validation failed: %v", err))
|
||||
}
|
||||
|
||||
if isAsync {
|
||||
return createAsyncPromise(vm, fn, args)
|
||||
return zero, fmt.Errorf("argument validation failed: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := fn(ctx, args)
|
||||
return fn(ctx, args)
|
||||
}
|
||||
}
|
||||
|
||||
func createAsyncWrapper[T Args, R any](fn Func[T, R]) interface{} {
|
||||
return func(rawArgs any) (any, error) {
|
||||
var args T
|
||||
|
||||
obj, ok := rawArgs.(*quickjs.Object)
|
||||
if ok {
|
||||
jsonData, err := obj.MarshalJSON()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("failed to marshal args: %w", err)
|
||||
}
|
||||
|
||||
return convertGoValueToJS(vm, reflect.ValueOf(result))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func createAsyncPromise[T Args, R any](vm *goja.Runtime, fn Func[T, R], args T) goja.Value {
|
||||
promise, resolve, reject := vm.NewPromise()
|
||||
if err := args.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("argument validation failed: %w", err)
|
||||
}
|
||||
|
||||
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)))
|
||||
return fn(ctx, args)
|
||||
}
|
||||
}()
|
||||
|
||||
return vm.ToValue(promise)
|
||||
}
|
||||
|
||||
@@ -5,55 +5,64 @@ 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
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runtime) RunFile(filePath string, stdout, stderr io.Writer) error {
|
||||
r.stdout = stdout
|
||||
@@ -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,
|
||||
|
||||
@@ -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:")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,6 @@ async function main() {
|
||||
console.log("Content-Type:", response.headers["content-type"]);
|
||||
}
|
||||
|
||||
console.log(1);
|
||||
main();
|
||||
console.log(2);
|
||||
|
||||
Reference in New Issue
Block a user