jsbuild.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package main
   4  
   5  import (
   6  	"fmt"
   7  	"io"
   8  	"io/ioutil"
   9  	"os"
  10  	"os/exec"
  11  	"path/filepath"
  12  	"strings"
  13  
  14  	"golang.org/x/tools/go/packages"
  15  )
  16  
  17  func buildJS(bi *buildInfo) error {
  18  	out := *destPath
  19  	if out == "" {
  20  		out = bi.name
  21  	}
  22  	if err := os.MkdirAll(out, 0700); err != nil {
  23  		return err
  24  	}
  25  	cmd := exec.Command(
  26  		"go",
  27  		"build",
  28  		"-ldflags="+bi.ldflags,
  29  		"-tags="+bi.tags,
  30  		"-o", filepath.Join(out, "main.wasm"),
  31  		bi.pkgPath,
  32  	)
  33  	cmd.Env = append(
  34  		os.Environ(),
  35  		"GOOS=js",
  36  		"GOARCH=wasm",
  37  	)
  38  	_, err := runCmd(cmd)
  39  	if err != nil {
  40  		return err
  41  	}
  42  	if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(jsIndex), 0600); err != nil {
  43  		return err
  44  	}
  45  	goroot, err := runCmd(exec.Command("go", "env", "GOROOT"))
  46  	if err != nil {
  47  		return err
  48  	}
  49  	wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
  50  	if _, err := os.Stat(wasmJS); err != nil {
  51  		return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err)
  52  	}
  53  	pkgs, err := packages.Load(&packages.Config{
  54  		Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
  55  		Env:  append(os.Environ(), "GOOS=js", "GOARCH=wasm"),
  56  	}, bi.pkgPath)
  57  	if err != nil {
  58  		return err
  59  	}
  60  	extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool))
  61  	if err != nil {
  62  		return err
  63  	}
  64  
  65  	return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...)
  66  }
  67  
  68  func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) {
  69  	if len(p.GoFiles) == 0 {
  70  		return nil, nil
  71  	}
  72  	js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js"))
  73  	if err != nil {
  74  		return nil, err
  75  	}
  76  	extraJS = append(extraJS, js...)
  77  	for _, imp := range p.Imports {
  78  		if !visited[imp.ID] {
  79  			extra, err := findPackagesJS(imp, visited)
  80  			if err != nil {
  81  				return nil, err
  82  			}
  83  			extraJS = append(extraJS, extra...)
  84  			visited[imp.ID] = true
  85  		}
  86  	}
  87  	return extraJS, nil
  88  }
  89  
  90  // mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo
  91  // and append the jsStartGo.
  92  func mergeJSFiles(dst string, files ...string) (err error) {
  93  	w, err := os.Create(dst)
  94  	if err != nil {
  95  		return err
  96  	}
  97  	defer func() {
  98  		if cerr := w.Close(); err != nil {
  99  			err = cerr
 100  		}
 101  	}()
 102  	_, err = io.Copy(w, strings.NewReader(jsSetGo))
 103  	if err != nil {
 104  		return err
 105  	}
 106  	for i := range files {
 107  		r, err := os.Open(files[i])
 108  		if err != nil {
 109  			return err
 110  		}
 111  		_, err = io.Copy(w, r)
 112  		r.Close()
 113  		if err != nil {
 114  			return err
 115  		}
 116  	}
 117  	_, err = io.Copy(w, strings.NewReader(jsStartGo))
 118  	return err
 119  }
 120  
 121  const (
 122  	jsIndex = `<!doctype html>
 123  <html>
 124  	<head>
 125  		<meta charset="utf-8">
 126  		<meta name="viewport" content="width=device-width, user-scalable=no">
 127  		<meta name="mobile-web-app-capable" content="yes">
 128  		<script src="wasm.js"></script>
 129  		<style>
 130  			body,pre { margin:0;padding:0; }
 131  		</style>
 132  	</head>
 133  	<body>
 134  	</body>
 135  </html>`
 136  	// jsSetGo sets the `window.go` variable.
 137  	jsSetGo = `(() => {
 138      window.go = {argv: [], env: {}, importObject: {go: {}}};
 139  	const argv = new URLSearchParams(location.search).get("argv");
 140  	if (argv) {
 141  		window.go["argv"] = argv.split(" ");
 142  	}
 143  })();`
 144  	// jsStartGo initializes the main.wasm.
 145  	jsStartGo = `(() => {
 146  	defaultGo = new Go();
 147  	Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"]));
 148  	Object.assign(defaultGo["env"], go["env"]);
 149  	for (let key in go["importObject"]) {
 150  		if (typeof defaultGo["importObject"][key] === "undefined") {
 151  			defaultGo["importObject"][key] = {};
 152  		}
 153  		Object.assign(defaultGo["importObject"][key], go["importObject"][key]);
 154  	}
 155  	window.go = defaultGo;
 156      if (!WebAssembly.instantiateStreaming) { // polyfill
 157          WebAssembly.instantiateStreaming = async (resp, importObject) => {
 158              const source = await (await resp).arrayBuffer();
 159              return await WebAssembly.instantiate(source, importObject);
 160          };
 161      }
 162      WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
 163          go.run(result.instance);
 164      });
 165  })();`
 166  )
 167