diff --git a/v2/internal/binding/binding.go b/v2/internal/binding/binding.go index 4908635e1..b7bf07ae0 100644 --- a/v2/internal/binding/binding.go +++ b/v2/internal/binding/binding.go @@ -123,7 +123,15 @@ func (b *Bindings) GenerateModels() ([]byte, error) { // if we have enums for this package, add them as well var enums, enumsExist = b.enumsToGenerateTS[packageName] if enumsExist { - for enumName, enum := range enums { + // Sort the enum names first to make the output deterministic + sortedEnumNames := make([]string, 0, len(enums)) + for enumName := range enums { + sortedEnumNames = append(sortedEnumNames, enumName) + } + sort.Strings(sortedEnumNames) + + for _, enumName := range sortedEnumNames { + enum := enums[enumName] fqemumname := packageName + "." + enumName if seen.Contains(fqemumname) { continue @@ -172,7 +180,7 @@ func (b *Bindings) GenerateModels() ([]byte, error) { } // Sort the package names first to make the output deterministic - sortedPackageNames := make([]string, 0) + sortedPackageNames := make([]string, 0, len(models)) for packageName := range models { sortedPackageNames = append(sortedPackageNames, packageName) } diff --git a/v2/internal/binding/binding_test/binding_enum_ordering_test.go b/v2/internal/binding/binding_test/binding_enum_ordering_test.go new file mode 100644 index 000000000..0939535ec --- /dev/null +++ b/v2/internal/binding/binding_test/binding_enum_ordering_test.go @@ -0,0 +1,271 @@ +package binding_test + +// Test for PR #4664: Fix generated enums ordering +// This test ensures that enum output is deterministic regardless of map iteration order + +// ZFirstEnum - named with Z prefix to test alphabetical sorting +type ZFirstEnum int + +const ( + ZFirstEnumValue1 ZFirstEnum = iota + ZFirstEnumValue2 +) + +var AllZFirstEnumValues = []struct { + Value ZFirstEnum + TSName string +}{ + {ZFirstEnumValue1, "ZValue1"}, + {ZFirstEnumValue2, "ZValue2"}, +} + +// ASecondEnum - named with A prefix to test alphabetical sorting +type ASecondEnum int + +const ( + ASecondEnumValue1 ASecondEnum = iota + ASecondEnumValue2 +) + +var AllASecondEnumValues = []struct { + Value ASecondEnum + TSName string +}{ + {ASecondEnumValue1, "AValue1"}, + {ASecondEnumValue2, "AValue2"}, +} + +// MMiddleEnum - named with M prefix to test alphabetical sorting +type MMiddleEnum int + +const ( + MMiddleEnumValue1 MMiddleEnum = iota + MMiddleEnumValue2 +) + +var AllMMiddleEnumValues = []struct { + Value MMiddleEnum + TSName string +}{ + {MMiddleEnumValue1, "MValue1"}, + {MMiddleEnumValue2, "MValue2"}, +} + +type EntityWithMultipleEnums struct { + Name string `json:"name"` + EnumZ ZFirstEnum `json:"enumZ"` + EnumA ASecondEnum `json:"enumA"` + EnumM MMiddleEnum `json:"enumM"` +} + +func (e EntityWithMultipleEnums) Get() EntityWithMultipleEnums { + return e +} + +// EnumOrderingTest tests that multiple enums in the same package are output +// in alphabetical order by enum name. Before PR #4664, the order was +// non-deterministic due to Go map iteration order. +var EnumOrderingTest = BindingTest{ + name: "EnumOrderingTest", + structs: []interface{}{ + &EntityWithMultipleEnums{}, + }, + enums: []interface{}{ + // Intentionally add enums in non-alphabetical order + AllZFirstEnumValues, + AllASecondEnumValues, + AllMMiddleEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enums in alphabetical order: ASecondEnum, MMiddleEnum, ZFirstEnum + want: `export namespace binding_test { + + export enum ASecondEnum { + AValue1 = 0, + AValue2 = 1, + } + export enum MMiddleEnum { + MValue1 = 0, + MValue2 = 1, + } + export enum ZFirstEnum { + ZValue1 = 0, + ZValue2 = 1, + } + export class EntityWithMultipleEnums { + name: string; + enumZ: ZFirstEnum; + enumA: ASecondEnum; + enumM: MMiddleEnum; + + static createFrom(source: any = {}) { + return new EntityWithMultipleEnums(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enumZ = source["enumZ"]; + this.enumA = source["enumA"]; + this.enumM = source["enumM"]; + } + } + +} +`, +} + +// EnumElementOrderingEnum tests sorting of enum elements by TSName +type EnumElementOrderingEnum string + +const ( + EnumElementZ EnumElementOrderingEnum = "z_value" + EnumElementA EnumElementOrderingEnum = "a_value" + EnumElementM EnumElementOrderingEnum = "m_value" +) + +// AllEnumElementOrderingValues intentionally lists values out of alphabetical order +// to test that AddEnum sorts them +var AllEnumElementOrderingValues = []struct { + Value EnumElementOrderingEnum + TSName string +}{ + {EnumElementZ, "Zebra"}, + {EnumElementA, "Apple"}, + {EnumElementM, "Mango"}, +} + +type EntityWithUnorderedEnumElements struct { + Name string `json:"name"` + Enum EnumElementOrderingEnum `json:"enum"` +} + +func (e EntityWithUnorderedEnumElements) Get() EntityWithUnorderedEnumElements { + return e +} + +// EnumElementOrderingTest tests that enum elements are sorted alphabetically +// by their TSName within an enum. Before PR #4664, elements appeared in the +// order they were added, which could be arbitrary. +var EnumElementOrderingTest = BindingTest{ + name: "EnumElementOrderingTest", + structs: []interface{}{ + &EntityWithUnorderedEnumElements{}, + }, + enums: []interface{}{ + AllEnumElementOrderingValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enum elements sorted: Apple, Mango, Zebra + want: `export namespace binding_test { + + export enum EnumElementOrderingEnum { + Apple = "a_value", + Mango = "m_value", + Zebra = "z_value", + } + export class EntityWithUnorderedEnumElements { + name: string; + enum: EnumElementOrderingEnum; + + static createFrom(source: any = {}) { + return new EntityWithUnorderedEnumElements(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + +} +`, +} + +// TSNameEnumElementOrdering tests sorting with TSName() method enum +type TSNameEnumElementOrdering string + +const ( + TSNameEnumZ TSNameEnumElementOrdering = "z_value" + TSNameEnumA TSNameEnumElementOrdering = "a_value" + TSNameEnumM TSNameEnumElementOrdering = "m_value" +) + +func (v TSNameEnumElementOrdering) TSName() string { + switch v { + case TSNameEnumZ: + return "Zebra" + case TSNameEnumA: + return "Apple" + case TSNameEnumM: + return "Mango" + default: + return "Unknown" + } +} + +// AllTSNameEnumValues intentionally out of order +var AllTSNameEnumValues = []TSNameEnumElementOrdering{TSNameEnumZ, TSNameEnumA, TSNameEnumM} + +type EntityWithTSNameEnumOrdering struct { + Name string `json:"name"` + Enum TSNameEnumElementOrdering `json:"enum"` +} + +func (e EntityWithTSNameEnumOrdering) Get() EntityWithTSNameEnumOrdering { + return e +} + +// TSNameEnumElementOrderingTest tests that enums using TSName() method +// also have their elements sorted alphabetically by the TSName. +var TSNameEnumElementOrderingTest = BindingTest{ + name: "TSNameEnumElementOrderingTest", + structs: []interface{}{ + &EntityWithTSNameEnumOrdering{}, + }, + enums: []interface{}{ + AllTSNameEnumValues, + }, + exemptions: nil, + shouldError: false, + TsGenerationOptionsTest: TsGenerationOptionsTest{ + TsPrefix: "", + TsSuffix: "", + }, + // Expected output should have enum elements sorted: Apple, Mango, Zebra + want: `export namespace binding_test { + + export enum TSNameEnumElementOrdering { + Apple = "a_value", + Mango = "m_value", + Zebra = "z_value", + } + export class EntityWithTSNameEnumOrdering { + name: string; + enum: TSNameEnumElementOrdering; + + static createFrom(source: any = {}) { + return new EntityWithTSNameEnumOrdering(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.enum = source["enum"]; + } + } + +} +`, +} diff --git a/v2/internal/binding/binding_test/binding_test.go b/v2/internal/binding/binding_test/binding_test.go index d358d8b0f..41f0618ce 100644 --- a/v2/internal/binding/binding_test/binding_test.go +++ b/v2/internal/binding/binding_test/binding_test.go @@ -55,6 +55,10 @@ func TestBindings_GenerateModels(t *testing.T) { Generics2Test, IgnoredTest, DeepElementsTest, + // PR #4664: Enum ordering tests + EnumOrderingTest, + EnumElementOrderingTest, + TSNameEnumElementOrderingTest, } testLogger := &logger.Logger{} diff --git a/v2/internal/typescriptify/typescriptify.go b/v2/internal/typescriptify/typescriptify.go index 216fba820..e732c5976 100644 --- a/v2/internal/typescriptify/typescriptify.go +++ b/v2/internal/typescriptify/typescriptify.go @@ -2,6 +2,7 @@ package typescriptify import ( "bufio" + "cmp" "fmt" "io" "log" @@ -9,6 +10,7 @@ import ( "path" "reflect" "regexp" + "slices" "strings" "time" @@ -372,6 +374,9 @@ func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify { elements = append(elements, el) } + slices.SortFunc(elements, func(a, b enumElement) int { + return cmp.Compare(a.name, b.name) + }) ty := reflect.TypeOf(elements[0].value) t.enums[ty] = elements t.enumTypes = append(t.enumTypes, EnumType{Type: ty}) @@ -516,9 +521,6 @@ func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error if _, err := f.WriteString(converted); err != nil { return err } - if err != nil { - return err - } return nil } diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index fba54f570..3c4b1822b 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed C compilation error in onWayland on Linux due to declaration after label [#4446](https://github.com/wailsapp/wails/pull/4446) by [@jaesung9507](https://github.com/jaesung9507) - Use computed style when adding 'wails-drop-target-active' [PR](https://github.com/wailsapp/wails/pull/4420) by [@riannucci](https://github.com/riannucci) - Fixed panic when adding menuroles on Linux [#4558](https://github.com/wailsapp/wails/pull/4558) by [@jaesung9507](https://github.com/jaesung9507) +- Fixed generated enums ordering [#4664](https://github.com/wailsapp/wails/pull/4664) by [@rprtr258](https://github.com/rprtr258). - Fixed Discord badge in README by @sharkmu in [PR](https://github.com/wailsapp/wails/pull/4626) - Fixed HTML DnD by @leaanthony