Skip to content
Open
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
21 changes: 21 additions & 0 deletions v3/UNRELEASED_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@ After processing, the content will be moved to the main changelog and this file

## Changed
<!-- Changes in existing functionality -->
- Start the native application in `wails3 dev` only after the frontend development server is accepting connections. Existing alpha projects must add a `dev:wait` task to `build/Taskfile.yml`:

```yaml
dev:wait:
summary: Waits for the frontend development server
vars:
TIMEOUT: 60
cmds:
- wails3 tool waitport --timeout {{.TIMEOUT}}
```

Then insert the readiness step in `build/config.yml` after the background frontend task and before the primary application task:

```yaml
- cmd: wails3 task common:dev:frontend
type: background
- cmd: wails3 task common:dev:wait
type: once
- cmd: wails3 task run
type: primary
```

## Fixed
<!-- Bug fixes -->
Expand Down
1 change: 1 addition & 0 deletions v3/cmd/wails3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func main() {

tool := app.NewSubCommand("tool", "Various tools")
tool.NewSubCommandFunction("checkport", "Checks if a port is open. Useful for testing if vite is running.", commands.ToolCheckPort)
tool.NewSubCommandFunction("waitport", "Waits for a port to open. Useful for gating dependent development tasks.", commands.ToolWaitPort)
tool.NewSubCommandFunction("watcher", "Watches files and runs a command when they change", commands.Watcher)
tool.NewSubCommandFunction("cp", "Copy files", commands.Cp)
tool.NewSubCommandFunction("buildinfo", "Show Build Info", commands.BuildInfo)
Expand Down
2 changes: 1 addition & 1 deletion v3/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
github.com/adrg/xdg v0.5.3
github.com/atotto/clipboard v0.1.4
github.com/atterpac/refresh v1.0.0
github.com/atterpac/refresh v1.1.2
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
Comment on lines 8 to 10
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
Expand Down
4 changes: 2 additions & 2 deletions v3/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/atterpac/refresh v1.0.0 h1:IK/rh3w5cD7nb6GuqzfScIdNuAz/E0sZz10k1pioIFE=
github.com/atterpac/refresh v1.0.0/go.mod h1:+vQ8OHgGmZ7wwoZfxxkT6Nr/gKA8j78Rbt+qcLLDEoc=
github.com/atterpac/refresh v1.1.2 h1:NvukuugqyZZ/vCArMVssBWSfg3dwfDbbTdav97d+leM=
github.com/atterpac/refresh v1.1.2/go.mod h1:+vQ8OHgGmZ7wwoZfxxkT6Nr/gKA8j78Rbt+qcLLDEoc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
Expand Down
7 changes: 7 additions & 0 deletions v3/internal/commands/build_assets/Taskfile.tmpl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ tasks:
cmds:
- task: frontend:dev:{{.Opn}}.PACKAGE_MANAGER{{.Cls}}

dev:wait:
summary: Waits for the frontend development server
vars:
TIMEOUT: 60
cmds:
- wails3 tool waitport --timeout {{.Opn}}.TIMEOUT{{.Cls}}

frontend:dev:npm:
dir: frontend
cmds:
Expand Down
4 changes: 3 additions & 1 deletion v3/internal/commands/build_assets/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ dev_mode:
type: blocking
- cmd: wails3 task common:dev:frontend
type: background
- cmd: wails3 task common:dev:wait
type: once
- cmd: wails3 task run
type: primary

Expand All @@ -76,4 +78,4 @@ fileAssociations:

# Other data
other:
- name: My Other Data
- name: My Other Data
47 changes: 47 additions & 0 deletions v3/internal/commands/dev_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package commands

import (
"testing"

"github.com/atterpac/refresh/process"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestDevConfigGatesApplicationOnFrontendReadiness(t *testing.T) {
data, err := buildAssets.ReadFile("build_assets/config.yml")
require.NoError(t, err)

var config struct {
DevMode struct {
Executes []process.Execute `yaml:"executes"`
} `yaml:"dev_mode"`
}
require.NoError(t, yaml.Unmarshal(data, &config))

require.Len(t, config.DevMode.Executes, 4)
require.Equal(t, process.Background, config.DevMode.Executes[1].Type)
require.Equal(t, "wails3 task common:dev:wait", config.DevMode.Executes[2].Cmd)
require.Equal(t, process.Once, config.DevMode.Executes[2].Type)
require.Equal(t, process.Primary, config.DevMode.Executes[3].Type)
}

func TestFrontendDevServerTimeoutIsTaskLocal(t *testing.T) {
data, err := buildAssets.ReadFile("build_assets/Taskfile.tmpl.yml")
require.NoError(t, err)

var taskfile struct {
Tasks map[string]yaml.Node `yaml:"tasks"`
}
require.NoError(t, yaml.Unmarshal(data, &taskfile))

waitTaskNode, exists := taskfile.Tasks["dev:wait"]
require.True(t, exists)
var waitTask struct {
Vars map[string]int `yaml:"vars"`
Cmds []string `yaml:"cmds"`
}
require.NoError(t, waitTaskNode.Decode(&waitTask))
require.Equal(t, 60, waitTask.Vars["TIMEOUT"])
require.Equal(t, []string{"wails3 tool waitport --timeout {{.Opn}}.TIMEOUT{{.Cls}}"}, waitTask.Cmds)
}
69 changes: 69 additions & 0 deletions v3/internal/commands/tool_waitport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package commands

import (
"fmt"
"os"
"strconv"
"time"
)

const portWaitInterval = 100 * time.Millisecond

type ToolWaitPortOptions struct {
Host string `name:"h" description:"Host to check" default:"localhost"`
Port int `name:"p" description:"Port to check; defaults to WAILS_VITE_PORT when set"`
Timeout int `name:"timeout" description:"Maximum number of seconds to wait for the port to open" default:"60"`
}

func waitForPort(check func() bool, timeout time.Duration) bool {
if check() {
return true
}
if timeout <= 0 {
return false
}

ticker := time.NewTicker(portWaitInterval)
defer ticker.Stop()
timer := time.NewTimer(timeout)
defer timer.Stop()

for {
select {
case <-ticker.C:
if check() {
return true
}
case <-timer.C:
return false
}
}
}

func ToolWaitPort(options *ToolWaitPortOptions) error {
DisableFooter = true

if options.Port == 0 {
port := os.Getenv(wailsVitePort)
if port == "" {
return fmt.Errorf("please use the -p flag to specify a port or set %s", wailsVitePort)
}
var err error
options.Port, err = strconv.Atoi(port)
if err != nil {
return fmt.Errorf("invalid %s value %q: %w", wailsVitePort, port, err)
}
}
if options.Port < 1 || options.Port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
if options.Timeout <= 0 {
return fmt.Errorf("timeout must be greater than zero")
}

timeout := time.Duration(options.Timeout) * time.Second
if !waitForPort(func() bool { return isPortOpen(options.Host, options.Port) }, timeout) {
return fmt.Errorf("timed out after %s waiting for port %d to open on %s", timeout, options.Port, options.Host)
Comment thread
savely-krasovsky marked this conversation as resolved.
}
return nil
}
36 changes: 36 additions & 0 deletions v3/internal/commands/tool_waitport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package commands

import (
"testing"
"time"
)

func TestWaitForPortRetriesUntilReady(t *testing.T) {
attempts := 0
ready := waitForPort(func() bool {
attempts++
return attempts == 3
}, time.Second)

if !ready {
t.Fatal("waitForPort reported that the port was unavailable")
}
if attempts != 3 {
t.Fatalf("attempt count = %d, want 3", attempts)
}
}

func TestWaitForPortDoesNotRetryWithoutTimeout(t *testing.T) {
attempts := 0
ready := waitForPort(func() bool {
attempts++
return false
}, 0)

if ready {
t.Fatal("waitForPort reported that the port was available")
}
if attempts != 1 {
t.Fatalf("attempt count = %d, want 1", attempts)
}
}
14 changes: 7 additions & 7 deletions v3/internal/commands/watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ package commands
import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEnsureIgnored(t *testing.T) {
t.Run("adds pattern when not present", func(t *testing.T) {
list := []string{".gitignore", ".DS_Store"}
ensureIgnored(&list, "*_test.go")
assert.Contains(t, list, "*_test.go")
assert.Len(t, list, 3)
require.Contains(t, list, "*_test.go")
require.Len(t, list, 3)
})

t.Run("does not duplicate pattern when already present", func(t *testing.T) {
list := []string{".gitignore", "*_test.go"}
ensureIgnored(&list, "*_test.go")
assert.Contains(t, list, "*_test.go")
assert.Len(t, list, 2)
require.Contains(t, list, "*_test.go")
require.Len(t, list, 2)
})

t.Run("adds to empty list", func(t *testing.T) {
var list []string
ensureIgnored(&list, "*_test.go")
assert.Contains(t, list, "*_test.go")
assert.Len(t, list, 1)
require.Contains(t, list, "*_test.go")
require.Len(t, list, 1)
})
}
6 changes: 0 additions & 6 deletions v3/pkg/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -650,12 +650,6 @@ func (a *App) Run() error {
// Ensure application context is cancelled in case of failures.
defer a.cancel()

// Call post-create hooks
err := a.preRun()
if err != nil {
return err
}

a.impl = newPlatformApp(a)

// Ensure services are shut down in case of failures.
Expand Down
47 changes: 5 additions & 42 deletions v3/pkg/application/application_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,13 @@

package application

import (
"net/http"
"time"

"github.com/wailsapp/wails/v3/internal/assetserver"
)

var devMode = false

func (a *App) preRun() error {
// Check for frontend server url
frontendURL := assetserver.GetDevServerURL()
if frontendURL != "" {
devMode = true
// We want to check if the frontend server is running by trying to http get the url
// and if it is not, we wait 500ms and try again for a maximum of 10 times. If it is
// still not available, we return an error.
// This is to allow the frontend server to start up before the backend server.
client := http.Client{}
a.Logger.Info("Waiting for frontend dev server to start...", "url", frontendURL)
for i := 0; i < 10; i++ {
_, err := client.Get(frontendURL)
if err == nil {
a.Logger.Info("Connected to frontend dev server!")
return nil
}
// Wait 500ms
time.Sleep(500 * time.Millisecond)
if i%2 == 0 {
a.Logger.Info("Retrying...")
}
}
a.fatal("unable to connect to frontend server. Please check it is running - FRONTEND_DEVSERVER_URL='%s'", frontendURL)
}
return nil
}
import "github.com/wailsapp/wails/v3/internal/assetserver"

func (a *App) postQuit() {
if devMode {
a.Logger.Info("The application has terminated, but the watcher is still running.")
a.Logger.Info("To terminate the watcher, press CTRL+C")
if assetserver.GetDevServerURL() == "" {
return
}
}

func (a *App) enableDevTools() {

a.Logger.Info("The application has terminated, but the watcher is still running.")
a.Logger.Info("To terminate the watcher, press CTRL+C")
}
4 changes: 1 addition & 3 deletions v3/pkg/application/application_production.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,4 @@ func newApplication(options Options) *App {

func (a *App) logStartup() {}

func (a *App) preRun() error { return nil }

func (a *App) postQuit() error { return nil }
func (a *App) postQuit() {}
Loading