diff --git a/pkg/visor/hypervisor_handlers_browse.go b/pkg/visor/hypervisor_handlers_browse.go index 542116106d..5e1ee19545 100644 --- a/pkg/visor/hypervisor_handlers_browse.go +++ b/pkg/visor/hypervisor_handlers_browse.go @@ -78,6 +78,13 @@ func (hv *Hypervisor) uiHandler() http.Handler { w.Header().Set("Cache-Control", "no-cache") _, _ = w.Write(browseui.WinBoxWasm()) //nolint:errcheck return + case "/gobrowser.wasm": + // The experimental Go/wasm browser, launched by + // globalThis.SkywireGoBrowser.open() from gobrowser-loader.js. + w.Header().Set("Content-Type", "application/wasm") + w.Header().Set("Cache-Control", "no-cache") + _, _ = w.Write(browseui.GoBrowserWasm()) //nolint:errcheck + return case "/skywire-browse-launcher.js": serveJS(w, []byte(nativeBrowseLauncherJS)) return diff --git a/pkg/visor/wasmserve.go b/pkg/visor/wasmserve.go index 69716c1dc2..9426efefcb 100644 --- a/pkg/visor/wasmserve.go +++ b/pkg/visor/wasmserve.go @@ -312,6 +312,7 @@ func ServeWasm(ctx context.Context, cfg WasmServeConfig) error { serveBytes("/desk", "text/html; charset=utf-8", deskHTML) } serveBytes("/winbox.wasm", "application/wasm", wasmhv.WinBoxWasm()) + serveBytes("/gobrowser.wasm", "application/wasm", wasmhv.GoBrowserWasm()) serveBytes("/autoupdate.js", "text/javascript", wasmhv.AutoUpdateJS) serveBytes("/manifest.webmanifest", "application/manifest+json", wasmhv.PWAManifest) serveBytes("/icon-192.png", "image/png", wasmhv.PWAIcon192) diff --git a/pkg/wasmhv/browseui/embed.go b/pkg/wasmhv/browseui/embed.go index 7b883cca45..73e662cb42 100644 --- a/pkg/wasmhv/browseui/embed.go +++ b/pkg/wasmhv/browseui/embed.go @@ -36,6 +36,18 @@ var seedSkywireJS []byte //go:embed skywire-exec.js var skywireExecJS []byte +// goBrowserLoaderJS defines globalThis.SkywireGoBrowser.open() — the launcher +// for the experimental Go/wasm browser (github.com/0magnet/shipyard cmd/browser). +// Concatenated into the bundle so the launcher is present on every desk page; +// the module itself is a separate fetch, served at /gobrowser.wasm from +// GoBrowserWasm() below. +// +//go:embed gobrowser-loader.js +var goBrowserLoaderJS []byte + +//go:embed gobrowser.wasm.gz +var goBrowserWasmGz []byte + // deskBootJS is the shared desk boot (skywireDeskBoot(opts)) behind both // desk-first pages: the docs playground and the converged visor page. Served // as its own asset (not part of the bundle) because it runs page-level @@ -67,6 +79,7 @@ var BrowseJS = func() []byte { winboxdist.LoaderJS(), netscrape.BrowseJS(), skywireExecJS, + goBrowserLoaderJS, } n := 0 for _, p := range parts { @@ -95,6 +108,32 @@ func VNetSWJS() []byte { return bottle.VNetSWJS() } // serving it. func WinBoxWasmGz() []byte { return winboxdist.WasmGz() } +// GoBrowserWasmGz is the compressed experimental Go browser module. +func GoBrowserWasmGz() []byte { return goBrowserWasmGz } + +var ( + goBrowserOnce sync.Once + goBrowserWasm []byte +) + +// GoBrowserWasm is the experimental Go browser module as served at +// /gobrowser.wasm. Inflated once on first use and kept. +func GoBrowserWasm() []byte { + goBrowserOnce.Do(func() { + zr, err := gzip.NewReader(bytes.NewReader(goBrowserWasmGz)) + if err != nil { + return + } + defer zr.Close() //nolint:errcheck + b, err := io.ReadAll(zr) + if err != nil { + return + } + goBrowserWasm = b + }) + return goBrowserWasm +} + var ( winBoxOnce sync.Once winBoxWasm []byte diff --git a/pkg/wasmhv/browseui/gobrowser-loader.js b/pkg/wasmhv/browseui/gobrowser-loader.js new file mode 100644 index 0000000000..25bf1a03b9 --- /dev/null +++ b/pkg/wasmhv/browseui/gobrowser-loader.js @@ -0,0 +1,73 @@ +// gobrowser-loader.js — opens the Go/wasm browser (github.com/0magnet/shipyard +// cmd/browser) in a winbox window, wired to the visor's transports. This is an +// EXPERIMENTAL alternative to netscrape's browse.js: the browser's chrome, page +// transcoding (sandboxed srcdoc, inlined stylesheets/images) and navigation are +// Go/syscall/js; only the network is delegated here, through the same +// skywireVisor.fetchDmsg / fetchClearnet the JS engine uses. +// +// Usage from the visor page (or its console): SkywireGoBrowser.open(). +// It reads globalThis.WinBox (window manager), globalThis.Go (the wasm_exec +// runtime) and globalThis.skywireVisor (transports) at call time, so it is safe +// to define at bundle load before those exist. +(function () { + var WASM_URL = "/gobrowser.wasm"; + + function meshHost(h) { + return /\.(dmsg|skysocks|skynet)$/i.test(h) || /^[0-9a-f]{66}$/i.test(h); + } + + // respond wraps the visor's {status, body:Uint8Array, headers} into a fetch + // Response, so the Go browser's fetchVia sees a normal Response either way. + function respond(r) { + var h = new Headers(); + if (r && r.headers) { + try { for (var k in r.headers) h.set(k, r.headers[k]); } catch (e) { /* ignore */ } + } + return new Response((r && r.body) || new Uint8Array(0), { status: (r && r.status) || 200, headers: h }); + } + + // The transport the Go browser calls (globalThis.__shipyardBrowserFetch): + // mesh hosts go over dmsg, everything else over clearnet, with a plain + // same-origin fetch as the last resort. + function transport(url) { + var sv = globalThis.skywireVisor || {}; + var u; + try { u = new URL(url); } catch (e) { return fetch(url); } + var path = (u.pathname || "/") + (u.search || ""); + if (sv.fetchDmsg && meshHost(u.hostname)) { + return Promise.resolve(sv.fetchDmsg(u.hostname, "GET", path, null)).then(respond); + } + if (sv.fetchClearnet) { + return Promise.resolve(sv.fetchClearnet(url, "GET", null)).then(respond); + } + return fetch(url); + } + + globalThis.SkywireGoBrowser = { + // open mounts the Go browser in a new winbox window and returns it. + open: function (title) { + if (typeof globalThis.WinBox !== "function") { + console.error("SkywireGoBrowser: window manager not ready"); + return null; + } + if (typeof globalThis.Go !== "function") { + console.error("SkywireGoBrowser: Go runtime (wasm_exec) not present"); + return null; + } + globalThis.__shipyardBrowserFetch = transport; + var wb = new globalThis.WinBox({ title: title || "Go Browser (beta)", width: "72%", height: "72%" }); + var mount = document.createElement("div"); + mount.style.cssText = "position:absolute;inset:0"; + wb.body.appendChild(mount); + globalThis.__shipyardMount = mount; + fetch(WASM_URL) + .then(function (r) { return r.arrayBuffer(); }) + .then(function (buf) { + var go = new globalThis.Go(); + return WebAssembly.instantiate(buf, go.importObject).then(function (res) { go.run(res.instance); }); + }) + .catch(function (e) { mount.textContent = "Go browser failed to load: " + e; }); + return wb; + }, + }; +})(); diff --git a/pkg/wasmhv/browseui/gobrowser.wasm.gz b/pkg/wasmhv/browseui/gobrowser.wasm.gz new file mode 100644 index 0000000000..d670d5ac45 Binary files /dev/null and b/pkg/wasmhv/browseui/gobrowser.wasm.gz differ diff --git a/pkg/wasmhv/embed.go b/pkg/wasmhv/embed.go index 50b3a2f22e..f34549fa73 100644 --- a/pkg/wasmhv/embed.go +++ b/pkg/wasmhv/embed.go @@ -37,6 +37,14 @@ func WinBoxWasm() []byte { return browseui.WinBoxWasm() } // generator to inline. func WinBoxWasmGz() []byte { return browseui.WinBoxWasmGz() } +// GoBrowserWasm is the experimental Go/wasm browser served at /gobrowser.wasm, +// launched by globalThis.SkywireGoBrowser.open(). Re-exported from the browseui +// leaf like WinBoxWasm. +func GoBrowserWasm() []byte { return browseui.GoBrowserWasm() } + +// GoBrowserWasmGz is the same module compressed, for the single-file generator. +func GoBrowserWasmGz() []byte { return browseui.GoBrowserWasmGz() } + // DeskBootJS is the shared desk boot (skywireDeskBoot) behind the desk-first // pages — the docs playground and `hv serve`'s /desk. Re-exported from the // browseui leaf like BrowseJS.