diff --git a/.gitignore b/.gitignore index 325406e5d..8b46aaac8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ v2/test/hidden/icon.png v2/internal/ffenestri/runtime.c v2/internal/runtime/assets/desktop.js v2/test/kitchensink/frontend/public/bundle.* +v2/pkg/parser/testproject/frontend/wails diff --git a/v2/cmd/wails/internal/commands/generate/generate.go b/v2/cmd/wails/internal/commands/generate/generate.go new file mode 100644 index 000000000..7e99a9997 --- /dev/null +++ b/v2/cmd/wails/internal/commands/generate/generate.go @@ -0,0 +1,91 @@ +package generate + +import ( + "io" + "time" + + "github.com/leaanthony/clir" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/parser" +) + +// AddSubcommand adds the `dev` command for the Wails application +func AddSubcommand(app *clir.Cli, w io.Writer) error { + + command := app.NewSubCommand("generate", "Code Generation Tools") + + // Backend API + backendAPI := command.NewSubCommand("module", "Generates a JS module for the frontend to interface with the backend") + + // Quiet Init + quiet := false + backendAPI.BoolFlag("q", "Supress output to console", &quiet) + + backendAPI.Action(func() error { + + // Create logger + logger := clilogger.New(w) + logger.Mute(quiet) + + app.PrintBanner() + + logger.Print("Generating Javascript module for Go code...") + + // Start Time + start := time.Now() + + p, err := parser.GenerateWailsFrontendPackage() + if err != nil { + return err + } + + logger.Println("done.") + logger.Println("") + + elapsed := time.Since(start) + packages := p.Packages + + // Print report + for _, pkg := range p.Packages { + if pkg.ShouldGenerate() { + logPackage(pkg, logger) + } + + } + + logger.Println("%d packages parsed in %s.", len(packages), elapsed) + + return nil + + }) + return nil +} + +func logPackage(pkg *parser.Package, logger *clilogger.CLILogger) { + + logger.Println("Processed Go package '" + pkg.Gopackage.Name + "' as '" + pkg.Name + "'") + for _, strct := range pkg.Structs() { + logger.Println("") + logger.Println(" Processed struct '" + strct.Name + "'") + if strct.IsBound { + for _, method := range strct.Methods { + logger.Println(" Bound method '" + method.Name + "'") + } + } + if strct.IsUsedAsData { + for _, field := range strct.Fields { + if !field.Ignored { + logger.Print(" Processed ") + if field.IsOptional { + logger.Print("optional ") + } + logger.Println("field '" + field.Name + "' as '" + field.JSName() + "'") + } + } + } + } + logger.Println("") + + // logger.Println(" Original Go Package Path:", pkg.Gopackage.PkgPath) + // logger.Println(" Original Go Package Path:", pkg.Gopackage.PkgPath) +} diff --git a/v2/cmd/wails/internal/commands/initialise/initialise.go b/v2/cmd/wails/internal/commands/initialise/initialise.go index 6bb7e785f..8652d412f 100644 --- a/v2/cmd/wails/internal/commands/initialise/initialise.go +++ b/v2/cmd/wails/internal/commands/initialise/initialise.go @@ -32,13 +32,17 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error { command.StringFlag("n", "Name of project", &projectName) // Setup project directory - projectDirectory := "." + projectDirectory := "" command.StringFlag("d", "Project directory", &projectDirectory) // Quiet Init quiet := false command.BoolFlag("q", "Supress output to console", &quiet) + // VSCode project files + vscode := false + command.BoolFlag("vscode", "Generate VSCode project files", &vscode) + // List templates list := false command.BoolFlag("l", "List templates", &list) @@ -83,10 +87,11 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error { // Create Template Options options := &templates.Options{ - ProjectName: projectName, - TargetDir: projectDirectory, - TemplateName: templateName, - Logger: logger, + ProjectName: projectName, + TargetDir: projectDirectory, + TemplateName: templateName, + Logger: logger, + GenerateVSCode: vscode, } return initProject(options) @@ -110,6 +115,14 @@ func initProject(options *templates.Options) error { // Output stats elapsed := time.Since(start) options.Logger.Println("") + options.Logger.Println("Project Name: " + options.ProjectName) + options.Logger.Println("Project Directory: " + options.TargetDir) + options.Logger.Println("Project Template: " + options.TemplateName) + options.Logger.Println("") + if options.GenerateVSCode { + options.Logger.Println("VSCode config files generated.") + } + options.Logger.Println("") options.Logger.Println(fmt.Sprintf("Initialised project '%s' in %s.", options.ProjectName, elapsed.Round(time.Millisecond).String())) options.Logger.Println("") diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go index 4d9bd4baf..9067d01e7 100644 --- a/v2/cmd/wails/main.go +++ b/v2/cmd/wails/main.go @@ -7,6 +7,7 @@ import ( "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build" "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/dev" "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor" + "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate" "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/initialise" ) @@ -37,6 +38,11 @@ func main() { fatal(err.Error()) } + err = generate.AddSubcommand(app, os.Stdout) + if err != nil { + fatal(err.Error()) + } + err = app.Run() if err != nil { println("\n\nERROR: " + err.Error()) diff --git a/v2/go.mod b/v2/go.mod index 38a7327c9..a30b7f702 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,19 +3,23 @@ module github.com/wailsapp/wails/v2 go 1.13 require ( + github.com/davecgh/go-spew v1.1.1 + github.com/fatih/structtag v1.2.0 github.com/fsnotify/fsnotify v1.4.9 github.com/imdario/mergo v0.3.11 github.com/leaanthony/clir v1.0.4 github.com/leaanthony/gosod v0.0.4 - github.com/leaanthony/slicer v1.4.1 + github.com/leaanthony/slicer v1.5.0 github.com/matryer/is v1.4.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/olekukonko/tablewriter v0.0.4 + github.com/pkg/errors v0.9.1 github.com/tdewolff/minify v2.3.6+incompatible - github.com/tdewolff/minify/v2 v2.9.5 github.com/tdewolff/parse v2.3.4+incompatible // indirect + github.com/tdewolff/test v1.0.6 // indirect github.com/xyproto/xpm v1.2.1 golang.org/x/net v0.0.0-20200822124328-c89045814202 + golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect nhooyr.io/websocket v1.8.6 diff --git a/v2/go.sum b/v2/go.sum index e97d9955d..e73c4ab56 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,8 +1,8 @@ -github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -44,13 +44,12 @@ github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= github.com/leaanthony/gosod v0.0.4 h1:v4hepo4IyL8E8c9qzDsvYcA0KGh7Npf8As74K5ibQpI= github.com/leaanthony/gosod v0.0.4/go.mod h1:nGMCb1PJfXwBDbOAike78jEYlpqge+xUKFf0iBKjKxU= -github.com/leaanthony/slicer v1.4.1 h1:X/SmRIDhkUAolP79mSTO0jTcVX1k504PJBqvV6TwP0w= -github.com/leaanthony/slicer v1.4.1/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= +github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= @@ -63,23 +62,19 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/tdewolff/minify v1.1.0 h1:nxHQi1ML+g3ZbZHffiZ6eC7vMqNvSRfX3KB5Y5y/kfw= github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo= github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= -github.com/tdewolff/minify/v2 v2.9.5 h1:+fHvqLencVdv14B+zgxQGhetF9qXl/nRTN/1mcyQwpM= -github.com/tdewolff/minify/v2 v2.9.5/go.mod h1:jshtBj/uUJH6JX1fuxTLnnHOA1RVJhF5MM+leJzDKb4= -github.com/tdewolff/parse v1.1.0 h1:tMjj9GCK8zzwjWyxdZ4pabzdWO1VG+G3bvCnG6aUIyQ= github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38= github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ= -github.com/tdewolff/parse/v2 v2.5.3 h1:fnPIstKgEfxd3+wwHnH73sAYydsR0o/jYhcQ6c5PkrA= -github.com/tdewolff/parse/v2 v2.5.3/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= diff --git a/v2/internal/binding/reflect.go b/v2/internal/binding/reflect.go index 5b2d47ba0..c0cb05ade 100755 --- a/v2/internal/binding/reflect.go +++ b/v2/internal/binding/reflect.go @@ -68,6 +68,8 @@ func getMethods(value interface{}) ([]*BoundMethod, error) { boundMethod.Inputs = inputs // Iterate outputs + // TODO: Determine what to do about limiting return types + // especially around errors. outputParamCount := methodType.NumOut() var outputs []*Parameter for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ { diff --git a/v2/internal/fs/fs.go b/v2/internal/fs/fs.go index a0b33d6cc..0867dfa04 100644 --- a/v2/internal/fs/fs.go +++ b/v2/internal/fs/fs.go @@ -20,6 +20,17 @@ func LocalDirectory() string { return filepath.Dir(thisFile) } +// RelativeToCwd returns an absolute path based on the cwd +// and the given relative path +func RelativeToCwd(relativePath string) (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", err + } + + return filepath.Join(cwd, relativePath), nil +} + // Mkdir will create the given directory func Mkdir(dirname string) error { return os.Mkdir(dirname, 0755) @@ -169,3 +180,23 @@ func GetSubdirectories(rootDir string) (*slicer.StringSlicer, error) { }) return &result, err } + +func DirIsEmpty(dir string) (bool, error) { + + if !DirExists(dir) { + return false, fmt.Errorf("DirIsEmpty called with a non-existant directory: %s", dir) + } + + // CREDIT: https://stackoverflow.com/a/30708914/8325411 + f, err := os.Open(dir) + if err != nil { + return false, err + } + defer f.Close() + + _, err = f.Readdirnames(1) // Or f.Readdir(1) + if err == io.EOF { + return true, nil + } + return false, err // Either not empty or error, suits both cases +} diff --git a/v2/internal/parse/parse.go b/v2/internal/parse/parse.go index f448c088d..e9d98236f 100644 --- a/v2/internal/parse/parse.go +++ b/v2/internal/parse/parse.go @@ -65,7 +65,6 @@ func ParseProject(projectPath string) (BoundStructs, error) { var wailsPkgVar = "" ast.Inspect(file, func(n ast.Node) bool { - var s string switch x := n.(type) { // Parse import declarations case *ast.ImportSpec: diff --git a/v2/internal/system/operatingsystem/os_windows.go b/v2/internal/system/operatingsystem/os_windows.go new file mode 100644 index 000000000..29c17f206 --- /dev/null +++ b/v2/internal/system/operatingsystem/os_windows.go @@ -0,0 +1,29 @@ +package operatingsystem + +import ( + "fmt" + + "golang.org/x/sys/windows/registry" +) + +func platformInfo() (*OS, error) { + // Default value + var result OS + result.ID = "Unknown" + result.Name = "Windows" + result.Version = "Unknown" + + // Credit: https://stackoverflow.com/a/33288328 + // Ignore errors as it isn't a showstopper + key, _ := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + + defer key.Close() + + fmt.Printf("%+v\n", key) + + // Ignore errors as it isn't a showstopper + productName, _, _ := key.GetStringValue("ProductName") + fmt.Println(productName) + + return nil, nil +} diff --git a/v2/internal/system/system_windows.go b/v2/internal/system/system_windows.go index 1f1478469..b3d530bc5 100644 --- a/v2/internal/system/system_windows.go +++ b/v2/internal/system/system_windows.go @@ -2,14 +2,20 @@ package system -import ( - "fmt" - "syscall" -) +import "github.com/wailsapp/wails/v2/internal/system/operatingsystem" -func (i *Info) discover() { - dll := syscall.MustLoadDLL("kernel32.dll") - p := dll.MustFindProc("GetVersion") - v, _, _ := p.Call() - fmt.Printf("Windows version %d.%d (Build %d)\n", byte(v), uint8(v>>8), uint16(v>>16)) +func (i *Info) discover() error { + + var err error + osinfo, err := operatingsystem.Info() + if err != nil { + return err + } + i.OS = osinfo + + // dll := syscall.MustLoadDLL("kernel32.dll") + // p := dll.MustFindProc("GetVersion") + // v, _, _ := p.Call() + // fmt.Printf("Windows version %d.%d (Build %d)\n", byte(v), uint8(v>>8), uint16(v>>16)) + return nil } diff --git a/v2/internal/templates/ides/vscode/launch.json.tmpl b/v2/internal/templates/ides/vscode/launch.json.tmpl new file mode 100644 index 000000000..344116114 --- /dev/null +++ b/v2/internal/templates/ides/vscode/launch.json.tmpl @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Wails: Debug {{.ProjectName}} (Desktop)", + "type": "go", + "request": "launch", + "mode": "exec", + "program": "${workspaceFolder}/{{.PathToDesktopBinary}}", + "preLaunchTask": "build_desktop", + "cwd": "", + "env": {}, + "args": [] + }, + { + "name": "Wails: Debug {{.ProjectName}} (Server)", + "type": "go", + "request": "launch", + "mode": "exec", + "program": "${workspaceFolder}/{{.PathToServerBinary}}", + "preLaunchTask": "build_server", + "cwd": "", + "env": {}, + "args": [] + }, + ] + } \ No newline at end of file diff --git a/v2/internal/templates/ides/vscode/tasks.json.tmpl b/v2/internal/templates/ides/vscode/tasks.json.tmpl new file mode 100644 index 000000000..4e536407a --- /dev/null +++ b/v2/internal/templates/ides/vscode/tasks.json.tmpl @@ -0,0 +1,23 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build_desktop", + "type": "shell", + "options": { + "cwd": "{{.TargetDir}}" + }, + "command": "wails build" + }, + { + "label": "build_server", + "type": "shell", + "options": { + "cwd": "{{.TargetDir}}" + }, + "command": "wails build -t server" + }, + + ] + } + \ No newline at end of file diff --git a/v2/internal/templates/templates.go b/v2/internal/templates/templates.go index 06dd1f5db..ea7363456 100644 --- a/v2/internal/templates/templates.go +++ b/v2/internal/templates/templates.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "strings" "github.com/leaanthony/gosod" @@ -31,11 +32,14 @@ type Data struct { // Options for installing a template type Options struct { - ProjectName string - TemplateName string - BinaryName string - TargetDir string - Logger *clilogger.CLILogger + ProjectName string + TemplateName string + BinaryName string + TargetDir string + Logger *clilogger.CLILogger + GenerateVSCode bool + PathToDesktopBinary string + PathToServerBinary string } // Template holds data relating to a template @@ -162,9 +166,24 @@ func Install(options *Options) error { } // Did the user want to install in current directory? - if options.TargetDir == "." { - // Yes - use cwd - options.TargetDir = cwd + if options.TargetDir == "" { + + // If the current directory is empty, use it + isEmpty, err := fs.DirIsEmpty(cwd) + if err != nil { + return err + } + + if isEmpty { + // Yes - use cwd + options.TargetDir = cwd + } else { + options.TargetDir = filepath.Join(cwd, options.ProjectName) + if fs.DirExists(options.TargetDir) { + return fmt.Errorf("cannot create project directory. Dir exists: %s", options.TargetDir) + } + } + } else { // Get the absolute path of the given directory targetDir, err := filepath.Abs(filepath.Join(cwd, options.TargetDir)) @@ -213,7 +232,11 @@ func Install(options *Options) error { return err } - // Calculate the directory name + err = generateIDEFiles(options) + if err != nil { + return err + } + return nil } @@ -243,3 +266,40 @@ func OutputList(logger *clilogger.CLILogger) error { table.Render() return nil } + +func generateIDEFiles(options *Options) error { + + if options.GenerateVSCode { + return generateVSCodeFiles(options) + } + + return nil +} + +func generateVSCodeFiles(options *Options) error { + + targetDir := filepath.Join(options.TargetDir, ".vscode") + sourceDir := fs.RelativePath(filepath.Join("./ides/vscode")) + + // Use Gosod to install the template + installer, err := gosod.TemplateDir(sourceDir) + if err != nil { + return err + } + + binaryName := filepath.Base(options.TargetDir) + if runtime.GOOS == "windows" { + // yay windows + binaryName += ".exe" + } + + options.PathToDesktopBinary = filepath.Join("build", runtime.GOOS, "desktop", binaryName) + options.PathToServerBinary = filepath.Join("build", runtime.GOOS, "server", binaryName) + + err = installer.Extract(targetDir, options) + if err != nil { + return err + } + + return nil +} diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go index e80008c29..2f0082cce 100644 --- a/v2/pkg/commands/build/build.go +++ b/v2/pkg/commands/build/build.go @@ -8,6 +8,7 @@ import ( "github.com/leaanthony/slicer" "github.com/wailsapp/wails/v2/internal/project" "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/parser" ) // Mode is the type used to indicate the build modes @@ -89,6 +90,13 @@ func Build(options *Options) (string, error) { // Initialise Builder builder.SetProjectData(projectData) + // Generate Frontend JS Package + outputLogger.Println(" - Generating Backend JS Package") + // Ignore the parser report coming back + _, err = parser.GenerateWailsFrontendPackage() + if err != nil { + return "", err + } if !options.IgnoreFrontend { outputLogger.Println(" - Building Wails Frontend") err = builder.BuildFrontend(outputLogger) @@ -115,6 +123,7 @@ func Build(options *Options) (string, error) { return "", err } outputLogger.Println("done.") + // Do we need to pack the app? if options.Pack { diff --git a/v2/pkg/commands/build/packager_windows.go b/v2/pkg/commands/build/packager_windows.go new file mode 100644 index 000000000..a64ba85c9 --- /dev/null +++ b/v2/pkg/commands/build/packager_windows.go @@ -0,0 +1,6 @@ +package build + +func packageApplication(options *Options) error { + // TBD + return nil +} diff --git a/v2/pkg/parser/applicationVariableName.go b/v2/pkg/parser/applicationVariableName.go new file mode 100644 index 000000000..de4252933 --- /dev/null +++ b/v2/pkg/parser/applicationVariableName.go @@ -0,0 +1,50 @@ +package parser + +import "go/ast" + +func (p *Package) getApplicationVariableName(file *ast.File, wailsImportName string) string { + + // Iterate through the whole file looking for the application name + applicationVariableName := "" + + // Inspect the file + ast.Inspect(file, func(n ast.Node) bool { + // Parse Assignments looking for application name + if assignStmt, ok := n.(*ast.AssignStmt); ok { + + // Check the RHS is of the form: + // `app := wails.CreateApp()` or + // `app := wails.CreateAppWithOptions` + for _, rhs := range assignStmt.Rhs { + ce, ok := rhs.(*ast.CallExpr) + if !ok { + continue + } + se, ok := ce.Fun.(*ast.SelectorExpr) + if !ok { + continue + } + i, ok := se.X.(*ast.Ident) + if !ok { + continue + } + // Have we found the wails import name? + if i.Name == wailsImportName { + // Check we are calling a function to create the app + if se.Sel.Name == "CreateApp" || se.Sel.Name == "CreateAppWithOptions" { + if len(assignStmt.Lhs) == 1 { + i, ok := assignStmt.Lhs[0].(*ast.Ident) + if ok { + // Found the app variable name + applicationVariableName = i.Name + return false + } + } + } + } + } + } + return true + }) + return applicationVariableName +} diff --git a/v2/pkg/parser/comments.go b/v2/pkg/parser/comments.go new file mode 100644 index 000000000..019e9880c --- /dev/null +++ b/v2/pkg/parser/comments.go @@ -0,0 +1,21 @@ +package parser + +import ( + "go/ast" + "strings" +) + +func parseComments(comments *ast.CommentGroup) []string { + var result []string + + if comments == nil { + return result + } + + for _, comment := range comments.List { + commentText := strings.TrimPrefix(comment.Text, "//") + result = append(result, commentText) + } + + return result +} diff --git a/v2/pkg/parser/conversion.go b/v2/pkg/parser/conversion.go new file mode 100644 index 000000000..99daa62f2 --- /dev/null +++ b/v2/pkg/parser/conversion.go @@ -0,0 +1,156 @@ +package parser + +import ( + "fmt" + "strings" + + "github.com/leaanthony/slicer" +) + +// JSType represents a javascript type +type JSType string + +const ( + // JsString is a JS string + JsString JSType = "string" + // JsBoolean is a JS bool + JsBoolean = "boolean" + // JsInt is a JS number + JsInt = "number" + // JsFloat is a JS number + JsFloat = "number" + // JsArray is a JS array + JsArray = "Array" + // JsObject is a JS object + JsObject = "Object" + // JsUnsupported represents a type that cannot be converted + JsUnsupported = "*" +) + +func goTypeToJS(input *Field) string { + switch input.Type { + case "string": + return "string" + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + return "number" + case "float32", "float64": + return "number" + case "bool": + return "boolean" + // case reflect.Array, reflect.Slice: + // return JsArray + // case reflect.Ptr, reflect.Struct, reflect.Map, reflect.Interface: + // return JsObject + case "struct": + return input.Struct.Name + default: + fmt.Printf("Unsupported input to goTypeToJS: %+v", input) + return "*" + } +} + +// goTypeToTS converts the given field into a Typescript type +// The pkgName is the package that the field is being output in. +// This is used to ensure we don't qualify local structs. +func goTypeToTS(input *Field, pkgName string) string { + var result string + switch input.Type { + case "string": + result = "string" + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + result = "number" + case "float32", "float64": + result = "number" + case "bool": + result = "boolean" + case "struct": + if input.Struct.Package.Name != "" { + if input.Struct.Package.Name != pkgName { + result = input.Struct.Package.Name + "." + } + } + result += input.Struct.Name + // case reflect.Array, reflect.Slice: + // return string(JsArray) + // case reflect.Ptr, reflect.Struct: + // fqt := input.Type().String() + // return strings.Split(fqt, ".")[1] + // case reflect.Map, reflect.Interface: + // return string(JsObject) + default: + fmt.Printf("Unsupported input to goTypeToTS: %+v", input) + return JsUnsupported + } + + if input.IsArray { + result = result + "[]" + } + + return result +} + +func goTypeToTSDeclaration(input *Field, pkgName string) string { + var result string + switch input.Type { + case "string": + result = "string" + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + result = "number" + case "float32", "float64": + result = "number" + case "bool": + result = "boolean" + case "struct": + if input.Struct.Package.Name != "" { + if input.Struct.Package.Name != pkgName { + result = `import("./_` + input.Struct.Package.Name + `").` + } + } + result += input.Struct.Name + // case reflect.Array, reflect.Slice: + // return string(JsArray) + // case reflect.Ptr, reflect.Struct: + // fqt := input.Type().String() + // return strings.Split(fqt, ".")[1] + // case reflect.Map, reflect.Interface: + // return string(JsObject) + default: + fmt.Printf("Unsupported input to goTypeToTS: %+v", input) + return JsUnsupported + } + + if input.IsArray { + result = result + "[]" + } + + return result +} + +func isUnresolvedType(typeName string) bool { + switch typeName { + case "string": + return false + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + return false + case "float32", "float64": + return false + case "bool": + return false + case "struct": + return false + default: + return true + } +} + +var reservedJSWords []string = []string{"abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var", "void", "volatile", "while", "with", "yield", "Array", "Date", "eval", "function", "hasOwnProperty", "Infinity", "isFinite", "isNaN", "isPrototypeOf", "length", "Math", "NaN", "Number", "Object", "prototype", "String", "toString", "undefined", "valueOf"} +var jsReservedWords *slicer.StringSlicer = slicer.String(reservedJSWords) + +func isJSReservedWord(input string) bool { + return jsReservedWords.Contains(input) +} + +func startsWithLowerCaseLetter(input string) bool { + firstLetter := string(input[0]) + return strings.ToLower(firstLetter) == firstLetter +} diff --git a/v2/pkg/parser/field.go b/v2/pkg/parser/field.go new file mode 100644 index 000000000..7ffd247a3 --- /dev/null +++ b/v2/pkg/parser/field.go @@ -0,0 +1,311 @@ +package parser + +import ( + "fmt" + "go/ast" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/fatih/structtag" +) + +// Field defines a parsed struct field +type Field struct { + + // Name of the field + Name string + + // The type of the field. + // "struct" if it's a struct + Type string + + // A pointer to the struct if the Type is "struct" + Struct *Struct + + // User comments on the field + Comments []string + + // Indicates if the Field is an array of type "Type" + IsArray bool + + // JSON field name defined by a json tag + JSONOptions +} + +type JSONOptions struct { + Name string + IsOptional bool + Ignored bool +} + +// JSType returns the Javascript type for this field +func (f *Field) JSType() string { + return string(goTypeToJS(f)) +} + +// JSName returns the Javascript name for this field +func (f *Field) JSName() string { + if f.JSONOptions.Name != "" { + return f.JSONOptions.Name + } + return f.Name +} + +// TSName returns the Typescript name for this field +func (f *Field) TSName() string { + result := f.Name + if f.JSONOptions.Name != "" { + result = f.JSONOptions.Name + } + if f.IsOptional { + result += "?" + } + return result +} + +// AsTSDeclaration returns a TS definition of a single type field +func (f *Field) AsTSDeclaration(pkgName string) string { + return f.TSName() + ": " + f.TypeAsTSType(pkgName) +} + +// NameForPropertyDoc returns a formatted name for the jsdoc @property declaration +func (f *Field) NameForPropertyDoc() string { + if f.IsOptional { + return "[" + f.JSName() + "]" + } + return f.JSName() +} + +// TypeForPropertyDoc returns a formatted name for the jsdoc @property declaration +func (f *Field) TypeForPropertyDoc() string { + result := goTypeToJS(f) + if f.IsArray { + result += "[]" + } + return result +} + +// TypeAsTSType converts the Field type to something TS wants +func (f *Field) TypeAsTSType(pkgName string) string { + var result = "" + switch f.Type { + case "string": + result = "string" + case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": + result = "number" + case "float32", "float64": + result = "number" + case "bool": + result = "boolean" + case "struct": + if f.Struct.Package != nil { + if f.Struct.Package.Name != pkgName { + result = f.Struct.Package.Name + "." + } + } + result = result + f.Struct.Name + default: + result = "any" + } + + return result +} + +func (p *Parser) parseField(file *ast.File, field *ast.Field, pkg *Package) ([]*Field, error) { + var result []*Field + + var fieldType string + var strct *Struct + var isArray bool + + var jsonOptions JSONOptions + + // Determine type + switch t := field.Type.(type) { + case *ast.Ident: + fieldType = t.Name + + unresolved := isUnresolvedType(fieldType) + + // Check if this type is actually a struct + if unresolved { + // Assume it is a struct + // Parse the struct + var err error + strct, err = p.parseStruct(pkg, t.Name) + if err != nil { + return nil, err + } + + if strct == nil { + fieldName := "" + if len(field.Names) > 0 { + fieldName = field.Names[0].Name + } + return nil, fmt.Errorf("unresolved type in field %s: %s", fieldName, fieldType) + } + + fieldType = "struct" + + } + case *ast.StarExpr: + fieldType = "struct" + packageName, structName, err := parseStructNameFromStarExpr(t) + if err != nil { + return nil, err + } + + // If this is an external package, find it + if packageName != "" { + referencedGoPackage := pkg.getImportByName(packageName, file) + referencedPackage := p.getPackageByID(referencedGoPackage.ID) + + // If we found the struct, save it as an external package reference + if referencedPackage != nil { + pkg.addExternalReference(referencedPackage) + } + + // We save this to pkg anyway, because we want to know if this package + // was NOT found + pkg = referencedPackage + } + + // If this is a package in our project, parse the struct! + if pkg != nil { + + // Parse the struct + strct, err = p.parseStruct(pkg, structName) + if err != nil { + return nil, err + } + + } + + case *ast.ArrayType: + isArray = true + // Parse the Elt (There must be a better way!) + switch t := t.Elt.(type) { + case *ast.Ident: + fieldType = t.Name + case *ast.StarExpr: + fieldType = "struct" + packageName, structName, err := parseStructNameFromStarExpr(t) + if err != nil { + return nil, err + } + + // If this is an external package, find it + if packageName != "" { + referencedGoPackage := pkg.getImportByName(packageName, file) + referencedPackage := p.getPackageByID(referencedGoPackage.ID) + + // If we found the struct, save it as an external package reference + if referencedPackage != nil { + pkg.addExternalReference(referencedPackage) + } + + // We save this to pkg anyway, because we want to know if this package + // was NOT found + pkg = referencedPackage + } + + // If this is a package in our project, parse the struct! + if pkg != nil { + + // Parse the struct + strct, err = p.parseStruct(pkg, structName) + if err != nil { + return nil, err + } + + } + default: + // We will default to "Array" for eg nested arrays + fieldType = "any" + } + + default: + spew.Dump(t) + return nil, fmt.Errorf("unsupported field found in struct: %+v", t) + } + + // Parse json tag if available + if field.Tag != nil { + err := parseJSONOptions(field.Tag.Value, &jsonOptions) + if err != nil { + return nil, err + } + } + + // Loop over names if we have + if len(field.Names) > 0 { + + for _, name := range field.Names { + + // TODO: Check field names are valid in JS + if isJSReservedWord(name.Name) { + return nil, fmt.Errorf("unable to use field name %s - reserved word in Javascript", name.Name) + } + + // Create a field per name + thisField := &Field{ + Comments: parseComments(field.Doc), + } + thisField.Name = name.Name + thisField.Type = fieldType + thisField.Struct = strct + thisField.IsArray = isArray + thisField.JSONOptions = jsonOptions + + result = append(result, thisField) + } + return result, nil + } + + // When we have no name + thisField := &Field{ + Comments: parseComments(field.Doc), + } + thisField.Type = fieldType + thisField.Struct = strct + thisField.IsArray = isArray + result = append(result, thisField) + + return result, nil +} + +func parseJSONOptions(fieldTag string, jsonOptions *JSONOptions) error { + + // Remove backticks + fieldTag = strings.Trim(fieldTag, "`") + + // Parse the tag + tags, err := structtag.Parse(fieldTag) + if err != nil { + return err + } + + jsonTag, err := tags.Get("json") + if err != nil { + return err + } + + if jsonTag == nil { + return nil + } + + // Save the name + jsonOptions.Name = jsonTag.Name + + // Check if this field is ignored + if jsonTag.Name == "-" { + jsonOptions.Ignored = true + } + + // Check if this field is optional + if jsonTag.HasOption("omitempty") { + jsonOptions.IsOptional = true + } + + return nil +} diff --git a/v2/pkg/parser/findBoundStructs.go b/v2/pkg/parser/findBoundStructs.go new file mode 100644 index 000000000..91876a8b2 --- /dev/null +++ b/v2/pkg/parser/findBoundStructs.go @@ -0,0 +1,152 @@ +package parser + +import ( + "fmt" + "go/ast" +) + +// findBoundStructs will search through the Wails project looking +// for which structs have been bound using the `Bind()` method +func (p *Parser) findBoundStructs(pkg *Package) error { + + // Iterate through the files in the package looking for the bound structs + for _, fileAst := range pkg.Gopackage.Syntax { + + // Find the wails import name + wailsImportName := pkg.getWailsImportName(fileAst) + + // If this file doesn't import wails, continue + if wailsImportName == "" { + continue + } + + applicationVariableName := pkg.getApplicationVariableName(fileAst, wailsImportName) + if applicationVariableName == "" { + continue + } + + var parseError error + + ast.Inspect(fileAst, func(n ast.Node) bool { + // Parse Call expressions looking for bind calls + callExpr, ok := n.(*ast.CallExpr) + if !ok { + return true + } + // Check this is the right kind of expression (something.something()) + f, ok := callExpr.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + ident, ok := f.X.(*ast.Ident) + if !ok { + return true + } + + if ident.Name != applicationVariableName { + return true + } + + if f.Sel.Name != "Bind" { + return true + } + + if len(callExpr.Args) != 1 { + return true + } + + // Work out what was bound + switch boundItem := callExpr.Args[0].(type) { + + // app.Bind( someFunction() ) + case *ast.CallExpr: + switch fn := boundItem.Fun.(type) { + case *ast.Ident: + // boundStructs = append(boundStructs, newStruct(pkg.Name, fn.Name)) + strct, err := p.getFunctionReturnType(pkg, fn.Name) + if err != nil { + parseError = err + return false + } + if strct == nil { + parseError = fmt.Errorf("unable to resolve function returntype: %s", fn.Name) + return false + } + strct.Package.boundStructs.Add(strct.Name) + case *ast.SelectorExpr: + ident, ok := fn.X.(*ast.Ident) + if !ok { + return true + } + packageName := ident.Name + functionName := fn.Sel.Name + println("Found bound function:", packageName+"."+functionName) + + // Get package for package name + externalPackageName := pkg.getImportByName(packageName, fileAst) + externalPackage := p.getPackageByID(externalPackageName.ID) + + strct, err := p.getFunctionReturnType(externalPackage, functionName) + if err != nil { + parseError = err + return false + } + if strct == nil { + // Unable to resolve function + return true + } + externalPackage.boundStructs.Add(strct.Name) + } + + // Binding struct pointer literals + case *ast.UnaryExpr: + + if boundItem.Op.String() != "&" { + return true + } + + cl, ok := boundItem.X.(*ast.CompositeLit) + if !ok { + return true + } + + switch boundStructExp := cl.Type.(type) { + + // app.Bind( &myStruct{} ) + case *ast.Ident: + pkg.boundStructs.Add(boundStructExp.Name) + + // app.Bind( &mypackage.myStruct{} ) + case *ast.SelectorExpr: + var structName = "" + var packageName = "" + switch x := boundStructExp.X.(type) { + case *ast.Ident: + packageName = x.Name + default: + // TODO: Save these warnings + // println("Identifier in binding not supported:") + return true + } + structName = boundStructExp.Sel.Name + referencedPackage := pkg.getImportByName(packageName, fileAst) + packageWrapper := p.getPackageByID(referencedPackage.ID) + packageWrapper.boundStructs.Add(structName) + } + + default: + // TODO: Save these warnings + // println("Unsupported bind expression:") + // spew.Dump(boundItem) + } + + return true + }) + + if parseError != nil { + return parseError + } + } + + return nil +} diff --git a/v2/pkg/parser/generate.go b/v2/pkg/parser/generate.go new file mode 100644 index 000000000..66518704c --- /dev/null +++ b/v2/pkg/parser/generate.go @@ -0,0 +1,248 @@ +package parser + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "text/template" + + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/fs" +) + +// GenerateWailsFrontendPackage will generate a Javascript/Typescript +// package in `/frontend/wails` that defines which methods +// and structs are bound to your frontend +func GenerateWailsFrontendPackage() (*ParserReport, error) { + + dir, err := os.Getwd() + if err != nil { + return nil, err + } + + p := NewParser() + + err = p.ParseProject(dir) + if err != nil { + return nil, err + } + + err = p.generateModule() + + return p.parserReport(), err +} + +func (p *Parser) generateModule() error { + + moduleDir, err := createBackendJSDirectory() + if err != nil { + return err + } + + packagesToGenerate := p.packagesToGenerate() + + for _, pkg := range packagesToGenerate { + + err := generatePackage(pkg, moduleDir) + if err != nil { + return err + } + } + + // Copy the standard files + srcFile := fs.RelativePath("./package.json") + tgtFile := filepath.Join(moduleDir, "package.json") + err = fs.CopyFile(srcFile, tgtFile) + if err != nil { + return err + } + + // Generate the globals.d.ts file + err = generateGlobalsTS(moduleDir, packagesToGenerate) + if err != nil { + return err + } + + // Generate the index.js file + err = generateIndexJS(moduleDir, packagesToGenerate) + if err != nil { + return err + } + // Generate the index.d.ts file + err = generateIndexTS(moduleDir, packagesToGenerate) + if err != nil { + return err + } + + return nil +} + +func createBackendJSDirectory() (string, error) { + + // Calculate the package directory + // Note this is *always* called from the project directory + // so using paths relative to CWD is fine + dir, err := fs.RelativeToCwd("./frontend/backend") + if err != nil { + return "", errors.Wrap(err, "Error creating backend module directory") + } + + // Remove directory if it exists - REGENERATION! + err = os.RemoveAll(dir) + if err != nil { + return "", errors.Wrap(err, "Error removing module directory") + } + + // Make the directory + err = fs.Mkdir(dir) + + return dir, err +} + +func generatePackage(pkg *Package, moduledir string) error { + + // Get path to local file + typescriptTemplateFile := fs.RelativePath("./package.d.template") + + // Load typescript template + typescriptTemplateData := fs.MustLoadString(typescriptTemplateFile) + typescriptTemplate, err := template.New("typescript").Parse(typescriptTemplateData) + if err != nil { + return errors.Wrap(err, "Error creating template") + } + + // Execute javascript template + var buffer bytes.Buffer + err = typescriptTemplate.Execute(&buffer, pkg) + if err != nil { + return errors.Wrap(err, "Error generating code") + } + + // Save typescript file + err = ioutil.WriteFile(filepath.Join(moduledir, "_"+pkg.Name+".d.ts"), buffer.Bytes(), 0755) + if err != nil { + return errors.Wrap(err, "Error writing backend package file") + } + + // Get path to local file + javascriptTemplateFile := fs.RelativePath("./package.template") + + // Load javascript template + javascriptTemplateData := fs.MustLoadString(javascriptTemplateFile) + javascriptTemplate, err := template.New("javascript").Parse(javascriptTemplateData) + if err != nil { + return errors.Wrap(err, "Error creating template") + } + + // Reset the buffer + buffer.Reset() + + err = javascriptTemplate.Execute(&buffer, pkg) + if err != nil { + return errors.Wrap(err, "Error generating code") + } + + // Save javascript file + err = ioutil.WriteFile(filepath.Join(moduledir, "_"+pkg.Name+".js"), buffer.Bytes(), 0755) + if err != nil { + return errors.Wrap(err, "Error writing backend package file") + } + + return nil +} + +func generateIndexJS(dir string, packages []*Package) error { + + // Get path to local file + templateFile := fs.RelativePath("./index.template") + + // Load template + templateData := fs.MustLoadString(templateFile) + packagesTemplate, err := template.New("index").Parse(templateData) + if err != nil { + return errors.Wrap(err, "Error creating template") + } + + // Execute template + var buffer bytes.Buffer + err = packagesTemplate.Execute(&buffer, packages) + if err != nil { + return errors.Wrap(err, "Error generating code") + } + + // Calculate target filename + indexJS := filepath.Join(dir, "index.js") + + err = ioutil.WriteFile(indexJS, buffer.Bytes(), 0755) + if err != nil { + return errors.Wrap(err, "Error writing backend package index.js file") + } + + return nil +} +func generateIndexTS(dir string, packages []*Package) error { + + // Get path to local file + templateFile := fs.RelativePath("./index.d.template") + + // Load template + templateData := fs.MustLoadString(templateFile) + indexTSTemplate, err := template.New("index.d").Parse(templateData) + if err != nil { + return errors.Wrap(err, "Error creating template") + } + + // Execute template + var buffer bytes.Buffer + err = indexTSTemplate.Execute(&buffer, packages) + if err != nil { + return errors.Wrap(err, "Error generating code") + } + + // Calculate target filename + indexJS := filepath.Join(dir, "index.d.ts") + + err = ioutil.WriteFile(indexJS, buffer.Bytes(), 0755) + if err != nil { + return errors.Wrap(err, "Error writing backend package index.d.ts file") + } + + return nil +} + +func generateGlobalsTS(dir string, packages []*Package) error { + + // Get path to local file + templateFile := fs.RelativePath("./globals.d.template") + + // Load template + templateData := fs.MustLoadString(templateFile) + packagesTemplate, err := template.New("globals").Parse(templateData) + if err != nil { + return errors.Wrap(err, "Error creating template") + } + + // Execute template + var buffer bytes.Buffer + err = packagesTemplate.Execute(&buffer, packages) + if err != nil { + return errors.Wrap(err, "Error generating code") + } + + // Calculate target filename + indexJS := filepath.Join(dir, "globals.d.ts") + + err = ioutil.WriteFile(indexJS, buffer.Bytes(), 0755) + if err != nil { + return errors.Wrap(err, "Error writing backend package globals.d.ts file") + } + + return nil +} + +func (p *Parser) parserReport() *ParserReport { + return &ParserReport{ + Packages: p.packagesToGenerate(), + } +} diff --git a/v2/pkg/parser/getFunctionReturnType.go b/v2/pkg/parser/getFunctionReturnType.go new file mode 100644 index 000000000..f5f2c6cd9 --- /dev/null +++ b/v2/pkg/parser/getFunctionReturnType.go @@ -0,0 +1,69 @@ +package parser + +import ( + "fmt" + "go/ast" +) + +func (p *Parser) getFunctionReturnType(pkg *Package, functionName string) (*Struct, error) { + + var result *Struct + + // Iterate through the files in the package looking for the bound structs + for _, fileAst := range pkg.Gopackage.Syntax { + + var parseError error + + ast.Inspect(fileAst, func(n ast.Node) bool { + // Parse Call expressions looking for bind calls + funcDecl, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + + if funcDecl.Name.Name == functionName { + result, parseError = p.parseFunctionReturnType(fileAst, funcDecl, pkg) + return false + } + + return true + }) + + if parseError != nil { + return nil, parseError + } + + if result != nil { + return result, nil + } + } + + return result, nil +} + +func (p *Parser) parseFunctionReturnType(file *ast.File, funcDecl *ast.FuncDecl, pkg *Package) (*Struct, error) { + + var result *Struct + + if funcDecl.Type.Results == nil { + return nil, fmt.Errorf("bound function %s has no return values", funcDecl.Name.Name) + } + + // We expect only 1 return value for a function return + if len(funcDecl.Type.Results.List) > 1 { + return nil, fmt.Errorf("bound function %s has more than 1 return value", funcDecl.Name.Name) + } + + parsedFields, err := p.parseField(file, funcDecl.Type.Results.List[0], pkg) + if err != nil { + return nil, err + } + + if len(parsedFields) > 1 { + return nil, fmt.Errorf("bound function %s has more than 1 return value", funcDecl.Name.Name) + } + + result = parsedFields[0].Struct + + return result, nil +} diff --git a/v2/pkg/parser/globals.d.template b/v2/pkg/parser/globals.d.template new file mode 100644 index 000000000..2309600a4 --- /dev/null +++ b/v2/pkg/parser/globals.d.template @@ -0,0 +1,27 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +interface Window { + + backend: { + + {{- range . }}{{$packageName:=.Name}} + {{- if .HasBoundStructs }} + {{ $packageName }}: { + {{- range .Structs }} + {{- if .IsBound }} + {{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{end}} + {{.Name}}: { + {{range .Methods}} + {{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{end}} + {{.Name}}: ({{.InputsAsTSText $packageName}}) => Promise<{{.OutputsAsTSText $packageName}}>, + {{end}} + } + {{- end}} + {{- end}} + } + {{- end}} + {{- end}} + } +} \ No newline at end of file diff --git a/v2/pkg/parser/index.d.template b/v2/pkg/parser/index.d.template new file mode 100644 index 000000000..f48417d09 --- /dev/null +++ b/v2/pkg/parser/index.d.template @@ -0,0 +1,7 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +{{- range .}} +export const {{.Name}}: typeof import("./_{{.Name}}"); +{{- end}} diff --git a/v2/pkg/parser/index.template b/v2/pkg/parser/index.template new file mode 100644 index 000000000..f1801e04e --- /dev/null +++ b/v2/pkg/parser/index.template @@ -0,0 +1,13 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +{{- range .}} +const {{.Name}} = require('./_{{.Name}}'); +{{- end}} + +module.exports = { + {{- range .}} + {{.Name}}: {{.Name}}, + {{- end}} +} \ No newline at end of file diff --git a/v2/pkg/parser/method.go b/v2/pkg/parser/method.go new file mode 100644 index 000000000..e9602c543 --- /dev/null +++ b/v2/pkg/parser/method.go @@ -0,0 +1,179 @@ +package parser + +import ( + "fmt" + "go/ast" + "strings" +) + +// Method defines a struct method +type Method struct { + Name string + Comments []string + Inputs []*Field + Returns []*Field +} + +func (p *Parser) parseStructMethods(boundStruct *Struct) error { + + for _, fileAst := range boundStruct.Package.Gopackage.Syntax { + + // Track errors + var parseError error + + ast.Inspect(fileAst, func(n ast.Node) bool { + + if funcDecl, ok := n.(*ast.FuncDecl); ok { + + if funcDecl.Recv == nil { + return true + } + + // This is a struct method + for _, field := range funcDecl.Recv.List { + switch f := field.Type.(type) { + case *ast.StarExpr: + // This is a struct pointer method + ident, ok := f.X.(*ast.Ident) // _ ? + if !ok { + continue + } + + // Check this method is for this struct + if ident.Name != boundStruct.Name { + continue + } + + // We want to ignore Internal functions + if funcDecl.Name.Name == "WailsInit" || funcDecl.Name.Name == "WailsShutdown" { + continue + } + + // If this method is not Public, ignore + if string(funcDecl.Name.Name[0]) != strings.ToUpper((string(funcDecl.Name.Name[0]))) { + continue + } + + // Create our struct + structMethod := &Method{ + Name: funcDecl.Name.Name, + Comments: parseComments(funcDecl.Doc), + } + + // Save the input parameters + if funcDecl.Type.Params != nil { + for _, inputField := range funcDecl.Type.Params.List { + fields, err := p.parseField(fileAst, inputField, boundStruct.Package) + if err != nil { + parseError = err + return false + } + + // If this field was a struct, flag that it is used as data + if len(fields) > 0 { + if fields[0].Struct != nil { + fields[0].Struct.IsUsedAsData = true + } + } + + structMethod.Inputs = append(structMethod.Inputs, fields...) + } + } + + // Save the output parameters + if funcDecl.Type.Results != nil { + for _, outputField := range funcDecl.Type.Results.List { + fields, err := p.parseField(fileAst, outputField, boundStruct.Package) + if err != nil { + parseError = err + return false + } + + // If this field was a struct, flag that it is used as data + if len(fields) > 0 { + if fields[0].Struct != nil { + fields[0].Struct.IsUsedAsData = true + } + } + + structMethod.Returns = append(structMethod.Returns, fields...) + } + } + + // Append this method to the parsed struct + boundStruct.Methods = append(boundStruct.Methods, structMethod) + + default: + // Unsupported + continue + } + } + } + return true + }) + + // If we got an error, return it + if parseError != nil { + return parseError + } + } + + return nil +} + +// InputsAsTSText generates a string with the method inputs +// formatted in a way acceptable to Typescript +func (m *Method) InputsAsTSText(pkgName string) string { + var inputs []string + + for _, input := range m.Inputs { + inputText := fmt.Sprintf("%s: %s", input.Name, goTypeToTS(input, pkgName)) + inputs = append(inputs, inputText) + } + + return strings.Join(inputs, ", ") +} + +// OutputsAsTSText generates a string with the method inputs +// formatted in a way acceptable to Javascript +func (m *Method) OutputsAsTSText(pkgName string) string { + + if len(m.Returns) == 0 { + return "void" + } + + var result []string + + for _, output := range m.Returns { + result = append(result, goTypeToTS(output, pkgName)) + } + return strings.Join(result, ", ") +} + +// OutputsAsTSDeclarationText generates a string with the method inputs +// formatted in a way acceptable to Javascript +func (m *Method) OutputsAsTSDeclarationText(pkgName string) string { + + if len(m.Returns) == 0 { + return "void" + } + + var result []string + + for _, output := range m.Returns { + result = append(result, goTypeToTSDeclaration(output, pkgName)) + } + return strings.Join(result, ", ") +} + +// InputsAsJSText generates a string with the method inputs +// formatted in a way acceptable to Javascript +func (m *Method) InputsAsJSText() string { + var inputs []string + + for _, input := range m.Inputs { + inputs = append(inputs, input.Name) + } + + return strings.Join(inputs, ", ") +} diff --git a/v2/pkg/parser/package.d copy.template b/v2/pkg/parser/package.d copy.template new file mode 100644 index 000000000..5206b9621 --- /dev/null +++ b/v2/pkg/parser/package.d copy.template @@ -0,0 +1,31 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +{{- if .DeclarationReferences }} +{{range .DeclarationReferences}} +/// {{end}}{{- end}} + +export namespace {{.Name}} { {{range .Structs}} + {{- if or .IsBound .IsUsedAsData}} + {{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{- end}} + interface {{.Name}} { {{ if .IsUsedAsData }} + {{- range .Fields}}{{if .Comments }} + {{range .Comments}}//{{ . }}{{end}}{{- end}} + {{.Name}}: {{.TypeAsTSType $.Name}}; {{- end}} {{ end }} + {{- if .IsBound }} + {{- range .Methods}} + /**{{if .Comments }} +{{range .Comments}} * {{ . }}{{end}} + *{{end}} + * @function {{.Name}} +{{range .Inputs}} * @param {{"{"}}{{.JSType}}{{"}"}} {{.Name}} +{{end}} * + * @returns {Promise<{{.OutputsAsTSText $.Name}}>} + */ + {{.Name}}({{.InputsAsTSText $.Name}}): Promise<{{.OutputsAsTSText $.Name}}>; + {{- end}}{{end}} + }{{- end}} + {{end}} + +} + diff --git a/v2/pkg/parser/package.d.template b/v2/pkg/parser/package.d.template new file mode 100644 index 000000000..6eccdb0fe --- /dev/null +++ b/v2/pkg/parser/package.d.template @@ -0,0 +1,42 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +{{- range .Structs}} +{{- if .IsBound}} +export namespace {{.Name}} { + {{- range .Methods}} + {{- if .Comments }} + {{range .Comments}} + // {{ . }}{{end}} + {{- end}} + function {{.Name}}({{.InputsAsTSText $.Name}}): Promise<{{.OutputsAsTSDeclarationText $.Name}}>; + {{- end}} +} +{{- end}} +{{- if .IsUsedAsData}} +{{if .Comments }} +/** +{{range .Comments}} *{{ . }}{{end}} + */ +export type {{.Name}} = { +{{- range .Fields}} + {{- if not .Ignored}} + {{- if .Comments }}{{range .Comments}} + //{{ . }}{{end}}{{- end}} + {{ .AsTSDeclaration $.Name}}; {{- end}} +{{- end}} +}; + +/** +{{if .Comments }}{{range .Comments}} *{{ . }}{{end}}{{end}} + * @typedef {object} {{.Name}} +{{- range .Fields}}{{- if not .JSONOptions.Ignored }} + * @property {{"{"}}{{.TypeForPropertyDoc}}{{"}"}} {{.NameForPropertyDoc}} {{- if .Comments}} - {{- range .Comments}}{{ . }}{{- end}}{{- end}}{{- end}} +{{- end}} + */ +export var {{.Name}}: any; + +{{- end}} +{{- end}} +{{- end}} diff --git a/v2/pkg/parser/package.go b/v2/pkg/parser/package.go new file mode 100644 index 000000000..e9357efd0 --- /dev/null +++ b/v2/pkg/parser/package.go @@ -0,0 +1,152 @@ +package parser + +import ( + "go/ast" + "strings" + + "github.com/leaanthony/slicer" + "golang.org/x/tools/go/packages" +) + +// Package is a wrapper around the go parsed package +type Package struct { + + // A unique Name for this package. + // This is calculated and may not be the same as the one + // defined in Go - but that's ok! + Name string + + // the package we are wrapping + Gopackage *packages.Package + + // a list of struct names that are bound in this package + boundStructs slicer.StringSlicer + + // Structs used in this package + parsedStructs map[string]*Struct + + // A list of external packages we reference from this package + externalReferences slicer.InterfaceSlicer +} + +func newPackage(pkg *packages.Package) *Package { + return &Package{ + Gopackage: pkg, + parsedStructs: make(map[string]*Struct), + } +} + +func (p *Package) getWailsImportName(file *ast.File) string { + // Scan the imports for the wails v2 import + for _, details := range file.Imports { + if details.Path.Value == `"github.com/wailsapp/wails/v2"` { + if details.Name != nil { + return details.Name.Name + } + + // Get the import name from the package + imp := p.getImportByPath("github.com/wailsapp/wails/v2") + if imp != nil { + return imp.Name + } + } + } + return "" +} + +func (p *Package) getImportByName(importName string, file *ast.File) *packages.Package { + + // Check if the file has aliased the import + for _, imp := range file.Imports { + if imp.Name != nil { + if imp.Name.Name == importName { + // Yes it has. Get the import by path + return p.getImportByPath(imp.Path.Value) + } + } + } + + // We need to find which package import has this name + for _, imp := range p.Gopackage.Imports { + if imp.Name == importName { + return imp + } + } + + // Looks like this package is outside the project... + return nil +} + +func (p *Package) getImportByPath(packagePath string) *packages.Package { + packagePath = strings.Trim(packagePath, "\"") + return p.Gopackage.Imports[packagePath] +} + +func (p *Package) getStruct(structName string) *Struct { + return p.parsedStructs[structName] +} + +func (p *Package) addStruct(strct *Struct) { + p.parsedStructs[strct.Name] = strct +} + +// HasBoundStructs returns true if any of its structs +// are bound +func (p *Package) HasBoundStructs() bool { + + for _, strct := range p.parsedStructs { + if strct.IsBound { + return true + } + } + + return false +} + +// HasDataStructs returns true if any of its structs +// are used as data +func (p *Package) HasDataStructs() bool { + for _, strct := range p.parsedStructs { + if strct.IsUsedAsData { + return true + } + } + + return false +} + +// ShouldGenerate returns true when this package should be generated +func (p *Package) ShouldGenerate() bool { + return p.HasBoundStructs() || p.HasDataStructs() +} + +// DeclarationReferences returns a list of external packages +// we reference from this package +func (p *Package) DeclarationReferences() []string { + + var referenceNames slicer.StringSlicer + + // Generics can't come soon enough! + p.externalReferences.Each(func(p interface{}) { + referenceNames.Add(p.(*Package).Name) + }) + + return referenceNames.AsSlice() +} + +// addExternalReference saves the given package as an external reference +func (p *Package) addExternalReference(pkg *Package) { + p.externalReferences.AddUnique(pkg) +} + +// Structs returns the structs that we want to generate +func (p *Package) Structs() []*Struct { + + var result []*Struct + + for _, elem := range p.parsedStructs { + result = append(result, elem) + } + + return result +} diff --git a/v2/pkg/parser/package.json b/v2/pkg/parser/package.json new file mode 100644 index 000000000..ab37717c7 --- /dev/null +++ b/v2/pkg/parser/package.json @@ -0,0 +1,13 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "Auto generated module wrapping your Wails backend", + "main": "index.js", + "types": "index.d.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/v2/pkg/parser/package.template b/v2/pkg/parser/package.template new file mode 100644 index 000000000..2c7362df1 --- /dev/null +++ b/v2/pkg/parser/package.template @@ -0,0 +1,44 @@ +// @ts-check +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +{{- if .DeclarationReferences }} +{{range .DeclarationReferences}} +const {{.}} = require('./_{{.}}');{{end}}{{- end}} + +{{- range $struct := .Structs }} +{{- if .IsUsedAsData }} + +/** +{{if .Comments }}{{range .Comments}} *{{ . }}{{end}}{{end}} + * @typedef {object} {{.Name}} +{{- range .Fields}}{{- if not .JSONOptions.Ignored }} + * @property {{"{"}}{{.TypeForPropertyDoc}}{{"}"}} {{.NameForPropertyDoc}} {{- if .Comments}} - {{- range .Comments}}{{ . }}{{- end}}{{- end}}{{- end}} +{{- end}} + */ +export var {{.Name}}; + +{{- end}} +{{- if .IsBound }} +{{- if .Methods }} + +{{if .Comments }}{{range .Comments}}// {{ . }}{{end}}{{end}} +export const {{.Name}} = { +{{range .Methods }} + /**{{if .Comments }} +{{range .Comments}} * {{ . }}{{end}} + *{{end}} + * @function {{.Name}} +{{range .Inputs}} * @param {{"{"}}{{.JSType}}{{"}"}} {{.Name}} +{{end}} * + * @returns {Promise<{{.OutputsAsTSText $.Name}}>} + */ + {{.Name}}: function({{.InputsAsJSText}}) { + return window.backend.{{$.Name}}.{{$struct.Name}}.{{.Name}}({{.InputsAsJSText}}); + }, +{{end}} +} + +{{- end}} +{{- end}} +{{- end}} diff --git a/v2/pkg/parser/parseBoundStructs.go b/v2/pkg/parser/parseBoundStructs.go new file mode 100644 index 000000000..c26839433 --- /dev/null +++ b/v2/pkg/parser/parseBoundStructs.go @@ -0,0 +1,75 @@ +package parser + +import "go/ast" + +func (p *Parser) parseBoundStructs(pkg *Package) error { + + // Loop over the bound structs + for _, structName := range pkg.boundStructs.AsSlice() { + strct, err := p.parseStruct(pkg, structName) + if err != nil { + return err + } + strct.IsBound = true + } + + return nil +} + +// ParseStruct will attempt to parse the given struct using +// the package it references +func (p *Parser) parseStruct(pkg *Package, structName string) (*Struct, error) { + + // Check the parser cache for this struct + result := pkg.getStruct(structName) + if result != nil { + return result, nil + } + + // Iterate through the whole package looking for the bound structs + for _, fileAst := range pkg.Gopackage.Syntax { + + // Track errors + var parseError error + + ast.Inspect(fileAst, func(n ast.Node) bool { + if genDecl, ok := n.(*ast.GenDecl); ok { + for _, spec := range genDecl.Specs { + if typeSpec, ok := spec.(*ast.TypeSpec); ok { + if structType, ok := typeSpec.Type.(*ast.StructType); ok { + structDefinitionName := typeSpec.Name.Name + if structDefinitionName == structName { + + // Create the new struct + result = &Struct{Name: structName, Package: pkg} + + // Save comments + result.Comments = parseComments(genDecl.Doc) + + parseError = p.parseStructMethods(result) + if parseError != nil { + return false + } + + // Parse the struct fields + parseError = p.parseStructFields(fileAst, structType, result) + + // Save this struct + pkg.addStruct(result) + + return false + } + } + } + } + } + return true + }) + + // If we got an error, return it + if parseError != nil { + return nil, parseError + } + } + return result, nil +} diff --git a/v2/pkg/parser/parseStructFields.go b/v2/pkg/parser/parseStructFields.go new file mode 100644 index 000000000..cdca538d1 --- /dev/null +++ b/v2/pkg/parser/parseStructFields.go @@ -0,0 +1,35 @@ +package parser + +import ( + "go/ast" + + "github.com/pkg/errors" +) + +func (p *Parser) parseStructFields(fileAst *ast.File, structType *ast.StructType, boundStruct *Struct) error { + + // Parse the fields + for _, field := range structType.Fields.List { + fields, err := p.parseField(fileAst, field, boundStruct.Package) + if err != nil { + return errors.Wrap(err, "error parsing struct "+boundStruct.Name) + } + + // If this field was a struct, flag that it is used as data + if len(fields) > 0 { + if fields[0].Struct != nil { + fields[0].Struct.IsUsedAsData = true + } + } + + // If this field name is lowercase, it won't be exported + for _, field := range fields { + if !startsWithLowerCaseLetter(field.Name) { + boundStruct.Fields = append(boundStruct.Fields, field) + } + } + + } + + return nil +} diff --git a/v2/pkg/parser/parser.go b/v2/pkg/parser/parser.go new file mode 100644 index 000000000..5b49b42da --- /dev/null +++ b/v2/pkg/parser/parser.go @@ -0,0 +1,122 @@ +// Package parser provides the ability to parse the data that is bound in Wails projects. +// Using this, it can also generate a Javascript module that represents the DTOs used, as +// well as providing wrappers for bound methods. +package parser + +import ( + "go/token" + + "github.com/pkg/errors" + "golang.org/x/tools/go/packages" +) + +// Parser is the Wails project parser +type Parser struct { + + // Placeholders for Go's parser + fileSet *token.FileSet + + // The packages we parse + // The map key is the package ID + packages map[string]*Package +} + +// NewParser creates a new Wails project parser +func NewParser() *Parser { + return &Parser{ + fileSet: token.NewFileSet(), + packages: make(map[string]*Package), + } +} + +// ParseProject will parse the Wails project in the given directory +func (p *Parser) ParseProject(dir string) error { + + var err error + + err = p.loadPackages(dir) + if err != nil { + return err + } + + // Find all the bound structs + for _, pkg := range p.packages { + err = p.findBoundStructs(pkg) + if err != nil { + return err + } + } + + // Parse the structs + for _, pkg := range p.packages { + err = p.parseBoundStructs(pkg) + if err != nil { + return err + } + } + + // Resolve package names + // We do this because some packages may have the same name + p.resolvePackageNames() + + return nil +} + +func (p *Parser) loadPackages(projectPath string) error { + mode := packages.NeedName | + packages.NeedFiles | + packages.NeedSyntax | + packages.NeedTypes | + packages.NeedImports | + packages.NeedTypesInfo | + packages.NeedModule + + cfg := &packages.Config{Fset: p.fileSet, Mode: mode, Dir: projectPath} + pkgs, err := packages.Load(cfg, "./...") + if err != nil { + return errors.Wrap(err, "Problem loading packages") + } + // Check for errors + var parseError error + for _, pkg := range pkgs { + for _, err := range pkg.Errors { + if parseError == nil { + parseError = errors.New(err.Error()) + } else { + parseError = errors.Wrap(parseError, err.Error()) + } + } + } + + if parseError != nil { + return parseError + } + + // Create a map of packages + for _, pkg := range pkgs { + p.packages[pkg.ID] = newPackage(pkg) + } + + return nil +} + +func (p *Parser) getPackageByID(id string) *Package { + return p.packages[id] +} + +func (p *Parser) packagesToGenerate() []*Package { + + var result []*Package + + for _, pkg := range p.packages { + if pkg.ShouldGenerate() { + result = append(result, pkg) + } + } + + return result +} + +type ParserReport struct { + Packages []*Package +} diff --git a/v2/pkg/parser/resolvePackageReferences.go b/v2/pkg/parser/resolvePackageReferences.go new file mode 100644 index 000000000..081b3f8cb --- /dev/null +++ b/v2/pkg/parser/resolvePackageReferences.go @@ -0,0 +1,35 @@ +package parser + +import ( + "fmt" + + "github.com/leaanthony/slicer" +) + +// resolvePackageNames will deterine the names for the packages, allowing +// us to create a flat structure for the imports in the frontend module +func (p *Parser) resolvePackageNames() { + + // A cache for the names + var packageNameCache slicer.StringSlicer + + // Process each package + for _, pkg := range p.packages { + pkgName := pkg.Gopackage.Name + + // Check for collision + if packageNameCache.Contains(pkgName) { + // https://www.youtube.com/watch?v=otNNGROI0Cs !!!!! + + // We start at 2 because having both "pkg" and "pkg1" is 🙄 + count := 2 + for ok := true; ok; ok = packageNameCache.Contains(pkgName) { + pkgName = fmt.Sprintf("%s%d", pkg.Gopackage.Name, count) + } + } + + // Save the name! + packageNameCache.Add(pkgName) + pkg.Name = pkgName + } +} diff --git a/v2/pkg/parser/struct.go b/v2/pkg/parser/struct.go new file mode 100644 index 000000000..6108309bd --- /dev/null +++ b/v2/pkg/parser/struct.go @@ -0,0 +1,68 @@ +package parser + +import ( + "fmt" + "go/ast" + + "github.com/pkg/errors" +) + +// Struct represents a struct that is used by the frontend +// in a Wails project +type Struct struct { + + // The name of the struct + Name string + + // The package this was declared in + Package *Package + + // Comments for the struct + Comments []string + + // The fields used in this struct + Fields []*Field + + // The methods available to the front end + Methods []*Method + + // Indicates if this struct is bound to the app + IsBound bool + + // Indicates if this struct is used as data + IsUsedAsData bool +} + +func parseStructNameFromStarExpr(starExpr *ast.StarExpr) (string, string, error) { + pkg := "" + name := "" + // Determine the FQN + switch x := starExpr.X.(type) { + case *ast.SelectorExpr: + switch i := x.X.(type) { + case *ast.Ident: + pkg = i.Name + default: + // TODO: Store warnings? + return "", "", errors.WithStack(fmt.Errorf("unknown type in selector for *ast.SelectorExpr: %+v", i)) + } + + name = x.Sel.Name + + // TODO: IS this used? + case *ast.StarExpr: + switch s := x.X.(type) { + case *ast.Ident: + name = s.Name + default: + // TODO: Store warnings? + return "", "", errors.WithStack(fmt.Errorf("unknown type in selector for *ast.StarExpr: %+v", s)) + } + case *ast.Ident: + name = x.Name + default: + // TODO: Store warnings? + return "", "", errors.WithStack(fmt.Errorf("unknown type in selector for *ast.StarExpr: %+v", starExpr)) + } + return pkg, name, nil +} diff --git a/v2/pkg/parser/testproject/basic.go b/v2/pkg/parser/testproject/basic.go new file mode 100644 index 000000000..4d9ad10de --- /dev/null +++ b/v2/pkg/parser/testproject/basic.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + + "testproject/mypackage" + + wails "github.com/wailsapp/wails/v2" +) + +// Basic application struct +type Basic struct { + runtime *wails.Runtime +} + +// // Another application struct +// type Another struct { +// runtime *wails.Runtime +// } + +// func (a *Another) Doit() { + +// } + +// // newBasicPointer creates a new Basic application struct +// func newBasicPointer() *Basic { +// return &Basic{} +// } + +// // newBasic creates a new Basic application struct +// func newBasic() Basic { +// return Basic{} +// } + +// WailsInit is called at application startup +func (b *Basic) WailsInit(runtime *wails.Runtime) error { + // Perform your setup here + b.runtime = runtime + runtime.Window.SetTitle("jsbundle") + return nil +} + +// WailsShutdown is called at application termination +func (b *Basic) WailsShutdown() { + // Perform your teardown here +} + +// NewPerson creates a new person +func (b *Basic) NewPerson(name string, age int) *mypackage.Person { + return &mypackage.Person{Name: name, Age: age} +} + +// Greet returns a greeting for the given name +func (b *Basic) Greet(name string) string { + return fmt.Sprintf("Hello %s!", name) +} + +// MultipleGreets returns greetings for the given name +func (b *Basic) MultipleGreets(name string) []string { + return []string{"hi", "hello", "croeso!"} +} + +// RemovePerson Removes the given person +func (b *Basic) RemovePerson(p *mypackage.Person) { + // dummy +} diff --git a/v2/pkg/parser/testproject/go.mod b/v2/pkg/parser/testproject/go.mod new file mode 100644 index 000000000..21f9e0d7e --- /dev/null +++ b/v2/pkg/parser/testproject/go.mod @@ -0,0 +1,9 @@ +module testproject + +go 1.13 + +require ( + github.com/wailsapp/wails/v2 v2.0.0-alpha +) + +replace github.com/wailsapp/wails/v2 v2.0.0-alpha => /home/lea/Data/projects/wails/v2 diff --git a/v2/pkg/parser/testproject/go.sum b/v2/pkg/parser/testproject/go.sum new file mode 100644 index 000000000..dae10ccef --- /dev/null +++ b/v2/pkg/parser/testproject/go.sum @@ -0,0 +1,83 @@ +github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/gosod v0.0.4/go.mod h1:nGMCb1PJfXwBDbOAike78jEYlpqge+xUKFf0iBKjKxU= +github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= +github.com/tdewolff/minify/v2 v2.9.5/go.mod h1:jshtBj/uUJH6JX1fuxTLnnHOA1RVJhF5MM+leJzDKb4= +github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ= +github.com/tdewolff/parse/v2 v2.5.3/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/v2/pkg/parser/testproject/main.go b/v2/pkg/parser/testproject/main.go new file mode 100644 index 000000000..0968d9112 --- /dev/null +++ b/v2/pkg/parser/testproject/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "testproject/mypackage" + + "github.com/wailsapp/wails/v2" +) + +func main() { + // Create application with options + app := wails.CreateApp("jsbundle", 1024, 768) + + /***** Struct Literal *****/ + + // Local struct pointer literal *WORKING* + app.Bind(&Basic{}) + + // External struct pointer literal + app.Bind(&mypackage.Manager{}) + +} diff --git a/v2/pkg/parser/testproject/mypackage/mypackage.go b/v2/pkg/parser/testproject/mypackage/mypackage.go new file mode 100644 index 000000000..5dcac83e3 --- /dev/null +++ b/v2/pkg/parser/testproject/mypackage/mypackage.go @@ -0,0 +1,36 @@ +// Package mypackage does all the things a mypackage can do +package mypackage + +type Address struct { + Number int + Street string + Town string + Postcode string +} + +// Person defines a Person in the application +type Person struct { + // Name is a name + Name string + Age int + Address *Address +} + +// Manager is the Mr Manager +type Manager struct { + Name string + TwoIC *Person +} + +// Hire me some peoples! +func (m *Manager) Hire(name, test string, bob int) *Person { + return &Person{Name: name} +} + +// func NewManagerPointer() *Manager { +// return &Manager{} +// } + +// func NewManager() Manager { +// return Manager{} +// }