This commit is contained in:
Simon 2026-03-01 17:17:53 +08:00 committed by GitHub
commit 8d19dfdecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 259 additions and 178 deletions

View file

@ -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<void>;
export function StartingWithMap(arg1:map_package.SomeStruct):Promise<void>;
export function StartingWithUint(arg1:uint_package.SomeStruct):Promise<void>;
`
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)
}
`,
}

View file

@ -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"];
}
}
}
`,
}

View file

@ -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<Array<binding_test.Pro
export function SingleReturnWithError(arg1:number):Promise<string>;
export function TwoReturn(arg1:any):Promise<string|number>;
`
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)
}
`,
}

View file

@ -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<Record<string, int_package.
export function Slice():Promise<Array<string>>;
export function SliceImportedStruct():Promise<Array<int_package.SomeStruct>>;
`
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)
}
`,
}

View file

@ -145,9 +145,20 @@ func (b *Bindings) GenerateGoBindings(baseDir string) error {
return nil
}
var customTypeMap = map[string]string{
"time.Time": "string",
"url.URL": "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 +184,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 +202,105 @@ 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)
// 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], "_")
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 {

View file

@ -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,40 @@ 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: "url.URL mapped to string",
input: "url.URL",
want: "string",
},
{
name: "uuid.UUID mapped to string",
input: "uuid.UUID",
want: "string",
},
{
name: "slice of time.Time",
input: "[]time.Time",
want: "Array<string>",
},
{
name: "map of uuid.UUID",
input: "map[string]uuid.UUID",
want: "Record<string, string>",
},
{
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)
}
})