package templates import ( "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "runtime" "strings" "github.com/leaanthony/gosod" "github.com/leaanthony/slicer" "github.com/olekukonko/tablewriter" "github.com/wailsapp/wails/v2/internal/fs" "github.com/wailsapp/wails/v2/pkg/clilogger" ) // Cahce for the templates // We use this because we need different views of the same data var templateCache []Template = nil // Data contains the data we wish to embed during template installation type Data struct { ProjectName string BinaryName string WailsVersion string NPMProjectName string AuthorName string AuthorEmail string AuthorNameAndEmail string WailsDirectory string } // Options for installing a template type Options struct { ProjectName string TemplateName string BinaryName string TargetDir string Logger *clilogger.CLILogger GenerateVSCode bool PathToDesktopBinary string PathToServerBinary string InitGit bool AuthorName string AuthorEmail string } // Template holds data relating to a template // including the metadata stored in template.json type Template struct { // Template details Name string `json:"name"` ShortName string `json:"shortname"` Author string `json:"author"` Description string `json:"description"` HelpURL string `json:"helpurl"` // Other data Directory string `json:"-"` } func parseTemplate(directory string) (Template, error) { templateJSON := filepath.Join(directory, "template.json") var result Template data, err := ioutil.ReadFile(templateJSON) if err != nil { return result, err } result.Directory = directory err = json.Unmarshal(data, &result) return result, err } // TemplateShortNames returns a slicer of short template names func TemplateShortNames() (*slicer.StringSlicer, error) { var result slicer.StringSlicer // If the cache isn't loaded, load it if templateCache == nil { err := loadTemplateCache() if err != nil { return nil, err } } for _, template := range templateCache { result.Add(template.ShortName) } return &result, nil } // List returns the list of available templates func List() ([]Template, error) { // If the cache isn't loaded, load it if templateCache == nil { err := loadTemplateCache() if err != nil { return nil, err } } return templateCache, nil } // getTemplateByShortname returns the template with the given short name func getTemplateByShortname(shortname string) (Template, error) { var result Template // If the cache isn't loaded, load it if templateCache == nil { err := loadTemplateCache() if err != nil { return result, err } } for _, template := range templateCache { if template.ShortName == shortname { return template, nil } } return result, fmt.Errorf("shortname '%s' is not a valid template shortname", shortname) } // Loads the template cache func loadTemplateCache() error { // Get local template directory templateDir := fs.RelativePath("templates") // Get directories files, err := ioutil.ReadDir(templateDir) if err != nil { return err } // Reset cache templateCache = []Template{} for _, file := range files { if file.IsDir() { templateDir := filepath.Join(templateDir, file.Name()) template, err := parseTemplate(templateDir) if err != nil { // Cannot parse this template, continue continue } templateCache = append(templateCache, template) } } return nil } // Install the given template func Install(options *Options) error { // Get cwd cwd, err := os.Getwd() if err != nil { return err } // Did the user want to install in current directory? 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)) if err != nil { return err } options.TargetDir = targetDir if !fs.DirExists(options.TargetDir) { err := fs.Mkdir(options.TargetDir) if err != nil { return err } } } // Get template template, err := getTemplateByShortname(options.TemplateName) if err != nil { return err } // Use Gosod to install the template installer, err := gosod.TemplateDir(template.Directory) if err != nil { return err } // Ignore template.json files installer.IgnoreFilename("template.json") // Setup the data. // We use the directory name for the binary name, like Go BinaryName := filepath.Base(options.TargetDir) NPMProjectName := strings.ToLower(strings.ReplaceAll(BinaryName, " ", "")) localWailsDirectory := fs.RelativePath("../..") templateData := &Data{ ProjectName: options.ProjectName, BinaryName: filepath.Base(options.TargetDir), NPMProjectName: NPMProjectName, WailsDirectory: localWailsDirectory, AuthorEmail: options.AuthorEmail, AuthorName: options.AuthorName, } // Create a formatted name and email combo. if options.AuthorName != "" { templateData.AuthorNameAndEmail = options.AuthorName + " " } if options.AuthorEmail != "" { templateData.AuthorNameAndEmail += "<" + options.AuthorEmail + ">" } templateData.AuthorNameAndEmail = strings.TrimSpace(templateData.AuthorNameAndEmail) // Extract the template err = installer.Extract(options.TargetDir, templateData) if err != nil { return err } err = generateIDEFiles(options) if err != nil { return err } return nil } // OutputList prints the list of available tempaltes to the given logger func OutputList(logger *clilogger.CLILogger) error { templates, err := List() if err != nil { return err } table := tablewriter.NewWriter(logger.Writer) table.SetHeader([]string{"Template", "Short Name", "Description"}) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) for _, template := range templates { table.Append([]string{template.Name, template.ShortName, template.Description}) } 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 }