5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 07:21:32 +08:00

Add support for interface generation and enums (#3047)

* Add support to output ts models as interfaces

* Add support to generate enums from golang

* cleanup logs

* add missing documentation

* fix package names for enum. Fix processing enums that are in separate packages

* revert golang 1.21

* Fix spelling

* Add support for simplified version of Enum for typescriptify

* update docs

* removed unused logs

* Add tests. Fix imported enums types in models

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
Andrey Pshenkin 2023-11-25 19:50:49 +00:00 committed by GitHub
parent 929dfb4123
commit b9de31e38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 717 additions and 44 deletions

View File

@ -43,10 +43,15 @@ func generateModule(f *flags.GenerateModule) error {
return err return err
} }
if projectConfig.Bindings.TsGeneration.OutputType == "" {
projectConfig.Bindings.TsGeneration.OutputType = "classes"
}
_, err = bindings.GenerateBindings(bindings.Options{ _, err = bindings.GenerateBindings(bindings.Options{
Tags: buildTags, Tags: buildTags,
TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, TsPrefix: projectConfig.Bindings.TsGeneration.Prefix,
TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, TsSuffix: projectConfig.Bindings.TsGeneration.Suffix,
TsOutputType: projectConfig.Bindings.TsGeneration.OutputType,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -31,6 +31,7 @@ func (a *App) Run() error {
var tsPrefixFlag *string var tsPrefixFlag *string
var tsPostfixFlag *string var tsPostfixFlag *string
var tsOutputTypeFlag *string
tsPrefix := os.Getenv("tsprefix") tsPrefix := os.Getenv("tsprefix")
if tsPrefix == "" { if tsPrefix == "" {
@ -42,6 +43,11 @@ func (a *App) Run() error {
tsPostfixFlag = bindingFlags.String("tssuffix", "", "Suffix for generated typescript entities") tsPostfixFlag = bindingFlags.String("tssuffix", "", "Suffix for generated typescript entities")
} }
tsOutputType := os.Getenv("tsoutputtype")
if tsOutputType == "" {
tsOutputTypeFlag = bindingFlags.String("tsoutputtype", "", "Output type for generated typescript entities (classes|interfaces)")
}
_ = bindingFlags.Parse(os.Args[1:]) _ = bindingFlags.Parse(os.Args[1:])
if tsPrefixFlag != nil { if tsPrefixFlag != nil {
tsPrefix = *tsPrefixFlag tsPrefix = *tsPrefixFlag
@ -49,11 +55,15 @@ func (a *App) Run() error {
if tsPostfixFlag != nil { if tsPostfixFlag != nil {
tsSuffix = *tsPostfixFlag tsSuffix = *tsPostfixFlag
} }
if tsOutputTypeFlag != nil {
tsOutputType = *tsOutputTypeFlag
}
appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated()) appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated(), a.options.EnumBind)
appBindings.SetTsPrefix(tsPrefix) appBindings.SetTsPrefix(tsPrefix)
appBindings.SetTsSuffix(tsSuffix) appBindings.SetTsSuffix(tsSuffix)
appBindings.SetOutputType(tsOutputType)
err := generateBindings(appBindings) err := generateBindings(appBindings)
if err != nil { if err != nil {

View File

@ -209,7 +209,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
appoptions.OnDomReady, appoptions.OnDomReady,
appoptions.OnBeforeClose, appoptions.OnBeforeClose,
} }
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false) appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false, appoptions.EnumBind)
eventHandler := runtime.NewEvents(myLogger) eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler) ctx = context.WithValue(ctx, "events", eventHandler)

View File

@ -72,7 +72,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
appoptions.OnDomReady, appoptions.OnDomReady,
appoptions.OnBeforeClose, appoptions.OnBeforeClose,
} }
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated()) appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated(), appoptions.EnumBind)
eventHandler := runtime.NewEvents(myLogger) eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler) ctx = context.WithValue(ctx, "events", eventHandler)
// Attach logger to context // Attach logger to context

View File

@ -23,17 +23,20 @@ type Bindings struct {
exemptions slicer.StringSlicer exemptions slicer.StringSlicer
structsToGenerateTS map[string]map[string]interface{} structsToGenerateTS map[string]map[string]interface{}
enumsToGenerateTS map[string]map[string]interface{}
tsPrefix string tsPrefix string
tsSuffix string tsSuffix string
tsInterface bool
obfuscate bool obfuscate bool
} }
// NewBindings returns a new Bindings object // NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool) *Bindings { func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings {
result := &Bindings{ result := &Bindings{
db: newDB(), db: newDB(),
logger: logger.CustomLogger("Bindings"), logger: logger.CustomLogger("Bindings"),
structsToGenerateTS: make(map[string]map[string]interface{}), structsToGenerateTS: make(map[string]map[string]interface{}),
enumsToGenerateTS: make(map[string]map[string]interface{}),
obfuscate: obfuscate, obfuscate: obfuscate,
} }
@ -47,6 +50,10 @@ func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exem
result.exemptions.Add(name) result.exemptions.Add(name)
} }
for _, enum := range enumsToBind {
result.AddEnumToGenerateTS(enum)
}
// Add the structs to bind // Add the structs to bind
for _, ptr := range structPointersToBind { for _, ptr := range structPointersToBind {
err := result.Add(ptr) err := result.Add(ptr)
@ -88,16 +95,21 @@ func (b *Bindings) ToJSON() (string, error) {
func (b *Bindings) GenerateModels() ([]byte, error) { func (b *Bindings) GenerateModels() ([]byte, error) {
models := map[string]string{} models := map[string]string{}
var seen slicer.StringSlicer var seen slicer.StringSlicer
var seenEnumsPackages slicer.StringSlicer
allStructNames := b.getAllStructNames() allStructNames := b.getAllStructNames()
allStructNames.Sort() allStructNames.Sort()
allEnumNames := b.getAllEnumNames()
allEnumNames.Sort()
for packageName, structsToGenerate := range b.structsToGenerateTS { for packageName, structsToGenerate := range b.structsToGenerateTS {
thisPackageCode := "" thisPackageCode := ""
w := typescriptify.New() w := typescriptify.New()
w.WithPrefix(b.tsPrefix) w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix) w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName w.Namespace = packageName
w.WithBackupDir("") w.WithBackupDir("")
w.KnownStructs = allStructNames w.KnownStructs = allStructNames
w.KnownEnums = allEnumNames
// sort the structs // sort the structs
var structNames []string var structNames []string
for structName := range structsToGenerate { for structName := range structsToGenerate {
@ -112,6 +124,20 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
structInterface := structsToGenerate[structName] structInterface := structsToGenerate[structName]
w.Add(structInterface) w.Add(structInterface)
} }
// if we have enums for this package, add them as well
var enums, enumsExist = b.enumsToGenerateTS[packageName]
if enumsExist {
for enumName, enum := range enums {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
seenEnumsPackages.Add(packageName)
}
str, err := w.Convert(nil) str, err := w.Convert(nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -121,6 +147,35 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
models[packageName] = thisPackageCode models[packageName] = thisPackageCode
} }
// Add outstanding enums to the models that were not in packages with structs
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
if seenEnumsPackages.Contains(packageName) {
continue
}
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
for enumName, enum := range enumsToGenerate {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
models[packageName] = thisPackageCode
}
// Sort the package names first to make the output deterministic // Sort the package names first to make the output deterministic
sortedPackageNames := make([]string, 0) sortedPackageNames := make([]string, 0)
for packageName := range models { for packageName := range models {
@ -163,6 +218,39 @@ func (b *Bindings) WriteModels(modelsDir string) error {
return nil return nil
} }
func (b *Bindings) AddEnumToGenerateTS(e interface{}) {
enumType := reflect.TypeOf(e)
var packageName string
var enumName string
// enums should be represented as array of all possible values
if hasElements(enumType) {
enum := enumType.Elem()
// simple enum represented by struct with Value/TSName fields
if enum.Kind() == reflect.Struct {
_, tsNamePresented := enum.FieldByName("TSName")
enumT, valuePresented := enum.FieldByName("Value")
if tsNamePresented && valuePresented {
packageName = getPackageName(enumT.Type.String())
enumName = enumT.Type.Name()
} else {
return
}
// otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname
} else {
packageName = getPackageName(enumType.Elem().String())
enumName = enumType.Elem().Name()
}
if b.enumsToGenerateTS[packageName] == nil {
b.enumsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.enumsToGenerateTS[packageName][enumName] != nil {
return
}
b.enumsToGenerateTS[packageName][enumName] = e
}
}
func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) { func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) {
if b.structsToGenerateTS[packageName] == nil { if b.structsToGenerateTS[packageName] == nil {
b.structsToGenerateTS[packageName] = make(map[string]interface{}) b.structsToGenerateTS[packageName] = make(map[string]interface{})
@ -231,6 +319,13 @@ func (b *Bindings) SetTsSuffix(postfix string) *Bindings {
return b return b
} }
func (b *Bindings) SetOutputType(outputType string) *Bindings {
if outputType == "interfaces" {
b.tsInterface = true
}
return b
}
func (b *Bindings) getAllStructNames() *slicer.StringSlicer { func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
var result slicer.StringSlicer var result slicer.StringSlicer
for packageName, structsToGenerate := range b.structsToGenerateTS { for packageName, structsToGenerate := range b.structsToGenerateTS {
@ -241,6 +336,16 @@ func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
return &result return &result
} }
func (b *Bindings) getAllEnumNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
for enumName := range enumsToGenerate {
result.Add(packageName + "." + enumName)
}
}
return &result
}
func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool { func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool {
for i := 0; i < typeOf.NumField(); i++ { for i := 0; i < typeOf.NumField(); i++ {
jsonFieldName := "" jsonFieldName := ""

View File

@ -42,7 +42,7 @@ func TestConflictingPackageName(t *testing.T) {
// setup // setup
testLogger := &logger.Logger{} testLogger := &logger.Logger{}
b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false) b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false, []interface{}{})
// then // then
err := b.GenerateGoBindings(generationDir) err := b.GenerateGoBindings(generationDir)

View File

@ -0,0 +1,50 @@
package binding_test
import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import"
type ImportedEnumStruct struct {
EnumValue binding_test_import.ImportedEnum `json:"EnumValue"`
}
func (s ImportedEnumStruct) Get() ImportedEnumStruct {
return s
}
var ImportedEnumTest = BindingTest{
name: "ImportedEnum",
structs: []interface{}{
&ImportedEnumStruct{},
},
enums: []interface{}{
binding_test_import.AllImportedEnumValues,
},
exemptions: nil,
shouldError: false,
want: `export namespace binding_test {
export class ImportedEnumStruct {
EnumValue: binding_test_import.ImportedEnum;
static createFrom(source: any = {}) {
return new ImportedEnumStruct(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.EnumValue = source["EnumValue"];
}
}
}
export namespace binding_test_import {
export enum ImportedEnum {
Value1 = "value1",
Value2 = "value2",
Value3 = "value3",
}
}
`,
}

View File

@ -59,7 +59,7 @@ func TestPromises(t *testing.T) {
// setup // setup
testLogger := &logger.Logger{} testLogger := &logger.Logger{}
b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false) b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false, []interface{}{})
// then // then
err := b.GenerateGoBindings(generationDir) err := b.GenerateGoBindings(generationDir)

View File

@ -13,6 +13,7 @@ import (
type BindingTest struct { type BindingTest struct {
name string name string
structs []interface{} structs []interface{}
enums []interface{}
exemptions []interface{} exemptions []interface{}
want string want string
shouldError bool shouldError bool
@ -22,6 +23,7 @@ type BindingTest struct {
type TsGenerationOptionsTest struct { type TsGenerationOptionsTest struct {
TsPrefix string TsPrefix string
TsSuffix string TsSuffix string
TsOutputType string
} }
func TestBindings_GenerateModels(t *testing.T) { func TestBindings_GenerateModels(t *testing.T) {
@ -31,12 +33,17 @@ func TestBindings_GenerateModels(t *testing.T) {
ImportedStructTest, ImportedStructTest,
ImportedSliceTest, ImportedSliceTest,
ImportedMapTest, ImportedMapTest,
ImportedEnumTest,
NestedFieldTest, NestedFieldTest,
NonStringMapKeyTest, NonStringMapKeyTest,
SingleFieldTest, SingleFieldTest,
MultistructTest, MultistructTest,
EmptyStructTest, EmptyStructTest,
GeneratedJsEntityTest, GeneratedJsEntityTest,
GeneratedJsEntityWithIntEnumTest,
GeneratedJsEntityWithStringEnumTest,
GeneratedJsEntityWithEnumTsName,
GeneratedJsEntityWithNestedStructInterfacesTest,
AnonymousSubStructTest, AnonymousSubStructTest,
AnonymousSubStructMultiLevelTest, AnonymousSubStructMultiLevelTest,
GeneratedJsEntityWithNestedStructTest, GeneratedJsEntityWithNestedStructTest,
@ -46,13 +53,14 @@ func TestBindings_GenerateModels(t *testing.T) {
testLogger := &logger.Logger{} testLogger := &logger.Logger{}
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
b := binding.NewBindings(testLogger, tt.structs, tt.exemptions, false) b := binding.NewBindings(testLogger, tt.structs, tt.exemptions, false, tt.enums)
for _, s := range tt.structs { for _, s := range tt.structs {
err := b.Add(s) err := b.Add(s)
require.NoError(t, err) require.NoError(t, err)
} }
b.SetTsPrefix(tt.TsPrefix) b.SetTsPrefix(tt.TsPrefix)
b.SetTsSuffix(tt.TsSuffix) b.SetTsSuffix(tt.TsSuffix)
b.SetOutputType(tt.TsOutputType)
got, err := b.GenerateModels() got, err := b.GenerateModels()
if (err != nil) != tt.shouldError { if (err != nil) != tt.shouldError {
t.Errorf("GenerateModels() error = %v, shouldError %v", err, tt.shouldError) t.Errorf("GenerateModels() error = %v, shouldError %v", err, tt.shouldError)

View File

@ -13,3 +13,20 @@ type ASliceWrapper struct {
type AMapWrapper struct { type AMapWrapper struct {
AMap map[string]binding_test_nestedimport.A `json:"AMap"` AMap map[string]binding_test_nestedimport.A `json:"AMap"`
} }
type ImportedEnum string
const (
ImportedEnumValue1 ImportedEnum = "value1"
ImportedEnumValue2 ImportedEnum = "value2"
ImportedEnumValue3 ImportedEnum = "value3"
)
var AllImportedEnumValues = []struct {
Value ImportedEnum
TSName string
}{
{ImportedEnumValue1, "Value1"},
{ImportedEnumValue2, "Value2"},
{ImportedEnumValue3, "Value3"},
}

View File

@ -275,3 +275,235 @@ export namespace binding_test {
`, `,
} }
type IntEnum int
const (
IntEnumValue1 IntEnum = iota
IntEnumValue2
IntEnumValue3
)
var AllIntEnumValues = []struct {
Value IntEnum
TSName string
}{
{IntEnumValue1, "Value1"},
{IntEnumValue2, "Value2"},
{IntEnumValue3, "Value3"},
}
type EntityWithIntEnum struct {
Name string `json:"name"`
Enum IntEnum `json:"enum"`
}
func (e EntityWithIntEnum) Get() EntityWithIntEnum {
return e
}
var GeneratedJsEntityWithIntEnumTest = BindingTest{
name: "GeneratedJsEntityWithIntEnumTest",
structs: []interface{}{
&EntityWithIntEnum{},
},
enums: []interface{}{
AllIntEnumValues,
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsPrefix: "MY_PREFIX_",
TsSuffix: "_MY_SUFFIX",
},
want: `export namespace binding_test {
export enum MY_PREFIX_IntEnum_MY_SUFFIX {
Value1 = 0,
Value2 = 1,
Value3 = 2,
}
export class MY_PREFIX_EntityWithIntEnum_MY_SUFFIX {
name: string;
enum: MY_PREFIX_IntEnum_MY_SUFFIX;
static createFrom(source: any = {}) {
return new MY_PREFIX_EntityWithIntEnum_MY_SUFFIX(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.enum = source["enum"];
}
}
}
`,
}
type StringEnum string
const (
StringEnumValue1 StringEnum = "value1"
StringEnumValue2 StringEnum = "value2"
StringEnumValue3 StringEnum = "value3"
)
var AllStringEnumValues = []struct {
Value StringEnum
TSName string
}{
{StringEnumValue1, "Value1"},
{StringEnumValue2, "Value2"},
{StringEnumValue3, "Value3"},
}
type EntityWithStringEnum struct {
Name string `json:"name"`
Enum StringEnum `json:"enum"`
}
func (e EntityWithStringEnum) Get() EntityWithStringEnum {
return e
}
var GeneratedJsEntityWithStringEnumTest = BindingTest{
name: "GeneratedJsEntityWithStringEnumTest",
structs: []interface{}{
&EntityWithStringEnum{},
},
enums: []interface{}{
AllStringEnumValues,
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsPrefix: "MY_PREFIX_",
TsSuffix: "_MY_SUFFIX",
},
want: `export namespace binding_test {
export enum MY_PREFIX_StringEnum_MY_SUFFIX {
Value1 = "value1",
Value2 = "value2",
Value3 = "value3",
}
export class MY_PREFIX_EntityWithStringEnum_MY_SUFFIX {
name: string;
enum: MY_PREFIX_StringEnum_MY_SUFFIX;
static createFrom(source: any = {}) {
return new MY_PREFIX_EntityWithStringEnum_MY_SUFFIX(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.enum = source["enum"];
}
}
}
`,
}
type EnumWithTsName string
const (
EnumWithTsName1 EnumWithTsName = "value1"
EnumWithTsName2 EnumWithTsName = "value2"
EnumWithTsName3 EnumWithTsName = "value3"
)
var AllEnumWithTsNameValues = []EnumWithTsName{EnumWithTsName1, EnumWithTsName2, EnumWithTsName3}
func (v EnumWithTsName) TSName() string {
switch v {
case EnumWithTsName1:
return "TsName1"
case EnumWithTsName2:
return "TsName2"
case EnumWithTsName3:
return "TsName3"
default:
return "???"
}
}
type EntityWithEnumTsName struct {
Name string `json:"name"`
Enum EnumWithTsName `json:"enum"`
}
func (e EntityWithEnumTsName) Get() EntityWithEnumTsName {
return e
}
var GeneratedJsEntityWithEnumTsName = BindingTest{
name: "GeneratedJsEntityWithEnumTsName",
structs: []interface{}{
&EntityWithEnumTsName{},
},
enums: []interface{}{
AllEnumWithTsNameValues,
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsPrefix: "MY_PREFIX_",
TsSuffix: "_MY_SUFFIX",
},
want: `export namespace binding_test {
export enum MY_PREFIX_EnumWithTsName_MY_SUFFIX {
TsName1 = "value1",
TsName2 = "value2",
TsName3 = "value3",
}
export class MY_PREFIX_EntityWithEnumTsName_MY_SUFFIX {
name: string;
enum: MY_PREFIX_EnumWithTsName_MY_SUFFIX;
static createFrom(source: any = {}) {
return new MY_PREFIX_EntityWithEnumTsName_MY_SUFFIX(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.enum = source["enum"];
}
}
}
`,
}
var GeneratedJsEntityWithNestedStructInterfacesTest = BindingTest{
name: "GeneratedJsEntityWithNestedStructInterfacesTest",
structs: []interface{}{
&ParentEntity{},
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsPrefix: "MY_PREFIX_",
TsSuffix: "_MY_SUFFIX",
TsOutputType: "interfaces",
},
want: `export namespace binding_test {
export interface MY_PREFIX_ChildEntity_MY_SUFFIX {
name: string;
childProp: number;
}
export interface MY_PREFIX_ParentEntity_MY_SUFFIX {
name: string;
ref: MY_PREFIX_ChildEntity_MY_SUFFIX;
parentProp: string;
}
}
`,
}

View File

@ -41,7 +41,7 @@ func TestAliases(t *testing.T) {
// setup // setup
testLogger := &logger.Logger{} testLogger := &logger.Logger{}
b := binding.NewBindings(testLogger, []interface{}{&AliasTest{}}, []interface{}{}, false) b := binding.NewBindings(testLogger, []interface{}{&AliasTest{}}, []interface{}{}, false, []interface{}{})
// then // then
err := b.GenerateGoBindings(generationDir) err := b.GenerateGoBindings(generationDir)

View File

@ -25,7 +25,7 @@ type B struct {
func TestNestedStruct(t *testing.T) { func TestNestedStruct(t *testing.T) {
bind := &BindForTest{} bind := &BindForTest{}
testBindings := NewBindings(logger.New(nil), []interface{}{bind}, []interface{}{}, false) testBindings := NewBindings(logger.New(nil), []interface{}{bind}, []interface{}{}, false, []interface{}{})
namesStrSlicer := testBindings.getAllStructNames() namesStrSlicer := testBindings.getAllStructNames()
names := []string{} names := []string{}

View File

@ -244,6 +244,7 @@ type Bindings struct {
type TsGeneration struct { type TsGeneration struct {
Prefix string `json:"prefix"` Prefix string `json:"prefix"`
Suffix string `json:"suffix"` Suffix string `json:"suffix"`
OutputType string `json:"outputType"`
} }
// Parse the given JSON data into a Project struct // Parse the given JSON data into a Project struct

View File

@ -104,6 +104,7 @@ type TypeScriptify struct {
Namespace string Namespace string
KnownStructs *slicer.StringSlicer KnownStructs *slicer.StringSlicer
KnownEnums *slicer.StringSlicer
} }
func New() *TypeScriptify { func New() *TypeScriptify {
@ -723,8 +724,17 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m
} }
} else { // Simple field: } else { // Simple field:
t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name) t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
// check if type is in known enum. If so, then replace TStype with enum name to avoid missing types
isKnownEnum := t.KnownEnums.Contains(getStructFQN(field.Type.String()))
if isKnownEnum {
err = builder.AddSimpleField(jsonFieldName, field, TypeOptions{
TSType: getStructFQN(field.Type.String()),
TSTransform: fldOpts.TSTransform,
})
} else {
err = builder.AddSimpleField(jsonFieldName, field, fldOpts) err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
} }
}
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -21,6 +21,7 @@ type Options struct {
GoModTidy bool GoModTidy bool
TsPrefix string TsPrefix string
TsSuffix string TsSuffix string
TsOutputType string
} }
// GenerateBindings generates bindings for the Wails project in the given ProjectDirectory. // GenerateBindings generates bindings for the Wails project in the given ProjectDirectory.
@ -65,6 +66,7 @@ func GenerateBindings(options Options) (string, error) {
env := os.Environ() env := os.Environ()
env = shell.SetEnv(env, "tsprefix", options.TsPrefix) env = shell.SetEnv(env, "tsprefix", options.TsPrefix)
env = shell.SetEnv(env, "tssuffix", options.TsSuffix) env = shell.SetEnv(env, "tssuffix", options.TsSuffix)
env = shell.SetEnv(env, "tsoutputtype", options.TsOutputType)
stdout, stderr, err = shell.RunCommandWithEnv(env, workingDirectory, filename) stdout, stderr, err = shell.RunCommandWithEnv(env, workingDirectory, filename)
if err != nil { if err != nil {

View File

@ -219,12 +219,17 @@ func GenerateBindings(buildOptions *Options) error {
printBulletPoint("Generating bindings: ") printBulletPoint("Generating bindings: ")
} }
if buildOptions.ProjectData.Bindings.TsGeneration.OutputType == "" {
buildOptions.ProjectData.Bindings.TsGeneration.OutputType = "classes"
}
// Generate Bindings // Generate Bindings
output, err := bindings.GenerateBindings(bindings.Options{ output, err := bindings.GenerateBindings(bindings.Options{
Tags: buildOptions.UserTags, Tags: buildOptions.UserTags,
GoModTidy: !buildOptions.SkipModTidy, GoModTidy: !buildOptions.SkipModTidy,
TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix, TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix,
TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix, TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix,
TsOutputType: buildOptions.ProjectData.Bindings.TsGeneration.OutputType,
}) })
if err != nil { if err != nil {
return err return err

View File

@ -63,6 +63,7 @@ type App struct {
OnShutdown func(ctx context.Context) `json:"-"` OnShutdown func(ctx context.Context) `json:"-"`
OnBeforeClose func(ctx context.Context) (prevent bool) `json:"-"` OnBeforeClose func(ctx context.Context) (prevent bool) `json:"-"`
Bind []interface{} Bind []interface{}
EnumBind []interface{}
WindowStartState WindowStartState WindowStartState WindowStartState
// ErrorFormatter overrides the formatting of errors returned by backend methods // ErrorFormatter overrides the formatting of errors returned by backend methods

View File

@ -144,6 +144,65 @@ func main() {
} }
``` ```
Also you might want to use Enums in your structs and have models for them on frontend.
In that case you should create array that will contain all possible enum values, instrument enum type and bind it to the app:
```go {16-18} title="app.go"
type Weekday string
const (
Sunday Weekday = "Sunday"
Monday Weekday = "Monday"
Tuesday Weekday = "Tuesday"
Wednesday Weekday = "Wednesday"
Thursday Weekday = "Thursday"
Friday Weekday = "Friday"
Saturday Weekday = "Saturday"
)
var AllWeekdays = []struct {
Value Weekday
TSName string
}{
{Sunday, "SUNDAY"},
{Monday, "MONDAY"},
{Tuesday, "TUESDAY"},
{Wednesday, "WEDNESDAY"},
{Thursday, "THURSDAY"},
{Friday, "FRIDAY"},
{Saturday, "SATURDAY"},
}
```
In the main application configuration, the `EnumBind` key is where we can tell Wails what we want to bind enums as well:
```go {11-13} title="main.go"
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "My App",
Width: 800,
Height: 600,
OnStartup: app.startup,
OnShutdown: app.shutdown,
Bind: []interface{}{
app,
},
EnumBind: []interface{}{
AllWeekdays,
},
})
if err != nil {
log.Fatal(err)
}
}
```
This will add missing enums to your `model.ts` file.
More information on Binding can be found [here](../howdoesitwork.mdx#method-binding). More information on Binding can be found [here](../howdoesitwork.mdx#method-binding).
## Application Menu ## Application Menu

View File

@ -206,6 +206,57 @@ You may bind as many structs as you like. Just make sure you create an instance
``` ```
You may bind enums types as well.
In that case you should create array that will contain all possible enum values, instrument enum type and bind it to the app via `EnumBind`:
```go {16-18} title="app.go"
type Weekday string
const (
Sunday Weekday = "Sunday"
Monday Weekday = "Monday"
Tuesday Weekday = "Tuesday"
Wednesday Weekday = "Wednesday"
Thursday Weekday = "Thursday"
Friday Weekday = "Friday"
Saturday Weekday = "Saturday"
)
var AllWeekdays = []struct {
Value Weekday
TSName string
}{
{Sunday, "SUNDAY"},
{Monday, "MONDAY"},
{Tuesday, "TUESDAY"},
{Wednesday, "WEDNESDAY"},
{Thursday, "THURSDAY"},
{Friday, "FRIDAY"},
{Saturday, "SATURDAY"},
}
```
```go {10-12}
//...
err := wails.Run(&options.App{
Title: "Basic Demo",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
Bind: []interface{}{
app,
&mystruct1{},
&mystruct2{},
},
EnumBind: []interface{}{
AllWeekdays,
},
})
```
When you run `wails dev` (or `wails generate module`), a frontend module will be generated containing the following: When you run `wails dev` (or `wails generate module`), a frontend module will be generated containing the following:
- JavaScript bindings for all bound methods - JavaScript bindings for all bound methods

View File

@ -58,7 +58,14 @@ func main() {
Bind: []interface{}{ Bind: []interface{}{
app, app,
}, },
EnumBind: []interface{}{
AllWeekdays,
},
ErrorFormatter: func(err error) any { return err.Error() }, ErrorFormatter: func(err error) any { return err.Error() },
SingleInstanceLock: &options.SingleInstanceLock{
UniqueId: "c9c8fd93-6758-4144-87d1-34bdb0a8bd60",
OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
},
Windows: &windows.Options{ Windows: &windows.Options{
WebviewIsTransparent: false, WebviewIsTransparent: false,
WindowIsTranslucent: false, WindowIsTranslucent: false,
@ -92,6 +99,8 @@ func main() {
FullSizeContent: false, FullSizeContent: false,
UseToolbar: false, UseToolbar: false,
HideToolbarSeparator: true, HideToolbarSeparator: true,
OnFileOpen: app.onFileOpen,
OnUrlOpen: app.onUrlOpen,
}, },
Appearance: mac.NSAppearanceNameDarkAqua, Appearance: mac.NSAppearanceNameDarkAqua,
WebviewIsTransparent: true, WebviewIsTransparent: true,
@ -479,6 +488,13 @@ A slice of struct instances defining methods that need to be bound to the fronte
Name: Bind<br/> Name: Bind<br/>
Type: `[]interface{}` Type: `[]interface{}`
### EnumBind
A slice of Enum arrays that need to be bound to the frontend.
Name: EnumBind<br/>
Type: `[]interface{}`
### ErrorFormatter ### ErrorFormatter
A function that determines how errors are formatted when returned by a JS-to-Go A function that determines how errors are formatted when returned by a JS-to-Go
@ -487,6 +503,28 @@ method call. The returned value will be marshalled as JSON.
Name: ErrorFormatter<br/> Name: ErrorFormatter<br/>
Type: `func (error) any` Type: `func (error) any`
### SingleInstanceLock
Enables single instance locking. This means that only one instance of your application can be running at a time.
Name: SingleInstanceLock<br/>
Type: `*options.SingleInstanceLock`
#### UniqueId
This id is used to generate the mutex name on Windows and macOS and the dbus name on Linux. Use a UUID to ensure that the id is unique.
Name: UniqueId<br/>
Type: `string`
#### OnSecondInstanceLaunch
Callback that is called when a second instance of your app is launched.
Name: OnSecondInstanceLaunch<br/>
Type: `func(secondInstanceData SecondInstanceData)`
### Windows ### Windows
This defines [Windows specific options](#windows). This defines [Windows specific options](#windows).
@ -797,6 +835,20 @@ with [WebviewIsTransparent](#WebviewIsTransparent) to make frosty-looking applic
Name: WindowIsTranslucent<br/> Name: WindowIsTranslucent<br/>
Type: `bool` Type: `bool`
#### OnFileOpen
Callback that is called when a file is opened with the application.
Name: OnFileOpen<br/>
Type: `func(filePath string)`
#### OnUrlOpen
Callback that is called when a URL is opened with the application.
Name: OnUrlOpen<br/>
Type: `func(filePath string)`
#### Preferences #### Preferences
The Preferences struct provides the ability to configure the Webview preferences. The Preferences struct provides the ability to configure the Webview preferences.

View File

@ -73,14 +73,52 @@ The project config resides in the `wails.json` file in the project directory. Th
// The copyright of the product. Default: 'Copyright.........' // The copyright of the product. Default: 'Copyright.........'
"copyright": "", "copyright": "",
// A short comment of the app. Default: 'Built using Wails (https://wails.app)' // A short comment of the app. Default: 'Built using Wails (https://wails.app)'
"comments": "" "comments": "",
// File associations for the app
"fileAssociations": [
{
// The extension (minus the leading period). e.g. png
"ext": "wails",
// The name. e.g. PNG File
"name": "Wails",
// Windows-only. The description. It is displayed on the `Type` column on Windows Explorer.
"description": "Wails file",
// The icon name without extension. Icons should be located in build folder. Proper icons will be generated from .png file for both macOS and Windows)
"iconName": "fileIcon",
// macOS-only. The apps role with respect to the type. Corresponds to CFBundleTypeRole.
"role": "Editor"
},
],
// Custom URI protocols that should be opened by the application
"protocols": [
{
// protocol scheme. e.g. myapp
"scheme": "myapp",
// Windows-only. The description. It is displayed on the `Type` column on Windows Explorer.
"description": "Myapp protocol",
// macOS-only. The apps role with respect to the type. Corresponds to CFBundleTypeRole.
"role": "Editor"
}
]
}, },
// 'multiple': One installer per architecture. 'single': Single universal installer for all architectures being built. Default: 'multiple' // 'multiple': One installer per architecture. 'single': Single universal installer for all architectures being built. Default: 'multiple'
"nsisType": "", "nsisType": "",
// Whether the app should be obfuscated. Default: false // Whether the app should be obfuscated. Default: false
"obfuscated": "", "obfuscated": "",
// The arguments to pass to the garble command when using the obfuscated flag // The arguments to pass to the garble command when using the obfuscated flag
"garbleargs": "" "garbleargs": "",
// Bindings configurations
"bindings": {
// model.ts file generation config
"ts_generation": {
// All generated JavaScript entities will be prefixed with this value
"prefix": "",
// All generated JavaScript entities will be suffixed with this value
"suffix": "",
// Type of output to generate (classes|interfaces)
"outputType": "classes",
}
}
} }
``` ```

View File

@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added CPU/GPU/Memory detection for `wails doctor`. Added by @leaanthony in #d51268b8d0680430f3a614775b13e6cd2b906d1c - Added CPU/GPU/Memory detection for `wails doctor`. Added by @leaanthony in #d51268b8d0680430f3a614775b13e6cd2b906d1c
- The [AssetServer](/docs/reference/options#assetserver) now injects the runtime/IPC into all index html files and into all html files returned when requesting a folder path. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2203) - The [AssetServer](/docs/reference/options#assetserver) now injects the runtime/IPC into all index html files and into all html files returned when requesting a folder path. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2203)
- Added Custom Protocol Schemes associations support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/3000) - Added Custom Protocol Schemes associations support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/3000)
Added support for TS interfaces generation as an option. Add support for Enums in TS types. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/3047)
### Changed ### Changed

View File

@ -251,6 +251,33 @@
"garbleargs": { "garbleargs": {
"type": "string", "type": "string",
"description": "The arguments to pass to the garble command when using the obfuscated flag" "description": "The arguments to pass to the garble command when using the obfuscated flag"
},
"bindings": {
"type": "object",
"description": "Bindings configurations",
"properties": {
"ts_generation": {
"type": "object",
"description": "model.ts file generation config",
"properties": {
"prefix": {
"type": "string",
"description": "All generated JavaScript entities will be prefixed with this value"
},
"suffix": {
"type": "string",
"description": "All generated JavaScript entities will be suffixed with this value"
},
"outputType": {
"allOf": [
{
"$ref": "#/definitions/BindingsOutputTypes"
}
]
}
}
}
}
} }
}, },
"dependencies": { "dependencies": {
@ -346,26 +373,25 @@
] ]
} }
] ]
}
}, },
"bindings": { "BindingsOutputTypes": {
"type": "object", "description": "Type of output to generate",
"description": "Bindings configurations", "oneOf": [
"properties": { {
"ts_generation": { "description": "Classes",
"type": "object",
"description": "model.ts file generation config",
"properties": {
"prefix": {
"type": "string", "type": "string",
"description": "All generated JavaScript entities will be prefixed with this value" "enum": [
"classes"
]
}, },
"suffix": { {
"description": "Interfaces",
"type": "string", "type": "string",
"description": "All generated JavaScript entities will be suffixed with this value" "enum": [
} "interfaces"
} ]
} }
]
} }
} }
} }