From 503f9c63202c79fadcfcc166e2e2d7f60f676d9d Mon Sep 17 00:00:00 2001 From: symball Date: Sat, 20 Dec 2025 09:57:41 +0800 Subject: [PATCH 1/3] Add support for special case type handling in the binding generation Add time.Time and uuid.UUID handling to start and demonstrate Ad# dress goTypeToJSDocType CodeRabbit issues --- .../binding_conflicting_package_name_test.go | 65 ++++----- .../binding_test/binding_custom_type_test.go | 41 ++++++ .../binding_returned_promises_test.go | 91 +++++-------- .../binding_test/binding_type_alias_test.go | 73 ++++------ v2/internal/binding/generate.go | 128 +++++++++++++----- v2/internal/binding/generate_test.go | 29 +++- 6 files changed, 250 insertions(+), 177 deletions(-) create mode 100644 v2/internal/binding/binding_test/binding_custom_type_test.go diff --git a/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go b/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go index b37334ec3..d6ef1dc85 100644 --- a/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go +++ b/v2/internal/binding/binding_test/binding_conflicting_package_name_test.go @@ -1,19 +1,35 @@ package binding_test import ( - "io/fs" - "os" - "testing" - - "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/float_package" "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/int_package" "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/map_package" "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/uint_package" - "github.com/wailsapp/wails/v2/internal/logger" ) -const expectedBindings = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +type HandlerTest struct{} + +func (h *HandlerTest) StartingWithInt(_ int_package.SomeStruct) {} +func (h *HandlerTest) StartingWithFloat(_ float_package.SomeStruct) {} +func (h *HandlerTest) StartingWithUint(_ uint_package.SomeStruct) {} +func (h *HandlerTest) StartingWithMap(_ map_package.SomeStruct) {} + +func (h *HandlerTest) Get() HandlerTest { + return *h +} + +var ConflictingPackageNameTest = BindingTest{ + name: "ConflictingPackageNameTest", + structs: []interface{}{ + &HandlerTest{}, + &float_package.SomeStruct{}, + &int_package.SomeStruct{}, + &map_package.SomeStruct{}, + &uint_package.SomeStruct{}, + }, + exemptions: nil, + shouldError: false, + want: `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT import {float_package} from '../models'; import {int_package} from '../models'; @@ -27,38 +43,5 @@ export function StartingWithInt(arg1:int_package.SomeStruct):Promise; export function StartingWithMap(arg1:map_package.SomeStruct):Promise; export function StartingWithUint(arg1:uint_package.SomeStruct):Promise; -` - -type HandlerTest struct{} - -func (h *HandlerTest) StartingWithInt(_ int_package.SomeStruct) {} -func (h *HandlerTest) StartingWithFloat(_ float_package.SomeStruct) {} -func (h *HandlerTest) StartingWithUint(_ uint_package.SomeStruct) {} -func (h *HandlerTest) StartingWithMap(_ map_package.SomeStruct) {} - -func TestConflictingPackageName(t *testing.T) { - // given - generationDir := t.TempDir() - - // setup - testLogger := &logger.Logger{} - b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false, []interface{}{}) - - // then - err := b.GenerateGoBindings(generationDir) - if err != nil { - t.Fatalf("could not generate the Go bindings: %v", err) - } - - // then - rawGeneratedBindings, err := fs.ReadFile(os.DirFS(generationDir), "binding_test/HandlerTest.d.ts") - if err != nil { - t.Fatalf("could not read the generated bindings: %v", err) - } - - // then - generatedBindings := string(rawGeneratedBindings) - if generatedBindings != expectedBindings { - t.Fatalf("the generated bindings does not match the expected ones.\nWanted:\n%s\n\nGot:\n%s", expectedBindings, generatedBindings) - } +`, } diff --git a/v2/internal/binding/binding_test/binding_custom_type_test.go b/v2/internal/binding/binding_test/binding_custom_type_test.go new file mode 100644 index 000000000..88e3fb74c --- /dev/null +++ b/v2/internal/binding/binding_test/binding_custom_type_test.go @@ -0,0 +1,41 @@ +package binding_test + +import ( + "time" + + "github.com/google/uuid" +) + +type CustomTypeStruct struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"createdAt"` +} + +func (s CustomTypeStruct) Get() CustomTypeStruct { + return s +} + +var CustomTypeGenerationTest = BindingTest{ + name: "CustomTypeGenerationTest", + structs: []interface{}{ + &CustomTypeStruct{}, + }, + exemptions: nil, + shouldError: false, + want: ` +export namespace binding_test { + export class CustomTypeStruct { + id: string; + createdAt: string; + static createFrom(source: any = {}) { + return new CustomTypeStruct(source); + } + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.createdAt = source["createdAt"]; + } + } +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_returned_promises_test.go b/v2/internal/binding/binding_test/binding_returned_promises_test.go index 94941d0a3..39b5bc1c4 100644 --- a/v2/internal/binding/binding_test/binding_returned_promises_test.go +++ b/v2/internal/binding/binding_test/binding_returned_promises_test.go @@ -1,15 +1,41 @@ package binding_test -import ( - "io/fs" - "os" - "testing" +// Define the Go types to be tested +type PromisesTest struct{} +type PromisesTestReturnStruct struct{} - "github.com/wailsapp/wails/v2/internal/binding" - "github.com/wailsapp/wails/v2/internal/logger" -) +// Methods returning different types to exercise promise handling +func (h *PromisesTest) NoReturn(_ string) {} +func (h *PromisesTest) ErrorReturn(_ int) error { return nil } +func (h *PromisesTest) SingleReturn(_ interface{}) int { return 0 } +func (h *PromisesTest) SingleReturnStruct(_ interface{}) PromisesTestReturnStruct { + return PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnStructPointer(_ interface{}) *PromisesTestReturnStruct { + return &PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnStructSlice(_ interface{}) []PromisesTestReturnStruct { + return []PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnStructPointerSlice(_ interface{}) []*PromisesTestReturnStruct { + return []*PromisesTestReturnStruct{} +} +func (h *PromisesTest) SingleReturnWithError(_ int) (string, error) { return "", nil } +func (h *PromisesTest) TwoReturn(_ interface{}) (string, int) { return "", 0 } -const expectedPromiseBindings = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// Optional Get() method for consistency with other tests +func (h *PromisesTest) Get() PromisesTest { return *h } + +// The test definition recognized by TestBindings_GenerateModels +var PromisesBindingTest = BindingTest{ + name: "PromisesBindingTest", + structs: []interface{}{ + &PromisesTest{}, + &PromisesTestReturnStruct{}, + }, + exemptions: nil, + shouldError: false, + want: `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT import {binding_test} from '../models'; @@ -30,52 +56,5 @@ export function SingleReturnStructSlice(arg1:any):Promise; export function TwoReturn(arg1:any):Promise; -` - -type PromisesTest struct{} -type PromisesTestReturnStruct struct{} - -func (h *PromisesTest) NoReturn(_ string) {} -func (h *PromisesTest) ErrorReturn(_ int) error { return nil } -func (h *PromisesTest) SingleReturn(_ interface{}) int { return 0 } -func (h *PromisesTest) SingleReturnStructPointer(_ interface{}) *PromisesTestReturnStruct { - return &PromisesTestReturnStruct{} -} -func (h *PromisesTest) SingleReturnStruct(_ interface{}) PromisesTestReturnStruct { - return PromisesTestReturnStruct{} -} -func (h *PromisesTest) SingleReturnStructSlice(_ interface{}) []PromisesTestReturnStruct { - return []PromisesTestReturnStruct{} -} -func (h *PromisesTest) SingleReturnStructPointerSlice(_ interface{}) []*PromisesTestReturnStruct { - return []*PromisesTestReturnStruct{} -} -func (h *PromisesTest) SingleReturnWithError(_ int) (string, error) { return "", nil } -func (h *PromisesTest) TwoReturn(_ interface{}) (string, int) { return "", 0 } - -func TestPromises(t *testing.T) { - // given - generationDir := t.TempDir() - - // setup - testLogger := &logger.Logger{} - b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false, []interface{}{}) - - // then - err := b.GenerateGoBindings(generationDir) - if err != nil { - t.Fatalf("could not generate the Go bindings: %v", err) - } - - // then - rawGeneratedBindings, err := fs.ReadFile(os.DirFS(generationDir), "binding_test/PromisesTest.d.ts") - if err != nil { - t.Fatalf("could not read the generated bindings: %v", err) - } - - // then - generatedBindings := string(rawGeneratedBindings) - if generatedBindings != expectedPromiseBindings { - t.Fatalf("the generated bindings does not match the expected ones.\nWanted:\n%s\n\nGot:\n%s", expectedPromiseBindings, generatedBindings) - } +`, } diff --git a/v2/internal/binding/binding_test/binding_type_alias_test.go b/v2/internal/binding/binding_test/binding_type_alias_test.go index 90b009c5f..b99b358e9 100644 --- a/v2/internal/binding/binding_test/binding_type_alias_test.go +++ b/v2/internal/binding/binding_test/binding_type_alias_test.go @@ -2,15 +2,36 @@ package binding_test import ( "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import/int_package" - "io/fs" - "os" - "testing" - - "github.com/wailsapp/wails/v2/internal/binding" - "github.com/wailsapp/wails/v2/internal/logger" ) -const expectedTypeAliasBindings = `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +type AliasTest struct{} +type MapAlias map[string]string + +func (h *AliasTest) Map() map[string]string { return nil } +func (h *AliasTest) MapAlias() MapAlias { return nil } +func (h *AliasTest) MapWithImportedStructValue() map[string]int_package.SomeStruct { + return nil +} +func (h *AliasTest) Slice() []string { return nil } +func (h *AliasTest) SliceImportedStruct() []int_package.SomeStruct { + return nil +} + +// Optional, to match other canonical binding tests +func (h *AliasTest) Get() AliasTest { + return *h +} + +var TypeAliasTest = BindingTest{ + name: "TypeAliasTest", + structs: []interface{}{ + &AliasTest{}, + &MapAlias{}, + &int_package.SomeStruct{}, + }, + exemptions: nil, + shouldError: false, + want: `// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT import {binding_test} from '../models'; import {int_package} from '../models'; @@ -24,41 +45,5 @@ export function MapWithImportedStructValue():Promise>; export function SliceImportedStruct():Promise>; -` - -type AliasTest struct{} -type MapAlias map[string]string - -func (h *AliasTest) Map() map[string]string { return nil } -func (h *AliasTest) MapAlias() MapAlias { return nil } -func (h *AliasTest) MapWithImportedStructValue() map[string]int_package.SomeStruct { return nil } -func (h *AliasTest) Slice() []string { return nil } -func (h *AliasTest) SliceImportedStruct() []int_package.SomeStruct { return nil } - -func TestAliases(t *testing.T) { - // given - generationDir := t.TempDir() - - // setup - testLogger := &logger.Logger{} - b := binding.NewBindings(testLogger, []interface{}{&AliasTest{}}, []interface{}{}, false, []interface{}{}) - - // then - err := b.GenerateGoBindings(generationDir) - if err != nil { - t.Fatalf("could not generate the Go bindings: %v", err) - } - - // then - rawGeneratedBindings, err := fs.ReadFile(os.DirFS(generationDir), "binding_test/AliasTest.d.ts") - if err != nil { - t.Fatalf("could not read the generated bindings: %v", err) - } - - // then - generatedBindings := string(rawGeneratedBindings) - if generatedBindings != expectedTypeAliasBindings { - t.Fatalf("the generated bindings does not match the expected ones.\nWanted:\n%s\n\nGot:\n%s", expectedTypeAliasBindings, - generatedBindings) - } +`, } diff --git a/v2/internal/binding/generate.go b/v2/internal/binding/generate.go index 77edc983d..0e6ed9423 100644 --- a/v2/internal/binding/generate.go +++ b/v2/internal/binding/generate.go @@ -145,9 +145,19 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { return nil } +var customTypeMap = map[string]string{ + "time.Time": "string", + "uuid.UUID": "string", +} + func fullyQualifiedName(packageName string, typeName string) string { + fullType := typeName if len(packageName) > 0 { - return packageName + "." + typeName + fullType = packageName + "." + typeName + } + + if tsType, ok := customTypeMap[fullType]; ok { + return tsType } switch true { @@ -173,6 +183,7 @@ func fullyQualifiedName(packageName string, typeName string) string { var ( jsVariableUnsafeChars = regexp.MustCompile(`[^A-Za-z0-9_]`) + genericTypePattern = regexp.MustCompile(`^(.*)\[(.*)\]$`) ) func arrayifyValue(valueArray string, valueType string) string { @@ -190,52 +201,103 @@ func arrayifyValue(valueArray string, valueType string) string { return "Array<" + valueType + ">" } -func goTypeToJSDocType(input string, importNamespaces *slicer.StringSlicer) string { - matches := mapRegex.FindStringSubmatch(input) - keyPackage := matches[keyPackageIndex] - keyType := matches[keyTypeIndex] - valueArray := matches[valueArrayIndex] - valuePackage := matches[valuePackageIndex] - valueType := matches[valueTypeIndex] - // fmt.Printf("input=%s, keyPackage=%s, keyType=%s, valueArray=%s, valuePackage=%s, valueType=%s\n", - // input, - // keyPackage, - // keyType, - // valueArray, - // valuePackage, - // valueType) +// goTypeToJSDocType converts a Go type string to a JSDoc/TypeScript type string. +func goTypeToJSDocType(goType string) string { + goType = strings.TrimSpace(goType) - // byte array is special case - if valueArray == "[]" && valueType == "byte" { + // Handle pointers + for strings.HasPrefix(goType, "*") { + goType = strings.TrimPrefix(goType, "*") + goType = strings.TrimSpace(goType) + } + + // Special-case []byte -> string + if goType == "[]byte" { return "string" } - // if any packages, make sure they're saved - if len(keyPackage) > 0 { - importNamespaces.Add(keyPackage) + // Handle arrays/slices in legacy "Array<>" style + if strings.HasPrefix(goType, "[]") { + elemType := strings.TrimPrefix(goType, "[]") + elemJS := goTypeToJSDocType(elemType) + return fmt.Sprintf("Array<%s>", elemJS) } - if len(valuePackage) > 0 { - importNamespaces.Add(valuePackage) + // Handle maps with bracket-depth logic + if strings.HasPrefix(goType, "map[") { + keyStart := len("map[") + depth := 1 + keyEnd := -1 + for i, r := range goType[keyStart:] { + switch r { + case '[': + depth++ + case ']': + depth-- + if depth == 0 { + keyEnd = keyStart + i + break + } + } + if keyEnd != -1 { + break + } + } + if keyEnd == -1 || keyEnd+1 >= len(goType) { + return "any" + } + keyType := goType[keyStart:keyEnd] + valType := strings.TrimSpace(goType[keyEnd+1:]) + return fmt.Sprintf("Record<%s, %s>", goTypeToJSDocType(keyType), goTypeToJSDocType(valType)) } - key := fullyQualifiedName(keyPackage, keyType) - var value string - if strings.HasPrefix(valueType, "map") { - value = goTypeToJSDocType(valueType, importNamespaces) - } else { - value = fullyQualifiedName(valuePackage, valueType) + // Generic type pattern (legacy underscore flattening) + if matches := genericTypePattern.FindStringSubmatch(goType); len(matches) == 3 { + base := matches[1] + inner := jsVariableUnsafeChars.ReplaceAllLiteralString(matches[2], "_") + inner = strings.TrimPrefix(inner, "_") // remove leading underscore if any + return fmt.Sprintf("%s_%s_", base, inner) } - if len(key) > 0 { - return fmt.Sprintf("Record<%s, %s>", key, arrayifyValue(valueArray, value)) + // If fully qualified and not in customTypeMap, use name unchanged + if strings.Contains(goType, ".") { + // Check if this is a custom type that should be mapped + if tsType, ok := customTypeMap[goType]; ok { + return tsType + } + + // Normal fully-qualified name (main.SomeType) + if !strings.Contains(goType, "[") { + return goType + } } - return arrayifyValue(valueArray, value) + // customTypeMap lookup for base name + if mapped, ok := customTypeMap[goType]; ok { + return mapped + } + + // Basic mappings + switch goType { + case "string": + return "string" + case "error": + return "Error" + case "bool": + return "boolean" + case "interface{}": + return "any" + case "int", "int8", "int16", "int32", "int64", + "uint", "uint8", "uint16", "uint32", "uint64", + "float32", "float64": + return "number" + } + + return "any" } -func goTypeToTypescriptType(input string, importNamespaces *slicer.StringSlicer) string { - return goTypeToJSDocType(input, importNamespaces) +func goTypeToTypescriptType(input string, _ *slicer.StringSlicer) string { + return goTypeToJSDocType(input) } func entityFullReturnType(input, prefix, suffix string, importNamespaces *slicer.StringSlicer) string { diff --git a/v2/internal/binding/generate_test.go b/v2/internal/binding/generate_test.go index 26d7c70df..38472479f 100644 --- a/v2/internal/binding/generate_test.go +++ b/v2/internal/binding/generate_test.go @@ -3,7 +3,6 @@ package binding import ( "testing" - "github.com/leaanthony/slicer" "github.com/stretchr/testify/assert" "github.com/wailsapp/wails/v2/internal/logger" ) @@ -138,11 +137,35 @@ func Test_goTypeToJSDocType(t *testing.T) { input: "main.ListData[*net/http.Request]", want: "main.ListData_net_http_Request_", }, + { + name: "time.Time mapped to string", + input: "time.Time", + want: "string", + }, + { + name: "uuid.UUID mapped to string", + input: "uuid.UUID", + want: "string", + }, + { + name: "slice of time.Time", + input: "[]time.Time", + want: "Array", + }, + { + name: "map of uuid.UUID", + input: "map[string]uuid.UUID", + want: "Record", + }, + { + name: "pointer to time.Time", + input: "*time.Time", + want: "string", + }, } - var importNamespaces slicer.StringSlicer for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := goTypeToJSDocType(tt.input, &importNamespaces); got != tt.want { + if got := goTypeToJSDocType(tt.input); got != tt.want { t.Errorf("goTypeToJSDocType() = %v, want %v", got, tt.want) } }) From cc8d112772b129afc300da7deb23674e4df185ef Mon Sep 17 00:00:00 2001 From: symball Date: Sat, 20 Dec 2025 11:35:51 +0800 Subject: [PATCH 2/3] Add small comment about matching inner brackets --- v2/internal/binding/generate.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v2/internal/binding/generate.go b/v2/internal/binding/generate.go index 0e6ed9423..7e1a0d909 100644 --- a/v2/internal/binding/generate.go +++ b/v2/internal/binding/generate.go @@ -252,6 +252,8 @@ func goTypeToJSDocType(goType string) string { } // Generic type pattern (legacy underscore flattening) + // Note: This pattern matches the innermost brackets for nested generics. + // Nested generics like Outer[Inner[T]] are not currently supported. if matches := genericTypePattern.FindStringSubmatch(goType); len(matches) == 3 { base := matches[1] inner := jsVariableUnsafeChars.ReplaceAllLiteralString(matches[2], "_") From 67c3f3a617a62a1e90ea3d44e7a2ac61cad50b79 Mon Sep 17 00:00:00 2001 From: symball Date: Sun, 21 Dec 2025 07:33:32 +0800 Subject: [PATCH 3/3] Add url.URL type cast to string in custom bindings map Add url.URL test --- v2/internal/binding/generate.go | 1 + v2/internal/binding/generate_test.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/v2/internal/binding/generate.go b/v2/internal/binding/generate.go index 7e1a0d909..bac2fd46d 100644 --- a/v2/internal/binding/generate.go +++ b/v2/internal/binding/generate.go @@ -147,6 +147,7 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error { var customTypeMap = map[string]string{ "time.Time": "string", + "url.URL": "string", "uuid.UUID": "string", } diff --git a/v2/internal/binding/generate_test.go b/v2/internal/binding/generate_test.go index 38472479f..f08ae063b 100644 --- a/v2/internal/binding/generate_test.go +++ b/v2/internal/binding/generate_test.go @@ -142,6 +142,11 @@ func Test_goTypeToJSDocType(t *testing.T) { input: "time.Time", want: "string", }, + { + name: "url.URL mapped to string", + input: "url.URL", + want: "string", + }, { name: "uuid.UUID mapped to string", input: "uuid.UUID",