From 1222e3aa1bf87e7d271988cc10ad7d0993f84dd6 Mon Sep 17 00:00:00 2001 From: stffabi Date: Thu, 20 Apr 2023 12:37:40 +0200 Subject: [PATCH] [v2, dev] Use custom schemes for in-app dev mode (#2610) This fixes some long-standing inconsistencies between dev mode builds and production builds but is a breaking change. Dev mode uses custom scheme for Vite versions >= 3.0.0 and for older it still behaves in the old way. --- v2/cmd/wails/internal/dev/dev.go | 44 +++++++++++++---- v2/cmd/wails/internal/dev/stdout_scanner.go | 39 ++++++++++++++- .../customlayout/myfrontend/package.json | 2 +- v2/internal/app/app_dev.go | 49 +++++++++++++++++-- v2/internal/frontend/devserver/devserver.go | 42 ++-------------- .../assetserver/assethandler_external.go} | 35 ++++++------- .../templates/svelte-ts/frontend/package.json | 2 +- .../templates/svelte/frontend/package.json | 2 +- .../vanilla-ts/frontend/package.json | 2 +- .../templates/vanilla/frontend/package.json | 2 +- website/src/pages/changelog.mdx | 4 ++ 11 files changed, 145 insertions(+), 78 deletions(-) rename v2/{internal/frontend/devserver/external.go => pkg/assetserver/assethandler_external.go} (68%) diff --git a/v2/cmd/wails/internal/dev/dev.go b/v2/cmd/wails/internal/dev/dev.go index ca40f36a4..b7fc4d10d 100644 --- a/v2/cmd/wails/internal/dev/dev.go +++ b/v2/cmd/wails/internal/dev/dev.go @@ -22,6 +22,7 @@ import ( "github.com/wailsapp/wails/v2/cmd/wails/flags" "github.com/wailsapp/wails/v2/cmd/wails/internal/gomod" "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" + "golang.org/x/mod/semver" "github.com/wailsapp/wails/v2/pkg/commands/buildtags" @@ -36,6 +37,10 @@ import ( "github.com/wailsapp/wails/v2/pkg/commands/build" ) +const ( + viteMinVersion = "v3.0.0" +) + func sliceToMap(input []string) map[string]struct{} { result := map[string]struct{}{} for _, value := range input { @@ -88,10 +93,11 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { buildOptions.IgnoreApplication = false } + legacyUseDevServerInsteadofCustomScheme := false // frontend:dev:watcher command. frontendDevAutoDiscovery := projectConfig.IsFrontendDevServerURLAutoDiscovery() if command := projectConfig.DevWatcherCommand; command != "" { - closer, devServerURL, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery) + closer, devServerURL, devServerViteVersion, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery) if err != nil { return err } @@ -100,6 +106,12 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { f.FrontendDevServerURL = devServerURL } defer closer() + + if devServerViteVersion != "" && semver.Compare(devServerViteVersion, viteMinVersion) < 0 { + logutils.LogRed("Please upgrade your Vite Server to at least '%s' future Wails versions will require at least Vite '%s'", viteMinVersion, viteMinVersion) + time.Sleep(3 * time.Second) + legacyUseDevServerInsteadofCustomScheme = true + } } else if frontendDevAutoDiscovery { return fmt.Errorf("unable to auto discover frontend:dev:serverUrl without a frontend:dev:watcher command, please either set frontend:dev:watcher or remove the auto discovery from frontend:dev:serverUrl") } @@ -107,7 +119,7 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { // Do initial build but only for the application. logger.Println("Building application for development...") buildOptions.IgnoreFrontend = true - debugBinaryProcess, appBinary, err := restartApp(buildOptions, nil, f, exitCodeChannel) + debugBinaryProcess, appBinary, err := restartApp(buildOptions, nil, f, exitCodeChannel, legacyUseDevServerInsteadofCustomScheme) buildOptions.IgnoreFrontend = ignoreFrontend || f.FrontendDevServerURL != "" if err != nil { return err @@ -153,7 +165,7 @@ func Application(f *flags.Dev, logger *clilogger.CLILogger) error { }() // Watch for changes and trigger restartApp() - debugBinaryProcess = doWatcherLoop(buildOptions, debugBinaryProcess, f, watcher, exitCodeChannel, quitChannel, f.DevServerURL()) + debugBinaryProcess = doWatcherLoop(buildOptions, debugBinaryProcess, f, watcher, exitCodeChannel, quitChannel, f.DevServerURL(), legacyUseDevServerInsteadofCustomScheme) // Kill the current program if running and remove dev binary if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil { @@ -202,7 +214,7 @@ func runCommand(dir string, exitOnError bool, command string, args ...string) er } // runFrontendDevWatcherCommand will run the `frontend:dev:watcher` command if it was given, ex- `npm run dev` -func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool) (func(), string, error) { +func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool) (func(), string, string, error) { ctx, cancel := context.WithCancel(context.Background()) scanner := NewStdoutScanner() cmdSlice := strings.Split(devCommand, " ") @@ -214,7 +226,7 @@ func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, d if err := cmd.Start(); err != nil { cancel() - return nil, "", fmt.Errorf("unable to start frontend DevWatcher: %w", err) + return nil, "", "", fmt.Errorf("unable to start frontend DevWatcher: %w", err) } var viteServerURL string @@ -224,10 +236,19 @@ func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, d viteServerURL = serverURL case <-time.After(time.Second * 10): cancel() - return nil, "", errors.New("failed to find Vite server URL") + return nil, "", "", errors.New("failed to find Vite server URL") } } + viteVersion := "" + select { + case version := <-scanner.ViteServerVersionC: + viteVersion = version + + case <-time.After(time.Second * 5): + // That's fine, then most probably it was not vite that was running + } + logutils.LogGreen("Running frontend DevWatcher command: '%s'", devCommand) var wg sync.WaitGroup wg.Add(1) @@ -255,11 +276,11 @@ func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, d } cancel() wg.Wait() - }, viteServerURL, nil + }, viteServerURL, viteVersion, nil } // restartApp does the actual rebuilding of the application when files change -func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int) (*process.Process, string, error) { +func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int, legacyUseDevServerInsteadofCustomScheme bool) (*process.Process, string, error) { appBinary, err := build.Build(buildOptions) println() @@ -297,6 +318,9 @@ func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process os.Setenv("assetdir", f.AssetDir) os.Setenv("devserver", f.DevServer) os.Setenv("frontenddevserverurl", f.FrontendDevServerURL) + if legacyUseDevServerInsteadofCustomScheme { + os.Setenv("legacyusedevsererinsteadofcustomscheme", "true") + } // Start up new binary with correct args newProcess := process.NewProcess(appBinary, args...) @@ -316,7 +340,7 @@ func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process } // doWatcherLoop is the main watch loop that runs while dev is active -func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, watcher *fsnotify.Watcher, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL) *process.Process { +func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, watcher *fsnotify.Watcher, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL, legacyUseDevServerInsteadofCustomScheme bool) *process.Process { // Main Loop var extensionsThatTriggerARebuild = sliceToMap(strings.Split(f.Extensions, ",")) var dirsThatTriggerAReload []string @@ -422,7 +446,7 @@ func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Proc rebuild = false logutils.LogGreen("[Rebuild triggered] files updated") // Try and build the app - newBinaryProcess, _, err := restartApp(buildOptions, debugBinaryProcess, f, exitCodeChannel) + newBinaryProcess, _, err := restartApp(buildOptions, debugBinaryProcess, f, exitCodeChannel, legacyUseDevServerInsteadofCustomScheme) if err != nil { logutils.LogRed("Error during build: %s", err.Error()) continue diff --git a/v2/cmd/wails/internal/dev/stdout_scanner.go b/v2/cmd/wails/internal/dev/stdout_scanner.go index d84e4785e..dad4e72cf 100644 --- a/v2/cmd/wails/internal/dev/stdout_scanner.go +++ b/v2/cmd/wails/internal/dev/stdout_scanner.go @@ -2,30 +2,47 @@ package dev import ( "bufio" + "fmt" "net/url" "os" "strings" "github.com/acarl005/stripansi" "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" + "golang.org/x/mod/semver" ) // stdoutScanner acts as a stdout target that will scan the incoming // data to find out the vite server url type stdoutScanner struct { - ViteServerURLChan chan string + ViteServerURLChan chan string + ViteServerVersionC chan string + versionDetected bool } // NewStdoutScanner creates a new stdoutScanner func NewStdoutScanner() *stdoutScanner { return &stdoutScanner{ - ViteServerURLChan: make(chan string, 2), + ViteServerURLChan: make(chan string, 2), + ViteServerVersionC: make(chan string, 2), } } // Write bytes to the scanner. Will copy the bytes to stdout func (s *stdoutScanner) Write(data []byte) (n int, err error) { input := stripansi.Strip(string(data)) + if !s.versionDetected { + v, err := detectViteVersion(input) + if v != "" || err != nil { + if err != nil { + logutils.LogRed("ViteStdoutScanner: %s", err) + v = "v0.0.0" + } + s.ViteServerVersionC <- v + s.versionDetected = true + } + } + match := strings.Index(input, "Local:") if match != -1 { sc := bufio.NewScanner(strings.NewReader(input)) @@ -47,3 +64,21 @@ func (s *stdoutScanner) Write(data []byte) (n int, err error) { } return os.Stdout.Write(data) } + +func detectViteVersion(line string) (string, error) { + s := strings.Split(strings.TrimSpace(line), " ") + if strings.ToLower(s[0]) != "vite" { + return "", nil + } + + if len(line) < 2 { + return "", fmt.Errorf("unable to parse vite version") + } + + v := s[1] + if !semver.IsValid(v) { + return "", fmt.Errorf("%s is not a valid vite version string", v) + } + + return v, nil +} diff --git a/v2/examples/customlayout/myfrontend/package.json b/v2/examples/customlayout/myfrontend/package.json index 4ac881798..a1b6f8e1a 100644 --- a/v2/examples/customlayout/myfrontend/package.json +++ b/v2/examples/customlayout/myfrontend/package.json @@ -8,6 +8,6 @@ "preview": "vite preview" }, "devDependencies": { - "vite": "^2.9.9" + "vite": "^3.0.7" } } \ No newline at end of file diff --git a/v2/internal/app/app_dev.go b/v2/internal/app/app_dev.go index 32c27fa2e..8373d399f 100644 --- a/v2/internal/app/app_dev.go +++ b/v2/internal/app/app_dev.go @@ -8,9 +8,11 @@ import ( "flag" "fmt" iofs "io/fs" + "net" "net/url" "os" "path/filepath" + "time" "github.com/wailsapp/wails/v2/pkg/assetserver" @@ -104,17 +106,35 @@ func CreateApp(appoptions *options.App) (*App, error) { } if frontendDevServerURL != "" { - if devServer == "" { - return nil, fmt.Errorf("Unable to use FrontendDevServerUrl without a DevServer address") + if os.Getenv("legacyusedevsererinsteadofcustomscheme") != "" { + startURL, err := url.Parse("http://" + devServer) + if err != nil { + return nil, err + } + + ctx = context.WithValue(ctx, "starturl", startURL) } - startURL, err := url.Parse("http://" + devServer) + ctx = context.WithValue(ctx, "frontenddevserverurl", frontendDevServerURL) + + externalURL, err := url.Parse(frontendDevServerURL) if err != nil { return nil, err } - ctx = context.WithValue(ctx, "starturl", startURL) - ctx = context.WithValue(ctx, "frontenddevserverurl", frontendDevServerURL) + if externalURL.Host == "" { + return nil, fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?") + } + + waitCb := func() { myLogger.Debug("Waiting for frontend DevServer '%s' to be ready", externalURL) } + if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) { + myLogger.Error("Timeout waiting for frontend DevServer") + } + + handler := assetserver.NewExternalAssetsHandler(myLogger, assetConfig, externalURL) + assetConfig.Assets = nil + assetConfig.Handler = handler + assetConfig.Middleware = nil myLogger.Info("Serving assets from frontend DevServer URL: %s", frontendDevServerURL) } else { @@ -246,3 +266,22 @@ func tryInferAssetDirFromFS(assets iofs.FS) (string, error) { return path, nil } + +func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) { + if timeout == 0 { + timeout = time.Minute + } + + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + conn, _ := net.DialTimeout("tcp", host, 2*time.Second) + if conn != nil { + conn.Close() + return true + } + + waitCB() + time.Sleep(1 * time.Second) + } + return false +} diff --git a/v2/internal/frontend/devserver/devserver.go b/v2/internal/frontend/devserver/devserver.go index 47dde2953..35b67e86a 100644 --- a/v2/internal/frontend/devserver/devserver.go +++ b/v2/internal/frontend/devserver/devserver.go @@ -10,13 +10,11 @@ import ( "encoding/json" "fmt" "log" - "net" "net/http" "net/http/httputil" "net/url" "strings" "sync" - "time" "github.com/wailsapp/wails/v2/pkg/assetserver" @@ -67,7 +65,6 @@ func (d *DevWebServer) Run(ctx context.Context) error { myLogger = _logger.(*logger.Logger) } - var assetHandler http.Handler var wsHandler http.Handler _fronendDevServerURL, _ := ctx.Value("frontenddevserverurl").(string) @@ -77,33 +74,23 @@ func (d *DevWebServer) Run(ctx context.Context) error { return c.String(http.StatusOK, assetdir) }) - var err error - assetHandler, err = assetserver.NewAssetHandler(assetServerConfig, myLogger) - if err != nil { - log.Fatal(err) - } } else { externalURL, err := url.Parse(_fronendDevServerURL) if err != nil { return err } - if externalURL.Host == "" { - return fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?") - } - - waitCb := func() { d.LogDebug("Waiting for frontend DevServer '%s' to be ready", externalURL) } - if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) { - d.logger.Error("Timeout waiting for frontend DevServer") - } - - assetHandler = newExternalDevServerAssetHandler(d.logger, externalURL, assetServerConfig) // WebSockets aren't currently supported in prod mode, so a WebSocket connection is the result of the // FrontendDevServer e.g. Vite to support auto reloads. // Therefore we direct WebSockets directly to the FrontendDevServer instead of returning a NotImplementedStatus. wsHandler = httputil.NewSingleHostReverseProxy(externalURL) } + assetHandler, err := assetserver.NewAssetHandler(assetServerConfig, myLogger) + if err != nil { + log.Fatal(err) + } + // Setup internal dev server bindingsJSON, err := d.appBindings.ToJSON() if err != nil { @@ -307,22 +294,3 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. result.server.HidePort = true return result } - -func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) { - if timeout == 0 { - timeout = time.Minute - } - - deadline := time.Now().Add(timeout) - for time.Now().Before(deadline) { - conn, _ := net.DialTimeout("tcp", host, 2*time.Second) - if conn != nil { - conn.Close() - return true - } - - waitCB() - time.Sleep(1 * time.Second) - } - return false -} diff --git a/v2/internal/frontend/devserver/external.go b/v2/pkg/assetserver/assethandler_external.go similarity index 68% rename from v2/internal/frontend/devserver/external.go rename to v2/pkg/assetserver/assethandler_external.go index fd717e723..588b350f5 100644 --- a/v2/internal/frontend/devserver/external.go +++ b/v2/pkg/assetserver/assethandler_external.go @@ -1,7 +1,7 @@ //go:build dev // +build dev -package devserver +package assetserver import ( "errors" @@ -10,21 +10,12 @@ import ( "net/http/httputil" "net/url" - "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/pkg/options/assetserver" ) -func newExternalDevServerAssetHandler(logger *logger.Logger, url *url.URL, options assetserver.Options) http.Handler { - handler := newExternalAssetsHandler(logger, url, options.Handler) +func NewExternalAssetsHandler(logger Logger, options assetserver.Options, url *url.URL) http.Handler { + baseHandler := options.Handler - if middleware := options.Middleware; middleware != nil { - handler = middleware(handler) - } - - return handler -} - -func newExternalAssetsHandler(logger *logger.Logger, url *url.URL, handler http.Handler) http.Handler { errSkipProxy := fmt.Errorf("skip proxying") proxy := httputil.NewSingleHostReverseProxy(url) @@ -37,7 +28,7 @@ func newExternalAssetsHandler(logger *logger.Logger, url *url.URL, handler http. } proxy.ModifyResponse = func(res *http.Response) error { - if handler == nil { + if baseHandler == nil { return nil } @@ -53,11 +44,11 @@ func newExternalAssetsHandler(logger *logger.Logger, url *url.URL, handler http. } proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) { - if handler != nil && errors.Is(err, errSkipProxy) { + if baseHandler != nil && errors.Is(err, errSkipProxy) { if logger != nil { - logger.Debug("[ExternalAssetHandler] Loading '%s' failed, using AssetHandler", r.URL) + logger.Debug("[ExternalAssetHandler] Loading '%s' failed, using original AssetHandler", r.URL) } - handler.ServeHTTP(rw, r) + baseHandler.ServeHTTP(rw, r) } else { if logger != nil { logger.Error("[ExternalAssetHandler] Proxy error: %v", err) @@ -66,18 +57,24 @@ func newExternalAssetsHandler(logger *logger.Logger, url *url.URL, handler http. } } - return http.HandlerFunc( + var result http.Handler = http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { if req.Method == http.MethodGet { proxy.ServeHTTP(rw, req) return } - if handler != nil { - handler.ServeHTTP(rw, req) + if baseHandler != nil { + baseHandler.ServeHTTP(rw, req) return } rw.WriteHeader(http.StatusMethodNotAllowed) }) + + if middleware := options.Middleware; middleware != nil { + result = middleware(result) + } + + return result } diff --git a/v2/pkg/templates/templates/svelte-ts/frontend/package.json b/v2/pkg/templates/templates/svelte-ts/frontend/package.json index 8bbb15b1b..2ee69eaf5 100644 --- a/v2/pkg/templates/templates/svelte-ts/frontend/package.json +++ b/v2/pkg/templates/templates/svelte-ts/frontend/package.json @@ -17,6 +17,6 @@ "svelte-preprocess": "^4.10.7", "tslib": "^2.4.0", "typescript": "^4.6.4", - "vite": "^3.0.0" + "vite": "^3.0.7" } } \ No newline at end of file diff --git a/v2/pkg/templates/templates/svelte/frontend/package.json b/v2/pkg/templates/templates/svelte/frontend/package.json index 8a9354150..8c9ae62a8 100644 --- a/v2/pkg/templates/templates/svelte/frontend/package.json +++ b/v2/pkg/templates/templates/svelte/frontend/package.json @@ -11,6 +11,6 @@ "devDependencies": { "@sveltejs/vite-plugin-svelte": "^1.0.1", "svelte": "^3.49.0", - "vite": "^3.0.0" + "vite": "^3.0.7" } } \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla-ts/frontend/package.json b/v2/pkg/templates/templates/vanilla-ts/frontend/package.json index 37305b4c0..c57eb8610 100644 --- a/v2/pkg/templates/templates/vanilla-ts/frontend/package.json +++ b/v2/pkg/templates/templates/vanilla-ts/frontend/package.json @@ -9,6 +9,6 @@ }, "devDependencies": { "typescript": "^4.5.4", - "vite": "^2.9.9" + "vite": "^3.0.7" } } \ No newline at end of file diff --git a/v2/pkg/templates/templates/vanilla/frontend/package.json b/v2/pkg/templates/templates/vanilla/frontend/package.json index 4ac881798..a1b6f8e1a 100644 --- a/v2/pkg/templates/templates/vanilla/frontend/package.json +++ b/v2/pkg/templates/templates/vanilla/frontend/package.json @@ -8,6 +8,6 @@ "preview": "vite preview" }, "devDependencies": { - "vite": "^2.9.9" + "vite": "^3.0.7" } } \ No newline at end of file diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 0a0ab8359..7b596377b 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Breaking Changes + +- `wails dev` now uses the custom schemes `wails://` on macOS and Linux if Vite >= `v3.0.0` is used. This makes the dev application consistent in behaviour with the final production application and fixes some long-standing inconsistencies. Changed by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2610) + ### Added - Added Nodejs version in `wails doctor`. Added by @misitebao in [PR](https://github.com/wailsapp/wails/pull/2546)