diff --git a/cmd/gogio/jsbuild.go b/cmd/gogio/jsbuild.go index 63bf07b3..72b4fb60 100644 --- a/cmd/gogio/jsbuild.go +++ b/cmd/gogio/jsbuild.go @@ -4,10 +4,13 @@ package main import ( "fmt" + "golang.org/x/tools/go/packages" + "io" "io/ioutil" "os" "os/exec" "path/filepath" + "strings" ) func buildJS(bi *buildInfo) error { @@ -35,34 +38,93 @@ func buildJS(bi *buildInfo) error { if err != nil { return err } - const indexhtml = ` + if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(jsIndex), 0600); err != nil { + return err + } + goroot, err := runCmd(exec.Command("go", "env", "GOROOT")) + if err != nil { + return err + } + wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js") + if _, err := os.Stat(wasmJS); err != nil { + return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err) + } + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps, + Env: append(os.Environ(), "GOOS=js", "GOARCH=wasm"), + }, bi.pkgPath) + if err != nil { + return err + } + extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool)) + if err != nil { + return err + } + + return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...) +} + +func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) { + if len(p.GoFiles) == 0 { + return nil, nil + } + js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js")) + if err != nil { + return nil, err + } + extraJS = append(extraJS, js...) + for _, imp := range p.Imports { + if !visited[imp.ID] { + extra, err := findPackagesJS(imp, visited) + if err != nil { + return nil, err + } + extraJS = append(extraJS, extra...) + visited[imp.ID] = true + } + } + return extraJS, nil +} + +// mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo +// and append the jsStartGo. +func mergeJSFiles(dst string, files ...string) (err error) { + w, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := w.Close(); err != nil { + err = cerr + } + }() + _, err = io.Copy(w, strings.NewReader(jsSetGo)) + if err != nil { + return err + } + for i := range files { + r, err := os.Open(files[i]) + if err != nil { + return err + } + _, err = io.Copy(w, r) + r.Close() + if err != nil { + return err + } + } + _, err = io.Copy(w, strings.NewReader(jsStartGo)) + return err +} + +const ( + jsIndex = `
- - + @@ -70,16 +132,31 @@ func buildJS(bi *buildInfo) error { ` - if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(indexhtml), 0600); err != nil { - return err + // jsSetGo sets the `window.go` variable. + jsSetGo = `(() => { + window.go = {argv: [], env: {}, importObject: {go: {}}}; + window.go["argv"] = (new URLSearchParams(location.search).get("argv") ?? "").split(" "); +})();` + // jsStartGo initializes the main.wasm. + jsStartGo = `(() => { + defaultGo = new Go(); + Object.assign(defaultGo["argv"], go["argv"]); + Object.assign(defaultGo["env"], go["env"]); + for (let key in go["importObject"]) { + if (typeof defaultGo["importObject"][key] === "undefined") { + defaultGo["importObject"][key] = {}; + } + Object.assign(defaultGo["importObject"][key], go["importObject"][key]); } - goroot, err := runCmd(exec.Command("go", "env", "GOROOT")) - if err != nil { - return err - } - wasmjs := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js") - if _, err := os.Stat(wasmjs); err != nil { - return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err) - } - return copyFile(filepath.Join(out, "wasm_exec.js"), wasmjs) -} + window.go = defaultGo; + if (!WebAssembly.instantiateStreaming) { // polyfill + WebAssembly.instantiateStreaming = async (resp, importObject) => { + const source = await (await resp).arrayBuffer(); + return await WebAssembly.instantiate(source, importObject); + }; + } + WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { + go.run(result.instance); + }); +})();` +)