From a275364cd1e753391a5fc0ed7686194bcbd6f778 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Tue, 27 Jan 2026 10:17:27 -0500 Subject: [PATCH] wip --- builtin.go | 296 +++++++++++++++++++++++++------- examples/builtin_example.go.txt | 46 +++++ test_data/simple_builtins.ts | 9 + 3 files changed, 286 insertions(+), 65 deletions(-) create mode 100644 examples/builtin_example.go.txt create mode 100644 test_data/simple_builtins.ts diff --git a/builtin.go b/builtin.go index 11fc483..189eea4 100644 --- a/builtin.go +++ b/builtin.go @@ -11,8 +11,6 @@ import ( "github.com/dop251/goja" ) -type BuiltinFunction any - type Builtin struct { Name string Function any @@ -24,20 +22,194 @@ var ( registryMutex sync.RWMutex ) -func RegisterBuiltin[T any](name string, fn T) { - builtinRegistry[name] = createBuiltin(name, fn) -} - -func createBuiltin(name string, fn any) Builtin { +func RegisterBuiltin(name string, fn any) { fnValue := reflect.ValueOf(fn) fnType := fnValue.Type() - tsDef := generateTypeScriptDefinition(name, fnType) + wrapper := createGenericWrapper(fnValue, fnType) + definition := generateTypeScriptDefinition(name, fnType) - return Builtin{ + builtinRegistry[name] = Builtin{ Name: name, - Function: fn, - Definition: tsDef, + Function: wrapper, + Definition: definition, + } +} + +func createGenericWrapper(fnValue reflect.Value, fnType reflect.Type) any { + return func(vm *goja.Runtime) any { + return func(call goja.FunctionCall) goja.Value { + args := make([]reflect.Value, fnType.NumIn()) + + for i := 0; i < fnType.NumIn(); i++ { + argType := fnType.In(i) + var jsArg goja.Value + + if i < len(call.Arguments) { + jsArg = call.Arguments[i] + } else { + jsArg = goja.Undefined() + } + + if goja.IsUndefined(jsArg) || goja.IsNull(jsArg) { + if argType.Kind() == reflect.Map { + args[i] = reflect.MakeMap(argType) + continue + } + if argType.Kind() == reflect.Interface { + args[i] = reflect.Zero(argType) + continue + } + } + + converted, err := convertJSValueToGo(vm, jsArg, argType) + if err != nil { + panic(fmt.Sprintf("argument %d: %v", i, err)) + } + args[i] = reflect.ValueOf(converted) + } + + results := fnValue.Call(args) + + if len(results) == 0 { + return goja.Undefined() + } + + lastResult := results[len(results)-1] + if lastResult.Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) { + if !lastResult.IsNil() { + panic(fmt.Sprintf("error: %v", lastResult.Interface())) + } + if len(results) == 1 { + return goja.Undefined() + } + return convertGoValueToJS(vm, results[0]) + } + + return convertGoValueToJS(vm, results[0]) + } + } +} + +func convertJSValueToGo(vm *goja.Runtime, jsValue goja.Value, targetType reflect.Type) (any, error) { + if goja.IsUndefined(jsValue) || goja.IsNull(jsValue) { + if targetType.Kind() == reflect.Interface || targetType.Kind() == reflect.Pointer { + 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 targetType.Key().Kind() == reflect.String && targetType.Elem().Kind() == reflect.Interface { + obj := jsValue.ToObject(vm) + if obj == nil { + return nil, fmt.Errorf("not an object") + } + + result := make(map[string]any) + for _, key := range obj.Keys() { + result[key] = obj.Get(key).Export() + } + return result, nil + } + return nil, fmt.Errorf("unsupported map type: %v", targetType) + + 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, 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) + + case FetchResult: + obj := vm.NewObject() + _ = obj.Set("ok", v.OK) + _ = obj.Set("status", v.Status) + _ = obj.Set("text", func() string { + return v.Body + }) + + headersObj := vm.NewObject() + headers := v.Headers + _ = headersObj.Set("get", func(c goja.FunctionCall) goja.Value { + if len(c.Arguments) < 1 { + return goja.Undefined() + } + key := c.Arguments[0].String() + return vm.ToValue(headers[key]) + }) + _ = obj.Set("headers", headersObj) + + return obj + + case *FetchResult: + if v == nil { + return goja.Null() + } + return convertGoValueToJS(vm, reflect.ValueOf(*v)) + + default: + return vm.ToValue(v) } } @@ -48,18 +220,44 @@ func generateTypeScriptDefinition(name string, fnType reflect.Type) string { var params []string for i := 0; i < fnType.NumIn(); i++ { - params = append(params, fmt.Sprintf("arg%d: %s", i, goTypeToTSType(fnType.In(i)))) + paramName := fmt.Sprintf("arg%d", i) + if fnType.In(i).Kind() == reflect.Pointer { + ptrType := fnType.In(i).Elem() + if ptrType.Kind() == reflect.Struct { + if s, ok := extractStructParamName(ptrType); ok { + paramName = s + } + } + } + params = append(params, fmt.Sprintf("%s: %s", paramName, goTypeToTSType(fnType.In(i)))) } returnSignature := "void" if fnType.NumOut() > 0 { - returnType := fnType.Out(0) - returnSignature = goTypeToTSType(returnType) + lastIndex := fnType.NumOut() - 1 + lastType := fnType.Out(lastIndex) + + if lastType.Implements(reflect.TypeOf((*error)(nil)).Elem()) { + if fnType.NumOut() > 1 { + returnSignature = goTypeToTSType(fnType.Out(0)) + } else { + returnSignature = "void" + } + } else { + returnSignature = goTypeToTSType(lastType) + } } return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature) } +func extractStructParamName(structType reflect.Type) (string, bool) { + if structType.Name() != "" { + return strings.ToLower(structType.Name()), true + } + return "", false +} + func goTypeToTSType(t reflect.Type) string { switch t.Kind() { case reflect.String: @@ -73,15 +271,18 @@ func goTypeToTSType(t reflect.Type) string { case reflect.Bool: return "boolean" case reflect.Interface, reflect.Pointer: - if t.String() == "goja.Value" { - return "any" - } return "any" case reflect.Slice: return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem())) case reflect.Map: + if t.Key().Kind() == reflect.String && t.Elem().Kind() == reflect.Interface { + return "Record" + } return "Record" case reflect.Struct: + if t.Name() == "FetchResult" { + return "Response" + } return "any" default: return "any" @@ -100,13 +301,13 @@ func GetBuiltinsDeclarations() string { } func RegisterBuiltins(vm *goja.Runtime) { - RegisterFetchBuiltin(vm) - registryMutex.RLock() defer registryMutex.RUnlock() for name, builtin := range builtinRegistry { - if builtin.Function != nil { + if wrapperFactory, ok := builtin.Function.(func(*goja.Runtime) any); ok { + _ = vm.Set(name, wrapperFactory(vm)) + } else { _ = vm.Set(name, builtin.Function) } } @@ -141,8 +342,9 @@ func Fetch(url string, options map[string]any) (*FetchResult, error) { headers := make(map[string]string) for key, values := range resp.Header { if len(values) > 0 { - headers[key] = values[0] - headers[strings.ToLower(key)] = values[0] + val := values[0] + headers[key] = val + headers[strings.ToLower(key)] = val } } @@ -154,48 +356,12 @@ func Fetch(url string, options map[string]any) (*FetchResult, error) { }, nil } -func RegisterFetchBuiltin(vm *goja.Runtime) { - _ = vm.Set("fetch", func(call goja.FunctionCall) goja.Value { - if len(call.Arguments) < 1 { - panic("fetch requires at least 1 argument") - } - - url := call.Arguments[0].String() - - result, err := Fetch(url, nil) - if err != nil { - panic(err) - } - - resultObj := vm.NewObject() - _ = resultObj.Set("ok", result.OK) - _ = resultObj.Set("status", result.Status) - - body := result.Body - _ = resultObj.Set("text", func() string { - return body - }) - - headersObj := vm.NewObject() - headers := result.Headers - _ = headersObj.Set("get", func(c goja.FunctionCall) goja.Value { - if len(c.Arguments) < 1 { - return goja.Undefined() - } - key := c.Arguments[0].String() - return vm.ToValue(headers[key]) - }) - _ = resultObj.Set("headers", headersObj) - - return resultObj - }) - - builtinRegistry["fetch"] = Builtin{ - Name: "fetch", - Function: nil, - Definition: "declare function fetch(url: string, options?: any): PromiseLike;", - } -} - func init() { + RegisterBuiltin("fetch", Fetch) + RegisterBuiltin("add", func(a, b int) int { + return a + b + }) + RegisterBuiltin("greet", func(name string) string { + return fmt.Sprintf("Hello, %s!", name) + }) } diff --git a/examples/builtin_example.go.txt b/examples/builtin_example.go.txt new file mode 100644 index 0000000..eb9ae41 --- /dev/null +++ b/examples/builtin_example.go.txt @@ -0,0 +1,46 @@ +// Example: How to add builtins to the framework +// Just write a Go function and register it - that's all! + +package main + +import "fmt" + +// Simple function - just register it! +func multiply(a, b int) int { + return a * b +} + +// Function returning multiple values with error +func divide(a, b int) (int, error) { + if b == 0 { + return 0, fmt.Errorf("cannot divide by zero") + } + return a / b, nil +} + +// Complex example with struct +type User struct { + Name string + Email string + Age int +} + +func getUser(id int) (User, error) { + return User{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + }, nil +} + +// Register all builtins in init +// func init() { +// RegisterBuiltin("multiply", multiply) +// RegisterBuiltin("divide", divide) +// RegisterBuiltin("getUser", getUser) +// } + +// That's it! TypeScript definitions are auto-generated: +// declare function multiply(arg0: number, arg1: number): number; +// declare function divide(arg0: number, arg1: number): number; +// declare function getUser(arg0: number): any; diff --git a/test_data/simple_builtins.ts b/test_data/simple_builtins.ts new file mode 100644 index 0000000..e25b8c6 --- /dev/null +++ b/test_data/simple_builtins.ts @@ -0,0 +1,9 @@ +const sum = add(5, 10); +console.log("5 + 10 =", sum); + +const greeting = greet("World"); +console.log(greeting); + +const response = fetch("https://httpbin.org/get"); +console.log("Fetch OK:", response.ok); +console.log("Fetch Status:", response.status);