wip
This commit is contained in:
296
builtin.go
296
builtin.go
@@ -11,8 +11,6 @@ import (
|
|||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BuiltinFunction any
|
|
||||||
|
|
||||||
type Builtin struct {
|
type Builtin struct {
|
||||||
Name string
|
Name string
|
||||||
Function any
|
Function any
|
||||||
@@ -24,20 +22,194 @@ var (
|
|||||||
registryMutex sync.RWMutex
|
registryMutex sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterBuiltin[T any](name string, fn T) {
|
func RegisterBuiltin(name string, fn any) {
|
||||||
builtinRegistry[name] = createBuiltin(name, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createBuiltin(name string, fn any) Builtin {
|
|
||||||
fnValue := reflect.ValueOf(fn)
|
fnValue := reflect.ValueOf(fn)
|
||||||
fnType := fnValue.Type()
|
fnType := fnValue.Type()
|
||||||
|
|
||||||
tsDef := generateTypeScriptDefinition(name, fnType)
|
wrapper := createGenericWrapper(fnValue, fnType)
|
||||||
|
definition := generateTypeScriptDefinition(name, fnType)
|
||||||
|
|
||||||
return Builtin{
|
builtinRegistry[name] = Builtin{
|
||||||
Name: name,
|
Name: name,
|
||||||
Function: fn,
|
Function: wrapper,
|
||||||
Definition: tsDef,
|
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
|
var params []string
|
||||||
for i := 0; i < fnType.NumIn(); i++ {
|
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"
|
returnSignature := "void"
|
||||||
if fnType.NumOut() > 0 {
|
if fnType.NumOut() > 0 {
|
||||||
returnType := fnType.Out(0)
|
lastIndex := fnType.NumOut() - 1
|
||||||
returnSignature = goTypeToTSType(returnType)
|
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)
|
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 {
|
func goTypeToTSType(t reflect.Type) string {
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
@@ -73,15 +271,18 @@ func goTypeToTSType(t reflect.Type) string {
|
|||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return "boolean"
|
return "boolean"
|
||||||
case reflect.Interface, reflect.Pointer:
|
case reflect.Interface, reflect.Pointer:
|
||||||
if t.String() == "goja.Value" {
|
|
||||||
return "any"
|
|
||||||
}
|
|
||||||
return "any"
|
return "any"
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem()))
|
return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem()))
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
|
if t.Key().Kind() == reflect.String && t.Elem().Kind() == reflect.Interface {
|
||||||
|
return "Record<string, any>"
|
||||||
|
}
|
||||||
return "Record<string, any>"
|
return "Record<string, any>"
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
|
if t.Name() == "FetchResult" {
|
||||||
|
return "Response"
|
||||||
|
}
|
||||||
return "any"
|
return "any"
|
||||||
default:
|
default:
|
||||||
return "any"
|
return "any"
|
||||||
@@ -100,13 +301,13 @@ func GetBuiltinsDeclarations() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RegisterBuiltins(vm *goja.Runtime) {
|
func RegisterBuiltins(vm *goja.Runtime) {
|
||||||
RegisterFetchBuiltin(vm)
|
|
||||||
|
|
||||||
registryMutex.RLock()
|
registryMutex.RLock()
|
||||||
defer registryMutex.RUnlock()
|
defer registryMutex.RUnlock()
|
||||||
|
|
||||||
for name, builtin := range builtinRegistry {
|
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)
|
_ = vm.Set(name, builtin.Function)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,8 +342,9 @@ func Fetch(url string, options map[string]any) (*FetchResult, error) {
|
|||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
for key, values := range resp.Header {
|
for key, values := range resp.Header {
|
||||||
if len(values) > 0 {
|
if len(values) > 0 {
|
||||||
headers[key] = values[0]
|
val := values[0]
|
||||||
headers[strings.ToLower(key)] = 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
|
}, 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<any>;",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
46
examples/builtin_example.go.txt
Normal file
46
examples/builtin_example.go.txt
Normal file
@@ -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;
|
||||||
9
test_data/simple_builtins.ts
Normal file
9
test_data/simple_builtins.ts
Normal file
@@ -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);
|
||||||
Reference in New Issue
Block a user