package builtin import ( "fmt" "reflect" "strings" "sync" "github.com/dop251/goja" ) type Builtin struct { Name string Function func(*goja.Runtime) func(goja.FunctionCall) goja.Value Definition string isPromise bool } type EmptyArgs struct{} type RegisterOption func(*Builtin) error func WithPromise() RegisterOption { return func(b *Builtin) error { b.isPromise = true return nil } } var ( builtinRegistry = make(map[string]Builtin) registryMutex sync.RWMutex ) func RegisterBuiltin[T any, R any](name string, fn any, opts ...RegisterOption) { var zeroT T tType := reflect.TypeOf(zeroT) if tType.Kind() != reflect.Struct { panic(fmt.Sprintf("builtin %s: argument must be a struct type, got %v", name, tType)) } fnType := reflect.TypeOf(fn) isPromise := false for _, opt := range opts { if opt != nil { isPromise = true break } } wrapper := createWrapper[T](fn, fnType, isPromise) registryMutex.Lock() b := Builtin{ Name: name, Function: wrapper, Definition: generateTypeScriptDefinition(name, tType, fnType, isPromise), } for _, opt := range opts { if opt != nil { if err := opt(&b); err != nil { panic(fmt.Sprintf("builtin %s: option error: %v", name, err)) } } } builtinRegistry[name] = b registryMutex.Unlock() } func createWrapper[T any](fn any, fnType reflect.Type, isPromise bool) func(*goja.Runtime) func(goja.FunctionCall) goja.Value { return func(vm *goja.Runtime) func(goja.FunctionCall) goja.Value { return func(call goja.FunctionCall) goja.Value { var args T argsValue := reflect.ValueOf(&args).Elem() for i := 0; i < argsValue.NumField() && i < len(call.Arguments); i++ { jsArg := call.Arguments[i] field := argsValue.Field(i) if goja.IsUndefined(jsArg) || goja.IsNull(jsArg) { if field.Kind() == reflect.Pointer { continue } } converted, err := convertJSValueToGo(vm, jsArg, field.Type()) if err != nil { panic(fmt.Sprintf("argument %d (%s): %v", i, getFieldName(argsValue.Type().Field(i)), err)) } if converted != nil { field.Set(reflect.ValueOf(converted)) } } if defaults, ok := any(args).(interface{ Defaults() T }); ok { args = defaults.Defaults() } fnValue := reflect.ValueOf(fn) firstParamType := fnType.In(0) argValue := reflect.ValueOf(args).Convert(firstParamType) results := fnValue.Call([]reflect.Value{argValue}) if len(results) == 0 { return goja.Undefined() } if err, isError := results[len(results)-1].Interface().(error); isError { if err != nil { if isPromise { return createRejectedPromise(vm, err) } panic(err) } if len(results) == 1 { if isPromise { return createResolvedPromise(vm) } return goja.Undefined() } if isPromise { return createResolvedPromise(vm, results[0]) } return convertGoValueToJS(vm, results[0]) } if isPromise { return createResolvedPromise(vm, results[0]) } return convertGoValueToJS(vm, results[0]) } } } func createResolvedPromise(vm *goja.Runtime, value ...reflect.Value) goja.Value { promise, resolve, _ := vm.NewPromise() go func() { if len(value) > 0 { jsValue := convertGoValueToJS(vm, value[0]) _ = resolve(jsValue) } else { _ = resolve(goja.Undefined()) } }() return vm.ToValue(promise) } func createRejectedPromise(vm *goja.Runtime, err error) goja.Value { promise, _, reject := vm.NewPromise() go func() { _ = reject(vm.ToValue(err.Error())) }() return vm.ToValue(promise) } func convertJSValueToGo(vm *goja.Runtime, jsValue goja.Value, targetType reflect.Type) (any, error) { if goja.IsNull(jsValue) { if targetType.Kind() == reflect.Pointer || targetType.Kind() == reflect.Map { return nil, nil } return nil, fmt.Errorf("cannot convert null/undefined to %v", targetType) } if goja.IsUndefined(jsValue) { if targetType.Kind() == reflect.Pointer || targetType.Kind() == reflect.Map { 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 goja.IsUndefined(jsValue) || goja.IsNull(jsValue) { return nil, nil } if targetType.Key().Kind() == reflect.String { obj := jsValue.ToObject(vm) if obj == nil { return nil, fmt.Errorf("not an object") } if targetType.Elem().Kind() == reflect.Interface { result := make(map[string]any) for _, key := range obj.Keys() { result[key] = obj.Get(key).Export() } return result, nil } else if targetType.Elem().Kind() == reflect.String { result := make(map[string]string) for _, key := range obj.Keys() { v := obj.Get(key) result[key] = v.String() } return result, nil } } return nil, fmt.Errorf("unsupported map type: %v", targetType) case reflect.Struct: obj := jsValue.ToObject(vm) if obj == nil { return nil, fmt.Errorf("not an object") } result := reflect.New(targetType).Elem() for i := 0; i < targetType.NumField(); i++ { field := targetType.Field(i) fieldName := getFieldName(field) jsField := obj.Get(fieldName) var err error var converted any func() { defer func() { if r := recover(); r != nil { // goja.Value was zero - treat as undefined err = nil converted = nil } }() converted, err = convertJSValueToGo(vm, jsField, field.Type) }() if err != nil { return nil, fmt.Errorf("field %s: %v", fieldName, err) } if converted == nil { if field.Type.Kind() == reflect.Pointer || field.Type.Kind() == reflect.Map { continue } } else { result.Field(i).Set(reflect.ValueOf(converted)) } } return result.Interface(), nil case reflect.Pointer: if goja.IsNull(jsValue) || goja.IsUndefined(jsValue) { return nil, nil } elemType := targetType.Elem() converted, err := convertJSValueToGo(vm, jsValue, elemType) if err != nil { return nil, err } ptr := reflect.New(elemType) ptr.Elem().Set(reflect.ValueOf(converted)) return ptr.Interface(), nil 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, convertGoValueToJS(vm, reflect.ValueOf(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: if goValue.Kind() == reflect.Pointer { if goValue.IsNil() { return goja.Null() } return convertGoValueToJS(vm, goValue.Elem()) } if goValue.Kind() == reflect.Struct { obj := vm.NewObject() for i := 0; i < goValue.NumField(); i++ { field := goValue.Type().Field(i) fieldName := getFieldName(field) _ = obj.Set(fieldName, convertGoValueToJS(vm, goValue.Field(i))) } return obj } return vm.ToValue(v) } } func getFieldName(field reflect.StructField) string { jsonTag := field.Tag.Get("json") if jsonTag != "" && jsonTag != "-" { name, _, _ := strings.Cut(jsonTag, ",") return name } return field.Name } func generateTypeScriptDefinition(name string, argsType reflect.Type, fnType reflect.Type, isPromise bool) string { if argsType.Kind() != reflect.Struct { return "" } var params []string for i := 0; i < argsType.NumField(); i++ { field := argsType.Field(i) fieldName := getFieldName(field) goType := field.Type tsType := goTypeToTSType(goType, goType.Kind() == reflect.Pointer) params = append(params, fmt.Sprintf("%s: %s", fieldName, tsType)) } returnSignature := "any" if fnType.Kind() == reflect.Func && 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), false) } } else { returnSignature = goTypeToTSType(lastType, false) } } if isPromise { returnSignature = fmt.Sprintf("Promise<%s>", returnSignature) } return fmt.Sprintf("declare function %s(%s): %s;", name, strings.Join(params, ", "), returnSignature) } func goTypeToTSType(t reflect.Type, isPointer bool) string { if isPointer { if t.Kind() == reflect.Pointer { return goTypeToTSType(t.Elem(), false) + " | null" } return goTypeToTSType(t, false) + " | null" } 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: return "any" case reflect.Slice: return fmt.Sprintf("%s[]", goTypeToTSType(t.Elem(), false)) case reflect.Map: if t.Key().Kind() == reflect.String && t.Elem().Kind() == reflect.Interface { return "Record" } return "Record" case reflect.Struct: fields := make([]string, 0, t.NumField()) for i := 0; i < t.NumField(); i++ { field := t.Field(i) name := getFieldName(field) tsType := goTypeToTSType(field.Type, field.Type.Kind() == reflect.Pointer) if field.Type.Kind() == reflect.Pointer { tsType = strings.TrimSuffix(tsType, " | null") tsType += "?" } else if strings.Contains(field.Tag.Get("json"), ",omitempty") { tsType += "?" } fields = append(fields, fmt.Sprintf("%s: %s", name, tsType)) } return fmt.Sprintf("{ %s }", strings.Join(fields, "; ")) case reflect.Pointer: if t.Elem().Kind() == reflect.Struct { return goTypeToTSType(t.Elem(), false) } 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 { _ = vm.Set(name, builtin.Function(vm)) } }