mirror of
https://github.com/wavetermdev/wails.git
synced 2026-04-22 15:26:15 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b764e8ce4 | |||
| 26bb58e128 | |||
| 0e20677116 | |||
| 62c47bd089 |
@@ -25,7 +25,7 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := clir.NewCli("wails", "The Wails CLI", "v3")
|
||||
app := clir.NewCli("wails", "The Wails3 CLI", "v3")
|
||||
app.NewSubCommandFunction("init", "Initialise a new project", commands.Init)
|
||||
app.NewSubCommandFunction("build", "Build the project", commands.Build)
|
||||
app.NewSubCommandFunction("dev", "Run in Dev mode", commands.Dev)
|
||||
@@ -37,7 +37,7 @@ func main() {
|
||||
task.Action(func() error {
|
||||
return commands.RunTask(&taskFlags, task.OtherArgs())
|
||||
})
|
||||
task.LongDescription("\nUsage: wails task [taskname] [flags]\n\nTasks are defined in the `Taskfile.yaml` file. See https://taskfile.dev for more information.")
|
||||
task.LongDescription("\nUsage: wails3 task [taskname] [flags]\n\nTasks are defined in the `Taskfile.yaml` file. See https://taskfile.dev for more information.")
|
||||
generate := app.NewSubCommand("generate", "Generation tools")
|
||||
generate.NewSubCommandFunction("build-assets", "Generate build assets", commands.GenerateBuildAssets)
|
||||
generate.NewSubCommandFunction("icons", "Generate icons", commands.GenerateIcons)
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.21.1
|
||||
toolchain go1.21.4
|
||||
|
||||
require (
|
||||
github.com/atterpac/refresh v0.2.4
|
||||
github.com/atterpac/refresh v0.4.29
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/ebitengine/purego v0.4.0-alpha.4
|
||||
|
||||
@@ -67,6 +67,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/atterpac/refresh v0.2.4 h1:rTD9qchbLa0qPIVr+qpyqkePe6+1Cs14ETXDguEvttI=
|
||||
github.com/atterpac/refresh v0.2.4/go.mod h1:KiBQUk5h76Z3Yvw/k3Ys2JGDrKmKmflwS9XB6B5N9qY=
|
||||
github.com/atterpac/refresh v0.4.29 h1:ObGnm1AxV3WWBXHFWlyOTNT/cPyLDtMW8JkexoXKdG4=
|
||||
github.com/atterpac/refresh v0.4.29/go.mod h1:nwaO4jT1N3pscamqKFExmlYi8K+kfgRFSmzwxFRJRKQ=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
|
||||
@@ -14,9 +14,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed defaultindex.html
|
||||
var defaultHTML []byte
|
||||
|
||||
const (
|
||||
indexHTML = "index.html"
|
||||
)
|
||||
@@ -30,7 +27,7 @@ type assetHandler struct {
|
||||
retryMissingFiles bool
|
||||
}
|
||||
|
||||
func NewAssetHandler(options *Options, log *slog.Logger) (http.Handler, error) {
|
||||
func NewDefaultAssetHandler(options *Options) (http.Handler, error) {
|
||||
|
||||
vfs := options.Assets
|
||||
if vfs != nil {
|
||||
@@ -62,7 +59,7 @@ func NewAssetHandler(options *Options, log *slog.Logger) (http.Handler, error) {
|
||||
var result http.Handler = &assetHandler{
|
||||
fs: vfs,
|
||||
handler: options.Handler,
|
||||
logger: log,
|
||||
logger: options.Logger,
|
||||
}
|
||||
|
||||
if middleware := options.Middleware; middleware != nil {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func NewExternalAssetsHandler(logger *slog.Logger, options Options, url *url.URL) http.Handler {
|
||||
baseHandler := options.Handler
|
||||
|
||||
errSkipProxy := fmt.Errorf("skip proxying")
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(url)
|
||||
baseDirector := proxy.Director
|
||||
proxy.Director = func(r *http.Request) {
|
||||
baseDirector(r)
|
||||
if logger != nil {
|
||||
logger.Debug("[ExternalAssetHandler] Loading '%s'", r.URL)
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ModifyResponse = func(res *http.Response) error {
|
||||
if baseHandler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusSwitchingProtocols {
|
||||
return nil
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed {
|
||||
return errSkipProxy
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
if baseHandler != nil && errors.Is(err, errSkipProxy) {
|
||||
if logger != nil {
|
||||
logger.Debug("[ExternalAssetHandler] Loading '%s' failed, using original AssetHandler", r.URL)
|
||||
}
|
||||
baseHandler.ServeHTTP(rw, r)
|
||||
} else {
|
||||
if logger != nil {
|
||||
logger.Error("[ExternalAssetHandler] Proxy error: %v", err)
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
var result http.Handler = http.HandlerFunc(
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == http.MethodGet {
|
||||
proxy.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
if baseHandler != nil {
|
||||
baseHandler.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
})
|
||||
|
||||
if middleware := options.Middleware; middleware != nil {
|
||||
result = middleware(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,157 +1,60 @@
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"net"
|
||||
"html"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
const (
|
||||
runtimeJSPath = "/wails/runtime.js"
|
||||
runtimePath = "/wails/runtime"
|
||||
capabilitiesPath = "/wails/capabilities"
|
||||
flagsPath = "/wails/flags"
|
||||
|
||||
webViewRequestHeaderWindowId = "x-wails-window-id"
|
||||
webViewRequestHeaderWindowName = "x-wails-window-name"
|
||||
)
|
||||
|
||||
const webViewRequestHeaderWindowId = "x-wails-window-id"
|
||||
const webViewRequestHeaderWindowName = "x-wails-window-name"
|
||||
|
||||
type RuntimeAssets interface {
|
||||
DesktopIPC() []byte
|
||||
WebsocketIPC() []byte
|
||||
RuntimeDesktopJS() []byte
|
||||
}
|
||||
|
||||
type RuntimeHandler interface {
|
||||
HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type AssetServer struct {
|
||||
handler http.Handler
|
||||
runtimeJS []byte
|
||||
debug bool
|
||||
ipcJS func(*http.Request) []byte
|
||||
|
||||
logger *slog.Logger
|
||||
runtime RuntimeAssets
|
||||
options *Options
|
||||
|
||||
servingFromDisk bool
|
||||
|
||||
// Use http based runtime
|
||||
runtimeHandler RuntimeHandler
|
||||
|
||||
// plugin scripts
|
||||
pluginScripts map[string]string
|
||||
|
||||
// GetCapabilities returns the capabilities of the runtime
|
||||
GetCapabilities func() []byte
|
||||
|
||||
// GetFlags returns the application flags
|
||||
GetFlags func() []byte
|
||||
|
||||
// External dev server proxy
|
||||
handler http.Handler
|
||||
wsHandler *httputil.ReverseProxy
|
||||
|
||||
// External dev server URL
|
||||
pluginScripts map[string]string
|
||||
|
||||
devServerURL string
|
||||
|
||||
assetServerWebView
|
||||
}
|
||||
|
||||
func NewAssetServer(options *Options, servingFromDisk bool, logger *slog.Logger, runtime RuntimeAssets, debug bool, runtimeHandler RuntimeHandler) (*AssetServer, error) {
|
||||
handler, err := NewAssetHandler(options, logger)
|
||||
func NewAssetServer(options *Options) (*AssetServer, error) {
|
||||
result := &AssetServer{
|
||||
options: options,
|
||||
}
|
||||
|
||||
var err error
|
||||
result.handler, err = result.setupHandler()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
buffer.Write(runtime.RuntimeDesktopJS())
|
||||
|
||||
result := &AssetServer{
|
||||
handler: handler,
|
||||
runtimeJS: buffer.Bytes(),
|
||||
runtimeHandler: runtimeHandler,
|
||||
options: options,
|
||||
|
||||
// Check if we have been given a directory to serve assets from.
|
||||
// If so, this means we are in dev mode and are serving assets off disk.
|
||||
// We indicate this through the `servingFromDisk` flag to ensure requests
|
||||
// aren't cached in dev mode.
|
||||
servingFromDisk: servingFromDisk,
|
||||
logger: logger,
|
||||
runtime: runtime,
|
||||
debug: debug,
|
||||
}
|
||||
|
||||
// Check if proxy required
|
||||
result.devServerURL = GetDevServerURL()
|
||||
if result.devServerURL != "" {
|
||||
logger.Info("Using External DevServer", "url", result.devServerURL)
|
||||
// Parse devServerURL into url.URL
|
||||
devServerURL, err := url.Parse(result.devServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = result.checkDevServerURL(devServerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.wsHandler = httputil.NewSingleHostReverseProxy(devServerURL)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *AssetServer) checkDevServerURL(devServerURL *url.URL) error {
|
||||
// Open a connection to the devserver URL
|
||||
hostPort := devServerURL.Hostname() + ":" + devServerURL.Port()
|
||||
_, err := net.DialTimeout("tcp", hostPort, 1*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to connect to dev server: %s. Please check it's running", d.devServerURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AssetServer) LogDetails() {
|
||||
if d.debug {
|
||||
var info = []any{
|
||||
"assetsFS", d.options.Assets != nil,
|
||||
"middleware", d.options.Middleware != nil,
|
||||
"handler", d.options.Handler != nil,
|
||||
"devServerURL", d.devServerURL,
|
||||
}
|
||||
if d.devServerURL != "" {
|
||||
info = append(info, "devServerURL", d.devServerURL)
|
||||
}
|
||||
d.logger.Info("AssetServer Info:", info...)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AssetServer) AddPluginScript(pluginName string, script string) {
|
||||
if d.pluginScripts == nil {
|
||||
d.pluginScripts = make(map[string]string)
|
||||
}
|
||||
pluginName = strings.ReplaceAll(pluginName, "/", "_")
|
||||
pluginName = html.EscapeString(pluginName)
|
||||
pluginScriptName := fmt.Sprintf("/plugin_%s_%d.js", pluginName, rand.Intn(100000))
|
||||
d.pluginScripts[pluginScriptName] = script
|
||||
}
|
||||
|
||||
func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
start := time.Now()
|
||||
wrapped := &contentTypeSniffer{rw: rw}
|
||||
d.serveHTTP(wrapped, req)
|
||||
d.logger.Info(
|
||||
a.serveHTTP(wrapped, req)
|
||||
a.options.Logger.Info(
|
||||
"Asset Request:",
|
||||
"windowName", req.Header.Get(webViewRequestHeaderWindowName),
|
||||
"windowID", req.Header.Get(webViewRequestHeaderWindowId),
|
||||
@@ -162,10 +65,10 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
)
|
||||
}
|
||||
|
||||
func (d *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if d.wsHandler != nil {
|
||||
d.wsHandler.ServeHTTP(rw, req)
|
||||
if a.wsHandler != nil {
|
||||
a.wsHandler.ServeHTTP(rw, req)
|
||||
return
|
||||
} else {
|
||||
if isWebSocket(req) {
|
||||
@@ -176,30 +79,26 @@ func (d *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
header := rw.Header()
|
||||
if d.servingFromDisk {
|
||||
header.Add(HeaderCacheControl, "no-cache")
|
||||
}
|
||||
// TODO: I don't think this is needed now?
|
||||
//if a.servingFromDisk {
|
||||
// header.Add(HeaderCacheControl, "no-cache")
|
||||
//}
|
||||
|
||||
path := req.URL.Path
|
||||
switch path {
|
||||
case "", "/", "/index.html":
|
||||
recorder := httptest.NewRecorder()
|
||||
d.handler.ServeHTTP(recorder, req)
|
||||
a.handler.ServeHTTP(recorder, req)
|
||||
for k, v := range recorder.Result().Header {
|
||||
header[k] = v
|
||||
}
|
||||
|
||||
switch recorder.Code {
|
||||
case http.StatusOK:
|
||||
content, err := d.processIndexHTML(recorder.Body.Bytes())
|
||||
if err != nil {
|
||||
d.serveError(rw, err, "Unable to processIndexHTML")
|
||||
return
|
||||
}
|
||||
d.writeBlob(rw, indexHTML, content)
|
||||
a.writeBlob(rw, indexHTML, recorder.Body.Bytes())
|
||||
|
||||
case http.StatusNotFound:
|
||||
d.writeBlob(rw, indexHTML, defaultHTML)
|
||||
a.writeBlob(rw, indexHTML, defaultIndexHTML())
|
||||
|
||||
default:
|
||||
rw.WriteHeader(recorder.Code)
|
||||
@@ -207,83 +106,62 @@ func (d *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
return
|
||||
|
||||
case runtimeJSPath:
|
||||
d.writeBlob(rw, path, d.runtimeJS)
|
||||
|
||||
case capabilitiesPath:
|
||||
var data = []byte("{}")
|
||||
if d.GetCapabilities != nil {
|
||||
data = d.GetCapabilities()
|
||||
}
|
||||
d.writeBlob(rw, path, data)
|
||||
var data = a.options.GetCapabilities()
|
||||
a.writeBlob(rw, path, data)
|
||||
|
||||
case flagsPath:
|
||||
var data = []byte("{}")
|
||||
if d.GetFlags != nil {
|
||||
data = d.GetFlags()
|
||||
}
|
||||
d.writeBlob(rw, path, data)
|
||||
var data = a.options.GetFlags()
|
||||
a.writeBlob(rw, path, data)
|
||||
|
||||
case runtimePath:
|
||||
d.runtimeHandler.HandleRuntimeCall(rw, req)
|
||||
a.options.RuntimeHandler.ServeHTTP(rw, req)
|
||||
return
|
||||
|
||||
default:
|
||||
// Check if this is a plugin script
|
||||
if script, ok := d.pluginScripts[path]; ok {
|
||||
d.writeBlob(rw, path, []byte(script))
|
||||
if script, ok := a.pluginScripts[path]; ok {
|
||||
a.writeBlob(rw, path, []byte(script))
|
||||
} else {
|
||||
d.handler.ServeHTTP(rw, req)
|
||||
a.handler.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
|
||||
htmlNode, err := getHTMLNode(indexHTML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if d.debug {
|
||||
err = appendSpinnerToBody(htmlNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Inject plugins
|
||||
for scriptName := range d.pluginScripts {
|
||||
if err := insertScriptInHead(htmlNode, scriptName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err = html.Render(&buffer, htmlNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (d *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
|
||||
func (a *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
|
||||
err := serveFile(rw, filename, blob)
|
||||
if err != nil {
|
||||
d.serveError(rw, err, "Unable to write content %s", filename)
|
||||
a.serveError(rw, err, "Unable to write content %s", filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) {
|
||||
func (a *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) {
|
||||
args = append(args, err)
|
||||
d.logError(msg+": %s", args...)
|
||||
a.options.Logger.Error(msg+":", args...)
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func (d *AssetServer) logInfo(message string, args ...interface{}) {
|
||||
d.logger.Info("Asset Request: "+message, args...)
|
||||
func (a *AssetServer) LogDetails() {
|
||||
if a.options.IsDebug {
|
||||
var info = []any{
|
||||
"assetsFS", a.options.Assets != nil,
|
||||
"middleware", a.options.Middleware != nil,
|
||||
"handler", a.options.Handler != nil,
|
||||
}
|
||||
if a.devServerURL != "" {
|
||||
info = append(info, "devServerURL", a.devServerURL)
|
||||
}
|
||||
a.options.Logger.Info("AssetServer Info:", info...)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AssetServer) logError(message string, args ...interface{}) {
|
||||
d.logger.Error("Asset Request: "+message, args...)
|
||||
func (a *AssetServer) AddPluginScript(pluginName string, script string) {
|
||||
if a.pluginScripts == nil {
|
||||
a.pluginScripts = make(map[string]string)
|
||||
}
|
||||
pluginName = strings.ReplaceAll(pluginName, "/", "_")
|
||||
pluginName = html.EscapeString(pluginName)
|
||||
pluginScriptName := fmt.Sprintf("/wails/plugin/%s.js", pluginName)
|
||||
a.pluginScripts[pluginScriptName] = script
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !production
|
||||
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func GetDevServerURL() string {
|
||||
return os.Getenv("WAILS_DEVSERVER_URL")
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build production
|
||||
|
||||
package assetserver
|
||||
|
||||
func GetDevServerURL() string {
|
||||
return ""
|
||||
}
|
||||
@@ -23,9 +23,9 @@ type assetServerWebView struct {
|
||||
// ServeWebViewRequest processes the HTTP Request asynchronously by faking a golang HTTP Server.
|
||||
// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
|
||||
// The AssetServer takes ownership of the request and the caller mustn't close it or access it in any other way.
|
||||
func (d *AssetServer) ServeWebViewRequest(req webview.Request) {
|
||||
d.dispatchInit.Do(func() {
|
||||
workers := d.dispatchWorkers
|
||||
func (a *AssetServer) ServeWebViewRequest(req webview.Request) {
|
||||
a.dispatchInit.Do(func() {
|
||||
workers := a.dispatchWorkers
|
||||
if workers <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func (d *AssetServer) ServeWebViewRequest(req webview.Request) {
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
for req := range workerC {
|
||||
d.processWebViewRequest(req)
|
||||
a.processWebViewRequest(req)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -42,34 +42,34 @@ func (d *AssetServer) ServeWebViewRequest(req webview.Request) {
|
||||
dispatchC := make(chan webview.Request)
|
||||
go queueingDispatcher(50, dispatchC, workerC)
|
||||
|
||||
d.dispatchReqC = dispatchC
|
||||
a.dispatchReqC = dispatchC
|
||||
})
|
||||
|
||||
if d.dispatchReqC == nil {
|
||||
go d.processWebViewRequest(req)
|
||||
if a.dispatchReqC == nil {
|
||||
go a.processWebViewRequest(req)
|
||||
} else {
|
||||
d.dispatchReqC <- req
|
||||
a.dispatchReqC <- req
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AssetServer) processWebViewRequest(r webview.Request) {
|
||||
func (a *AssetServer) processWebViewRequest(r webview.Request) {
|
||||
uri, _ := r.URL()
|
||||
d.processWebViewRequestInternal(r)
|
||||
a.processWebViewRequestInternal(r)
|
||||
if err := r.Close(); err != nil {
|
||||
d.logError("Unable to call close for request for uri '%s'", uri)
|
||||
a.options.Logger.Error("Unable to call close for request for uri '%s'", uri)
|
||||
}
|
||||
}
|
||||
|
||||
// processHTTPRequest processes the HTTP Request by faking a golang HTTP Server.
|
||||
// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
|
||||
func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
|
||||
func (a *AssetServer) processWebViewRequestInternal(r webview.Request) {
|
||||
uri := "unknown"
|
||||
var err error
|
||||
|
||||
wrw := r.Response()
|
||||
defer func() {
|
||||
if err := wrw.Finish(); err != nil {
|
||||
d.logError("Error finishing request '%s': %s", uri, err)
|
||||
a.options.Logger.Error("Error finishing request '%s': %s", uri, err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -78,26 +78,26 @@ func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
|
||||
|
||||
uri, err = r.URL()
|
||||
if err != nil {
|
||||
d.logError("Error processing request, unable to get URL: %s (HttpResponse=500)", err)
|
||||
a.options.Logger.Error("Error processing request, unable to get URL: %s (HttpResponse=500)", err)
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
method, err := r.Method()
|
||||
if err != nil {
|
||||
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err))
|
||||
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
header, err := r.Header()
|
||||
if err != nil {
|
||||
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err))
|
||||
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
body, err := r.Body()
|
||||
if err != nil {
|
||||
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err))
|
||||
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
|
||||
|
||||
req, err := http.NewRequest(method, uri, body)
|
||||
if err != nil {
|
||||
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err))
|
||||
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
|
||||
req.URL.Fragment = ""
|
||||
req.URL.RawFragment = ""
|
||||
|
||||
if url := req.URL; req.RequestURI == "" && url != nil {
|
||||
req.RequestURI = url.String()
|
||||
if requestURL := req.URL; req.RequestURI == "" && requestURL != nil {
|
||||
req.RequestURI = requestURL.String()
|
||||
}
|
||||
|
||||
req.Header = header
|
||||
@@ -137,28 +137,28 @@ func (d *AssetServer) processWebViewRequestInternal(r webview.Request) {
|
||||
if req.ContentLength == 0 {
|
||||
req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64)
|
||||
} else {
|
||||
req.Header.Set(HeaderContentLength, fmt.Sprintf("%d", req.ContentLength))
|
||||
req.Header.Set(HeaderContentLength, fmt.Sprintf("%a", req.ContentLength))
|
||||
}
|
||||
|
||||
if host := req.Header.Get(HeaderHost); host != "" {
|
||||
req.Host = host
|
||||
}
|
||||
|
||||
if expectedHost := d.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host {
|
||||
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host))
|
||||
if expectedHost := a.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host {
|
||||
a.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host))
|
||||
return
|
||||
}
|
||||
|
||||
d.ServeHTTP(rw, req)
|
||||
a.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (d *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) {
|
||||
func (a *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) {
|
||||
logInfo := uri
|
||||
if uri, err := url.ParseRequestURI(uri); err == nil {
|
||||
logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1)
|
||||
}
|
||||
|
||||
d.logError("Error processing request (HttpResponse=500)", "details", logInfo, "error", err.Error())
|
||||
a.options.Logger.Error("Error processing request (HttpResponse=500)", "details", logInfo, "error", err.Error())
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//go:build !production
|
||||
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
//go:embed defaultindex.html
|
||||
var defaultHTML []byte
|
||||
|
||||
func defaultIndexHTML() []byte {
|
||||
return defaultHTML
|
||||
}
|
||||
|
||||
func (a *AssetServer) setupHandler() (http.Handler, error) {
|
||||
|
||||
// Do we have an external dev server URL?
|
||||
a.devServerURL = GetDevServerURL()
|
||||
if a.devServerURL == "" {
|
||||
return NewDefaultAssetHandler(a.options)
|
||||
}
|
||||
|
||||
// Parse the URL
|
||||
parsedURL, err := url.Parse(a.devServerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid FRONTEND_DEVSERVER_URL. Should be valid URL: %s", err.Error())
|
||||
}
|
||||
|
||||
baseHandler := a.options.Handler
|
||||
|
||||
errSkipProxy := fmt.Errorf("skip proxying")
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(parsedURL)
|
||||
baseDirector := proxy.Director
|
||||
proxy.Director = func(r *http.Request) {
|
||||
baseDirector(r)
|
||||
if a.options.Logger != nil {
|
||||
a.options.Logger.Debug("ExternalAssetHandler: loading", "url", r.URL)
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ModifyResponse = func(res *http.Response) error {
|
||||
if baseHandler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusSwitchingProtocols {
|
||||
return nil
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed {
|
||||
return errSkipProxy
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
if baseHandler != nil && errors.Is(err, errSkipProxy) {
|
||||
if a.options.Logger != nil {
|
||||
a.options.Logger.Debug("ExternalAssetHandler: Loading file failed, using original AssetHandler", "url", r.URL)
|
||||
}
|
||||
baseHandler.ServeHTTP(rw, r)
|
||||
} else {
|
||||
if a.options.Logger != nil {
|
||||
a.options.Logger.Error("ExternalAssetHandler: Proxy error", "error", err.Error())
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
var result http.Handler = http.HandlerFunc(
|
||||
func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == http.MethodGet {
|
||||
proxy.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
if baseHandler != nil {
|
||||
baseHandler.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
})
|
||||
|
||||
if middleware := a.options.Middleware; middleware != nil {
|
||||
result = middleware(result)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetDevServerURL() string {
|
||||
return os.Getenv("FRONTEND_DEVSERVER_URL")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build production
|
||||
|
||||
package assetserver
|
||||
|
||||
import "net/http"
|
||||
|
||||
func defaultIndexHTML() []byte {
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
func (a *AssetServer) setupHandler() (http.Handler, error) {
|
||||
return NewDefaultAssetHandler(a.options)
|
||||
}
|
||||
|
||||
func GetDevServerURL() string {
|
||||
return ""
|
||||
}
|
||||
@@ -2,13 +2,10 @@ package assetserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,8 +13,9 @@ const (
|
||||
HeaderContentType = "Content-Type"
|
||||
HeaderContentLength = "Content-Length"
|
||||
HeaderUserAgent = "User-Agent"
|
||||
HeaderCacheControl = "Cache-Control"
|
||||
HeaderUpgrade = "Upgrade"
|
||||
// TODO: Is this needed?
|
||||
HeaderCacheControl = "Cache-Control"
|
||||
HeaderUpgrade = "Upgrade"
|
||||
|
||||
WailsUserAgentValue = "wails.io"
|
||||
)
|
||||
@@ -35,80 +33,6 @@ func serveFile(rw http.ResponseWriter, filename string, blob []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func createScriptNode(scriptName string) *html.Node {
|
||||
return &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: "script",
|
||||
Attr: []html.Attribute{
|
||||
{
|
||||
Key: "src",
|
||||
Val: scriptName,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createDivNode(id string) *html.Node {
|
||||
return &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: "div",
|
||||
Attr: []html.Attribute{
|
||||
{
|
||||
Namespace: "",
|
||||
Key: "id",
|
||||
Val: id,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func insertScriptInHead(htmlNode *html.Node, scriptName string) error {
|
||||
headNode := findFirstTag(htmlNode, "head")
|
||||
if headNode == nil {
|
||||
return errors.New("cannot find head in HTML")
|
||||
}
|
||||
scriptNode := createScriptNode(scriptName)
|
||||
if headNode.FirstChild != nil {
|
||||
headNode.InsertBefore(scriptNode, headNode.FirstChild)
|
||||
} else {
|
||||
headNode.AppendChild(scriptNode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendSpinnerToBody(htmlNode *html.Node) error {
|
||||
bodyNode := findFirstTag(htmlNode, "body")
|
||||
if bodyNode == nil {
|
||||
return errors.New("cannot find body in HTML")
|
||||
}
|
||||
scriptNode := createDivNode("wails-spinner")
|
||||
bodyNode.AppendChild(scriptNode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHTMLNode(htmldata []byte) (*html.Node, error) {
|
||||
return html.Parse(bytes.NewReader(htmldata))
|
||||
}
|
||||
|
||||
func findFirstTag(htmlnode *html.Node, tagName string) *html.Node {
|
||||
var extractor func(*html.Node) *html.Node
|
||||
var result *html.Node
|
||||
extractor = func(node *html.Node) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == tagName {
|
||||
return node
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
result := extractor(child)
|
||||
if result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
result = extractor(htmlnode)
|
||||
return result
|
||||
}
|
||||
|
||||
func isWebSocket(req *http.Request) bool {
|
||||
upgrade := req.Header.Get(HeaderUpgrade)
|
||||
return strings.EqualFold(upgrade, "websocket")
|
||||
|
||||
@@ -2,6 +2,7 @@ package assetserver
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -10,18 +11,18 @@ import (
|
||||
)
|
||||
|
||||
// FindEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files.
|
||||
func FindEmbedRootPath(fsys embed.FS) (string, error) {
|
||||
func FindEmbedRootPath(fileSystem embed.FS) (string, error) {
|
||||
stopErr := fmt.Errorf("files or multiple dirs found")
|
||||
|
||||
fPath := ""
|
||||
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
fPath = path
|
||||
if entries, dErr := fs.ReadDir(fsys, path); dErr != nil {
|
||||
if entries, dErr := fs.ReadDir(fileSystem, path); dErr != nil {
|
||||
return dErr
|
||||
} else if len(entries) <= 1 {
|
||||
return nil
|
||||
@@ -31,20 +32,20 @@ func FindEmbedRootPath(fsys embed.FS) (string, error) {
|
||||
return stopErr
|
||||
})
|
||||
|
||||
if err != nil && err != stopErr {
|
||||
if err != nil && !errors.Is(err, stopErr) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fPath, nil
|
||||
}
|
||||
|
||||
func FindPathToFile(fsys fs.FS, file string) (string, error) {
|
||||
stat, _ := fs.Stat(fsys, file)
|
||||
func FindPathToFile(fileSystem fs.FS, file string) (string, error) {
|
||||
stat, _ := fs.Stat(fileSystem, file)
|
||||
if stat != nil {
|
||||
return ".", nil
|
||||
}
|
||||
var indexFiles []string
|
||||
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ var (
|
||||
// The list of builtin mime-types by extension as defined by
|
||||
// the golang standard lib package "mime"
|
||||
// The standard lib also takes into account mime type definitions from
|
||||
// etc files like '/etc/apache2/mime.types' but we want to have the
|
||||
// same behavivour on all platforms and not depend on some external file.
|
||||
// /etc files like '/etc/apache2/mime.types' but we want to have the
|
||||
// same behaviour on all platforms and not depend on some external file.
|
||||
mimeTypesByExt = map[string]string{
|
||||
".avif": "image/avif",
|
||||
".css": "text/css; charset=utf-8",
|
||||
|
||||
@@ -3,6 +3,7 @@ package assetserver
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -33,6 +34,21 @@ type Options struct {
|
||||
// Multiple Middlewares can be chained together with:
|
||||
// ChainMiddleware(middleware ...Middleware) Middleware
|
||||
Middleware Middleware
|
||||
|
||||
// Logger is the logger used by the AssetServer. If not defined, no logging will be done.
|
||||
Logger *slog.Logger
|
||||
|
||||
// RuntimeHandler is the handler used for the runtime calls.
|
||||
RuntimeHandler http.Handler
|
||||
|
||||
// GetCapabilities returns the capabilities of the runtime
|
||||
GetCapabilities func() []byte
|
||||
|
||||
// GetFlags returns the application flags
|
||||
GetFlags func() []byte
|
||||
|
||||
// IsDebug is true if the server is running in debug mode
|
||||
IsDebug bool
|
||||
}
|
||||
|
||||
// Validate the options
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="/main.css" rel="stylesheet">
|
||||
</head>
|
||||
<body data-wails-drag>
|
||||
<div id="logo"></div>
|
||||
</body>
|
||||
</html>
|
||||
-39
File diff suppressed because one or more lines are too long
-20
@@ -1,20 +0,0 @@
|
||||
import {ready} from '@wails/runtime';
|
||||
|
||||
ready(() => {
|
||||
// Get input + focus
|
||||
let nameElement = document.getElementById("name");
|
||||
nameElement.focus();
|
||||
|
||||
// Setup the greet function
|
||||
window.greet = function () {
|
||||
|
||||
// Get name
|
||||
let name = nameElement.value;
|
||||
|
||||
// Call App.Greet(name)
|
||||
window.backend.main.App.Greet(name).then((result) => {
|
||||
// Update result with data back from App.Greet()
|
||||
document.getElementById("result").innerText = result;
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="/main.css" rel="stylesheet">
|
||||
</head>
|
||||
<body data-wails-drag>
|
||||
<div id="logo"></div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user