package main import ( "fmt" "io" "net/http" "reflect" "strings" "sync" "github.com/dop251/goja" ) type Builtin struct { Name string Function any Definition string } var ( builtinRegistry = make(map[string]Builtin) registryMutex sync.RWMutex ) func RegisterBuiltin(name string, fn any) { fnValue := reflect.ValueOf(fn) fnType := fnValue.Type() wrapper := createGenericWrapper(fnValue, fnType) definition := generateTypeScriptDefinition(name, fnType) builtinRegistry[name] = Builtin{ Name: name, 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) } } func generateTypeScriptDefinition(name string, fnType reflect.Type) string { if fnType.Kind() != reflect.Func { return "" } var params []string for i := 0; i < fnType.NumIn(); 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 { 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: return "string" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return "number" case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return "number" case reflect.Float32, reflect.Float64: return "number" case reflect.Bool: return "boolean" case reflect.Interface, reflect.Pointer: 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" } } func GetBuiltinsDeclarations() string { registryMutex.RLock() defer registryMutex.RUnlock() var decls []string for _, builtin := range builtinRegistry { decls = append(decls, builtin.Definition) } return strings.Join(decls, "\n") } func RegisterBuiltins(vm *goja.Runtime) { registryMutex.RLock() defer registryMutex.RUnlock() for name, builtin := range builtinRegistry { if wrapperFactory, ok := builtin.Function.(func(*goja.Runtime) any); ok { _ = vm.Set(name, wrapperFactory(vm)) } else { _ = vm.Set(name, builtin.Function) } } } type FetchResult struct { OK bool Status int Body string Headers map[string]string } func Fetch(url string, options map[string]any) (*FetchResult, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch: %w", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read body: %w", err) } headers := make(map[string]string) for key, values := range resp.Header { if len(values) > 0 { val := values[0] headers[key] = val headers[strings.ToLower(key)] = val } } return &FetchResult{ OK: resp.StatusCode >= 200 && resp.StatusCode < 300, Status: resp.StatusCode, Body: string(body), Headers: headers, }, nil } 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) }) }