5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-05 02:40:32 +08:00
wails/v3/internal/templates/templates.go

374 lines
8.2 KiB
Go

package templates
import (
"embed"
"encoding/json"
"fmt"
"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"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/wailsapp/wails/v3/internal/flags"
"github.com/leaanthony/gosod"
"github.com/samber/lo"
)
//go:embed lit
var lit embed.FS
//go:embed lit-ts
var litTS embed.FS
//go:embed vue
var vue embed.FS
//go:embed vue-ts
var vueTS embed.FS
//go:embed react
var react embed.FS
//go:embed react-ts
var reactTS embed.FS
//go:embed react-swc
var reactSWC embed.FS
//go:embed react-swc-ts
var reactSWCTS embed.FS
//go:embed svelte
var svelte embed.FS
//go:embed svelte-ts
var svelteTS embed.FS
//go:embed preact
var preact embed.FS
//go:embed preact-ts
var preactTS embed.FS
//go:embed vanilla
var vanilla embed.FS
//go:embed vanilla-ts
var vanillaTS embed.FS
type TemplateData struct {
Name string
Description string
FS embed.FS
}
var defaultTemplates = []TemplateData{
{
Name: "lit",
Description: "Template using Lit Web Components: https://lit.dev",
FS: lit,
},
{
Name: "lit-ts",
Description: "Template using Lit Web Components (TypeScript) : https://lit.dev",
FS: litTS,
},
{
Name: "vue",
Description: "Template using Vue: https://vuejs.org",
FS: vue,
},
{
Name: "vue-ts",
Description: "Template using Vue (TypeScript): https://vuejs.org",
FS: vueTS,
},
{
Name: "react",
Description: "Template using React: https://reactjs.org",
FS: react,
},
{
Name: "react-ts",
Description: "Template using React (TypeScript): https://reactjs.org",
FS: reactTS,
},
{
Name: "react-swc",
Description: "Template using React with SWC: https://reactjs.org & https://swc.rs",
FS: reactSWC,
},
{
Name: "react-swc-ts",
Description: "Template using React with SWC (TypeScript): https://reactjs.org & https://swc.rs",
FS: reactSWCTS,
},
{
Name: "svelte",
Description: "Template using Svelte: https://svelte.dev",
FS: svelte,
},
{
Name: "svelte-ts",
Description: "Template using Svelte (TypeScript): https://svelte.dev",
FS: svelteTS,
},
{
Name: "preact",
Description: "Template using Preact: https://preactjs.com",
FS: preact,
},
{
Name: "preact-ts",
Description: "Template using Preact (TypeScript): https://preactjs.com",
FS: preactTS,
},
{
Name: "vanilla",
Description: "Template using Vanilla JS",
FS: vanilla,
},
{
Name: "vanilla-ts",
Description: "Template using Vanilla JS (TypeScript)",
FS: vanillaTS,
},
}
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
}
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
}
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), templateName)
if err != nil {
return nil, err
}
return &template, nil
}
// Template holds data relating to a template including the metadata stored in template.yaml
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"`
Version int8 `json:"version"`
// Other data
FS fs.FS `json:"-"`
}
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.Version == 0 {
return result, fmt.Errorf("template not supported by wails 3. This template is probably for wails 2")
}
if result.Version != 3 {
return result, fmt.Errorf("template version %d 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 *Template, err error) {
// git clone to temporary dir
var tempDir string
tempDir, err = gitclone(uri)
defer func(path string) {
_ = os.RemoveAll(path)
}(tempDir)
if err != nil {
return
}
// Remove the .git directory
err = os.RemoveAll(filepath.Join(tempDir, ".git"))
if err != nil {
return
}
templateFS := os.DirFS(tempDir)
var parsedTemplate Template
parsedTemplate, err = parseTemplate(templateFS, "")
if err != nil {
return
}
return &parsedTemplate, nil
}
func Install(options *flags.Init) error {
templateData := TemplateOptions{
options,
filepath.FromSlash(debug.LocalModulePath + "/"),
}
defer func() {
// if `template.json` exists, remove it
_ = os.Remove(filepath.Join(templateData.ProjectDir, "template.json"))
}()
var err error
var template *Template
template, err = getInternalTemplate(options.TemplateName)
if err != nil {
return err
}
if template == nil {
template, err = getLocalTemplate(options.TemplateName)
}
if err != nil {
return err
}
if template == nil {
template, err = getRemoteTemplate(options.TemplateName)
}
if err != nil {
return err
}
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)
}
if options.ProjectDir == "." || options.ProjectDir == "" {
templateData.ProjectDir = lo.Must(os.Getwd())
}
templateData.ProjectDir = filepath.Join(options.ProjectDir, options.ProjectName)
// 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)
}
}
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},
}
err = pterm.DefaultTable.WithData(table).Render()
if err != nil {
return err
}
tfs, err := fs.Sub(template.FS, options.TemplateName)
if err != nil {
return err
}
err = gosod.New(tfs).Extract(options.ProjectDir, templateData)
if err != nil {
return err
}
// Change to project directory
err = os.Chdir(templateData.ProjectDir)
if err != nil {
return err
}
// Run `go mod tidy`
err = exec.Command("go", "mod", "tidy").Run()
if err != nil {
return err
}
pterm.Printf("\nProject '%s' created successfully.\n", options.ProjectName)
return nil
}