diff --git a/v2/internal/frontend/assetserver/assetserver_dev.go b/v2/internal/frontend/assetserver/assetserver_browser_dev.go similarity index 50% rename from v2/internal/frontend/assetserver/assetserver_dev.go rename to v2/internal/frontend/assetserver/assetserver_browser_dev.go index 8ab9ec918..ffe758c49 100644 --- a/v2/internal/frontend/assetserver/assetserver_dev.go +++ b/v2/internal/frontend/assetserver/assetserver_browser_dev.go @@ -21,63 +21,63 @@ import ( "os" ) -type AssetServer struct { - indexFile []byte +type BrowserAssetServer struct { runtimeJS []byte assetdir string appOptions *options.App } -func NewAssetServer(assetdir string, bindingsJSON string, appOptions *options.App) (*AssetServer, error) { - result := &AssetServer{ +func NewBrowserAssetServer(assetdir string, bindingsJSON string, appOptions *options.App) (*BrowserAssetServer, error) { + result := &BrowserAssetServer{ assetdir: assetdir, appOptions: appOptions, } - err := result.init() - if err != nil { - return nil, err - } - var buffer bytes.Buffer buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n") buffer.Write(runtime.RuntimeDesktopJS) result.runtimeJS = buffer.Bytes() - err = result.init() - return result, err + return result, nil } -func (a *AssetServer) loadFileFromDisk(filename string) ([]byte, error) { +func (a *BrowserAssetServer) loadFileFromDisk(filename string) ([]byte, error) { return os.ReadFile(filepath.Join(a.assetdir, filename)) } -func (a *AssetServer) init() error { - var err error - a.indexFile, err = a.loadFileFromDisk("index.html") +func (a *BrowserAssetServer) processIndexHTML() ([]byte, error) { + indexHTML, err := a.loadFileFromDisk("index.html") if err != nil { - return err + return nil, err } - a.indexFile, err = injectHTML(string(a.indexFile), `
`) + indexHTML, err = injectHTML(string(indexHTML), `
`) if err != nil { - return err + return nil, err } - a.indexFile, err = injectHTML(string(a.indexFile), ``) + wailsOptions, err := extractOptions(indexHTML) if err != nil { - return err + return nil, err } - a.indexFile, err = injectHTML(string(a.indexFile), ``) - if err != nil { - return err + if wailsOptions.disableRuntimeInjection == false { + indexHTML, err = injectHTML(string(indexHTML), ``) + if err != nil { + return nil, err + } } - return nil + if wailsOptions.disableBindingsInjection == false { + indexHTML, err = injectHTML(string(indexHTML), ``) + if err != nil { + return nil, err + } + } + return indexHTML, nil } -func (a *AssetServer) Load(filename string) ([]byte, string, error) { +func (a *BrowserAssetServer) Load(filename string) ([]byte, string, error) { var content []byte var err error switch filename { case "/": - content = a.indexFile + content, err = a.processIndexHTML() case "/wails/runtime.js": content = a.runtimeJS case "/wails/ipc.js": diff --git a/v2/internal/frontend/assetserver/assetserver_desktop.go b/v2/internal/frontend/assetserver/assetserver_desktop.go index 557fbe958..8a7039782 100644 --- a/v2/internal/frontend/assetserver/assetserver_desktop.go +++ b/v2/internal/frontend/assetserver/assetserver_desktop.go @@ -10,13 +10,13 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend/runtime" "github.com/wailsapp/wails/v2/internal/logger" "io/fs" + "log" "path/filepath" "strings" ) type DesktopAssetServer struct { assets debme.Debme - indexFile []byte runtimeJS []byte assetdir string logger *logger.Logger @@ -106,27 +106,42 @@ func (a *DesktopAssetServer) init(assets embed.FS) error { if err != nil { return err } - indexHTML, err := a.assets.ReadFile("index.html") - if err != nil { - return err - } - a.indexFile, err = injectHTML(string(indexHTML), ``) - if err != nil { - return err - } - a.indexFile, err = injectHTML(string(a.indexFile), ``) - if err != nil { - return err - } return nil } +func (a *DesktopAssetServer) processIndexHTML() ([]byte, error) { + indexHTML, err := a.ReadFile("index.html") + if err != nil { + return nil, err + } + wailsOptions, err := extractOptions(indexHTML) + if err != nil { + log.Fatal(err) + return nil, err + } + fmt.Printf("%+v\n", wailsOptions) + if wailsOptions.disableRuntimeInjection == false { + indexHTML, err = injectHTML(string(indexHTML), ``) + if err != nil { + return nil, err + } + } + if wailsOptions.disableBindingsInjection == false { + indexHTML, err = injectHTML(string(indexHTML), ``) + if err != nil { + return nil, err + } + } + + return indexHTML, nil +} + func (a *DesktopAssetServer) Load(filename string) ([]byte, string, error) { var content []byte var err error switch filename { case "/": - content = a.indexFile + content, err = a.processIndexHTML() case "/wails/runtime.js": content = a.runtimeJS case "/wails/ipc.js": diff --git a/v2/internal/frontend/assetserver/common.go b/v2/internal/frontend/assetserver/common.go index 826aee4c3..1d9c270ef 100644 --- a/v2/internal/frontend/assetserver/common.go +++ b/v2/internal/frontend/assetserver/common.go @@ -3,19 +3,86 @@ package assetserver import ( "bytes" "fmt" + "golang.org/x/net/html" "strings" ) +type optionType string + +const ( + noAutoInject optionType = "noautoinject" + noAutoInjectRuntime optionType = "noautoinjectruntime" + noAutoInjectBindings optionType = "noautoinjectbindings" +) + +type Options struct { + disableRuntimeInjection bool + disableBindingsInjection bool +} + +func newOptions(optionString string) *Options { + var result = &Options{} + optionString = strings.ToLower(optionString) + options := strings.Split(optionString, ",") + for _, option := range options { + switch optionType(strings.TrimSpace(option)) { + case noAutoInject: + result.disableRuntimeInjection = true + result.disableBindingsInjection = true + case noAutoInjectBindings: + result.disableBindingsInjection = true + case noAutoInjectRuntime: + result.disableRuntimeInjection = true + } + } + return result +} + func injectHTML(input string, html string) ([]byte, error) { - splits := strings.Split(input, "") + splits := strings.Split(input, "") if len(splits) != 2 { - return nil, fmt.Errorf("unable to locate a tag in your html") + return nil, fmt.Errorf("unable to locate a tag in your html") } var result bytes.Buffer result.WriteString(splits[0]) result.WriteString(html) - result.WriteString("") + result.WriteString("") result.WriteString(splits[1]) return result.Bytes(), nil } + +func extractOptions(htmldata []byte) (*Options, error) { + doc, err := html.Parse(bytes.NewReader(htmldata)) + if err != nil { + return nil, err + } + var extractor func(*html.Node) *Options + extractor = func(node *html.Node) *Options { + if node.Type == html.ElementNode && node.Data == "meta" { + isWailsOptionsTag := false + wailsOptions := "" + for _, attr := range node.Attr { + if isWailsOptionsTag && attr.Key == "content" { + wailsOptions = attr.Val + } + if attr.Val == "wails-options" { + isWailsOptionsTag = true + } + } + return newOptions(wailsOptions) + } + for child := node.FirstChild; child != nil; child = child.NextSibling { + result := extractor(child) + if result != nil { + return result + } + } + return nil + } + result := extractor(doc) + if result == nil { + result = &Options{} + } + return result, nil +} diff --git a/v2/internal/frontend/assetserver/common_test.go b/v2/internal/frontend/assetserver/common_test.go new file mode 100644 index 000000000..2c20b672e --- /dev/null +++ b/v2/internal/frontend/assetserver/common_test.go @@ -0,0 +1,70 @@ +package assetserver + +import ( + "reflect" + "testing" +) + +const realHTML = ` + + + test3 + + + + + + +
Please enter your name below �
+
+ + +
+ + + + + +` + +func genMeta(content string) []byte { + return []byte("") +} + +func genOptions(runtime bool, bindings bool) *Options { + return &Options{ + disableRuntimeInjection: runtime, + disableBindingsInjection: bindings, + } +} + +func Test_extractOptions(t *testing.T) { + tests := []struct { + name string + htmldata []byte + want *Options + wantError bool + }{ + {"empty", []byte(""), &Options{}, false}, + {"bad data", []byte("<"), &Options{}, false}, + {"bad options", genMeta("noauto"), genOptions(false, false), false}, + {"realhtml", []byte(realHTML), genOptions(true, true), false}, + {"noautoinject", genMeta("noautoinject"), genOptions(true, true), false}, + {"noautoinjectbindings", genMeta("noautoinjectbindings"), genOptions(false, true), false}, + {"noautoinjectruntime", genMeta("noautoinjectruntime"), genOptions(true, false), false}, + {"spaces", genMeta(" noautoinjectruntime "), genOptions(true, false), false}, + {"multiple", genMeta("noautoinjectruntime,noautoinjectbindings"), genOptions(true, true), false}, + {"multiple spaces", genMeta(" noautoinjectruntime, noautoinjectbindings "), genOptions(true, true), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := extractOptions(tt.htmldata) + if !tt.wantError && err != nil { + t.Errorf("did not want error but got it") + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("extractOptions() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/v2/internal/frontend/devserver/devserver.go b/v2/internal/frontend/devserver/devserver.go index 0faef8198..58711d590 100644 --- a/v2/internal/frontend/devserver/devserver.go +++ b/v2/internal/frontend/devserver/devserver.go @@ -33,7 +33,7 @@ type DevWebServer struct { logger *logger.Logger appBindings *binding.Bindings dispatcher frontend.Dispatcher - assetServer *assetserver.AssetServer + assetServer *assetserver.BrowserAssetServer socketMutex sync.Mutex websocketClients map[*websocket.Conn]struct{} menuManager *menumanager.Manager @@ -103,7 +103,7 @@ func (d *DevWebServer) Run(ctx context.Context) error { if err != nil { log.Fatal(err) } - d.assetServer, err = assetserver.NewAssetServer(assetdir, bindingsJSON, d.appoptions) + d.assetServer, err = assetserver.NewBrowserAssetServer(assetdir, bindingsJSON, d.appoptions) if err != nil { log.Fatal(err) } diff --git a/website/docs/guides/frontend.mdx b/website/docs/guides/frontend.mdx new file mode 100644 index 000000000..a0f33856d --- /dev/null +++ b/website/docs/guides/frontend.mdx @@ -0,0 +1,77 @@ + +# Frontend + +## Execution Order + +When Wails serves your `index.html`, by default, it will inject 2 script entries into the `` tag to load `/wails/bindings.js` +and `/wails/runtime.js`. These files install the bindings and runtime respectively. + +The code below shows where these are injected by default: + +```html + + + injection example + + + + + + + +
Please enter your name below 👇
+
+ + +
+ + + + + +``` + +### Overriding Default Execution Order + +To provide more flexibility to developers, there is a meta tag that may be used to customise this behaviour: + +```html + +``` + +The options are as follows: + +| Value | Description | +| -------------------- | ------------------------------------------------- | +| noautoinjectruntime | Disable the autoinjection of `/wails/runtime.js` | +| noautoinjectbindings | Disable the autoinjection of `/wails/bindings.js` | +| noautoinject | Disable all autoinjection of scripts | + +Multiple options may be used provided they are comma seperated. + +This code is perfectly valid and operates the same as the autoinjection version: + +```html + + + + injection example + + + + + + +
Please enter your name below 👇
+
+ + +
+ + + + + + + +``` \ No newline at end of file