This commit is contained in:
2026-01-27 20:04:36 -05:00
parent c3a16c9e92
commit f9d3753806
13 changed files with 247 additions and 352 deletions

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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 ""

View File

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

View File

@@ -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,

View File

@@ -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:")
}

View File

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

View File

@@ -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);