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