diff --git a/v3/internal/parser/README.md b/v3/internal/parser/README.md index 491dc20d7..fc6a1c700 100644 --- a/v3/internal/parser/README.md +++ b/v3/internal/parser/README.md @@ -53,10 +53,10 @@ This package contains the static analyser used for parsing Wails projects so tha - [x] Recursive - [x] Anonymous - [ ] Generation of models - - [ ] Scalars + - [x] Scalars - [ ] Arrays - [ ] Maps - - [ ] Structs + - [x] Structs - [ ] Generation of bindings ## Limitations diff --git a/v3/internal/parser/models.go b/v3/internal/parser/models.go index 3f52a7ad5..07ecb3052 100644 --- a/v3/internal/parser/models.go +++ b/v3/internal/parser/models.go @@ -4,6 +4,8 @@ import ( "bytes" "embed" "io" + "sort" + "strings" "text/template" ) @@ -35,13 +37,34 @@ const modelsHeader = `// @ts-check // This file is automatically generated. DO NOT EDIT ` +func pkgAlias(fullPkg string) string { + pkgParts := strings.Split(fullPkg, "/") + return pkgParts[len(pkgParts)-1] +} + func GenerateModels(models map[packagePath]map[structName]*StructDef) (string, error) { + if models == nil { + return "", nil + } + var buffer bytes.Buffer buffer.WriteString(modelsHeader) - for pkg, pkgModels := range models { + + // sort pkgs by alias (e.g. services) instead of full pkg name (e.g. github.com/wailsapp/wails/somedir/services) + // and then sort resulting list by the alias + var keys []string + for pkg, _ := range models { + keys = append(keys, pkg) + } + + sort.Slice(keys, func(i, j int) bool { + return pkgAlias(keys[i]) < pkgAlias(keys[j]) + }) + + for _, pkg := range keys { err := GenerateModel(&buffer, &ModelDefinitions{ - Package: pkg, - Models: pkgModels, + Package: pkgAlias(pkg), + Models: models[pkg], }) if err != nil { return "", err diff --git a/v3/internal/parser/models_test.go b/v3/internal/parser/models_test.go index 8eb74f7d7..b2668511e 100644 --- a/v3/internal/parser/models_test.go +++ b/v3/internal/parser/models_test.go @@ -2,132 +2,76 @@ package parser import ( "github.com/google/go-cmp/cmp" - "strings" + "os" + "path/filepath" "testing" ) -const expected = ` -export namespace main { - - export class Person { - name: string; - parent: Person; - details: anon1; - address: package.Address; - - static createFrom(source: any = {}) { - return new Person(source); - } +func TestGenerateModels(t *testing.T) { - constructor(source: any = {}) { - if ('string' === typeof source) { - source = JSON.parse(source); - } - - this.name = source["name"] - this.parent = source["parent"] - this.details = source["details"] - this.address = source["address"] - - } - } - - export class anon1 { - age: int; - address: string; - - static createFrom(source: any = {}) { - return new anon1(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) { - source = JSON.parse(source); - } - - this.age = source["age"] - this.address = source["address"] - - } - } - -} -` - -func TestGenerateClass(t *testing.T) { - person := StructDef{ - Name: "Person", - Fields: []*Field{ - { - Name: "Name", - Type: &ParameterType{ - Name: "string", - }, - }, - { - Name: "Parent", - Type: &ParameterType{ - Name: "Person", - IsStruct: true, - IsPointer: true, - Package: "main", - }, - }, - { - Name: "Details", - Type: &ParameterType{ - Name: "anon1", - IsStruct: true, - Package: "main", - }, - }, - { - Name: "Address", - Type: &ParameterType{ - Name: "Address", - IsStruct: true, - IsPointer: true, - Package: "github.com/some/other/package", - }, - }, - }, - } - anon1 := StructDef{ - Name: "anon1", - Fields: []*Field{ - { - Name: "Age", - Type: &ParameterType{ - Name: "int", - }, - }, - { - Name: "Address", - Type: &ParameterType{ - Name: "string", - }, - }, + tests := []struct { + dir string + want string + }{ + { + "testdata/function_single", + "", + }, + { + "testdata/function_from_imported_package", + getFile("testdata/function_from_imported_package/models.ts"), + }, + { + "testdata/variable_single", + "", + }, + { + "testdata/variable_single_from_function", + "", + }, + { + "testdata/variable_single_from_other_function", + getFile("testdata/variable_single_from_other_function/models.ts"), + }, + { + "testdata/struct_literal_single", + getFile("testdata/struct_literal_single/models.ts"), + }, + { + "testdata/struct_literal_multiple", + "", + }, + { + "testdata/struct_literal_multiple_other", + getFile("testdata/struct_literal_multiple_other/models.ts"), + }, + { + "testdata/struct_literal_multiple_files", + "", }, } + for _, tt := range tests { + t.Run(tt.dir, func(t *testing.T) { + // Run parser on directory + project, err := ParseProject(tt.dir) + if err != nil { + t.Fatalf("ParseProject() error = %v", err) + } - var builder strings.Builder - models := make(map[string]*StructDef) - models["Person"] = &person - models["anon1"] = &anon1 - def := ModelDefinitions{ - Package: "main", - Models: models, - } + // Generate Models + got, err := GenerateModels(project.Models) + if err != nil { + t.Fatalf("GenerateModels() error = %v", err) + } - err := GenerateModel(&builder, &def) - if err != nil { - t.Fatal(err) - } - - text := builder.String() - println("Built string") - println(text) - if diff := cmp.Diff(expected, text); diff != "" { - t.Errorf("GenerateClass() failed:\n" + diff) + if diff := cmp.Diff(tt.want, got); diff != "" { + err = os.WriteFile(filepath.Join(tt.dir, "models.got.ts"), []byte(got), 0644) + if err != nil { + t.Errorf("os.WriteFile() error = %v", err) + return + } + t.Fatalf("GenerateModels() mismatch (-want +got):\n%s", diff) + } + }) } } diff --git a/v3/internal/parser/parser.go b/v3/internal/parser/parser.go index d022cd219..cae51fa13 100644 --- a/v3/internal/parser/parser.go +++ b/v3/internal/parser/parser.go @@ -90,16 +90,39 @@ func (f *Field) JSName() string { return strings.ToLower(f.Name[0:1]) + f.Name[1:] } -func (f *Field) JSDef(pkg string) string { - name := f.JSName() - - var result string +// TSBuild contains the typescript to build a field for a JS object +// via assignment for simple types or constructors for structs +func (f *Field) TSBuild(pkg string) string { + if !f.Type.IsStruct { + return fmt.Sprintf("source['%s']", f.JSName()) + } if f.Type.Package == "" || f.Type.Package == pkg { - result += fmt.Sprintf("%s: %s;", name, f.Type.Name) + return fmt.Sprintf("%s.createFrom(source['%s'])", f.Type.Name, f.JSName()) + } + + return fmt.Sprintf("%s.%s.createFrom(source['%s'])", pkgAlias(f.Type.Package), f.Type.Name, f.JSName()) +} + +func (f *Field) JSDef(pkg string) string { + var jsType string + switch f.Type.Name { + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "float32", "float64": + jsType = "number" + case "string": + jsType = "string" + case "bool": + jsType = "boolean" + default: + jsType = f.Type.Name + } + + var result string + if f.Type.Package == "" || f.Type.Package == pkg { + result += fmt.Sprintf("%s: %s;", f.JSName(), jsType) } else { parts := strings.Split(f.Type.Package, "/") - result += fmt.Sprintf("%s: %s.%s;", name, parts[len(parts)-1], f.Type.Name) + result += fmt.Sprintf("%s: %s.%s;", f.JSName(), parts[len(parts)-1], jsType) } if !ast.IsExported(f.Name) { diff --git a/v3/internal/parser/templates/model.ts.tmpl b/v3/internal/parser/templates/model.ts.tmpl index b2863e37e..232db7479 100644 --- a/v3/internal/parser/templates/model.ts.tmpl +++ b/v3/internal/parser/templates/model.ts.tmpl @@ -13,7 +13,7 @@ export namespace {{.Package}} { source = JSON.parse(source); } - {{range $def.Fields}}this.{{.JSName}} = source["{{.JSName}}"] + {{range $def.Fields}}this.{{.JSName}} = {{.TSBuild $pkg}}; {{end}} } } diff --git a/v3/internal/parser/testdata/function_from_imported_package/models.ts b/v3/internal/parser/testdata/function_from_imported_package/models.ts new file mode 100644 index 000000000..a6639f8eb --- /dev/null +++ b/v3/internal/parser/testdata/function_from_imported_package/models.ts @@ -0,0 +1,51 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export namespace main { + + export class Person { + name: string; + address: services.Address; + + static createFrom(source: any = {}) { + return new Person(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.name = source['name']; + this.address = services.Address.createFrom(source['address']); + + } + } + +} + +export namespace services { + + export class Address { + street: string; + state: string; + country: string; + + static createFrom(source: any = {}) { + return new Address(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.street = source['street']; + this.state = source['state']; + this.country = source['country']; + + } + } + +} diff --git a/v3/internal/parser/testdata/function_single/models.ts b/v3/internal/parser/testdata/function_single/models.ts new file mode 100644 index 000000000..0817f259b --- /dev/null +++ b/v3/internal/parser/testdata/function_single/models.ts @@ -0,0 +1,5 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// TODO : nothing generated yet \ No newline at end of file diff --git a/v3/internal/parser/testdata/struct_literal_multiple/models.ts b/v3/internal/parser/testdata/struct_literal_multiple/models.ts new file mode 100644 index 000000000..0817f259b --- /dev/null +++ b/v3/internal/parser/testdata/struct_literal_multiple/models.ts @@ -0,0 +1,5 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// TODO : nothing generated yet \ No newline at end of file diff --git a/v3/internal/parser/testdata/struct_literal_multiple_files/models.ts b/v3/internal/parser/testdata/struct_literal_multiple_files/models.ts new file mode 100644 index 000000000..0817f259b --- /dev/null +++ b/v3/internal/parser/testdata/struct_literal_multiple_files/models.ts @@ -0,0 +1,5 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// TODO : nothing generated yet \ No newline at end of file diff --git a/v3/internal/parser/testdata/struct_literal_multiple_other/models.ts b/v3/internal/parser/testdata/struct_literal_multiple_other/models.ts new file mode 100644 index 000000000..a6639f8eb --- /dev/null +++ b/v3/internal/parser/testdata/struct_literal_multiple_other/models.ts @@ -0,0 +1,51 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export namespace main { + + export class Person { + name: string; + address: services.Address; + + static createFrom(source: any = {}) { + return new Person(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.name = source['name']; + this.address = services.Address.createFrom(source['address']); + + } + } + +} + +export namespace services { + + export class Address { + street: string; + state: string; + country: string; + + static createFrom(source: any = {}) { + return new Address(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.street = source['street']; + this.state = source['state']; + this.country = source['country']; + + } + } + +} diff --git a/v3/internal/parser/testdata/struct_literal_single/models.ts b/v3/internal/parser/testdata/struct_literal_single/models.ts new file mode 100644 index 000000000..91bab56ac --- /dev/null +++ b/v3/internal/parser/testdata/struct_literal_single/models.ts @@ -0,0 +1,64 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export namespace main { + + export class Person { + name: string; + parent: Person; + details: anon1; + + static createFrom(source: any = {}) { + return new Person(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.name = source['name']; + this.parent = Person.createFrom(source['parent']); + this.details = anon1.createFrom(source['details']); + + } + } + + export class anon1 { + age: number; + address: anon2; + + static createFrom(source: any = {}) { + return new anon1(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.age = source['age']; + this.address = anon2.createFrom(source['address']); + + } + } + + export class anon2 { + street: string; + + static createFrom(source: any = {}) { + return new anon2(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.street = source['street']; + + } + } + +} diff --git a/v3/internal/parser/testdata/variable_single/models.ts b/v3/internal/parser/testdata/variable_single/models.ts new file mode 100644 index 000000000..0817f259b --- /dev/null +++ b/v3/internal/parser/testdata/variable_single/models.ts @@ -0,0 +1,5 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// TODO : nothing generated yet \ No newline at end of file diff --git a/v3/internal/parser/testdata/variable_single_from_function/models.ts b/v3/internal/parser/testdata/variable_single_from_function/models.ts new file mode 100644 index 000000000..0817f259b --- /dev/null +++ b/v3/internal/parser/testdata/variable_single_from_function/models.ts @@ -0,0 +1,5 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// TODO : nothing generated yet \ No newline at end of file diff --git a/v3/internal/parser/testdata/variable_single_from_other_function/models.ts b/v3/internal/parser/testdata/variable_single_from_other_function/models.ts new file mode 100644 index 000000000..a6639f8eb --- /dev/null +++ b/v3/internal/parser/testdata/variable_single_from_other_function/models.ts @@ -0,0 +1,51 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export namespace main { + + export class Person { + name: string; + address: services.Address; + + static createFrom(source: any = {}) { + return new Person(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.name = source['name']; + this.address = services.Address.createFrom(source['address']); + + } + } + +} + +export namespace services { + + export class Address { + street: string; + state: string; + country: string; + + static createFrom(source: any = {}) { + return new Address(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) { + source = JSON.parse(source); + } + + this.street = source['street']; + this.state = source['state']; + this.country = source['country']; + + } + } + +} diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index c17f3d733..3a77eaeb8 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -14,14 +14,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- [v3] Typescript model generation using `StructDef`s from new AST-based parser. Added by @ATenderholt in [PR1](https://github.com/wailsapp/wails/pull/2428/files) and [PR2](https://github.com/wailsapp/wails/pull/2485). + + ## v2.4.1 - 2022-03-20 ### Changed - Support single clicks on items with `--wails-draggable: drag` again on Windows. Changed by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2482) ### Fixed -- Fixed panic when using `wails dev` and the AssetServer tried to log to the logger. Fixed by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2481) -- Fixed compatibility with WebView2 Runtime > `110.0.1587.69` which showed a `connection refused` html page before doing a reload of the frontend. Fixed by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2496) +- Fixed panic when using `wails dev` and the AssetServer tried to log to the logger. Fixed by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2481) +- Fixed compatibility with WebView2 Runtime > `110.0.1587.69` which showed a `connection refused` html page before doing a reload of the frontend. Fixed by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2496) ## v2.4.0 - 2022-03-08