From 332e9d66ea2fc1671e913a08fa32f96e8249c36e Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 11 Jan 2020 22:35:05 +1100 Subject: [PATCH] Initial support for Typescript decl file (#330) --- cmd/helpers.go | 13 +++++++-- cmd/project.go | 36 ++++++++++++++---------- cmd/wails/4_build.go | 23 +++++++++------ go.mod | 4 ++- go.sum | 2 ++ lib/binding/manager.go | 63 +++++++++++++++++++++++++++++++++++++++++- 6 files changed, 113 insertions(+), 28 deletions(-) diff --git a/cmd/helpers.go b/cmd/helpers.go index 6c5b29e9c..1f4410d8b 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -99,8 +99,7 @@ func BuildApplication(binaryName string, forceRebuild bool, buildMode string, pa binaryName = strings.TrimSuffix(binaryName, ".exe") } } - buildCommand.Add("-o") - buildCommand.Add(binaryName) + buildCommand.Add("-o", binaryName) } // If we are forcing a rebuild @@ -121,6 +120,16 @@ func BuildApplication(binaryName string, forceRebuild bool, buildMode string, pa ldflags += "-X github.com/wailsapp/wails.BuildMode=" + buildMode + // If we wish to generate typescript + if projectOptions.typescriptDefsFilename != "" { + cwd, err := os.Getwd() + if err != nil { + return err + } + filename := filepath.Join(cwd, projectOptions.FrontEnd.Dir, projectOptions.typescriptDefsFilename) + ldflags += " -X github.com/wailsapp/wails/lib/binding.typescriptDefinitionFilename=" + filename + } + buildCommand.AddSlice([]string{"-ldflags", ldflags}) err = NewProgramHelper().RunCommandArray(buildCommand.AsSlice()) if err != nil { diff --git a/cmd/project.go b/cmd/project.go index 91de152c8..1ecb52726 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -142,21 +142,22 @@ func (ph *ProjectHelper) NewProjectOptions() *ProjectOptions { // ProjectOptions holds all the options available for a project type ProjectOptions struct { - Name string `json:"name"` - Description string `json:"description"` - Author *author `json:"author,omitempty"` - Version string `json:"version"` - OutputDirectory string `json:"-"` - UseDefaults bool `json:"-"` - Template string `json:"-"` - BinaryName string `json:"binaryname"` - FrontEnd *frontend `json:"frontend,omitempty"` - NPMProjectName string `json:"-"` - system *SystemHelper - log *Logger - templates *TemplateHelper - selectedTemplate *TemplateDetails - WailsVersion string + Name string `json:"name"` + Description string `json:"description"` + Author *author `json:"author,omitempty"` + Version string `json:"version"` + OutputDirectory string `json:"-"` + UseDefaults bool `json:"-"` + Template string `json:"-"` + BinaryName string `json:"binaryname"` + FrontEnd *frontend `json:"frontend,omitempty"` + NPMProjectName string `json:"-"` + system *SystemHelper + log *Logger + templates *TemplateHelper + selectedTemplate *TemplateDetails + WailsVersion string + typescriptDefsFilename string } // Defaults sets the default project template @@ -165,6 +166,11 @@ func (po *ProjectOptions) Defaults() { po.WailsVersion = Version } +// SetTypescriptDefsFilename indicates that we want to generate typescript bindings to the given file +func (po *ProjectOptions) SetTypescriptDefsFilename(filename string) { + po.typescriptDefsFilename = filename +} + // GetNPMBinaryName returns the type of package manager used by the project func (po *ProjectOptions) GetNPMBinaryName() (PackageManager, error) { if po.FrontEnd == nil { diff --git a/cmd/wails/4_build.go b/cmd/wails/4_build.go index 725f9eec9..f1fbb8ce5 100644 --- a/cmd/wails/4_build.go +++ b/cmd/wails/4_build.go @@ -13,6 +13,8 @@ func init() { var packageApp = false var forceRebuild = false var debugMode = false + var typescriptFilename = "" + buildSpinner := spinner.NewSpinner() buildSpinner.SetSpinSpeed(50) @@ -21,7 +23,8 @@ func init() { LongDescription(commandDescription). BoolFlag("p", "Package application on successful build", &packageApp). BoolFlag("f", "Force rebuild of application components", &forceRebuild). - BoolFlag("d", "Build in Debug mode", &debugMode) + BoolFlag("d", "Build in Debug mode", &debugMode). + StringFlag("t", "Generate Typescript definitions to given file (at runtime)", &typescriptFilename) initCmd.Action(func() error { @@ -73,16 +76,13 @@ func init() { return err } - // Ensure that runtime init.js is the production version - err = cmd.InstallProdRuntime(projectDir, projectOptions) - if err != nil { - return err - } + // Ensure that runtime init.js is the production version + err = cmd.InstallProdRuntime(projectDir, projectOptions) + if err != nil { + return err + } } - - - // Move to project directory err = os.Chdir(projectDir) if err != nil { @@ -101,6 +101,11 @@ func init() { buildMode = cmd.BuildModeDebug } + // Save if we wish to dump typescript or not + if typescriptFilename != "" { + projectOptions.SetTypescriptDefsFilename(typescriptFilename) + } + err = cmd.BuildApplication(projectOptions.BinaryName, forceRebuild, buildMode, packageApp, projectOptions) if err != nil { return err diff --git a/go.mod b/go.mod index 435d02d39..a37f6006b 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/kennygrant/sanitize v1.2.4 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/leaanthony/mewn v0.10.7 - github.com/leaanthony/slicer v1.3.2 + github.com/leaanthony/slicer v1.4.0 github.com/leaanthony/spinner v0.5.3 github.com/mattn/go-colorable v0.1.1 // indirect github.com/mattn/go-isatty v0.0.7 // indirect @@ -31,3 +31,5 @@ require ( gopkg.in/AlecAivazis/survey.v1 v1.8.4 gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 ) + +go 1.13 diff --git a/go.sum b/go.sum index 96ea28f11..3c9d0b42a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/leaanthony/mewn v0.10.7 h1:jCcNJyIUOpwj+I5SuATvCugDjHkoo+j6ubEOxxrxmP github.com/leaanthony/mewn v0.10.7/go.mod h1:CRkTx8unLiSSilu/Sd7i1LwrdaAL+3eQ3ses99qGMEQ= github.com/leaanthony/slicer v1.3.2 h1:kGWWFoyaY5WzwGrUsHXMmGbssuYthP4qYBNlkNpNAB8= github.com/leaanthony/slicer v1.3.2/go.mod h1:VMB/HGvr3uR3MRpFWHWAm0w+DHQLzPHYe2pKfpFlQIQ= +github.com/leaanthony/slicer v1.4.0 h1:Q9u4w+UBU4WHjXnEDdz+eRLMKF/rnyosRBiqULnc1J8= +github.com/leaanthony/slicer v1.4.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/leaanthony/spinner v0.5.3 h1:IMTvgdQCec5QA4qRy0wil4XsRP+QcG1OwLWVK/LPZ5Y= github.com/leaanthony/spinner v0.5.3/go.mod h1:oHlrvWicr++CVV7ALWYi+qHk/XNA91D9IJ48IqmpVUo= github.com/leaanthony/synx v0.1.0 h1:R0lmg2w6VMb8XcotOwAe5DLyzwjLrskNkwU7LLWsyL8= diff --git a/lib/binding/manager.go b/lib/binding/manager.go index 43b3959dc..35c378271 100644 --- a/lib/binding/manager.go +++ b/lib/binding/manager.go @@ -2,7 +2,11 @@ package binding import ( "fmt" + "io/ioutil" + "os" + "path/filepath" "reflect" + "strings" "unicode" "github.com/wailsapp/wails/lib/interfaces" @@ -10,6 +14,8 @@ import ( "github.com/wailsapp/wails/lib/messages" ) +var typescriptDefinitionFilename = "" + // Manager handles method binding type Manager struct { methods map[string]*boundMethod @@ -21,16 +27,19 @@ type Manager struct { renderer interfaces.Renderer runtime interfaces.Runtime // The runtime object to pass to bound structs objectsToBind []interface{} - bindPackageNames bool // Package name should be considered when binding + bindPackageNames bool // Package name should be considered when binding + structList map[string][]string // structList["mystruct"] = []string{"Method1", "Method2"} } // NewManager creates a new Manager struct func NewManager() interfaces.BindingManager { + result := &Manager{ methods: make(map[string]*boundMethod), functions: make(map[string]*boundFunction), log: logger.NewCustomLogger("Bind"), internalMethods: newInternalMethods(), + structList: make(map[string][]string), } return result } @@ -88,9 +97,53 @@ func (b *Manager) initialise() error { return err } } + + // If we wish to generate a typescript definition file... + if typescriptDefinitionFilename != "" { + err := b.generateTypescriptDefinitions() + if err != nil { + return err + } + } return nil } +// Generate typescript +func (b *Manager) generateTypescriptDefinitions() error { + + var output strings.Builder + + for structname, methodList := range b.structList { + output.WriteString(fmt.Sprintf("Interface %s {\n", structname)) + for _, method := range methodList { + output.WriteString(fmt.Sprintf("\t%s: (...args : any[]) => Promise\n", method)) + } + output.WriteString("}\n") + } + + output.WriteString("\n") + output.WriteString("Interface Backend {\n") + + for structname := range b.structList { + output.WriteString(fmt.Sprintf("\t%[1]s: %[1]s\n", structname)) + } + output.WriteString("}\n") + + globals := ` +declare global { + interface Window { + backend: Backend; + } +}` + output.WriteString(globals) + + b.log.Info("Written Typescript file: " + typescriptDefinitionFilename) + + dir := filepath.Dir(typescriptDefinitionFilename) + os.MkdirAll(dir, 0755) + return ioutil.WriteFile(typescriptDefinitionFilename, []byte(output.String()), 0755) +} + // bind the given struct method func (b *Manager) bindMethod(object interface{}) error { @@ -104,6 +157,12 @@ func (b *Manager) bindMethod(object interface{}) error { b.log.Debugf("Processing struct: %s", baseName) + // Calc actual name + actualName := strings.TrimPrefix(baseName, "main.") + if b.structList[actualName] == nil { + b.structList[actualName] = []string{} + } + // Iterate over method definitions for i := 0; i < objectType.NumMethod(); i++ { @@ -113,6 +172,8 @@ func (b *Manager) bindMethod(object interface{}) error { fullMethodName := baseName + "." + methodName method := reflect.ValueOf(object).MethodByName(methodName) + b.structList[actualName] = append(b.structList[actualName], methodName) + // Skip unexported methods if !unicode.IsUpper([]rune(methodName)[0]) { continue