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

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