package main import ( "fmt" "io" "net/http" "reflect" "strings" "sync" "github.com/dop251/goja" ) type BuiltinFunction any type Builtin struct { Name string Function any Definition string } var ( builtinRegistry = make(map[string]Builtin) registryMutex sync.RWMutex ) func RegisterBuiltin[T any](name string, fn T) { builtinRegistry[name] = createBuiltin(name, fn) } func createBuiltin(name string, fn any) Builtin { fnValue := reflect.ValueOf(fn) fnType := fnValue.Type() tsDef := generateTypeScriptDefinition(name, fnType) return Builtin{ Name: name, Function: fn, Definition: tsDef, } } func generateTypeScriptDefinition(name string, fnType reflect.Type) string { if fnType.Kind() != reflect.Func { return "" } var params []string for i := 0; i < fnType.NumIn(); i++ { params = append(params, fmt.Sprintf("arg%d: %s", i, goTypeToTSType(fnType.In(i)))) } returnSignature := "void" if fnType.NumOut() > 0 { returnType := fnType.Out(0) returnSignature = goTypeToTSType(returnType) } return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature) } 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: if t.String() == "goja.Value" { return "any" } return "any" case reflect.Slice: return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem())) case reflect.Map: return "Record" case reflect.Struct: 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) { RegisterFetchBuiltin(vm) registryMutex.RLock() defer registryMutex.RUnlock() for name, builtin := range builtinRegistry { if builtin.Function != nil { _ = 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 { headers[key] = values[0] headers[strings.ToLower(key)] = values[0] } } return &FetchResult{ OK: resp.StatusCode >= 200 && resp.StatusCode < 300, Status: resp.StatusCode, Body: string(body), Headers: headers, }, 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() { }