Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion v3/internal/commands/build-assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type BuildAssetsOptions struct {
Silent bool `description:"Suppress output to console"`
Typescript bool `description:"Use typescript" default:"false"`
UseInterfaces bool `description:"Generate TypeScript interfaces instead of classes"`
PreserveConfig bool `description:"Preserve an existing config.yml while generating build assets"`
}

type TemplateEnrichment struct {
Expand Down Expand Up @@ -161,7 +162,13 @@ func GenerateBuildAssets(options *BuildAssetsOptions) error {
if !options.Silent {
println("Generating build assets in " + options.Dir)
}
err = gosod.New(tfs).Extract(options.Dir, config)
buildAssetsTemplate := gosod.New(tfs)
if options.PreserveConfig {
// A remote template may provide its own build/config.yml. Keep it instead
// of replacing it with Wails' default configuration during init.
buildAssetsTemplate.IgnoreFile("config.yml")
}
err = buildAssetsTemplate.Extract(options.Dir, config)
if err != nil {
return err
}
Expand Down
31 changes: 31 additions & 0 deletions v3/internal/commands/build-assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,37 @@ func TestGenerateBuildAssets(t *testing.T) {
}
}

func TestGenerateBuildAssetsPreservesExistingConfig(t *testing.T) {
buildDir := filepath.Join(t.TempDir(), "build")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}

const templateConfig = "# remote template settings\ndev_mode:\n debounce: 1000\n"
configPath := filepath.Join(buildDir, "config.yml")
if err := os.WriteFile(configPath, []byte(templateConfig), 0o644); err != nil {
t.Fatal(err)
}

err := GenerateBuildAssets(&BuildAssetsOptions{
Dir: buildDir,
Name: "PreservedConfig",
Silent: true,
PreserveConfig: true,
})
if err != nil {
t.Fatalf("GenerateBuildAssets() error = %v", err)
}

got, err := os.ReadFile(configPath)
if err != nil {
t.Fatal(err)
}
if string(got) != templateConfig {
t.Fatalf("config.yml was overwritten: got %q, want %q", got, templateConfig)
}
}

func TestUpdateBuildAssets(t *testing.T) {
// Create a temporary directory for testing
tempDir, err := os.MkdirTemp("", "wails-update-assets-test-*")
Expand Down
51 changes: 51 additions & 0 deletions v3/internal/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,20 @@ func Init(options *flags.Init) error {
}
}

remoteTemplate := templates.IsRemoteTemplate(options.TemplateName)
err := templates.Install(options)
if err != nil {
return err
}

remoteConfigPath := ""
if remoteTemplate {
remoteConfigPath, err = prepareRemoteTemplateConfig(options.ProjectDir)
if err != nil {
return err
}
}

// Rename gitignore to .gitignore
err = os.Rename(filepath.Join(options.ProjectDir, "gitignore"), filepath.Join(options.ProjectDir, ".gitignore"))
if err != nil {
Expand Down Expand Up @@ -286,11 +295,15 @@ func Init(options *flags.Init) error {
ProductComments: options.ProductComments,
Typescript: isTypescript,
UseInterfaces: options.UseInterfaces,
PreserveConfig: remoteConfigPath != "",
}
err = GenerateBuildAssets(buildAssetsOptions)
if err != nil {
return err
}
if remoteConfigPath != "" {
term.Warningf("Copied %s from the remote template. Please review this file before using the project.\n", filepath.ToSlash(remoteConfigPath))
}

// In UI mode the wizard is explicitly about the project config, so write the
// chosen values into build/config.yml itself (the generated assets already
Expand All @@ -314,6 +327,44 @@ func Init(options *flags.Init) error {
return nil
}

// prepareRemoteTemplateConfig keeps a remote template's build configuration
// from being replaced by the default build assets. Wails uses config.yml as
// the default path, so a config.yaml supplied by a template is also copied to
// that canonical path while retaining the original file.
func prepareRemoteTemplateConfig(projectDir string) (string, error) {
buildDir := filepath.Join(projectDir, "build")
for _, name := range []string{"config.yml", "config.yaml"} {
path := filepath.Join(buildDir, name)
info, err := os.Lstat(path)
if err != nil {
if os.IsNotExist(err) {
continue
}
return "", err
}
if info.Mode()&os.ModeSymlink != 0 {
return "", errors.New("remote template build configuration must not be a symbolic link")
}

if name == "config.yaml" {
canonicalPath := filepath.Join(buildDir, "config.yml")
if _, err := os.Stat(canonicalPath); os.IsNotExist(err) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
if err := os.WriteFile(canonicalPath, data, 0o644); err != nil {
return "", err
}
}
}

return filepath.ToSlash(filepath.Join("build", name)), nil
}

return "", nil
}

// writeProjectConfigYML rewrites the `info:` values in the freshly scaffolded
// build/config.yml from the chosen options, preserving the file's comments. The
// info keys map 1:1 onto the Product* fields.
Expand Down
74 changes: 74 additions & 0 deletions v3/internal/commands/init_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,87 @@
package commands

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/wailsapp/wails/v3/internal/defaults"
"github.com/wailsapp/wails/v3/internal/flags"
)

func TestPrepareRemoteTemplateConfig(t *testing.T) {
tests := []struct {
name string
filename string
wantPath string
wantCopied bool
}{
{name: "yml", filename: "config.yml", wantPath: "build/config.yml"},
{name: "yaml", filename: "config.yaml", wantPath: "build/config.yaml", wantCopied: true},
{name: "missing", wantPath: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
projectDir := t.TempDir()
buildDir := filepath.Join(projectDir, "build")
if tt.filename != "" {
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(buildDir, tt.filename), []byte("template config\n"), 0o644); err != nil {
t.Fatal(err)
}
}

gotPath, err := prepareRemoteTemplateConfig(projectDir)
if err != nil {
t.Fatalf("prepareRemoteTemplateConfig() error = %v", err)
}
if gotPath != tt.wantPath {
t.Fatalf("prepareRemoteTemplateConfig() path = %q, want %q", gotPath, tt.wantPath)
}

canonicalPath := filepath.Join(buildDir, "config.yml")
_, canonicalErr := os.Stat(canonicalPath)
if tt.wantCopied {
if canonicalErr != nil {
t.Fatalf("expected config.yaml to be copied to config.yml: %v", canonicalErr)
}
got, err := os.ReadFile(canonicalPath)
if err != nil {
t.Fatal(err)
}
if string(got) != "template config\n" {
t.Errorf("copied config = %q, want original template config", got)
}
} else if tt.filename == "" && !os.IsNotExist(canonicalErr) {
t.Errorf("unexpected canonical config file for missing source")
}
})
}
}

func TestPrepareRemoteTemplateConfigRejectsSymlink(t *testing.T) {
projectDir := t.TempDir()
buildDir := filepath.Join(projectDir, "build")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
t.Fatal(err)
}
target := filepath.Join(projectDir, "outside-config.yml")
if err := os.WriteFile(target, []byte("not for copying\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, filepath.Join(buildDir, "config.yaml")); err != nil {
t.Skipf("symbolic links are not supported: %v", err)
}

if _, err := prepareRemoteTemplateConfig(projectDir); err == nil {
t.Fatal("expected symbolic-link configuration to be rejected")
}
}

func TestGitURLToModulePath(t *testing.T) {
tests := []struct {
name string
Expand Down
12 changes: 12 additions & 0 deletions v3/internal/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ func ValidTemplateName(name string) bool {
})
}

// IsRemoteTemplate reports whether name will be resolved as a remote template
// by Install. Any path lookup error is treated as remote here, matching the
// local-template lookup in Install; Install remains responsible for validating
// and fetching the template.
func IsRemoteTemplate(name string) bool {
if ValidTemplateName(name) {
return false
}
_, err := os.Stat(name)
return err != nil
}

func GetDefaultTemplates() []TemplateData {
return defaultTemplates
}
Expand Down
16 changes: 15 additions & 1 deletion v3/internal/templates/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ import (
"testing/fstest"
)

func TestIsRemoteTemplate(t *testing.T) {
if IsRemoteTemplate("vanilla") {
t.Error("built-in template should not be treated as remote")
}
if !IsRemoteTemplate("https://github.com/example/template") {
t.Error("URL template should be treated as remote")
}
if !IsRemoteTemplate(filepath.Join(t.TempDir(), "missing-template")) {
t.Error("missing local path should be treated as remote")
}
if IsRemoteTemplate(t.TempDir()) {
t.Error("existing local directory should not be treated as remote")
}
}

// --- parseTemplate ---

func TestParseTemplate_YAML_Valid(t *testing.T) {
Expand Down Expand Up @@ -280,4 +295,3 @@ func TestGenerateTemplate_GeneratedTemplateCanBeInstalled(t *testing.T) {
t.Errorf("WailsVersion = %d, want 3", tmpl.WailsVersion)
}
}

Loading