diff --git a/Makefile b/Makefile
index 590514a..c493c25 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,5 @@
+GO_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go' -not -name '*_test.go')
+
.PHONY: gen
gen:
@echo "Generating code..."
@@ -5,4 +7,11 @@ gen:
.PHONY: clean-testdata
clean-testdata:
- git clean -xfd testdata
\ No newline at end of file
+ git clean -xfd testdata
+
+.PHONY: build-wasm
+build-wasm: site/public/build/preview.wasm
+ mkdir -p ./site/public/build/
+
+site/public/build/preview.wasm: $(GO_SRC_FILES)
+ GOOS=js GOARCH=wasm go build -o site/public/build/preview.wasm ./cmd/wasm
\ No newline at end of file
diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go
new file mode 100644
index 0000000..3c29b10
--- /dev/null
+++ b/cmd/wasm/main.go
@@ -0,0 +1,87 @@
+//go:build js && wasm
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "path/filepath"
+ "syscall/js"
+
+ "github.com/spf13/afero"
+
+ "github.com/coder/preview"
+ "github.com/coder/preview/types"
+)
+
+func main() {
+ // Create a channel to keep the Go program alive
+ done := make(chan struct{}, 0)
+
+ // Expose the Go function `fibonacciSum` to JavaScript
+ js.Global().Set("go_preview", js.FuncOf(tfpreview))
+ js.Global()
+
+ // Block the program from exiting
+ <-done
+}
+
+func tfpreview(this js.Value, p []js.Value) any {
+ tf, err := fileTreeFS(p[0])
+ if err != nil {
+ return err
+ }
+
+ output, diags := preview.Preview(context.Background(), preview.Input{
+ PlanJSONPath: "",
+ PlanJSON: nil,
+ ParameterValues: nil,
+ Owner: types.WorkspaceOwner{},
+ }, tf)
+
+ data, _ := json.Marshal(map[string]any{
+ "output": output,
+ "diags": diags,
+ })
+ return js.ValueOf(string(data))
+}
+
+func fileTreeFS(value js.Value) (fs.FS, error) {
+ data := js.Global().Get("JSON").Call("stringify", value).String()
+ var filetree map[string]any
+ if err := json.Unmarshal([]byte(data), &filetree); err != nil {
+ return nil, err
+ }
+
+ mem := afero.NewMemMapFs()
+ loadTree(mem, filetree)
+
+ return afero.NewIOFS(mem), nil
+}
+
+func loadTree(mem afero.Fs, fileTree map[string]any, path ...string) {
+ dir := filepath.Join(path...)
+ err := mem.MkdirAll(dir, 0755)
+ if err != nil {
+ fmt.Printf("error creating directory %q: %v\n", dir, err)
+ }
+ for k, v := range fileTree {
+ switch vv := v.(type) {
+ case string:
+ fn := filepath.Join(dir, k)
+ f, err := mem.Create(fn)
+ if err != nil {
+ fmt.Printf("error creating file %q: %v\n", fn, err)
+ continue
+ }
+ _, _ = f.WriteString(vv)
+ f.Close()
+ case map[string]any:
+ loadTree(mem, vv, append(path, k)...)
+ default:
+ fmt.Printf("unknown type %T for %q\n", v, k)
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index eefb086..a4fd0d6 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
github.com/hashicorp/terraform-exec v0.22.0
github.com/hashicorp/terraform-json v0.24.0
github.com/jedib0t/go-pretty/v6 v6.6.7
+ github.com/spf13/afero v1.12.0
github.com/stretchr/testify v1.10.0
github.com/zclconf/go-cty v1.16.2
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
diff --git a/go.sum b/go.sum
index 7433fcd..881f530 100644
--- a/go.sum
+++ b/go.sum
@@ -1151,6 +1151,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
+github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
+github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/site/.gitignore b/site/.gitignore
index a547bf3..fbc1776 100644
--- a/site/.gitignore
+++ b/site/.gitignore
@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# wasm
+public/build/preview.wasm
diff --git a/site/index.html b/site/index.html
index 37efa3e..e97a0ab 100644
--- a/site/index.html
+++ b/site/index.html
@@ -6,7 +6,7 @@
Conditional Parameters
-
+
diff --git a/site/package.json b/site/package.json
index b9165bd..6c7f504 100644
--- a/site/package.json
+++ b/site/package.json
@@ -27,6 +27,7 @@
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-json-view": "^1.21.3",
+ "react-router-dom": "^7.5.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.9",
"tailwindcss-animate": "^1.0.7"
diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml
index 0d13409..a3b6e33 100644
--- a/site/pnpm-lock.yaml
+++ b/site/pnpm-lock.yaml
@@ -59,6 +59,9 @@ importers:
react-json-view:
specifier: ^1.21.3
version: 1.21.3(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react-router-dom:
+ specifier: ^7.5.0
+ version: 7.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
tailwind-merge:
specifier: ^3.0.2
version: 3.0.2
@@ -1138,6 +1141,9 @@ packages:
'@types/babel__traverse@7.20.6':
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
+ '@types/cookie@0.6.0':
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
@@ -1293,6 +1299,10 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie@1.0.2:
+ resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
+ engines: {node: '>=18'}
+
cross-fetch@3.2.0:
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
@@ -1792,6 +1802,23 @@ packages:
'@types/react':
optional: true
+ react-router-dom@7.5.0:
+ resolution: {integrity: sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+
+ react-router@7.5.0:
+ resolution: {integrity: sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==}
+ engines: {node: '>=20.0.0'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -1843,6 +1870,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ set-cookie-parser@2.7.1:
+ resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
+
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
@@ -1897,6 +1927,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+ turbo-stream@2.4.0:
+ resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -2961,6 +2994,8 @@ snapshots:
dependencies:
'@babel/types': 7.26.9
+ '@types/cookie@0.6.0': {}
+
'@types/estree@1.0.6': {}
'@types/json-schema@7.0.15': {}
@@ -3149,6 +3184,8 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie@1.0.2: {}
+
cross-fetch@3.2.0:
dependencies:
node-fetch: 2.7.0
@@ -3632,6 +3669,22 @@ snapshots:
optionalDependencies:
'@types/react': 19.0.10
+ react-router-dom@7.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ react-router: 7.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+
+ react-router@7.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@types/cookie': 0.6.0
+ cookie: 1.0.2
+ react: 19.0.0
+ set-cookie-parser: 2.7.1
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ react-dom: 19.0.0(react@19.0.0)
+
react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0):
dependencies:
get-nonce: 1.0.1
@@ -3692,6 +3745,8 @@ snapshots:
semver@7.7.1: {}
+ set-cookie-parser@2.7.1: {}
+
setimmediate@1.0.5: {}
shebang-command@2.0.0:
@@ -3730,6 +3785,8 @@ snapshots:
tslib@2.8.1: {}
+ turbo-stream@2.4.0: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
diff --git a/site/public/build/placeholder b/site/public/build/placeholder
new file mode 100644
index 0000000..e69de29
diff --git a/site/src/App.tsx b/site/src/App.tsx
index 6e7bfc2..80e3a35 100644
--- a/site/src/App.tsx
+++ b/site/src/App.tsx
@@ -1,11 +1,19 @@
import './App.css'
import { DemoPage } from './DemoPage'
+import { Live } from './components/Live'
import { ThemeProvider } from "./components/theme-provider"
+import { BrowserRouter, Routes, Route } from 'react-router-dom'
+import { PreviewProvider } from './contexts/PreviewContext/PreviewContext'
function App() {
return (
-
+
+
+ } />
+ } />
+
+
);
}
diff --git a/site/src/components/Live.tsx b/site/src/components/Live.tsx
new file mode 100644
index 0000000..36e2f45
--- /dev/null
+++ b/site/src/components/Live.tsx
@@ -0,0 +1,30 @@
+import { usePreview } from '../contexts/PreviewContext/PreviewContext';
+import { useState } from 'react';
+
+export function Live() {
+ const { isWasmLoaded, preview } = usePreview();
+ const [previewResult, setPreviewResult] = useState(null);
+
+ const handlePreviewClick = async () => {
+ try {
+ const result = await preview();
+ setPreviewResult(result);
+ } catch (error) {
+ console.error('Error calling preview:', error);
+ }
+ };
+
+ return (
+
+ {isWasmLoaded &&
Wasm Loaded
}
+ {!isWasmLoaded &&
Wasm not Loaded
}
+
+
+ {previewResult !== null && (
+
+
Preview Result: {previewResult}
+
+ )}
+
+ );
+}
diff --git a/site/src/contexts/PreviewContext/PreviewContext.tsx b/site/src/contexts/PreviewContext/PreviewContext.tsx
new file mode 100644
index 0000000..ae890ce
--- /dev/null
+++ b/site/src/contexts/PreviewContext/PreviewContext.tsx
@@ -0,0 +1,58 @@
+import { createContext, FC, PropsWithChildren, useContext, useEffect, useState } from "react";
+import "./wasm_exec.js";
+
+export interface PreviewContextValue {
+ isWasmLoaded: boolean;
+ preview?: GoPreviewDef;
+}
+
+export const PreviewContext = createContext(
+ undefined,
+);
+
+export const PreviewProvider: FC = ({ children }) => {
+ const [isWasmLoaded, setIsWasmLoaded] = useState(false);
+
+ // useEffect hook to load WebAssembly when the component mounts
+ useEffect(() => {
+ // Function to asynchronously load WebAssembly
+ async function loadWasm(): Promise {
+ // Create a new Go object
+ const goWasm = new window.Go();
+ const result = await WebAssembly.instantiateStreaming(
+ // Fetch and instantiate the main.wasm file
+ fetch('build/preview.wasm'),
+ // Provide the import object to Go for communication with JavaScript
+ goWasm.importObject
+ );
+ // Run the Go program with the WebAssembly instance
+ goWasm.run(result.instance);
+ setIsWasmLoaded(true);
+ }
+
+ loadWasm();
+ }, []);
+
+
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const usePreview = (): PreviewContextValue => {
+ const context = useContext(PreviewContext);
+
+ if (!context) {
+ throw new Error("usePreview should be used inside of ");
+ }
+
+ return context;
+};
diff --git a/site/src/contexts/PreviewContext/preview.d.ts b/site/src/contexts/PreviewContext/preview.d.ts
new file mode 100644
index 0000000..94df13d
--- /dev/null
+++ b/site/src/contexts/PreviewContext/preview.d.ts
@@ -0,0 +1,17 @@
+type GoPreviewDef = () => Promise;
+
+interface Window {
+ // Loaded from wasm
+ go_preview?: GoPreviewDef;
+ Go: { new(): Go };
+}
+
+declare class Go {
+ argv: string[];
+ env: { [envKey: string]: string };
+ exit: (code: number) => void;
+ importObject: WebAssembly.Imports;
+ exited: boolean;
+ mem: DataView;
+ run(instance: WebAssembly.Instance): Promise;
+}
\ No newline at end of file
diff --git a/site/src/contexts/PreviewContext/wasm_exec.js b/site/src/contexts/PreviewContext/wasm_exec.js
new file mode 100644
index 0000000..d71af9e
--- /dev/null
+++ b/site/src/contexts/PreviewContext/wasm_exec.js
@@ -0,0 +1,575 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+"use strict";
+
+(() => {
+ const enosys = () => {
+ const err = new Error("not implemented");
+ err.code = "ENOSYS";
+ return err;
+ };
+
+ if (!globalThis.fs) {
+ let outputBuf = "";
+ globalThis.fs = {
+ constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
+ writeSync(fd, buf) {
+ outputBuf += decoder.decode(buf);
+ const nl = outputBuf.lastIndexOf("\n");
+ if (nl != -1) {
+ console.log(outputBuf.substring(0, nl));
+ outputBuf = outputBuf.substring(nl + 1);
+ }
+ return buf.length;
+ },
+ write(fd, buf, offset, length, position, callback) {
+ if (offset !== 0 || length !== buf.length || position !== null) {
+ callback(enosys());
+ return;
+ }
+ const n = this.writeSync(fd, buf);
+ callback(null, n);
+ },
+ chmod(path, mode, callback) { callback(enosys()); },
+ chown(path, uid, gid, callback) { callback(enosys()); },
+ close(fd, callback) { callback(enosys()); },
+ fchmod(fd, mode, callback) { callback(enosys()); },
+ fchown(fd, uid, gid, callback) { callback(enosys()); },
+ fstat(fd, callback) { callback(enosys()); },
+ fsync(fd, callback) { callback(null); },
+ ftruncate(fd, length, callback) { callback(enosys()); },
+ lchown(path, uid, gid, callback) { callback(enosys()); },
+ link(path, link, callback) { callback(enosys()); },
+ lstat(path, callback) { callback(enosys()); },
+ mkdir(path, perm, callback) { callback(enosys()); },
+ open(path, flags, mode, callback) { callback(enosys()); },
+ read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
+ readdir(path, callback) { callback(enosys()); },
+ readlink(path, callback) { callback(enosys()); },
+ rename(from, to, callback) { callback(enosys()); },
+ rmdir(path, callback) { callback(enosys()); },
+ stat(path, callback) { callback(enosys()); },
+ symlink(path, link, callback) { callback(enosys()); },
+ truncate(path, length, callback) { callback(enosys()); },
+ unlink(path, callback) { callback(enosys()); },
+ utimes(path, atime, mtime, callback) { callback(enosys()); },
+ };
+ }
+
+ if (!globalThis.process) {
+ globalThis.process = {
+ getuid() { return -1; },
+ getgid() { return -1; },
+ geteuid() { return -1; },
+ getegid() { return -1; },
+ getgroups() { throw enosys(); },
+ pid: -1,
+ ppid: -1,
+ umask() { throw enosys(); },
+ cwd() { throw enosys(); },
+ chdir() { throw enosys(); },
+ }
+ }
+
+ if (!globalThis.path) {
+ globalThis.path = {
+ resolve(...pathSegments) {
+ return pathSegments.join("/");
+ }
+ }
+ }
+
+ if (!globalThis.crypto) {
+ throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
+ }
+
+ if (!globalThis.performance) {
+ throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
+ }
+
+ if (!globalThis.TextEncoder) {
+ throw new Error("globalThis.TextEncoder is not available, polyfill required");
+ }
+
+ if (!globalThis.TextDecoder) {
+ throw new Error("globalThis.TextDecoder is not available, polyfill required");
+ }
+
+ const encoder = new TextEncoder("utf-8");
+ const decoder = new TextDecoder("utf-8");
+
+ globalThis.Go = class {
+ constructor() {
+ this.argv = ["js"];
+ this.env = {};
+ this.exit = (code) => {
+ if (code !== 0) {
+ console.warn("exit code:", code);
+ }
+ };
+ this._exitPromise = new Promise((resolve) => {
+ this._resolveExitPromise = resolve;
+ });
+ this._pendingEvent = null;
+ this._scheduledTimeouts = new Map();
+ this._nextCallbackTimeoutID = 1;
+
+ const setInt64 = (addr, v) => {
+ this.mem.setUint32(addr + 0, v, true);
+ this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
+ }
+
+ const setInt32 = (addr, v) => {
+ this.mem.setUint32(addr + 0, v, true);
+ }
+
+ const getInt64 = (addr) => {
+ const low = this.mem.getUint32(addr + 0, true);
+ const high = this.mem.getInt32(addr + 4, true);
+ return low + high * 4294967296;
+ }
+
+ const loadValue = (addr) => {
+ const f = this.mem.getFloat64(addr, true);
+ if (f === 0) {
+ return undefined;
+ }
+ if (!isNaN(f)) {
+ return f;
+ }
+
+ const id = this.mem.getUint32(addr, true);
+ return this._values[id];
+ }
+
+ const storeValue = (addr, v) => {
+ const nanHead = 0x7FF80000;
+
+ if (typeof v === "number" && v !== 0) {
+ if (isNaN(v)) {
+ this.mem.setUint32(addr + 4, nanHead, true);
+ this.mem.setUint32(addr, 0, true);
+ return;
+ }
+ this.mem.setFloat64(addr, v, true);
+ return;
+ }
+
+ if (v === undefined) {
+ this.mem.setFloat64(addr, 0, true);
+ return;
+ }
+
+ let id = this._ids.get(v);
+ if (id === undefined) {
+ id = this._idPool.pop();
+ if (id === undefined) {
+ id = this._values.length;
+ }
+ this._values[id] = v;
+ this._goRefCounts[id] = 0;
+ this._ids.set(v, id);
+ }
+ this._goRefCounts[id]++;
+ let typeFlag = 0;
+ switch (typeof v) {
+ case "object":
+ if (v !== null) {
+ typeFlag = 1;
+ }
+ break;
+ case "string":
+ typeFlag = 2;
+ break;
+ case "symbol":
+ typeFlag = 3;
+ break;
+ case "function":
+ typeFlag = 4;
+ break;
+ }
+ this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
+ this.mem.setUint32(addr, id, true);
+ }
+
+ const loadSlice = (addr) => {
+ const array = getInt64(addr + 0);
+ const len = getInt64(addr + 8);
+ return new Uint8Array(this._inst.exports.mem.buffer, array, len);
+ }
+
+ const loadSliceOfValues = (addr) => {
+ const array = getInt64(addr + 0);
+ const len = getInt64(addr + 8);
+ const a = new Array(len);
+ for (let i = 0; i < len; i++) {
+ a[i] = loadValue(array + i * 8);
+ }
+ return a;
+ }
+
+ const loadString = (addr) => {
+ const saddr = getInt64(addr + 0);
+ const len = getInt64(addr + 8);
+ return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
+ }
+
+ const testCallExport = (a, b) => {
+ this._inst.exports.testExport0();
+ return this._inst.exports.testExport(a, b);
+ }
+
+ const timeOrigin = Date.now() - performance.now();
+ this.importObject = {
+ _gotest: {
+ add: (a, b) => a + b,
+ callExport: testCallExport,
+ },
+ gojs: {
+ // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
+ // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
+ // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
+ // This changes the SP, thus we have to update the SP used by the imported function.
+
+ // func wasmExit(code int32)
+ "runtime.wasmExit": (sp) => {
+ sp >>>= 0;
+ const code = this.mem.getInt32(sp + 8, true);
+ this.exited = true;
+ delete this._inst;
+ delete this._values;
+ delete this._goRefCounts;
+ delete this._ids;
+ delete this._idPool;
+ this.exit(code);
+ },
+
+ // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
+ "runtime.wasmWrite": (sp) => {
+ sp >>>= 0;
+ const fd = getInt64(sp + 8);
+ const p = getInt64(sp + 16);
+ const n = this.mem.getInt32(sp + 24, true);
+ fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
+ },
+
+ // func resetMemoryDataView()
+ "runtime.resetMemoryDataView": (sp) => {
+ sp >>>= 0;
+ this.mem = new DataView(this._inst.exports.mem.buffer);
+ },
+
+ // func nanotime1() int64
+ "runtime.nanotime1": (sp) => {
+ sp >>>= 0;
+ setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
+ },
+
+ // func walltime() (sec int64, nsec int32)
+ "runtime.walltime": (sp) => {
+ sp >>>= 0;
+ const msec = (new Date).getTime();
+ setInt64(sp + 8, msec / 1000);
+ this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
+ },
+
+ // func scheduleTimeoutEvent(delay int64) int32
+ "runtime.scheduleTimeoutEvent": (sp) => {
+ sp >>>= 0;
+ const id = this._nextCallbackTimeoutID;
+ this._nextCallbackTimeoutID++;
+ this._scheduledTimeouts.set(id, setTimeout(
+ () => {
+ this._resume();
+ while (this._scheduledTimeouts.has(id)) {
+ // for some reason Go failed to register the timeout event, log and try again
+ // (temporary workaround for https://github.com/golang/go/issues/28975)
+ console.warn("scheduleTimeoutEvent: missed timeout event");
+ this._resume();
+ }
+ },
+ getInt64(sp + 8),
+ ));
+ this.mem.setInt32(sp + 16, id, true);
+ },
+
+ // func clearTimeoutEvent(id int32)
+ "runtime.clearTimeoutEvent": (sp) => {
+ sp >>>= 0;
+ const id = this.mem.getInt32(sp + 8, true);
+ clearTimeout(this._scheduledTimeouts.get(id));
+ this._scheduledTimeouts.delete(id);
+ },
+
+ // func getRandomData(r []byte)
+ "runtime.getRandomData": (sp) => {
+ sp >>>= 0;
+ crypto.getRandomValues(loadSlice(sp + 8));
+ },
+
+ // func finalizeRef(v ref)
+ "syscall/js.finalizeRef": (sp) => {
+ sp >>>= 0;
+ const id = this.mem.getUint32(sp + 8, true);
+ this._goRefCounts[id]--;
+ if (this._goRefCounts[id] === 0) {
+ const v = this._values[id];
+ this._values[id] = null;
+ this._ids.delete(v);
+ this._idPool.push(id);
+ }
+ },
+
+ // func stringVal(value string) ref
+ "syscall/js.stringVal": (sp) => {
+ sp >>>= 0;
+ storeValue(sp + 24, loadString(sp + 8));
+ },
+
+ // func valueGet(v ref, p string) ref
+ "syscall/js.valueGet": (sp) => {
+ sp >>>= 0;
+ const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 32, result);
+ },
+
+ // func valueSet(v ref, p string, x ref)
+ "syscall/js.valueSet": (sp) => {
+ sp >>>= 0;
+ Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
+ },
+
+ // func valueDelete(v ref, p string)
+ "syscall/js.valueDelete": (sp) => {
+ sp >>>= 0;
+ Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
+ },
+
+ // func valueIndex(v ref, i int) ref
+ "syscall/js.valueIndex": (sp) => {
+ sp >>>= 0;
+ storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
+ },
+
+ // valueSetIndex(v ref, i int, x ref)
+ "syscall/js.valueSetIndex": (sp) => {
+ sp >>>= 0;
+ Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
+ },
+
+ // func valueCall(v ref, m string, args []ref) (ref, bool)
+ "syscall/js.valueCall": (sp) => {
+ sp >>>= 0;
+ try {
+ const v = loadValue(sp + 8);
+ const m = Reflect.get(v, loadString(sp + 16));
+ const args = loadSliceOfValues(sp + 32);
+ const result = Reflect.apply(m, v, args);
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 56, result);
+ this.mem.setUint8(sp + 64, 1);
+ } catch (err) {
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 56, err);
+ this.mem.setUint8(sp + 64, 0);
+ }
+ },
+
+ // func valueInvoke(v ref, args []ref) (ref, bool)
+ "syscall/js.valueInvoke": (sp) => {
+ sp >>>= 0;
+ try {
+ const v = loadValue(sp + 8);
+ const args = loadSliceOfValues(sp + 16);
+ const result = Reflect.apply(v, undefined, args);
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, result);
+ this.mem.setUint8(sp + 48, 1);
+ } catch (err) {
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, err);
+ this.mem.setUint8(sp + 48, 0);
+ }
+ },
+
+ // func valueNew(v ref, args []ref) (ref, bool)
+ "syscall/js.valueNew": (sp) => {
+ sp >>>= 0;
+ try {
+ const v = loadValue(sp + 8);
+ const args = loadSliceOfValues(sp + 16);
+ const result = Reflect.construct(v, args);
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, result);
+ this.mem.setUint8(sp + 48, 1);
+ } catch (err) {
+ sp = this._inst.exports.getsp() >>> 0; // see comment above
+ storeValue(sp + 40, err);
+ this.mem.setUint8(sp + 48, 0);
+ }
+ },
+
+ // func valueLength(v ref) int
+ "syscall/js.valueLength": (sp) => {
+ sp >>>= 0;
+ setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
+ },
+
+ // valuePrepareString(v ref) (ref, int)
+ "syscall/js.valuePrepareString": (sp) => {
+ sp >>>= 0;
+ const str = encoder.encode(String(loadValue(sp + 8)));
+ storeValue(sp + 16, str);
+ setInt64(sp + 24, str.length);
+ },
+
+ // valueLoadString(v ref, b []byte)
+ "syscall/js.valueLoadString": (sp) => {
+ sp >>>= 0;
+ const str = loadValue(sp + 8);
+ loadSlice(sp + 16).set(str);
+ },
+
+ // func valueInstanceOf(v ref, t ref) bool
+ "syscall/js.valueInstanceOf": (sp) => {
+ sp >>>= 0;
+ this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
+ },
+
+ // func copyBytesToGo(dst []byte, src ref) (int, bool)
+ "syscall/js.copyBytesToGo": (sp) => {
+ sp >>>= 0;
+ const dst = loadSlice(sp + 8);
+ const src = loadValue(sp + 32);
+ if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
+ this.mem.setUint8(sp + 48, 0);
+ return;
+ }
+ const toCopy = src.subarray(0, dst.length);
+ dst.set(toCopy);
+ setInt64(sp + 40, toCopy.length);
+ this.mem.setUint8(sp + 48, 1);
+ },
+
+ // func copyBytesToJS(dst ref, src []byte) (int, bool)
+ "syscall/js.copyBytesToJS": (sp) => {
+ sp >>>= 0;
+ const dst = loadValue(sp + 8);
+ const src = loadSlice(sp + 16);
+ if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
+ this.mem.setUint8(sp + 48, 0);
+ return;
+ }
+ const toCopy = src.subarray(0, dst.length);
+ dst.set(toCopy);
+ setInt64(sp + 40, toCopy.length);
+ this.mem.setUint8(sp + 48, 1);
+ },
+
+ "debug": (value) => {
+ console.log(value);
+ },
+ }
+ };
+ }
+
+ async run(instance) {
+ if (!(instance instanceof WebAssembly.Instance)) {
+ throw new Error("Go.run: WebAssembly.Instance expected");
+ }
+ this._inst = instance;
+ this.mem = new DataView(this._inst.exports.mem.buffer);
+ this._values = [ // JS values that Go currently has references to, indexed by reference id
+ NaN,
+ 0,
+ null,
+ true,
+ false,
+ globalThis,
+ this,
+ ];
+ this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
+ this._ids = new Map([ // mapping from JS values to reference ids
+ [0, 1],
+ [null, 2],
+ [true, 3],
+ [false, 4],
+ [globalThis, 5],
+ [this, 6],
+ ]);
+ this._idPool = []; // unused ids that have been garbage collected
+ this.exited = false; // whether the Go program has exited
+
+ // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
+ let offset = 4096;
+
+ const strPtr = (str) => {
+ const ptr = offset;
+ const bytes = encoder.encode(str + "\0");
+ new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
+ offset += bytes.length;
+ if (offset % 8 !== 0) {
+ offset += 8 - (offset % 8);
+ }
+ return ptr;
+ };
+
+ const argc = this.argv.length;
+
+ const argvPtrs = [];
+ this.argv.forEach((arg) => {
+ argvPtrs.push(strPtr(arg));
+ });
+ argvPtrs.push(0);
+
+ const keys = Object.keys(this.env).sort();
+ keys.forEach((key) => {
+ argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
+ });
+ argvPtrs.push(0);
+
+ const argv = offset;
+ argvPtrs.forEach((ptr) => {
+ this.mem.setUint32(offset, ptr, true);
+ this.mem.setUint32(offset + 4, 0, true);
+ offset += 8;
+ });
+
+ // The linker guarantees global data starts from at least wasmMinDataAddr.
+ // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
+ const wasmMinDataAddr = 4096 + 8192;
+ if (offset >= wasmMinDataAddr) {
+ throw new Error("total length of command line and environment variables exceeds limit");
+ }
+
+ this._inst.exports.run(argc, argv);
+ if (this.exited) {
+ this._resolveExitPromise();
+ }
+ await this._exitPromise;
+ }
+
+ _resume() {
+ if (this.exited) {
+ throw new Error("Go program has already exited");
+ }
+ this._inst.exports.resume();
+ if (this.exited) {
+ this._resolveExitPromise();
+ }
+ }
+
+ _makeFuncWrapper(id) {
+ const go = this;
+ return function () {
+ const event = { id: id, this: this, args: arguments };
+ go._pendingEvent = event;
+ go._resume();
+ return event.result;
+ };
+ }
+ }
+})();