From c867a864a19d258c113f8844c7a85012e6f16de4 Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 6 May 2023 14:16:44 +0100 Subject: [PATCH 1/2] Use `GOOS=js` as a predicate for the client-side code instead of `wasm`. `GOOS` usually indicates the availability of certain OS APIs, whereas `GOARCH` targets a specific bytecode architecture. For go-app purposes, it's important that the `syscall/js` package is available, but it's less interesting whether the code is compiled into wasm or something else. By using `GOOS=js` constraint go-app becomes more open for use with alternative client-side toolchains, such as GopherJS, which also supports the `syscall/js` package. I also fixed a small bug in the `log.go` file, where `goarch == "window"` could never be true. I believe the intent was to check for `runtime.GOOS == "windows"`. --- pkg/app/app.go | 4 ++-- pkg/app/gen/scripts.go | 2 +- pkg/app/js.go | 2 +- pkg/app/{js_wasm.go => js_js.go} | 0 pkg/app/{js_nowasm.go => js_nojs.go} | 4 ++-- pkg/app/log.go | 5 ++--- pkg/app/log_test.go | 4 ++-- pkg/app/page_test.go | 2 +- pkg/app/resource_test.go | 6 +++--- pkg/app/scripts.go | 2 +- pkg/app/{scripts_wasm.go => scripts_js.go} | 0 pkg/app/static.go | 4 ++-- pkg/app/{wasm.go => static_js.go} | 5 +---- pkg/app/static_test.go | 6 +++--- pkg/app/storage_test.go | 6 +++--- pkg/app/testing_test.go | 20 ++++++++++---------- 16 files changed, 34 insertions(+), 38 deletions(-) rename pkg/app/{js_wasm.go => js_js.go} (100%) rename pkg/app/{js_nowasm.go => js_nojs.go} (99%) rename pkg/app/{scripts_wasm.go => scripts_js.go} (100%) rename pkg/app/{wasm.go => static_js.go} (83%) diff --git a/pkg/app/app.go b/pkg/app/app.go index 2b66ea712..b6937b08a 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -26,11 +26,11 @@ import ( const ( // IsClient reports whether the code is running as a client in the // WebAssembly binary (app.wasm). - IsClient = runtime.GOARCH == "wasm" && runtime.GOOS == "js" + IsClient = runtime.GOOS == "js" // IsServer reports whether the code is running on a server for // pre-rendering purposes. - IsServer = runtime.GOARCH != "wasm" || runtime.GOOS != "js" + IsServer = runtime.GOOS != "js" orientationChangeDelay = time.Millisecond * 500 engineUpdateRate = 120 diff --git a/pkg/app/gen/scripts.go b/pkg/app/gen/scripts.go index 2f64e4efd..766615e61 100644 --- a/pkg/app/gen/scripts.go +++ b/pkg/app/gen/scripts.go @@ -18,7 +18,7 @@ func main() { } defer f.Close() - fmt.Fprintln(f, "//go:build !wasm") + fmt.Fprintln(f, "//go:build !js") fmt.Fprintln(f) fmt.Fprintln(f, "package app") fmt.Fprintln(f) diff --git a/pkg/app/js.go b/pkg/app/js.go index d42b6949d..513070396 100644 --- a/pkg/app/js.go +++ b/pkg/app/js.go @@ -5,7 +5,7 @@ import ( ) const ( -// isClientSide = runtime.GOARCH == "wasm" && runtime.GOOS == "js" +// isClientSide = runtime.GOOS == "js" ) // Type represents the JavaScript type of a Value. diff --git a/pkg/app/js_wasm.go b/pkg/app/js_js.go similarity index 100% rename from pkg/app/js_wasm.go rename to pkg/app/js_js.go diff --git a/pkg/app/js_nowasm.go b/pkg/app/js_nojs.go similarity index 99% rename from pkg/app/js_nowasm.go rename to pkg/app/js_nojs.go index 42c8525b6..23963cbbc 100644 --- a/pkg/app/js_nowasm.go +++ b/pkg/app/js_nojs.go @@ -1,5 +1,5 @@ -//go:build !wasm -// +build !wasm +//go:build !js +// +build !js package app diff --git a/pkg/app/log.go b/pkg/app/log.go index e07be52d8..d8138e072 100644 --- a/pkg/app/log.go +++ b/pkg/app/log.go @@ -16,13 +16,12 @@ var ( ) func init() { - goarch := runtime.GOARCH - if goarch == "wasm" { + if runtime.GOOS == "js" { DefaultLogger = clientLog return } - if goarch != "window" { + if runtime.GOOS != "windows" { defaultColor = "\033[00m" errorColor = "\033[91m" infoColor = "\033[94m" diff --git a/pkg/app/log_test.go b/pkg/app/log_test.go index 777bd0e32..664d67457 100644 --- a/pkg/app/log_test.go +++ b/pkg/app/log_test.go @@ -14,12 +14,12 @@ func TestLog(t *testing.T) { } func TestServerLog(t *testing.T) { - testSkipWasm(t) + testSkipJS(t) testLogger(t, serverLog) } func TestClientLog(t *testing.T) { - testSkipNonWasm(t) + testSkipNonJS(t) testLogger(t, clientLog) } diff --git a/pkg/app/page_test.go b/pkg/app/page_test.go index b520d7236..d32d8b8e4 100644 --- a/pkg/app/page_test.go +++ b/pkg/app/page_test.go @@ -16,7 +16,7 @@ func TestRequestPage(t *testing.T) { } func TestBrowserPage(t *testing.T) { - testSkipNonWasm(t) + testSkipNonJS(t) client := NewClientTester(Div()) defer client.Close() diff --git a/pkg/app/resource_test.go b/pkg/app/resource_test.go index 23a8b74e1..66b078064 100644 --- a/pkg/app/resource_test.go +++ b/pkg/app/resource_test.go @@ -1,5 +1,5 @@ -//go:build !wasm -// +build !wasm +//go:build !js +// +build !js package app @@ -14,7 +14,7 @@ import ( ) func TestLocalDir(t *testing.T) { - testSkipWasm(t) + testSkipJS(t) h, _ := LocalDir("test").(localDir) require.Equal(t, "test", h.Static()) diff --git a/pkg/app/scripts.go b/pkg/app/scripts.go index cc07dee9a..b6ef70502 100644 --- a/pkg/app/scripts.go +++ b/pkg/app/scripts.go @@ -1,4 +1,4 @@ -//go:build !wasm +//go:build !js package app diff --git a/pkg/app/scripts_wasm.go b/pkg/app/scripts_js.go similarity index 100% rename from pkg/app/scripts_wasm.go rename to pkg/app/scripts_js.go diff --git a/pkg/app/static.go b/pkg/app/static.go index c810a6482..d36a22024 100644 --- a/pkg/app/static.go +++ b/pkg/app/static.go @@ -1,5 +1,5 @@ -//go:build !wasm -// +build !wasm +//go:build !js +// +build !js package app diff --git a/pkg/app/wasm.go b/pkg/app/static_js.go similarity index 83% rename from pkg/app/wasm.go rename to pkg/app/static_js.go index a552808b6..e027f4b70 100644 --- a/pkg/app/wasm.go +++ b/pkg/app/static_js.go @@ -1,6 +1,3 @@ -//go:build wasm -// +build wasm - package app import ( @@ -19,7 +16,7 @@ const ( var ( errBadInstruction = errors.New("unsupported instruction"). - WithTag("architecture", runtime.GOARCH) + WithTag("os", runtime.GOOS) ) func GenerateStaticWebsite(dir string, h *Handler, pages ...string) error { diff --git a/pkg/app/static_test.go b/pkg/app/static_test.go index f480d0d8f..0f7ce1ae9 100644 --- a/pkg/app/static_test.go +++ b/pkg/app/static_test.go @@ -1,5 +1,5 @@ -//go:build !wasm -// +build !wasm +//go:build !js +// +build !js package app @@ -12,7 +12,7 @@ import ( ) func TestGenerateStaticWebsite(t *testing.T) { - testSkipWasm(t) + testSkipJS(t) dir := "static-test" defer os.RemoveAll(dir) diff --git a/pkg/app/storage_test.go b/pkg/app/storage_test.go index fb103e385..797eb3e95 100644 --- a/pkg/app/storage_test.go +++ b/pkg/app/storage_test.go @@ -12,12 +12,12 @@ func TestMemoryStorage(t *testing.T) { } func TestJSLocalStorage(t *testing.T) { - testSkipNonWasm(t) + testSkipNonJS(t) testBrowserStorage(t, newJSStorage("localStorage")) } func TestJSSessionStorage(t *testing.T) { - testSkipNonWasm(t) + testSkipNonJS(t) testBrowserStorage(t, newJSStorage("sessionStorage")) } @@ -143,7 +143,7 @@ func testBrowserStorageGetError(t *testing.T, s BrowserStorage) { } func testBrowserStorageFull(t *testing.T, s BrowserStorage) { - testSkipNonWasm(t) + testSkipNonJS(t) var err error data := make([]byte, 4096) diff --git a/pkg/app/testing_test.go b/pkg/app/testing_test.go index a7d2e79f9..c10d82dd4 100644 --- a/pkg/app/testing_test.go +++ b/pkg/app/testing_test.go @@ -10,22 +10,22 @@ import ( "github.com/stretchr/testify/require" ) -func testSkipNonWasm(t *testing.T) { - if goarch := runtime.GOARCH; goarch != "wasm" { +func testSkipNonJS(t *testing.T) { + if goos := runtime.GOOS; goos != "js" { t.Skip(logs.New("skipping test"). - WithTag("reason", "unsupported architecture"). - WithTag("required-architecture", "wasm"). - WithTag("current-architecture", goarch), + WithTag("reason", "unsupported OS"). + WithTag("required-os", "js"). + WithTag("current-os", goos), ) } } -func testSkipWasm(t *testing.T) { - if goarch := runtime.GOARCH; goarch == "wasm" { +func testSkipJS(t *testing.T) { + if goos := runtime.GOOS; goos == "js" { t.Skip(logs.New("skipping test"). - WithTag("reason", "unsupported architecture"). - WithTag("required-architecture", "!= than wasm"). - WithTag("current-architecture", goarch), + WithTag("reason", "unsupported OS"). + WithTag("required-os", "!= than js"). + WithTag("current-os", goos), ) } } From 254f39a62c49c81a5b3bb8e0b83a2bbfad60fdfa Mon Sep 17 00:00:00 2001 From: Nevkontakte Date: Sat, 6 May 2023 20:58:03 +0100 Subject: [PATCH 2/2] Factor out management of wasm-specific resources into `Driver` type. The new `Driver` type can be used to customize how go-app environment is set up on the client side. It is responsible for pre-rendering and registering scripts and styles that are necessary to load and start app's business logic. By default, a Go WebAssembly driver is assumed, which maintains the original behavior with showing a loader progress bar and starting up a Go wasm binary. Users can customize the loading process and the scripts necessary for it by providing their own Driver implementations - at their own peril. One notable change is that app.js has been split into two parts: - The code responsible for managing service worker remains in app.js and is included regardless of the driver used. - The code responsible for setting up wasm was moved to driver-wasm.js and is used by the `goWasmDriver` implementation. --- pkg/app/gen/app.js | 101 ------------------ pkg/app/gen/driver-wasm.js | 102 ++++++++++++++++++ pkg/app/gen/scripts.go | 4 + pkg/app/http.go | 206 +++++++++++++++++++++++++------------ pkg/app/resource.go | 16 +++ pkg/app/scripts.go | 4 +- pkg/app/static.go | 9 +- pkg/app/static_js.go | 1 + 8 files changed, 277 insertions(+), 166 deletions(-) create mode 100644 pkg/app/gen/driver-wasm.js diff --git a/pkg/app/gen/app.js b/pkg/app/gen/app.js index bd8603c04..13dbbbae0 100644 --- a/pkg/app/gen/app.js +++ b/pkg/app/gen/app.js @@ -6,8 +6,6 @@ var goappOnUpdate = function () {}; var goappOnAppInstallChange = function () {}; const goappEnv = {{.Env}}; -const goappLoadingLabel = "{{.LoadingLabel}}"; -const goappWasmContentLengthHeader = "{{.WasmContentLengthHeader}}"; let goappServiceWorkerRegistration; let deferredPrompt = null; @@ -15,7 +13,6 @@ let deferredPrompt = null; goappInitServiceWorker(); goappWatchForUpdate(); goappWatchForInstallable(); -goappInitWebAssembly(); // ----------------------------------------------------------------------------- // Service Worker @@ -185,101 +182,3 @@ function goappKeepBodyClean() { return () => mutationObserver.disconnect(); } - -// ----------------------------------------------------------------------------- -// Web Assembly -// ----------------------------------------------------------------------------- -async function goappInitWebAssembly() { - if (!goappCanLoadWebAssembly()) { - document.getElementById("app-wasm-loader").style.display = "none"; - return; - } - - let instantiateStreaming = WebAssembly.instantiateStreaming; - if (!instantiateStreaming) { - instantiateStreaming = async (resp, importObject) => { - const source = await (await resp).arrayBuffer(); - return await WebAssembly.instantiate(source, importObject); - }; - } - - const loaderIcon = document.getElementById("app-wasm-loader-icon"); - const loaderLabel = document.getElementById("app-wasm-loader-label"); - - try { - const showProgress = (progress) => { - loaderLabel.innerText = goappLoadingLabel.replace("{progress}", progress); - }; - showProgress(0); - - const go = new Go(); - const wasm = await instantiateStreaming( - fetchWithProgress("{{.Wasm}}", showProgress), - go.importObject - ); - - go.run(wasm.instance); - } catch (err) { - loaderIcon.className = "goapp-logo"; - loaderLabel.innerText = err; - console.error("loading wasm failed: ", err); - } -} - -function goappCanLoadWebAssembly() { - return !/bot|googlebot|crawler|spider|robot|crawling/i.test( - navigator.userAgent - ); -} - -async function fetchWithProgress(url, progess) { - const response = await fetch(url); - - let contentLength; - try { - contentLength = response.headers.get(goappWasmContentLengthHeader); - } catch {} - if (!goappWasmContentLengthHeader || !contentLength) { - contentLength = response.headers.get("Content-Length"); - } - - const total = parseInt(contentLength, 10); - let loaded = 0; - - const progressHandler = function (loaded, total) { - progess(Math.round((loaded * 100) / total)); - }; - - var res = new Response( - new ReadableStream( - { - async start(controller) { - var reader = response.body.getReader(); - for (;;) { - var { done, value } = await reader.read(); - - if (done) { - progressHandler(total, total); - break; - } - - loaded += value.byteLength; - progressHandler(loaded, total); - controller.enqueue(value); - } - controller.close(); - }, - }, - { - status: response.status, - statusText: response.statusText, - } - ) - ); - - for (var pair of response.headers.entries()) { - res.headers.set(pair[0], pair[1]); - } - - return res; -} diff --git a/pkg/app/gen/driver-wasm.js b/pkg/app/gen/driver-wasm.js new file mode 100644 index 000000000..cc45ff4f8 --- /dev/null +++ b/pkg/app/gen/driver-wasm.js @@ -0,0 +1,102 @@ +// ----------------------------------------------------------------------------- +// Web Assembly +// ----------------------------------------------------------------------------- +const goappLoadingLabel = "{{.LoadingLabel}}"; +const goappWasmContentLengthHeader = "{{.WasmContentLengthHeader}}"; + +goappInitWebAssembly(); + +async function goappInitWebAssembly() { + if (!goappCanLoadWebAssembly()) { + document.getElementById("app-wasm-loader").style.display = "none"; + return; + } + + let instantiateStreaming = WebAssembly.instantiateStreaming; + if (!instantiateStreaming) { + instantiateStreaming = async (resp, importObject) => { + const source = await (await resp).arrayBuffer(); + return await WebAssembly.instantiate(source, importObject); + }; + } + + const loaderIcon = document.getElementById("app-wasm-loader-icon"); + const loaderLabel = document.getElementById("app-wasm-loader-label"); + + try { + const showProgress = (progress) => { + loaderLabel.innerText = goappLoadingLabel.replace("{progress}", progress); + }; + showProgress(0); + + const go = new Go(); + const wasm = await instantiateStreaming( + fetchWithProgress("{{.Wasm}}", showProgress), + go.importObject + ); + + go.run(wasm.instance); + } catch (err) { + loaderIcon.className = "goapp-logo"; + loaderLabel.innerText = err; + console.error("loading wasm failed: ", err); + } +} + +function goappCanLoadWebAssembly() { + return !/bot|googlebot|crawler|spider|robot|crawling/i.test( + navigator.userAgent + ); +} + +async function fetchWithProgress(url, progess) { + const response = await fetch(url); + + let contentLength; + try { + contentLength = response.headers.get(goappWasmContentLengthHeader); + } catch {} + if (!goappWasmContentLengthHeader || !contentLength) { + contentLength = response.headers.get("Content-Length"); + } + + const total = parseInt(contentLength, 10); + let loaded = 0; + + const progressHandler = function (loaded, total) { + progess(Math.round((loaded * 100) / total)); + }; + + var res = new Response( + new ReadableStream( + { + async start(controller) { + var reader = response.body.getReader(); + for (;;) { + var { done, value } = await reader.read(); + + if (done) { + progressHandler(total, total); + break; + } + + loaded += value.byteLength; + progressHandler(loaded, total); + controller.enqueue(value); + } + controller.close(); + }, + }, + { + status: response.status, + statusText: response.statusText, + } + ) + ); + + for (var pair of response.headers.entries()) { + res.headers.set(pair[0], pair[1]); + } + + return res; +} diff --git a/pkg/app/gen/scripts.go b/pkg/app/gen/scripts.go index 766615e61..0c76b0a5e 100644 --- a/pkg/app/gen/scripts.go +++ b/pkg/app/gen/scripts.go @@ -44,6 +44,10 @@ func main() { "wasm_exec.js", ), }, + { + Var: "wasmDriverJS", + Filename: "gen/driver-wasm.js", + }, { Var: "appJS", Filename: "gen/app.js", diff --git a/pkg/app/http.go b/pkg/app/http.go index 76d553f7f..f90fb5229 100644 --- a/pkg/app/http.go +++ b/pkg/app/http.go @@ -190,6 +190,15 @@ type Handler struct { // worker template is not supported and will be closed. ServiceWorkerTemplate string + // The driver that is responsible for setting up go-app execution environment + // on the client side. + // + // By default uses the internal implementation targeting Go WebAssembly. + // Changing the driver has high chances to mess up go-app usage. Any issue + // with go-app using a custom driver must be directed to the author of the + // driver. + Driver Driver + once sync.Once etag string pwaResources PreRenderCache @@ -199,6 +208,7 @@ type Handler struct { func (h *Handler) init() { h.initVersion() h.initStaticResources() + h.initDriver() h.initImage() h.initStyles() h.initScripts() @@ -207,6 +217,7 @@ func (h *Handler) init() { h.initIcon() h.initPWA() h.initPageContent() + h.initEnv() h.initPreRenderedResources() h.initProxyResources() } @@ -225,6 +236,16 @@ func (h *Handler) initStaticResources() { } } +func (h *Handler) initDriver() { + if h.Driver == nil { + h.Driver = &goWasmDriver{ + LoadingLabel: h.LoadingLabel, + Resources: h.Resources, + WasmContentLengthHeader: h.WasmContentLengthHeader, + } + } +} + func (h *Handler) initImage() { if h.Image != "" { h.Image = h.resolveStaticPath(h.Image) @@ -309,15 +330,35 @@ func (h *Handler) initPageContent() { } +func (h *Handler) initEnv() { + if h.Env == nil { + h.Env = make(map[string]string) + } + internalURLs, _ := json.Marshal(h.InternalURLs) + h.Env["GOAPP_INTERNAL_URLS"] = string(internalURLs) + h.Env["GOAPP_VERSION"] = h.Version + h.Env["GOAPP_STATIC_RESOURCES_URL"] = h.Resources.Static() + h.Env["GOAPP_ROOT_PREFIX"] = h.Resources.Package() + + for k, v := range h.Env { + if err := os.Setenv(k, v); err != nil { + Log(errors.New("setting app env variable failed"). + WithTag("name", k). + WithTag("value", v). + Wrap(err)) + } + } +} + func (h *Handler) initPreRenderedResources() { - h.pwaResources = newPreRenderCache(5) + driverItems := h.Driver.PreRenderItems() + + h.pwaResources = newPreRenderCache(4 + len(driverItems)) ctx := context.TODO() - h.pwaResources.Set(ctx, PreRenderedItem{ - Path: "/wasm_exec.js", - ContentType: "application/javascript", - Body: []byte(wasmExecJS), - }) + for _, item := range driverItems { + h.pwaResources.Set(ctx, item) + } h.pwaResources.Set(ctx, PreRenderedItem{ Path: "/app.js", @@ -352,41 +393,17 @@ func (h *Handler) initPreRenderedResources() { } func (h *Handler) makeAppJS() []byte { - if h.Env == nil { - h.Env = make(map[string]string) - } - internalURLs, _ := json.Marshal(h.InternalURLs) - h.Env["GOAPP_INTERNAL_URLS"] = string(internalURLs) - h.Env["GOAPP_VERSION"] = h.Version - h.Env["GOAPP_STATIC_RESOURCES_URL"] = h.Resources.Static() - h.Env["GOAPP_ROOT_PREFIX"] = h.Resources.Package() - - for k, v := range h.Env { - if err := os.Setenv(k, v); err != nil { - Log(errors.New("setting app env variable failed"). - WithTag("name", k). - WithTag("value", v). - Wrap(err)) - } - } - var b bytes.Buffer if err := template. Must(template.New("app.js").Parse(appJS)). Execute(&b, struct { - Env string - LoadingLabel string - Wasm string - WasmContentLengthHeader string - WorkerJS string - AutoUpdateInterval int64 + Env string + WorkerJS string + AutoUpdateInterval int64 }{ - Env: jsonString(h.Env), - LoadingLabel: h.LoadingLabel, - Wasm: h.Resources.AppWASM(), - WasmContentLengthHeader: h.WasmContentLengthHeader, - WorkerJS: h.resolvePackagePath("/app-worker.js"), - AutoUpdateInterval: h.AutoUpdateInterval.Milliseconds(), + Env: jsonString(h.Env), + WorkerJS: h.resolvePackagePath("/app-worker.js"), + AutoUpdateInterval: h.AutoUpdateInterval.Milliseconds(), }); err != nil { panic(errors.New("initializing app.js failed").Wrap(err)) } @@ -407,14 +424,15 @@ func (h *Handler) makeAppWorkerJS() []byte { h.resolvePackagePath("/app.css"), h.resolvePackagePath("/app.js"), h.resolvePackagePath("/manifest.webmanifest"), - h.resolvePackagePath("/wasm_exec.js"), h.resolvePackagePath("/"), - h.Resources.AppWASM(), ) setResources(h.Icon.Default, h.Icon.Large, h.Icon.AppleTouch) setResources(h.Styles...) setResources(h.Scripts...) setResources(h.CacheableResources...) + setResources(h.Driver.Scripts()...) + setResources(h.Driver.Styles()...) + setResources(h.Driver.CacheableResources()...) resourcesTocache := make([]string, 0, len(resources)) for k := range resources { @@ -720,6 +738,9 @@ func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) { icon = h.Icon.Default } + driverScripts := h.Driver.Scripts() + driverStyles := h.Driver.Styles() + var b bytes.Buffer b.WriteString("\n") PrintHTML(&b, h.HTML(). @@ -771,12 +792,20 @@ func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) { Type("text/css"). Rel("stylesheet"). Href(h.resolvePackagePath("/app.css")), - Script(). - Defer(true). - Src(h.resolvePackagePath("/wasm_exec.js")), Script(). Defer(true). Src(h.resolvePackagePath("/app.js")), + Range(driverScripts).Slice(func(i int) UI { + return Script(). + Defer(true). + Src(driverScripts[i]) + }), + Range(driverStyles).Slice(func(i int) UI { + return Link(). + Type("text/css"). + Rel("stylesheet"). + Href(driverStyles[i]) + }), Range(h.Styles).Slice(func(i int) UI { return Link(). Type("text/css"). @@ -806,19 +835,7 @@ func (h *Handler) servePage(w http.ResponseWriter, r *http.Request) { } func (h *Handler) resolvePackagePath(path string) string { - var b strings.Builder - - b.WriteByte('/') - appResources := strings.Trim(h.Resources.Package(), "/") - b.WriteString(appResources) - - path = strings.Trim(path, "/") - if b.Len() != 1 && path != "" { - b.WriteByte('/') - } - b.WriteString(path) - - return b.String() + return resolvePackagePath(h.Resources, path) } func (h *Handler) resolveStaticPath(path string) string { @@ -882,17 +899,80 @@ func isStaticResourcePath(path string) bool { strings.HasPrefix(path, "web/") } -type httpResource struct { - Path string - ContentType string - Body []byte - ExpireAt time.Time +// Driver sets up go-app execution environment on the client side. +type Driver interface { + // A list of scripts that must be included into every pre-rendered page to + // initialize the app. + Scripts() []string + + // A list of CSS style sheets required by the driver scripts. + Styles() []string + + // A list of additional HTTP resources that should be cached by the service + // worker, such as the wasm binary. + // + // Scripts and styles are cached automatically and don't need to be repeated + // here. + CacheableResources() []string + + // A list of pre-rendered resources (scripts, styles, etc.) that are + // generated by the driver at startup. + PreRenderItems() []PreRenderedItem +} + +// goWasmDriver sets up go-app execution environment when the client side is +// compiles using Go WebAssembly. +type goWasmDriver struct { + LoadingLabel string + Resources ResourceProvider + WasmContentLengthHeader string +} + +func (d *goWasmDriver) Scripts() []string { + return []string{ + resolvePackagePath(d.Resources, "/wasm_exec.js"), + resolvePackagePath(d.Resources, "/driver-wasm.js"), + } +} + +func (d *goWasmDriver) Styles() []string { return nil } + +func (d *goWasmDriver) CacheableResources() []string { + return []string{ + d.Resources.AppWASM(), + } } -func (r httpResource) Len() int { - return len(r.Body) +func (d *goWasmDriver) PreRenderItems() []PreRenderedItem { + items := []PreRenderedItem{ + { + Path: "/wasm_exec.js", + ContentType: "application/javascript", + Body: []byte(wasmExecJS), + }, + { + Path: "/driver-wasm.js", + ContentType: "application/javascript", + Body: d.makeDriverJS(), + }, + } + return items } -func (r httpResource) IsExpired() bool { - return r.ExpireAt != time.Time{} && r.ExpireAt.Before(time.Now()) +func (d *goWasmDriver) makeDriverJS() []byte { + var b bytes.Buffer + if err := template. + Must(template.New("driver-wasm.js").Parse(wasmDriverJS)). + Execute(&b, struct { + LoadingLabel string + Wasm string + WasmContentLengthHeader string + }{ + LoadingLabel: d.LoadingLabel, + Wasm: d.Resources.AppWASM(), + WasmContentLengthHeader: d.WasmContentLengthHeader, + }); err != nil { + panic(errors.New("initializing driver-wasm.js failed").Wrap(err)) + } + return b.Bytes() } diff --git a/pkg/app/resource.go b/pkg/app/resource.go index 4c62476e0..5cc53cf36 100644 --- a/pkg/app/resource.go +++ b/pkg/app/resource.go @@ -115,3 +115,19 @@ type ProxyResource struct { // "/web/". ResourcePath string } + +func resolvePackagePath(resources ResourceProvider, path string) string { + var b strings.Builder + + b.WriteByte('/') + appResources := strings.Trim(resources.Package(), "/") + b.WriteString(appResources) + + path = strings.Trim(path, "/") + if b.Len() != 1 && path != "" { + b.WriteByte('/') + } + b.WriteString(path) + + return b.String() +} diff --git a/pkg/app/scripts.go b/pkg/app/scripts.go index b6ef70502..a05793b5a 100644 --- a/pkg/app/scripts.go +++ b/pkg/app/scripts.go @@ -10,7 +10,9 @@ const ( wasmExecJS = "// Copyright 2018 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n\"use strict\";\n\n(() => {\n\tconst enosys = () => {\n\t\tconst err = new Error(\"not implemented\");\n\t\terr.code = \"ENOSYS\";\n\t\treturn err;\n\t};\n\n\tif (!globalThis.fs) {\n\t\tlet outputBuf = \"\";\n\t\tglobalThis.fs = {\n\t\t\tconstants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused\n\t\t\twriteSync(fd, buf) {\n\t\t\t\toutputBuf += decoder.decode(buf);\n\t\t\t\tconst nl = outputBuf.lastIndexOf(\"\\n\");\n\t\t\t\tif (nl != -1) {\n\t\t\t\t\tconsole.log(outputBuf.substr(0, nl));\n\t\t\t\t\toutputBuf = outputBuf.substr(nl + 1);\n\t\t\t\t}\n\t\t\t\treturn buf.length;\n\t\t\t},\n\t\t\twrite(fd, buf, offset, length, position, callback) {\n\t\t\t\tif (offset !== 0 || length !== buf.length || position !== null) {\n\t\t\t\t\tcallback(enosys());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst n = this.writeSync(fd, buf);\n\t\t\t\tcallback(null, n);\n\t\t\t},\n\t\t\tchmod(path, mode, callback) { callback(enosys()); },\n\t\t\tchown(path, uid, gid, callback) { callback(enosys()); },\n\t\t\tclose(fd, callback) { callback(enosys()); },\n\t\t\tfchmod(fd, mode, callback) { callback(enosys()); },\n\t\t\tfchown(fd, uid, gid, callback) { callback(enosys()); },\n\t\t\tfstat(fd, callback) { callback(enosys()); },\n\t\t\tfsync(fd, callback) { callback(null); },\n\t\t\tftruncate(fd, length, callback) { callback(enosys()); },\n\t\t\tlchown(path, uid, gid, callback) { callback(enosys()); },\n\t\t\tlink(path, link, callback) { callback(enosys()); },\n\t\t\tlstat(path, callback) { callback(enosys()); },\n\t\t\tmkdir(path, perm, callback) { callback(enosys()); },\n\t\t\topen(path, flags, mode, callback) { callback(enosys()); },\n\t\t\tread(fd, buffer, offset, length, position, callback) { callback(enosys()); },\n\t\t\treaddir(path, callback) { callback(enosys()); },\n\t\t\treadlink(path, callback) { callback(enosys()); },\n\t\t\trename(from, to, callback) { callback(enosys()); },\n\t\t\trmdir(path, callback) { callback(enosys()); },\n\t\t\tstat(path, callback) { callback(enosys()); },\n\t\t\tsymlink(path, link, callback) { callback(enosys()); },\n\t\t\ttruncate(path, length, callback) { callback(enosys()); },\n\t\t\tunlink(path, callback) { callback(enosys()); },\n\t\t\tutimes(path, atime, mtime, callback) { callback(enosys()); },\n\t\t};\n\t}\n\n\tif (!globalThis.process) {\n\t\tglobalThis.process = {\n\t\t\tgetuid() { return -1; },\n\t\t\tgetgid() { return -1; },\n\t\t\tgeteuid() { return -1; },\n\t\t\tgetegid() { return -1; },\n\t\t\tgetgroups() { throw enosys(); },\n\t\t\tpid: -1,\n\t\t\tppid: -1,\n\t\t\tumask() { throw enosys(); },\n\t\t\tcwd() { throw enosys(); },\n\t\t\tchdir() { throw enosys(); },\n\t\t}\n\t}\n\n\tif (!globalThis.crypto) {\n\t\tthrow new Error(\"globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)\");\n\t}\n\n\tif (!globalThis.performance) {\n\t\tthrow new Error(\"globalThis.performance is not available, polyfill required (performance.now only)\");\n\t}\n\n\tif (!globalThis.TextEncoder) {\n\t\tthrow new Error(\"globalThis.TextEncoder is not available, polyfill required\");\n\t}\n\n\tif (!globalThis.TextDecoder) {\n\t\tthrow new Error(\"globalThis.TextDecoder is not available, polyfill required\");\n\t}\n\n\tconst encoder = new TextEncoder(\"utf-8\");\n\tconst decoder = new TextDecoder(\"utf-8\");\n\n\tglobalThis.Go = class {\n\t\tconstructor() {\n\t\t\tthis.argv = [\"js\"];\n\t\t\tthis.env = {};\n\t\t\tthis.exit = (code) => {\n\t\t\t\tif (code !== 0) {\n\t\t\t\t\tconsole.warn(\"exit code:\", code);\n\t\t\t\t}\n\t\t\t};\n\t\t\tthis._exitPromise = new Promise((resolve) => {\n\t\t\t\tthis._resolveExitPromise = resolve;\n\t\t\t});\n\t\t\tthis._pendingEvent = null;\n\t\t\tthis._scheduledTimeouts = new Map();\n\t\t\tthis._nextCallbackTimeoutID = 1;\n\n\t\t\tconst setInt64 = (addr, v) => {\n\t\t\t\tthis.mem.setUint32(addr + 0, v, true);\n\t\t\t\tthis.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);\n\t\t\t}\n\n\t\t\tconst getInt64 = (addr) => {\n\t\t\t\tconst low = this.mem.getUint32(addr + 0, true);\n\t\t\t\tconst high = this.mem.getInt32(addr + 4, true);\n\t\t\t\treturn low + high * 4294967296;\n\t\t\t}\n\n\t\t\tconst loadValue = (addr) => {\n\t\t\t\tconst f = this.mem.getFloat64(addr, true);\n\t\t\t\tif (f === 0) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\tif (!isNaN(f)) {\n\t\t\t\t\treturn f;\n\t\t\t\t}\n\n\t\t\t\tconst id = this.mem.getUint32(addr, true);\n\t\t\t\treturn this._values[id];\n\t\t\t}\n\n\t\t\tconst storeValue = (addr, v) => {\n\t\t\t\tconst nanHead = 0x7FF80000;\n\n\t\t\t\tif (typeof v === \"number\" && v !== 0) {\n\t\t\t\t\tif (isNaN(v)) {\n\t\t\t\t\t\tthis.mem.setUint32(addr + 4, nanHead, true);\n\t\t\t\t\t\tthis.mem.setUint32(addr, 0, true);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.mem.setFloat64(addr, v, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (v === undefined) {\n\t\t\t\t\tthis.mem.setFloat64(addr, 0, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet id = this._ids.get(v);\n\t\t\t\tif (id === undefined) {\n\t\t\t\t\tid = this._idPool.pop();\n\t\t\t\t\tif (id === undefined) {\n\t\t\t\t\t\tid = this._values.length;\n\t\t\t\t\t}\n\t\t\t\t\tthis._values[id] = v;\n\t\t\t\t\tthis._goRefCounts[id] = 0;\n\t\t\t\t\tthis._ids.set(v, id);\n\t\t\t\t}\n\t\t\t\tthis._goRefCounts[id]++;\n\t\t\t\tlet typeFlag = 0;\n\t\t\t\tswitch (typeof v) {\n\t\t\t\t\tcase \"object\":\n\t\t\t\t\t\tif (v !== null) {\n\t\t\t\t\t\t\ttypeFlag = 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"string\":\n\t\t\t\t\t\ttypeFlag = 2;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"symbol\":\n\t\t\t\t\t\ttypeFlag = 3;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"function\":\n\t\t\t\t\t\ttypeFlag = 4;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthis.mem.setUint32(addr + 4, nanHead | typeFlag, true);\n\t\t\t\tthis.mem.setUint32(addr, id, true);\n\t\t\t}\n\n\t\t\tconst loadSlice = (addr) => {\n\t\t\t\tconst array = getInt64(addr + 0);\n\t\t\t\tconst len = getInt64(addr + 8);\n\t\t\t\treturn new Uint8Array(this._inst.exports.mem.buffer, array, len);\n\t\t\t}\n\n\t\t\tconst loadSliceOfValues = (addr) => {\n\t\t\t\tconst array = getInt64(addr + 0);\n\t\t\t\tconst len = getInt64(addr + 8);\n\t\t\t\tconst a = new Array(len);\n\t\t\t\tfor (let i = 0; i < len; i++) {\n\t\t\t\t\ta[i] = loadValue(array + i * 8);\n\t\t\t\t}\n\t\t\t\treturn a;\n\t\t\t}\n\n\t\t\tconst loadString = (addr) => {\n\t\t\t\tconst saddr = getInt64(addr + 0);\n\t\t\t\tconst len = getInt64(addr + 8);\n\t\t\t\treturn decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));\n\t\t\t}\n\n\t\t\tconst timeOrigin = Date.now() - performance.now();\n\t\t\tthis.importObject = {\n\t\t\t\tgo: {\n\t\t\t\t\t// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)\n\t\t\t\t\t// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported\n\t\t\t\t\t// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).\n\t\t\t\t\t// This changes the SP, thus we have to update the SP used by the imported function.\n\n\t\t\t\t\t// func wasmExit(code int32)\n\t\t\t\t\t\"runtime.wasmExit\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst code = this.mem.getInt32(sp + 8, true);\n\t\t\t\t\t\tthis.exited = true;\n\t\t\t\t\t\tdelete this._inst;\n\t\t\t\t\t\tdelete this._values;\n\t\t\t\t\t\tdelete this._goRefCounts;\n\t\t\t\t\t\tdelete this._ids;\n\t\t\t\t\t\tdelete this._idPool;\n\t\t\t\t\t\tthis.exit(code);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)\n\t\t\t\t\t\"runtime.wasmWrite\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst fd = getInt64(sp + 8);\n\t\t\t\t\t\tconst p = getInt64(sp + 16);\n\t\t\t\t\t\tconst n = this.mem.getInt32(sp + 24, true);\n\t\t\t\t\t\tfs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func resetMemoryDataView()\n\t\t\t\t\t\"runtime.resetMemoryDataView\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tthis.mem = new DataView(this._inst.exports.mem.buffer);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func nanotime1() int64\n\t\t\t\t\t\"runtime.nanotime1\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tsetInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func walltime() (sec int64, nsec int32)\n\t\t\t\t\t\"runtime.walltime\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst msec = (new Date).getTime();\n\t\t\t\t\t\tsetInt64(sp + 8, msec / 1000);\n\t\t\t\t\t\tthis.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func scheduleTimeoutEvent(delay int64) int32\n\t\t\t\t\t\"runtime.scheduleTimeoutEvent\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst id = this._nextCallbackTimeoutID;\n\t\t\t\t\t\tthis._nextCallbackTimeoutID++;\n\t\t\t\t\t\tthis._scheduledTimeouts.set(id, setTimeout(\n\t\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t\tthis._resume();\n\t\t\t\t\t\t\t\twhile (this._scheduledTimeouts.has(id)) {\n\t\t\t\t\t\t\t\t\t// for some reason Go failed to register the timeout event, log and try again\n\t\t\t\t\t\t\t\t\t// (temporary workaround for https://github.com/golang/go/issues/28975)\n\t\t\t\t\t\t\t\t\tconsole.warn(\"scheduleTimeoutEvent: missed timeout event\");\n\t\t\t\t\t\t\t\t\tthis._resume();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tgetInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early\n\t\t\t\t\t\t));\n\t\t\t\t\t\tthis.mem.setInt32(sp + 16, id, true);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func clearTimeoutEvent(id int32)\n\t\t\t\t\t\"runtime.clearTimeoutEvent\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst id = this.mem.getInt32(sp + 8, true);\n\t\t\t\t\t\tclearTimeout(this._scheduledTimeouts.get(id));\n\t\t\t\t\t\tthis._scheduledTimeouts.delete(id);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func getRandomData(r []byte)\n\t\t\t\t\t\"runtime.getRandomData\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tcrypto.getRandomValues(loadSlice(sp + 8));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func finalizeRef(v ref)\n\t\t\t\t\t\"syscall/js.finalizeRef\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst id = this.mem.getUint32(sp + 8, true);\n\t\t\t\t\t\tthis._goRefCounts[id]--;\n\t\t\t\t\t\tif (this._goRefCounts[id] === 0) {\n\t\t\t\t\t\t\tconst v = this._values[id];\n\t\t\t\t\t\t\tthis._values[id] = null;\n\t\t\t\t\t\t\tthis._ids.delete(v);\n\t\t\t\t\t\t\tthis._idPool.push(id);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func stringVal(value string) ref\n\t\t\t\t\t\"syscall/js.stringVal\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tstoreValue(sp + 24, loadString(sp + 8));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueGet(v ref, p string) ref\n\t\t\t\t\t\"syscall/js.valueGet\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));\n\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\tstoreValue(sp + 32, result);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueSet(v ref, p string, x ref)\n\t\t\t\t\t\"syscall/js.valueSet\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tReflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueDelete(v ref, p string)\n\t\t\t\t\t\"syscall/js.valueDelete\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tReflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueIndex(v ref, i int) ref\n\t\t\t\t\t\"syscall/js.valueIndex\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tstoreValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));\n\t\t\t\t\t},\n\n\t\t\t\t\t// valueSetIndex(v ref, i int, x ref)\n\t\t\t\t\t\"syscall/js.valueSetIndex\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tReflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueCall(v ref, m string, args []ref) (ref, bool)\n\t\t\t\t\t\"syscall/js.valueCall\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst v = loadValue(sp + 8);\n\t\t\t\t\t\t\tconst m = Reflect.get(v, loadString(sp + 16));\n\t\t\t\t\t\t\tconst args = loadSliceOfValues(sp + 32);\n\t\t\t\t\t\t\tconst result = Reflect.apply(m, v, args);\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 56, result);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 64, 1);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 56, err);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 64, 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueInvoke(v ref, args []ref) (ref, bool)\n\t\t\t\t\t\"syscall/js.valueInvoke\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst v = loadValue(sp + 8);\n\t\t\t\t\t\t\tconst args = loadSliceOfValues(sp + 16);\n\t\t\t\t\t\t\tconst result = Reflect.apply(v, undefined, args);\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, result);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, err);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueNew(v ref, args []ref) (ref, bool)\n\t\t\t\t\t\"syscall/js.valueNew\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst v = loadValue(sp + 8);\n\t\t\t\t\t\t\tconst args = loadSliceOfValues(sp + 16);\n\t\t\t\t\t\t\tconst result = Reflect.construct(v, args);\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, result);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tsp = this._inst.exports.getsp() >>> 0; // see comment above\n\t\t\t\t\t\t\tstoreValue(sp + 40, err);\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueLength(v ref) int\n\t\t\t\t\t\"syscall/js.valueLength\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tsetInt64(sp + 16, parseInt(loadValue(sp + 8).length));\n\t\t\t\t\t},\n\n\t\t\t\t\t// valuePrepareString(v ref) (ref, int)\n\t\t\t\t\t\"syscall/js.valuePrepareString\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst str = encoder.encode(String(loadValue(sp + 8)));\n\t\t\t\t\t\tstoreValue(sp + 16, str);\n\t\t\t\t\t\tsetInt64(sp + 24, str.length);\n\t\t\t\t\t},\n\n\t\t\t\t\t// valueLoadString(v ref, b []byte)\n\t\t\t\t\t\"syscall/js.valueLoadString\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst str = loadValue(sp + 8);\n\t\t\t\t\t\tloadSlice(sp + 16).set(str);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func valueInstanceOf(v ref, t ref) bool\n\t\t\t\t\t\"syscall/js.valueInstanceOf\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tthis.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func copyBytesToGo(dst []byte, src ref) (int, bool)\n\t\t\t\t\t\"syscall/js.copyBytesToGo\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst dst = loadSlice(sp + 8);\n\t\t\t\t\t\tconst src = loadValue(sp + 32);\n\t\t\t\t\t\tif (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst toCopy = src.subarray(0, dst.length);\n\t\t\t\t\t\tdst.set(toCopy);\n\t\t\t\t\t\tsetInt64(sp + 40, toCopy.length);\n\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t},\n\n\t\t\t\t\t// func copyBytesToJS(dst ref, src []byte) (int, bool)\n\t\t\t\t\t\"syscall/js.copyBytesToJS\": (sp) => {\n\t\t\t\t\t\tsp >>>= 0;\n\t\t\t\t\t\tconst dst = loadValue(sp + 8);\n\t\t\t\t\t\tconst src = loadSlice(sp + 16);\n\t\t\t\t\t\tif (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {\n\t\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 0);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst toCopy = src.subarray(0, dst.length);\n\t\t\t\t\t\tdst.set(toCopy);\n\t\t\t\t\t\tsetInt64(sp + 40, toCopy.length);\n\t\t\t\t\t\tthis.mem.setUint8(sp + 48, 1);\n\t\t\t\t\t},\n\n\t\t\t\t\t\"debug\": (value) => {\n\t\t\t\t\t\tconsole.log(value);\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\n\t\tasync run(instance) {\n\t\t\tif (!(instance instanceof WebAssembly.Instance)) {\n\t\t\t\tthrow new Error(\"Go.run: WebAssembly.Instance expected\");\n\t\t\t}\n\t\t\tthis._inst = instance;\n\t\t\tthis.mem = new DataView(this._inst.exports.mem.buffer);\n\t\t\tthis._values = [ // JS values that Go currently has references to, indexed by reference id\n\t\t\t\tNaN,\n\t\t\t\t0,\n\t\t\t\tnull,\n\t\t\t\ttrue,\n\t\t\t\tfalse,\n\t\t\t\tglobalThis,\n\t\t\t\tthis,\n\t\t\t];\n\t\t\tthis._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id\n\t\t\tthis._ids = new Map([ // mapping from JS values to reference ids\n\t\t\t\t[0, 1],\n\t\t\t\t[null, 2],\n\t\t\t\t[true, 3],\n\t\t\t\t[false, 4],\n\t\t\t\t[globalThis, 5],\n\t\t\t\t[this, 6],\n\t\t\t]);\n\t\t\tthis._idPool = []; // unused ids that have been garbage collected\n\t\t\tthis.exited = false; // whether the Go program has exited\n\n\t\t\t// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.\n\t\t\tlet offset = 4096;\n\n\t\t\tconst strPtr = (str) => {\n\t\t\t\tconst ptr = offset;\n\t\t\t\tconst bytes = encoder.encode(str + \"\\0\");\n\t\t\t\tnew Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);\n\t\t\t\toffset += bytes.length;\n\t\t\t\tif (offset % 8 !== 0) {\n\t\t\t\t\toffset += 8 - (offset % 8);\n\t\t\t\t}\n\t\t\t\treturn ptr;\n\t\t\t};\n\n\t\t\tconst argc = this.argv.length;\n\n\t\t\tconst argvPtrs = [];\n\t\t\tthis.argv.forEach((arg) => {\n\t\t\t\targvPtrs.push(strPtr(arg));\n\t\t\t});\n\t\t\targvPtrs.push(0);\n\n\t\t\tconst keys = Object.keys(this.env).sort();\n\t\t\tkeys.forEach((key) => {\n\t\t\t\targvPtrs.push(strPtr(`${key}=${this.env[key]}`));\n\t\t\t});\n\t\t\targvPtrs.push(0);\n\n\t\t\tconst argv = offset;\n\t\t\targvPtrs.forEach((ptr) => {\n\t\t\t\tthis.mem.setUint32(offset, ptr, true);\n\t\t\t\tthis.mem.setUint32(offset + 4, 0, true);\n\t\t\t\toffset += 8;\n\t\t\t});\n\n\t\t\t// The linker guarantees global data starts from at least wasmMinDataAddr.\n\t\t\t// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.\n\t\t\tconst wasmMinDataAddr = 4096 + 8192;\n\t\t\tif (offset >= wasmMinDataAddr) {\n\t\t\t\tthrow new Error(\"total length of command line and environment variables exceeds limit\");\n\t\t\t}\n\n\t\t\tthis._inst.exports.run(argc, argv);\n\t\t\tif (this.exited) {\n\t\t\t\tthis._resolveExitPromise();\n\t\t\t}\n\t\t\tawait this._exitPromise;\n\t\t}\n\n\t\t_resume() {\n\t\t\tif (this.exited) {\n\t\t\t\tthrow new Error(\"Go program has already exited\");\n\t\t\t}\n\t\t\tthis._inst.exports.resume();\n\t\t\tif (this.exited) {\n\t\t\t\tthis._resolveExitPromise();\n\t\t\t}\n\t\t}\n\n\t\t_makeFuncWrapper(id) {\n\t\t\tconst go = this;\n\t\t\treturn function () {\n\t\t\t\tconst event = { id: id, this: this, args: arguments };\n\t\t\t\tgo._pendingEvent = event;\n\t\t\t\tgo._resume();\n\t\t\t\treturn event.result;\n\t\t\t};\n\t\t}\n\t}\n})();\n" - appJS = "// -----------------------------------------------------------------------------\n// go-app\n// -----------------------------------------------------------------------------\nvar goappNav = function () {};\nvar goappOnUpdate = function () {};\nvar goappOnAppInstallChange = function () {};\n\nconst goappEnv = {{.Env}};\nconst goappLoadingLabel = \"{{.LoadingLabel}}\";\nconst goappWasmContentLengthHeader = \"{{.WasmContentLengthHeader}}\";\n\nlet goappServiceWorkerRegistration;\nlet deferredPrompt = null;\n\ngoappInitServiceWorker();\ngoappWatchForUpdate();\ngoappWatchForInstallable();\ngoappInitWebAssembly();\n\n// -----------------------------------------------------------------------------\n// Service Worker\n// -----------------------------------------------------------------------------\nasync function goappInitServiceWorker() {\n if (\"serviceWorker\" in navigator) {\n try {\n const registration = await navigator.serviceWorker.register(\n \"{{.WorkerJS}}\"\n );\n\n goappServiceWorkerRegistration = registration;\n goappSetupNotifyUpdate(registration);\n goappSetupAutoUpdate(registration);\n goappSetupPushNotification();\n } catch (err) {\n console.error(\"goapp service worker registration failed\", err);\n }\n }\n}\n\n// -----------------------------------------------------------------------------\n// Update\n// -----------------------------------------------------------------------------\nfunction goappWatchForUpdate() {\n window.addEventListener(\"beforeinstallprompt\", (e) => {\n e.preventDefault();\n deferredPrompt = e;\n goappOnAppInstallChange();\n });\n}\n\nfunction goappSetupNotifyUpdate(registration) {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n\n installingWorker.onstatechange = () => {\n if (installingWorker.state != \"installed\") {\n return;\n }\n\n if (!navigator.serviceWorker.controller) {\n return;\n }\n\n goappOnUpdate();\n };\n };\n}\n\nfunction goappSetupAutoUpdate(registration) {\n const autoUpdateInterval = \"{{.AutoUpdateInterval}}\";\n if (autoUpdateInterval == 0) {\n return;\n }\n\n window.setInterval(() => {\n registration.update();\n }, autoUpdateInterval);\n}\n\n// -----------------------------------------------------------------------------\n// Install\n// -----------------------------------------------------------------------------\nfunction goappWatchForInstallable() {\n window.addEventListener(\"appinstalled\", () => {\n deferredPrompt = null;\n goappOnAppInstallChange();\n });\n}\n\nfunction goappIsAppInstallable() {\n return !goappIsAppInstalled() && deferredPrompt != null;\n}\n\nfunction goappIsAppInstalled() {\n const isStandalone = window.matchMedia(\"(display-mode: standalone)\").matches;\n return isStandalone || navigator.standalone;\n}\n\nasync function goappShowInstallPrompt() {\n deferredPrompt.prompt();\n await deferredPrompt.userChoice;\n deferredPrompt = null;\n}\n\n// -----------------------------------------------------------------------------\n// Environment\n// -----------------------------------------------------------------------------\nfunction goappGetenv(k) {\n return goappEnv[k];\n}\n\n// -----------------------------------------------------------------------------\n// Notifications\n// -----------------------------------------------------------------------------\nfunction goappSetupPushNotification() {\n navigator.serviceWorker.addEventListener(\"message\", (event) => {\n const msg = event.data.goapp;\n if (!msg) {\n return;\n }\n\n if (msg.type !== \"notification\") {\n return;\n }\n\n goappNav(msg.path);\n });\n}\n\nasync function goappSubscribePushNotifications(vapIDpublicKey) {\n try {\n const subscription =\n await goappServiceWorkerRegistration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: vapIDpublicKey,\n });\n return JSON.stringify(subscription);\n } catch (err) {\n console.error(err);\n return \"\";\n }\n}\n\nfunction goappNewNotification(jsonNotification) {\n let notification = JSON.parse(jsonNotification);\n\n const title = notification.title;\n delete notification.title;\n\n let path = notification.path;\n if (!path) {\n path = \"/\";\n }\n\n const webNotification = new Notification(title, notification);\n\n webNotification.onclick = () => {\n goappNav(path);\n webNotification.close();\n };\n}\n\n// -----------------------------------------------------------------------------\n// Keep Clean Body\n// -----------------------------------------------------------------------------\nfunction goappKeepBodyClean() {\n const body = document.body;\n const bodyChildrenCount = body.children.length;\n\n const mutationObserver = new MutationObserver(function (mutationList) {\n mutationList.forEach((mutation) => {\n switch (mutation.type) {\n case \"childList\":\n while (body.children.length > bodyChildrenCount) {\n body.removeChild(body.lastChild);\n }\n break;\n }\n });\n });\n\n mutationObserver.observe(document.body, {\n childList: true,\n });\n\n return () => mutationObserver.disconnect();\n}\n\n// -----------------------------------------------------------------------------\n// Web Assembly\n// -----------------------------------------------------------------------------\nasync function goappInitWebAssembly() {\n if (!goappCanLoadWebAssembly()) {\n document.getElementById(\"app-wasm-loader\").style.display = \"none\";\n return;\n }\n\n let instantiateStreaming = WebAssembly.instantiateStreaming;\n if (!instantiateStreaming) {\n instantiateStreaming = async (resp, importObject) => {\n const source = await (await resp).arrayBuffer();\n return await WebAssembly.instantiate(source, importObject);\n };\n }\n\n const loaderIcon = document.getElementById(\"app-wasm-loader-icon\");\n const loaderLabel = document.getElementById(\"app-wasm-loader-label\");\n\n try {\n const showProgress = (progress) => {\n loaderLabel.innerText = goappLoadingLabel.replace(\"{progress}\", progress);\n };\n showProgress(0);\n\n const go = new Go();\n const wasm = await instantiateStreaming(\n fetchWithProgress(\"{{.Wasm}}\", showProgress),\n go.importObject\n );\n\n go.run(wasm.instance);\n } catch (err) {\n loaderIcon.className = \"goapp-logo\";\n loaderLabel.innerText = err;\n console.error(\"loading wasm failed: \", err);\n }\n}\n\nfunction goappCanLoadWebAssembly() {\n return !/bot|googlebot|crawler|spider|robot|crawling/i.test(\n navigator.userAgent\n );\n}\n\nasync function fetchWithProgress(url, progess) {\n const response = await fetch(url);\n\n let contentLength;\n try {\n contentLength = response.headers.get(goappWasmContentLengthHeader);\n } catch {}\n if (!goappWasmContentLengthHeader || !contentLength) {\n contentLength = response.headers.get(\"Content-Length\");\n }\n\n const total = parseInt(contentLength, 10);\n let loaded = 0;\n\n const progressHandler = function (loaded, total) {\n progess(Math.round((loaded * 100) / total));\n };\n\n var res = new Response(\n new ReadableStream(\n {\n async start(controller) {\n var reader = response.body.getReader();\n for (;;) {\n var { done, value } = await reader.read();\n\n if (done) {\n progressHandler(total, total);\n break;\n }\n\n loaded += value.byteLength;\n progressHandler(loaded, total);\n controller.enqueue(value);\n }\n controller.close();\n },\n },\n {\n status: response.status,\n statusText: response.statusText,\n }\n )\n );\n\n for (var pair of response.headers.entries()) {\n res.headers.set(pair[0], pair[1]);\n }\n\n return res;\n}\n" + wasmDriverJS = "// -----------------------------------------------------------------------------\n// Web Assembly\n// -----------------------------------------------------------------------------\nconst goappLoadingLabel = \"{{.LoadingLabel}}\";\nconst goappWasmContentLengthHeader = \"{{.WasmContentLengthHeader}}\";\n\ngoappInitWebAssembly();\n\nasync function goappInitWebAssembly() {\n if (!goappCanLoadWebAssembly()) {\n document.getElementById(\"app-wasm-loader\").style.display = \"none\";\n return;\n }\n\n let instantiateStreaming = WebAssembly.instantiateStreaming;\n if (!instantiateStreaming) {\n instantiateStreaming = async (resp, importObject) => {\n const source = await (await resp).arrayBuffer();\n return await WebAssembly.instantiate(source, importObject);\n };\n }\n\n const loaderIcon = document.getElementById(\"app-wasm-loader-icon\");\n const loaderLabel = document.getElementById(\"app-wasm-loader-label\");\n\n try {\n const showProgress = (progress) => {\n loaderLabel.innerText = goappLoadingLabel.replace(\"{progress}\", progress);\n };\n showProgress(0);\n\n const go = new Go();\n const wasm = await instantiateStreaming(\n fetchWithProgress(\"{{.Wasm}}\", showProgress),\n go.importObject\n );\n\n go.run(wasm.instance);\n } catch (err) {\n loaderIcon.className = \"goapp-logo\";\n loaderLabel.innerText = err;\n console.error(\"loading wasm failed: \", err);\n }\n}\n\nfunction goappCanLoadWebAssembly() {\n return !/bot|googlebot|crawler|spider|robot|crawling/i.test(\n navigator.userAgent\n );\n}\n\nasync function fetchWithProgress(url, progess) {\n const response = await fetch(url);\n\n let contentLength;\n try {\n contentLength = response.headers.get(goappWasmContentLengthHeader);\n } catch {}\n if (!goappWasmContentLengthHeader || !contentLength) {\n contentLength = response.headers.get(\"Content-Length\");\n }\n\n const total = parseInt(contentLength, 10);\n let loaded = 0;\n\n const progressHandler = function (loaded, total) {\n progess(Math.round((loaded * 100) / total));\n };\n\n var res = new Response(\n new ReadableStream(\n {\n async start(controller) {\n var reader = response.body.getReader();\n for (;;) {\n var { done, value } = await reader.read();\n\n if (done) {\n progressHandler(total, total);\n break;\n }\n\n loaded += value.byteLength;\n progressHandler(loaded, total);\n controller.enqueue(value);\n }\n controller.close();\n },\n },\n {\n status: response.status,\n statusText: response.statusText,\n }\n )\n );\n\n for (var pair of response.headers.entries()) {\n res.headers.set(pair[0], pair[1]);\n }\n\n return res;\n}\n" + + appJS = "// -----------------------------------------------------------------------------\n// go-app\n// -----------------------------------------------------------------------------\nvar goappNav = function () {};\nvar goappOnUpdate = function () {};\nvar goappOnAppInstallChange = function () {};\n\nconst goappEnv = {{.Env}};\n\nlet goappServiceWorkerRegistration;\nlet deferredPrompt = null;\n\ngoappInitServiceWorker();\ngoappWatchForUpdate();\ngoappWatchForInstallable();\n\n// -----------------------------------------------------------------------------\n// Service Worker\n// -----------------------------------------------------------------------------\nasync function goappInitServiceWorker() {\n if (\"serviceWorker\" in navigator) {\n try {\n const registration = await navigator.serviceWorker.register(\n \"{{.WorkerJS}}\"\n );\n\n goappServiceWorkerRegistration = registration;\n goappSetupNotifyUpdate(registration);\n goappSetupAutoUpdate(registration);\n goappSetupPushNotification();\n } catch (err) {\n console.error(\"goapp service worker registration failed\", err);\n }\n }\n}\n\n// -----------------------------------------------------------------------------\n// Update\n// -----------------------------------------------------------------------------\nfunction goappWatchForUpdate() {\n window.addEventListener(\"beforeinstallprompt\", (e) => {\n e.preventDefault();\n deferredPrompt = e;\n goappOnAppInstallChange();\n });\n}\n\nfunction goappSetupNotifyUpdate(registration) {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing;\n\n installingWorker.onstatechange = () => {\n if (installingWorker.state != \"installed\") {\n return;\n }\n\n if (!navigator.serviceWorker.controller) {\n return;\n }\n\n goappOnUpdate();\n };\n };\n}\n\nfunction goappSetupAutoUpdate(registration) {\n const autoUpdateInterval = \"{{.AutoUpdateInterval}}\";\n if (autoUpdateInterval == 0) {\n return;\n }\n\n window.setInterval(() => {\n registration.update();\n }, autoUpdateInterval);\n}\n\n// -----------------------------------------------------------------------------\n// Install\n// -----------------------------------------------------------------------------\nfunction goappWatchForInstallable() {\n window.addEventListener(\"appinstalled\", () => {\n deferredPrompt = null;\n goappOnAppInstallChange();\n });\n}\n\nfunction goappIsAppInstallable() {\n return !goappIsAppInstalled() && deferredPrompt != null;\n}\n\nfunction goappIsAppInstalled() {\n const isStandalone = window.matchMedia(\"(display-mode: standalone)\").matches;\n return isStandalone || navigator.standalone;\n}\n\nasync function goappShowInstallPrompt() {\n deferredPrompt.prompt();\n await deferredPrompt.userChoice;\n deferredPrompt = null;\n}\n\n// -----------------------------------------------------------------------------\n// Environment\n// -----------------------------------------------------------------------------\nfunction goappGetenv(k) {\n return goappEnv[k];\n}\n\n// -----------------------------------------------------------------------------\n// Notifications\n// -----------------------------------------------------------------------------\nfunction goappSetupPushNotification() {\n navigator.serviceWorker.addEventListener(\"message\", (event) => {\n const msg = event.data.goapp;\n if (!msg) {\n return;\n }\n\n if (msg.type !== \"notification\") {\n return;\n }\n\n goappNav(msg.path);\n });\n}\n\nasync function goappSubscribePushNotifications(vapIDpublicKey) {\n try {\n const subscription =\n await goappServiceWorkerRegistration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: vapIDpublicKey,\n });\n return JSON.stringify(subscription);\n } catch (err) {\n console.error(err);\n return \"\";\n }\n}\n\nfunction goappNewNotification(jsonNotification) {\n let notification = JSON.parse(jsonNotification);\n\n const title = notification.title;\n delete notification.title;\n\n let path = notification.path;\n if (!path) {\n path = \"/\";\n }\n\n const webNotification = new Notification(title, notification);\n\n webNotification.onclick = () => {\n goappNav(path);\n webNotification.close();\n };\n}\n\n// -----------------------------------------------------------------------------\n// Keep Clean Body\n// -----------------------------------------------------------------------------\nfunction goappKeepBodyClean() {\n const body = document.body;\n const bodyChildrenCount = body.children.length;\n\n const mutationObserver = new MutationObserver(function (mutationList) {\n mutationList.forEach((mutation) => {\n switch (mutation.type) {\n case \"childList\":\n while (body.children.length > bodyChildrenCount) {\n body.removeChild(body.lastChild);\n }\n break;\n }\n });\n });\n\n mutationObserver.observe(document.body, {\n childList: true,\n });\n\n return () => mutationObserver.disconnect();\n}\n" manifestJSON = "{\n \"short_name\": \"{{.ShortName}}\",\n \"name\": \"{{.Name}}\",\n \"description\": \"{{.Description}}\",\n \"icons\": [\n {\n \"src\": \"{{.SVGIcon}}\",\n \"type\": \"image/svg+xml\",\n \"sizes\": \"any\"\n },\n {\n \"src\": \"{{.LargeIcon}}\",\n \"type\": \"image/png\",\n \"sizes\": \"512x512\"\n },\n {\n \"src\": \"{{.DefaultIcon}}\",\n \"type\": \"image/png\",\n \"sizes\": \"192x192\"\n }\n ],\n \"scope\": \"{{.Scope}}\",\n \"start_url\": \"{{.StartURL}}\",\n \"background_color\": \"{{.BackgroundColor}}\",\n \"theme_color\": \"{{.ThemeColor}}\",\n \"display\": \"standalone\"\n}" diff --git a/pkg/app/static.go b/pkg/app/static.go index d36a22024..990b81482 100644 --- a/pkg/app/static.go +++ b/pkg/app/static.go @@ -27,7 +27,6 @@ func GenerateStaticWebsite(dir string, h *Handler, pages ...string) error { resources := map[string]struct{}{ "/": {}, - "/wasm_exec.js": {}, "/app.js": {}, "/app-worker.js": {}, "/manifest.webmanifest": {}, @@ -35,6 +34,14 @@ func GenerateStaticWebsite(dir string, h *Handler, pages ...string) error { "/web": {}, } + for _, path := range h.Driver.Scripts() { + resources[path] = struct{}{} + } + + for _, path := range h.Driver.Styles() { + resources[path] = struct{}{} + } + for path := range routes.routes { resources[path] = struct{}{} } diff --git a/pkg/app/static_js.go b/pkg/app/static_js.go index e027f4b70..d1b44cd6e 100644 --- a/pkg/app/static_js.go +++ b/pkg/app/static_js.go @@ -8,6 +8,7 @@ import ( const ( wasmExecJS = "" + wasmDriverJS = "" appJS = "" appWorkerJS = "" manifestJSON = ""