5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-04 20:11:27 +08:00
wails/v3/internal/templates/templates.go
2025-01-20 19:56:03 +11:00

464 lines
12 KiB
Go

package templates
import (
"embed"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v3/internal/buildinfo"
"github.com/wailsapp/wails/v3/internal/s"
"github.com/wailsapp/wails/v3/internal/version"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/wailsapp/wails/v3/internal/debug"
"github.com/wailsapp/wails/v3/internal/flags"
"github.com/leaanthony/gosod"
"github.com/samber/lo"
)
//go:embed *
var templates embed.FS
type TemplateData struct {
Name string
Description string
FS fs.FS
}
var defaultTemplates = []TemplateData{}
func init() {
dirs, err := templates.ReadDir(".")
if err != nil {
return
}
for _, dir := range dirs {
if strings.HasPrefix(dir.Name(), "_") {
continue
}
if dir.IsDir() {
template, err := parseTemplate(templates, dir.Name())
if err != nil {
continue
}
defaultTemplates = append(defaultTemplates,
TemplateData{
Name: dir.Name(),
Description: template.Description,
FS: templates,
})
}
}
}
func ValidTemplateName(name string) bool {
return lo.ContainsBy(defaultTemplates, func(template TemplateData) bool {
return template.Name == name
})
}
func GetDefaultTemplates() []TemplateData {
return defaultTemplates
}
type TemplateOptions struct {
*flags.Init
LocalModulePath string
UseTypescript bool
WailsVersion string
}
func getInternalTemplate(templateName string) (*Template, error) {
templateData, found := lo.Find(defaultTemplates, func(template TemplateData) bool {
return template.Name == templateName
})
if !found {
return nil, nil
}
template, err := parseTemplate(templateData.FS, templateData.Name)
if err != nil {
return nil, err
}
template.source = sourceInternal
return &template, nil
}
func getLocalTemplate(templateName string) (*Template, error) {
var template Template
var err error
_, err = os.Stat(templateName)
if err != nil {
return nil, nil
}
template, err = parseTemplate(os.DirFS(templateName), "")
if err != nil {
println("err2 = ", err.Error())
return nil, err
}
template.source = sourceLocal
return &template, nil
}
type BaseTemplate struct {
Name string `json:"name" description:"The name of the template"`
ShortName string `json:"shortname" description:"The short name of the template"`
Author string `json:"author" description:"The author of the template"`
Description string `json:"description" description:"The template description"`
HelpURL string `json:"helpurl" description:"The help url for the template"`
Version string `json:"version" description:"The version of the template" default:"v0.0.1"`
Dir string `json:"-" description:"The directory to generate the template" default:"."`
Frontend string `json:"-" description:"The frontend directory to migrate"`
}
type source int
const (
sourceInternal source = 1
sourceLocal source = 2
sourceRemote source = 3
)
// Template holds data relating to a template including the metadata stored in template.yaml
type Template struct {
BaseTemplate
Schema uint8 `json:"schema"`
// Other data
FS fs.FS `json:"-"`
source source
tempDir string
}
func parseTemplate(template fs.FS, templateName string) (Template, error) {
var result Template
jsonFile := "template.json"
if templateName != "" {
jsonFile = templateName + "/template.json"
}
data, err := fs.ReadFile(template, jsonFile)
if err != nil {
return result, errors.Wrap(err, "Error parsing template")
}
err = json.Unmarshal(data, &result)
if err != nil {
return result, err
}
result.FS = template
// We need to do a version check here
if result.Schema == 0 {
return result, fmt.Errorf("template not supported by wails 3. This template is probably for wails 2")
}
if result.Schema != 3 {
return result, fmt.Errorf("template version %s is not supported by wails 3. Ensure 'version' is set to 3 in the `template.json` file", result.Version)
}
return result, nil
}
// Clones the given uri and returns the temporary cloned directory
func gitclone(uri string) (string, error) {
// Create temporary directory
dirname, err := os.MkdirTemp("", "wails-template-*")
if err != nil {
return "", err
}
// Parse remote template url and version number
templateInfo := strings.Split(uri, "@")
cloneOption := &git.CloneOptions{
URL: templateInfo[0],
}
if len(templateInfo) > 1 {
cloneOption.ReferenceName = plumbing.NewTagReferenceName(templateInfo[1])
}
_, err = git.PlainClone(dirname, false, cloneOption)
return dirname, err
}
func getRemoteTemplate(uri string) (*Template, error) {
// git clone to temporary dir
var tempDir string
tempDir, err := gitclone(uri)
if err != nil {
return nil, err
}
// Remove the .git directory
err = os.RemoveAll(filepath.Join(tempDir, ".git"))
if err != nil {
return nil, err
}
templateFS := os.DirFS(tempDir)
var parsedTemplate Template
parsedTemplate, err = parseTemplate(templateFS, "")
if err != nil {
return nil, err
}
parsedTemplate.tempDir = tempDir
parsedTemplate.source = sourceRemote
return &parsedTemplate, nil
}
func Install(options *flags.Init) error {
var wd = lo.Must(os.Getwd())
var projectDir string
if options.ProjectDir == "." || options.ProjectDir == "" {
projectDir = wd
} else {
projectDir = options.ProjectDir
}
var err error
projectDir, err = filepath.Abs(filepath.Join(projectDir, options.ProjectName))
if err != nil {
return err
}
buildInfo, err := buildinfo.Get()
if err != nil {
return err
}
// Calculate relative path from project directory to LocalModulePath
var localModulePath string
// Use module path if it is set
if buildInfo.Development {
var relativePath string
// Check if the project directory and LocalModulePath are in the same drive
if filepath.VolumeName(wd) != filepath.VolumeName(debug.LocalModulePath) {
relativePath = debug.LocalModulePath
} else {
relativePath, err = filepath.Rel(projectDir, debug.LocalModulePath)
}
if err != nil {
return err
}
localModulePath = filepath.ToSlash(relativePath + "/")
}
UseTypescript := strings.HasSuffix(options.TemplateName, "-ts")
templateData := TemplateOptions{
Init: options,
LocalModulePath: localModulePath,
UseTypescript: UseTypescript,
WailsVersion: version.String(),
}
defer func() {
// if `template.json` exists, remove it
_ = os.Remove(filepath.Join(templateData.ProjectDir, "template.json"))
}()
var template *Template
if ValidTemplateName(options.TemplateName) {
template, err = getInternalTemplate(options.TemplateName)
if err != nil {
return err
}
} else {
template, err = getLocalTemplate(options.TemplateName)
if err != nil {
return err
}
if template == nil {
template, err = getRemoteTemplate(options.TemplateName)
}
}
if template == nil {
return fmt.Errorf("invalid template name: %s. Use -l flag to view available templates or use a valid filepath / url to a template", options.TemplateName)
}
templateData.ProjectDir = projectDir
// If project directory already exists and is not empty, error
if _, err := os.Stat(templateData.ProjectDir); !os.IsNotExist(err) {
// Check if the directory is empty
files := lo.Must(os.ReadDir(templateData.ProjectDir))
if len(files) > 0 {
return fmt.Errorf("project directory '%s' already exists and is not empty", templateData.ProjectDir)
}
}
if template.source == sourceRemote && !options.SkipWarning {
var confirmed = confirmRemote(template)
if !confirmed {
return nil
}
}
pterm.Printf("Creating project\n")
pterm.Printf("----------------\n\n")
table := pterm.TableData{
{"Project Name", options.ProjectName},
{"Project Directory", filepath.FromSlash(options.ProjectDir)},
{"Template", template.Name},
{"Template Source", template.HelpURL},
{"Template Version", template.Version},
}
err = pterm.DefaultTable.WithData(table).Render()
if err != nil {
return err
}
switch template.source {
case sourceInternal:
tfs, err := fs.Sub(template.FS, options.TemplateName)
if err != nil {
return err
}
common, err := fs.Sub(templates, "_common")
if err != nil {
return err
}
err = gosod.New(common).Extract(options.ProjectDir, templateData)
if err != nil {
return err
}
err = gosod.New(tfs).Extract(options.ProjectDir, templateData)
if err != nil {
return err
}
case sourceLocal, sourceRemote:
data := struct {
TemplateOptions
Dir string
Name string
BinaryName string
ProductName string
ProductDescription string
ProductVersion string
ProductCompany string
ProductCopyright string
ProductComments string
ProductIdentifier string
Silent bool
Typescript bool
}{
Name: options.ProjectName,
Silent: true,
ProductCompany: options.ProductCompany,
ProductName: options.ProductName,
ProductDescription: options.ProductDescription,
ProductVersion: options.ProductVersion,
ProductIdentifier: options.ProductIdentifier,
ProductCopyright: options.ProductCopyright,
ProductComments: options.ProductComments,
Typescript: templateData.UseTypescript,
TemplateOptions: templateData,
}
// If options.ProjectDir does not exist, create it
if _, err := os.Stat(options.ProjectDir); os.IsNotExist(err) {
err = os.Mkdir(options.ProjectDir, 0755)
if err != nil {
return err
}
}
err = gosod.New(template.FS).Extract(options.ProjectDir, data)
if err != nil {
return err
}
if template.tempDir != "" {
s.RMDIR(template.tempDir)
}
}
// Change to project directory
err = os.Chdir(templateData.ProjectDir)
if err != nil {
return err
}
pterm.Printf("\nProject '%s' created successfully.\n", options.ProjectName)
return nil
}
func GenerateTemplate(options *BaseTemplate) error {
if options.Name == "" {
return fmt.Errorf("please provide a template name using the -name flag")
}
// Get current directory
baseOutputDir, err := filepath.Abs(options.Dir)
if err != nil {
return err
}
outDir := filepath.Join(baseOutputDir, options.Name)
// Extract base files
_, filename, _, _ := runtime.Caller(0)
basePath := filepath.Join(filepath.Dir(filename), "_common")
s.COPYDIR2(basePath, outDir)
s.RMDIR(filepath.Join(outDir, "build"))
// Copy frontend
targetFrontendPath := filepath.Join(outDir, "frontend")
sourceFrontendPath := options.Frontend
if sourceFrontendPath == "" {
sourceFrontendPath = filepath.Join(filepath.Dir(filename), "base", "frontend")
}
s.COPYDIR2(sourceFrontendPath, targetFrontendPath)
// Copy files from relative directory ../commands/build_assets
// Get the path to THIS file
assetPath := filepath.Join(filepath.Dir(filename), "..", "commands", "build_assets")
assetdir := filepath.Join(outDir, "build")
s.COPYDIR2(assetPath, assetdir)
// Copy the template NEXTSTEPS.md
s.COPY(filepath.Join(filepath.Dir(filename), "base", "NEXTSTEPS.md"), filepath.Join(outDir, "NEXTSTEPS.md"))
// Write the template.json file
templateJSON := filepath.Join(outDir, "template.json")
// Marshall
optionsJSON, err := json.MarshalIndent(&Template{
BaseTemplate: *options,
Schema: 3,
}, "", " ")
if err != nil {
return err
}
err = os.WriteFile(templateJSON, optionsJSON, 0o755)
if err != nil {
return err
}
fmt.Printf("Successfully generated template in %s\n", outDir)
return nil
}
func confirmRemote(template *Template) bool {
pterm.Println(pterm.LightRed("\n--- REMOTE TEMPLATES ---"))
// Create boxes with the title positioned differently and containing different content
pterm.Println(pterm.LightYellow("You are creating a project using a remote template.\nThe Wails project takes no responsibility for 3rd party templates.\nOnly use remote templates that you trust."))
result, _ := pterm.DefaultInteractiveConfirm.WithConfirmText("Are you sure you want to continue?").WithConfirmText("y").WithRejectText("n").Show()
return result
}