Update fetch for static page gen, fix DOM patching
This commit is contained in:
@@ -27,8 +27,11 @@ answers `/api/quotes` with `httputil.RespondGob([]Quote)` and the client decodes
|
|||||||
it straight back into `[]Quote` with `httputil.FetchGob` (the same Go type on
|
it straight back into `[]Quote` with `httputil.FetchGob` (the same Go type on
|
||||||
both ends — no JSON); and a **user-entered** GitHub repo (`owner/name`) is
|
both ends — no JSON); and a **user-entered** GitHub repo (`owner/name`) is
|
||||||
fetched with `httputil.FetchJSON` into a tagged Go struct. The client HTTP
|
fetched with `httputil.FetchJSON` into a tagged Go struct. The client HTTP
|
||||||
transport is `wasmruntime.FetchBytes`, injected once via
|
transport is `wasmruntime.FetchBytes`, installed by the runtime itself (override
|
||||||
`httputil.SetClientTransport`.
|
it with `httputil.SetClientTransport` for auth headers or a base URL). `/data` is
|
||||||
|
a `static` route, and fetching only exists on the client, so the fetches no-op
|
||||||
|
during SSR: the server pre-renders the page's **spinner**, and the client runs
|
||||||
|
them for real after hydration.
|
||||||
|
|
||||||
Styling is **Tailwind**: the dev server (and `build.sh`) run `kjol/cmd/twcss`,
|
Styling is **Tailwind**: the dev server (and `build.sh`) run `kjol/cmd/twcss`,
|
||||||
which scans the Go markup + the `webui` kit for utility classes and compiles
|
which scans the Go markup + the `webui` kit for utility classes and compiles
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type repoInfo struct {
|
|||||||
Stars int `json:"stargazers_count"`
|
Stars int `json:"stargazers_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//gowasm:page /data layout=app
|
//gowasm:page /data layout=app static
|
||||||
func DataPage(d Deps) func() *VNode {
|
func DataPage(d Deps) func() *VNode {
|
||||||
// (1) gob from our own server via httputil.RespondGob / FetchGob.
|
// (1) gob from our own server via httputil.RespondGob / FetchGob.
|
||||||
quotes := NewSignal([]Quote{})
|
quotes := NewSignal([]Quote{})
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var StaticPaths = map[string]bool{
|
|||||||
"/": true,
|
"/": true,
|
||||||
"/about": true,
|
"/about": true,
|
||||||
"/chart": true,
|
"/chart": true,
|
||||||
|
"/data": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteLayout maps each route to the name of the layout that wraps it.
|
// RouteLayout maps each route to the name of the layout that wraps it.
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gowasmweb/app"
|
"gowasmweb/app"
|
||||||
"kjol/httputil"
|
|
||||||
"kjol/vdom"
|
"kjol/vdom"
|
||||||
"kjol/wasmruntime"
|
"kjol/wasmruntime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// (The client transport for httputil.FetchGob / FetchJSON needs no wiring —
|
||||||
|
// importing wasmruntime installs it. On the server it stays nil, so those
|
||||||
|
// fetches no-op during SSR and the page ships its loading state.)
|
||||||
|
//
|
||||||
// Collect the signals created during setup so their values can be preserved
|
// Collect the signals created during setup so their values can be preserved
|
||||||
// across an in-place hot swap. On a normal load RestoreState is nil (fresh
|
// across an in-place hot swap. On a normal load RestoreState is nil (fresh
|
||||||
// state); after a dev hot-reload it carries the previous instance's values,
|
// state); after a dev hot-reload it carries the previous instance's values,
|
||||||
// which NewSignal restores by creation order.
|
// which NewSignal restores by creation order.
|
||||||
httputil.SetClientTransport(wasmruntime.FetchBytes) // typed gob/json fetches for app pages
|
|
||||||
collector := vdom.BeginCollect(wasmruntime.RestoreState())
|
collector := vdom.BeginCollect(wasmruntime.RestoreState())
|
||||||
router := wasmruntime.NewRouter()
|
router := wasmruntime.NewRouter()
|
||||||
deps := app.Deps{Path: router.Path, Navigate: router.Navigate}
|
deps := app.Deps{Path: router.Path, Navigate: router.Navigate}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client-side fetch helpers — the counterparts to RespondGob / RespondJSON. In a
|
// Client-side fetch helpers — the counterparts to RespondGob / RespondJSON. In a
|
||||||
@@ -12,24 +11,33 @@ import (
|
|||||||
// the server encodes with RespondGob(data) and the client decodes straight back
|
// the server encodes with RespondGob(data) and the client decodes straight back
|
||||||
// into that type with FetchGob — no JSON, no hand-written unmarshalling. These
|
// into that type with FetchGob — no JSON, no hand-written unmarshalling. These
|
||||||
// are neutral (they only touch encoding/*), so app code can call them while
|
// are neutral (they only touch encoding/*), so app code can call them while
|
||||||
// staying SSR-able; the actual HTTP transport is client-only and injected at
|
// staying SSR-able; the actual HTTP transport is client-only and installed at
|
||||||
// startup by the wasm runtime (SetClientTransport), the inversion vdom.Schedule
|
// startup by the wasm runtime (SetClientTransport), the inversion vdom.Schedule
|
||||||
// uses.
|
// uses.
|
||||||
|
|
||||||
// clientTransport does an HTTP GET returning the raw body; nil on the server.
|
// clientTransport does an HTTP GET returning the raw body. The wasm runtime
|
||||||
|
// installs it in its init, so it is always set in the browser and always nil
|
||||||
|
// everywhere else — "nil transport" therefore means "not the client", which is
|
||||||
|
// what makes the SSR behavior in fetchInto safe.
|
||||||
var clientTransport func(url string) ([]byte, error)
|
var clientTransport func(url string) ([]byte, error)
|
||||||
|
|
||||||
// SetClientTransport installs the HTTP GET used by FetchGob / FetchJSON. The
|
// SetClientTransport installs the HTTP GET used by FetchGob / FetchJSON. The
|
||||||
// wasm client runtime calls this at startup with wasmruntime.FetchBytes.
|
// wasm client runtime installs wasmruntime.FetchBytes automatically; call this
|
||||||
|
// only to wrap it (auth headers, a base URL, a test double).
|
||||||
func SetClientTransport(get func(url string) ([]byte, error)) { clientTransport = get }
|
func SetClientTransport(get func(url string) ([]byte, error)) { clientTransport = get }
|
||||||
|
|
||||||
func fetchInto[T any](url string, cb func(T, error), decode func([]byte, *T) error) {
|
func fetchInto[T any](url string, cb func(T, error), decode func([]byte, *T) error) {
|
||||||
|
// Off the client (SSR of a static page, a native build) there is no transport
|
||||||
|
// and no browser to fetch with, so the request simply never happens: cb is not
|
||||||
|
// called, and the caller stays in the loading state it started in. That is
|
||||||
|
// what SSR should ship — a spinner or skeleton, not a "fetch failed" error the
|
||||||
|
// server was never able to avoid. The client re-runs the fetch for real on its
|
||||||
|
// first render after hydration.
|
||||||
|
if clientTransport == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
var out T
|
var out T
|
||||||
if clientTransport == nil {
|
|
||||||
cb(out, errors.New("httputil: no client transport installed (client-only)"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
body, err := clientTransport(url)
|
body, err := clientTransport(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cb(out, err)
|
cb(out, err)
|
||||||
@@ -51,6 +59,10 @@ func fetchInto[T any](url string, cb func(T, error), decode func([]byte, *T) err
|
|||||||
// httputil.FetchGob("/api/quotes", func(qs []Quote, err error) {
|
// httputil.FetchGob("/api/quotes", func(qs []Quote, err error) {
|
||||||
// if err != nil { /* … */ } else { quotes.Set(qs) }
|
// if err != nil { /* … */ } else { quotes.Set(qs) }
|
||||||
// })
|
// })
|
||||||
|
//
|
||||||
|
// Fetching is client-only: during SSR this does nothing and cb is never called,
|
||||||
|
// so a page that starts out loading renders its spinner into the static HTML and
|
||||||
|
// fetches for real once the client takes over.
|
||||||
func FetchGob[T any](url string, cb func(T, error)) {
|
func FetchGob[T any](url string, cb func(T, error)) {
|
||||||
fetchInto(url, cb, func(b []byte, out *T) error {
|
fetchInto(url, cb, func(b []byte, out *T) error {
|
||||||
return gob.NewDecoder(bytes.NewReader(b)).Decode(out)
|
return gob.NewDecoder(bytes.NewReader(b)).Decode(out)
|
||||||
|
|||||||
58
go/httputil/fetch_test.go
Normal file
58
go/httputil/fetch_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package httputil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type quote struct{ Author, Text string }
|
||||||
|
|
||||||
|
// With no transport installed we are not on the client, so the fetch must not
|
||||||
|
// happen and the callback must not fire — that is what leaves an SSR'd page in
|
||||||
|
// its loading state (spinner/skeleton) rather than rendering a fetch error the
|
||||||
|
// server had no way to avoid.
|
||||||
|
func TestFetchWithoutTransportDoesNotCallBack(t *testing.T) {
|
||||||
|
defer SetClientTransport(nil)
|
||||||
|
SetClientTransport(nil)
|
||||||
|
|
||||||
|
called := make(chan error, 1)
|
||||||
|
FetchGob("/api/quotes", func(_ []quote, err error) { called <- err })
|
||||||
|
FetchJSON("/api/quotes", func(_ []quote, err error) { called <- err })
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-called:
|
||||||
|
t.Fatalf("callback ran with no transport installed (err=%v); SSR would render an error instead of the loading state", err)
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client path is unchanged: with a transport installed the body is fetched
|
||||||
|
// and decoded into the same Go type the server encoded.
|
||||||
|
func TestFetchGobWithTransport(t *testing.T) {
|
||||||
|
defer SetClientTransport(nil)
|
||||||
|
want := []quote{{Author: "Rob Pike", Text: "When in doubt, use brute force."}}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := gob.NewEncoder(&buf).Encode(want); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
SetClientTransport(func(string) ([]byte, error) { return buf.Bytes(), nil })
|
||||||
|
|
||||||
|
got := make(chan []quote, 1)
|
||||||
|
FetchGob("/api/quotes", func(qs []quote, err error) {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("FetchGob: %v", err)
|
||||||
|
}
|
||||||
|
got <- qs
|
||||||
|
})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case qs := <-got:
|
||||||
|
if len(qs) != 1 || qs[0] != want[0] {
|
||||||
|
t.Fatalf("decoded %v, want %v", qs, want)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("FetchGob callback never ran")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
|
|
||||||
|
"kjol/httputil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Installing the transport here (rather than leaving it to each app's main)
|
||||||
|
// keeps httputil's invariant true by construction: FetchGob / FetchJSON have a
|
||||||
|
// transport exactly when they are running in the browser. That is what lets them
|
||||||
|
// no-op during SSR instead of reporting a failure. Apps that need custom headers
|
||||||
|
// or a base URL can wrap FetchBytes with httputil.SetClientTransport at startup.
|
||||||
|
func init() { httputil.SetClientTransport(FetchBytes) }
|
||||||
|
|
||||||
// absURL resolves a relative path against the current origin (a browser resolves
|
// absURL resolves a relative path against the current origin (a browser resolves
|
||||||
// relative fetch URLs itself, but building the absolute URL keeps behavior
|
// relative fetch URLs itself, but building the absolute URL keeps behavior
|
||||||
// uniform — and lets non-browser hosts, e.g. tests, fetch too).
|
// uniform — and lets non-browser hosts, e.g. tests, fetch too).
|
||||||
|
|||||||
@@ -129,10 +129,22 @@ func patch(parent js.Value, o, x *vdom.VNode) {
|
|||||||
updateEvents(o, x)
|
updateEvents(o, x)
|
||||||
if x.HTML != "" {
|
if x.HTML != "" {
|
||||||
if x.HTML != o.HTML {
|
if x.HTML != o.HTML {
|
||||||
|
// innerHTML discards whatever DOM the old children owned, so their
|
||||||
|
// listeners go with it.
|
||||||
|
for _, c := range o.Children {
|
||||||
|
release(c)
|
||||||
|
}
|
||||||
dom.Set("innerHTML", x.HTML)
|
dom.Set("innerHTML", x.HTML)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// o was raw HTML and x isn't. A Raw node keeps its markup only in the DOM
|
||||||
|
// (VNode.HTML, no Children), so patchChildren below would diff x's children
|
||||||
|
// against an empty list and *append* them after markup nothing will ever
|
||||||
|
// remove — e.g. /chart's SVG surviving a route change into /data. Clear it.
|
||||||
|
if o.HTML != "" {
|
||||||
|
dom.Set("innerHTML", "")
|
||||||
|
}
|
||||||
patchChildren(dom, o.Children, x.Children)
|
patchChildren(dom, o.Children, x.Children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
go/wasmruntime/reconcile_test.go
Normal file
86
go/wasmruntime/reconcile_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
package wasmruntime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"kjol/vdom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The reconciler is wasm-only and needs a DOM, so these do not run under a plain
|
||||||
|
// `go test ./...` (the native build of this package is an empty placeholder).
|
||||||
|
// Run them against the minimal DOM in testdata/domexec.js — from kjol/go:
|
||||||
|
//
|
||||||
|
// GOOS=js GOARCH=wasm go test -exec="node testdata/domexec.js" ./wasmruntime
|
||||||
|
|
||||||
|
// A Raw() node carries its markup in VNode.HTML and has NO children — the markup
|
||||||
|
// only exists in the DOM. So when the diff reuses that element for a node that
|
||||||
|
// has children instead (same tag, same index — e.g. a route change from a page
|
||||||
|
// with a server-rendered SVG to one without), the new children must not simply be
|
||||||
|
// appended alongside the surviving markup.
|
||||||
|
func TestPatchClearsRawHTMLWhenElementIsReused(t *testing.T) {
|
||||||
|
root := document.Call("createElement", "div")
|
||||||
|
|
||||||
|
chart := vdom.El("div", vdom.Attr("class", "grid"),
|
||||||
|
vdom.El("div", vdom.Attr("class", "cell"), vdom.Raw(`<svg id="bar"></svg>`)),
|
||||||
|
)
|
||||||
|
patchChildren(root, nil, one(chart))
|
||||||
|
if got := root.Get("innerHTML").String(); !strings.Contains(got, `<svg id="bar">`) {
|
||||||
|
t.Fatalf("raw HTML was not mounted: %s", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same tags at the same indexes, so the diff adopts the DOM rather than
|
||||||
|
// replacing it.
|
||||||
|
data := vdom.El("div", vdom.Attr("class", "cards"),
|
||||||
|
vdom.El("div", vdom.Attr("class", "card"), vdom.Text("gob section")),
|
||||||
|
)
|
||||||
|
patchChildren(root, one(chart), one(data))
|
||||||
|
|
||||||
|
got := root.Get("innerHTML").String()
|
||||||
|
if strings.Contains(got, "svg") {
|
||||||
|
t.Errorf("stale raw HTML survived the patch (the chart would follow you to the next page):\n%s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "gob section") {
|
||||||
|
t.Errorf("new children were not rendered:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The reverse transition: an element with real children reused for a Raw() node.
|
||||||
|
// innerHTML replaces the children wholesale, so nothing may linger.
|
||||||
|
func TestPatchReplacesChildrenWithRawHTML(t *testing.T) {
|
||||||
|
root := document.Call("createElement", "div")
|
||||||
|
|
||||||
|
withKids := vdom.El("div", vdom.El("span", vdom.Text("hello")))
|
||||||
|
patchChildren(root, nil, one(withKids))
|
||||||
|
|
||||||
|
withRaw := vdom.El("div", vdom.Raw(`<svg id="pie"></svg>`))
|
||||||
|
patchChildren(root, one(withKids), one(withRaw))
|
||||||
|
|
||||||
|
got := root.Get("innerHTML").String()
|
||||||
|
if strings.Contains(got, "hello") || strings.Contains(got, "span") {
|
||||||
|
t.Errorf("old children survived under raw HTML:\n%s", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, `<svg id="pie">`) {
|
||||||
|
t.Errorf("raw HTML was not applied:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw markup that does not change must be left alone (it is not re-set on every
|
||||||
|
// render, which would blow away any DOM state inside it).
|
||||||
|
func TestPatchKeepsUnchangedRawHTML(t *testing.T) {
|
||||||
|
root := document.Call("createElement", "div")
|
||||||
|
|
||||||
|
a := vdom.El("div", vdom.Raw(`<svg id="bar"></svg>`))
|
||||||
|
patchChildren(root, nil, one(a))
|
||||||
|
before := rt(a).dom.Get("childNodes").Index(0)
|
||||||
|
|
||||||
|
b := vdom.El("div", vdom.Raw(`<svg id="bar"></svg>`))
|
||||||
|
patchChildren(root, one(a), one(b))
|
||||||
|
|
||||||
|
after := rt(b).dom.Get("childNodes").Index(0)
|
||||||
|
if !before.Equal(after) {
|
||||||
|
t.Error("unchanged raw HTML was re-parsed instead of being left in place")
|
||||||
|
}
|
||||||
|
}
|
||||||
109
go/wasmruntime/testdata/domexec.js
vendored
Normal file
109
go/wasmruntime/testdata/domexec.js
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// A `go test -exec` wrapper for GOOS=js GOARCH=wasm that installs a minimal DOM
|
||||||
|
// before starting the Go runtime, so the reconciler can be driven headlessly
|
||||||
|
// under node instead of only in a browser. From kjol/go:
|
||||||
|
//
|
||||||
|
// GOOS=js GOARCH=wasm go test -exec="node testdata/domexec.js" ./wasmruntime
|
||||||
|
//
|
||||||
|
// The subtlety that matters: in a real browser, setting .innerHTML *parses* the
|
||||||
|
// markup into real child nodes, so a later appendChild lands after it. The shim
|
||||||
|
// models that (one opaque "#raw" child) rather than keeping innerHTML as a
|
||||||
|
// detached string — treating it as a string would hide the very class of bug
|
||||||
|
// these tests exist to catch (stale raw markup surviving a diff).
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
|
||||||
|
class DNode {
|
||||||
|
constructor(tag) {
|
||||||
|
this.tag = tag;
|
||||||
|
this.childNodes = [];
|
||||||
|
this.attrs = {};
|
||||||
|
this.parentNode = null;
|
||||||
|
this.listeners = {};
|
||||||
|
this.nodeValue = null;
|
||||||
|
this.rawHTML = null;
|
||||||
|
}
|
||||||
|
get firstChild() { return this.childNodes[0] ?? null; }
|
||||||
|
appendChild(c) { c.parentNode = this; this.childNodes.push(c); return c; }
|
||||||
|
removeChild(c) {
|
||||||
|
const i = this.childNodes.indexOf(c);
|
||||||
|
if (i < 0) throw new Error("removeChild: node is not a child");
|
||||||
|
this.childNodes.splice(i, 1);
|
||||||
|
c.parentNode = null;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
replaceChild(next, old) {
|
||||||
|
const i = this.childNodes.indexOf(old);
|
||||||
|
if (i < 0) throw new Error("replaceChild: node is not a child");
|
||||||
|
this.childNodes[i] = next;
|
||||||
|
next.parentNode = this;
|
||||||
|
old.parentNode = null;
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
setAttribute(k, v) { this.attrs[k] = String(v); }
|
||||||
|
removeAttribute(k) { delete this.attrs[k]; }
|
||||||
|
addEventListener(name, fn) { (this.listeners[name] ??= []).push(fn); }
|
||||||
|
removeEventListener(name, fn) {
|
||||||
|
const l = this.listeners[name] ?? [];
|
||||||
|
const i = l.indexOf(fn);
|
||||||
|
if (i >= 0) l.splice(i, 1);
|
||||||
|
}
|
||||||
|
set innerHTML(html) {
|
||||||
|
for (const c of this.childNodes) c.parentNode = null;
|
||||||
|
this.childNodes = [];
|
||||||
|
if (html !== "") {
|
||||||
|
const raw = new DNode("#raw");
|
||||||
|
raw.rawHTML = html;
|
||||||
|
raw.parentNode = this;
|
||||||
|
this.childNodes.push(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get innerHTML() { return this.childNodes.map(serialize).join(""); }
|
||||||
|
get outerHTML() { return serialize(this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function serialize(n) {
|
||||||
|
if (n.tag === "#text") return n.nodeValue ?? "";
|
||||||
|
if (n.tag === "#raw") return n.rawHTML ?? "";
|
||||||
|
const attrs = Object.keys(n.attrs).sort().map((k) => ` ${k}="${n.attrs[k]}"`).join("");
|
||||||
|
return `<${n.tag}${attrs}>${n.childNodes.map(serialize).join("")}</${n.tag}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.document = {
|
||||||
|
createElement: (tag) => new DNode(tag),
|
||||||
|
createTextNode: (text) => { const n = new DNode("#text"); n.nodeValue = text; return n; },
|
||||||
|
getElementById: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- go_js_wasm_exec boilerplate ----
|
||||||
|
globalThis.require = require;
|
||||||
|
globalThis.fs = require("fs");
|
||||||
|
globalThis.TextEncoder = require("util").TextEncoder;
|
||||||
|
globalThis.TextDecoder = require("util").TextDecoder;
|
||||||
|
globalThis.performance ??= require("performance");
|
||||||
|
globalThis.crypto ??= require("crypto");
|
||||||
|
|
||||||
|
// wasm_exec.js moved from misc/wasm to lib/wasm in Go 1.24.
|
||||||
|
const goroot = execSync("go env GOROOT").toString().trim();
|
||||||
|
try {
|
||||||
|
require(goroot + "/lib/wasm/wasm_exec.js");
|
||||||
|
} catch {
|
||||||
|
require(goroot + "/misc/wasm/wasm_exec.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
const go = new Go();
|
||||||
|
go.argv = process.argv.slice(2);
|
||||||
|
go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env);
|
||||||
|
go.exit = process.exit;
|
||||||
|
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
|
||||||
|
process.on("exit", (code) => {
|
||||||
|
if (code === 0 && !go.exited) {
|
||||||
|
go._pendingEvent = { id: 0 };
|
||||||
|
go._resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return go.run(result.instance);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user