package builtin import ( "fmt" "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 customConverters = make(map[reflect.Type]func(*goja.Runtime, reflect.Value) goja.Value) ) func RegisterBuiltin(name string, fn any) { fnValue := reflect.ValueOf(fn) fnType := fnValue.Type() wrapper := createGenericWrapper(fnValue, fnType) definition := generateTypeScriptDefinition(name, fnType) registryMutex.Lock() builtinRegistry[name] = Builtin{ Name: name, Function: wrapper, Definition: definition, } registryMutex.Unlock() } func RegisterCustomConverter[T any](converter func(vm *goja.Runtime, value T) goja.Value) { var t T typeOf := reflect.TypeOf(t) registryMutex.Lock() wrappedConverter := func(vm *goja.Runtime, value reflect.Value) goja.Value { return converter(vm, value.Interface().(T)) } customConverters[typeOf] = wrappedConverter if typeOf.Kind() == reflect.Pointer { elemType := typeOf.Elem() customConverters[elemType] = func(vm *goja.Runtime, value reflect.Value) goja.Value { if value.IsNil() { return goja.Null() } return converter(vm, value.Interface().(T)) } } registryMutex.Unlock() } 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() valueType := goValue.Type() registryMutex.RLock() converter, ok := customConverters[valueType] registryMutex.RUnlock() if ok { return converter(vm, goValue) } if goValue.Kind() == reflect.Pointer && !goValue.IsNil() { elemType := goValue.Type().Elem() registryMutex.RLock() converter, ok := customConverters[elemType] registryMutex.RUnlock() if ok { return converter(vm, goValue.Elem()) } } 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) 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: 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) } } }