From 2f2f6d664f06d064c4b3525ea34a789c1ac95cda Mon Sep 17 00:00:00 2001 From: Dave Cunningham Date: Fri, 3 Sep 2021 23:47:38 +0100 Subject: [PATCH] Add wasm build --- .gitignore | 2 + README.md | 12 ++++ cmd/wasm/BUILD.bazel | 22 +++++++ cmd/wasm/main.go | 143 +++++++++++++++++++++++++++++++++++++++++++ tests.sh | 1 + 5 files changed, 180 insertions(+) create mode 100644 cmd/wasm/BUILD.bazel create mode 100644 cmd/wasm/main.go diff --git a/.gitignore b/.gitignore index 7701c01..1cbc83e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ gojsonnet.egg-info/ /jsonnet-lint /jsonnet-deps /builtin-benchmark-results + +libjsonnet.wasm diff --git a/README.md b/README.md index 9e925e9..a4beb68 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,18 @@ For additional target platform names, see the per-Go release definitions [here]( Additionally if any files were moved around, see the section [Keeping the Bazel files up to date](#keeping-the-bazel-files-up-to-date). +## Building libjsonnet.wasm + +```bash +GOOS=js GOARCH=wasm go build -o libjsonnet.wasm ./cmd/wasm +``` + +Or if using bazel: + +``` +bazel build //cmd/wasm:libjsonnet.wasm +``` + ## Running tests ```bash diff --git a/cmd/wasm/BUILD.bazel b/cmd/wasm/BUILD.bazel new file mode 100644 index 0000000..9b56352 --- /dev/null +++ b/cmd/wasm/BUILD.bazel @@ -0,0 +1,22 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "main.go", + ], + importpath = "github.com/google/go-jsonnet/cmd/wasm", + visibility = ["//visibility:private"], + deps = [ + "//:go_default_library", + "//internal/formatter:go_default_library", + ], +) + +go_binary( + name = "libjsonnet.wasm", + embed = [":go_default_library"], + goarch = "wasm", + goos = "js", + visibility = ["//visibility:public"], +) diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go new file mode 100644 index 0000000..e70f01f --- /dev/null +++ b/cmd/wasm/main.go @@ -0,0 +1,143 @@ +//go:build js && wasm +// +build js,wasm + +package main + +import ( + "fmt" + "path/filepath" + "syscall/js" + + "github.com/google/go-jsonnet" + "github.com/google/go-jsonnet/internal/formatter" +) + +// JavascriptImporter allows importing files from a pre-defined map of absolute +// paths. +type JavascriptImporter struct { + files map[string]string +} + +// Import looks up files in JavascriptImporter +func (importer *JavascriptImporter) Import(importedFrom, importedPath string) (jsonnet.Contents, string, error) { + fileRootPath := filepath.Dir(importedFrom) + fullFilePath := filepath.Clean(fmt.Sprintf("%s/%s", fileRootPath, importedPath)) + + fileContent, exists := importer.files[fullFilePath] + if exists { + return jsonnet.MakeContents(fileContent), importedPath, nil + } else { + return jsonnet.Contents{}, "", fmt.Errorf("File not found %v", fullFilePath) + } +} + +func processObjectParam(name string, value js.Value) (map[string]string, error) { + if value.Type() != js.TypeObject { + return nil, fmt.Errorf("'%s' was not an object: %v", name, value) + } + jsKeysArray := js.Global().Get("Object").Get("keys").Invoke(value) + result := make(map[string]string) + for i := 0; i < jsKeysArray.Length(); i++ { + filename := jsKeysArray.Index(i).String() + keyValue := value.Get(filename) + if keyValue.Type() != js.TypeString { + return nil, fmt.Errorf("'%s' key '%s' was not bound to a string: %v", name, filename, keyValue) + } + result[filename] = keyValue.String() + } + return result, nil +} + +func jsonnetEvaluateSnippet(this js.Value, p []js.Value) (interface{}, error) { + if len(p) != 7 { + return "", fmt.Errorf("wrong number of parameters: %d", len(p)) + } + if p[0].Type() != js.TypeString { + return "", fmt.Errorf("filename was not a string: %v", p[0]) + } + if p[1].Type() != js.TypeString { + return "", fmt.Errorf("code was not a string: %v", p[0]) + } + filename := p[0].String() + code := p[1].String() + files, err := processObjectParam("files", p[2].JSValue()) + if err != nil { + return "", err + } + extStrs, err := processObjectParam("extStrs", p[3].JSValue()) + if err != nil { + return "", err + } + extCodes, err := processObjectParam("extCodes", p[4].JSValue()) + if err != nil { + return "", err + } + tlaStrs, err := processObjectParam("tlaStrs", p[5].JSValue()) + if err != nil { + return "", err + } + tlaCodes, err := processObjectParam("tlaCodes", p[6].JSValue()) + if err != nil { + return "", err + } + + vm := jsonnet.MakeVM() + vm.Importer(&JavascriptImporter{files: files}) + for key, val := range extStrs { + vm.ExtVar(key, val) + } + for key, val := range extCodes { + vm.ExtCode(key, val) + } + for key, val := range tlaStrs { + vm.TLAVar(key, val) + } + for key, val := range tlaCodes { + vm.TLACode(key, val) + } + + return vm.EvaluateAnonymousSnippet(filename, code) +} + +func jsonnetFmtSnippet(this js.Value, p []js.Value) (interface{}, error) { + if len(p) != 2 { + return "", fmt.Errorf("wrong number of parameters: %d", len(p)) + } + if p[0].Type() != js.TypeString { + return "", fmt.Errorf("filename was not a string: %v", p[0]) + } + if p[1].Type() != js.TypeString { + return "", fmt.Errorf("code was not a string: %v", p[0]) + } + filename := p[0].String() + code := p[1].String() + + return formatter.Format(filename, code, formatter.DefaultOptions()) +} + +// promiseFuncOf is like js.FuncOf but returns a promise. +// The promise is able to propagate errors naturally across the wasm / +// javascript bridge. +func promiseFuncOf(jsFunc func(this js.Value, p []js.Value) (interface{}, error)) js.Func { + return js.FuncOf(func(this js.Value, p []js.Value) interface{} { + return js.Global().Get("Promise").New(js.FuncOf(func(this js.Value, args []js.Value) interface{} { + resolve := args[0] + reject := args[1] + go func() { + value, err := jsFunc(this, p) + if err != nil { + reject.Invoke(js.Global().Get("Error").New(err.Error())) + } else { + resolve.Invoke(js.ValueOf(value)) + } + }() + return nil + })) + }) +} + +func main() { + js.Global().Set("jsonnet_evaluate_snippet", promiseFuncOf(jsonnetEvaluateSnippet)) + js.Global().Set("jsonnet_fmt_snippet", promiseFuncOf(jsonnetFmtSnippet)) + <-make(chan bool) +} diff --git a/tests.sh b/tests.sh index e1d6740..4f80a6e 100755 --- a/tests.sh +++ b/tests.sh @@ -24,6 +24,7 @@ export OVERRIDE_DIR="$PWD/testdata/cpp-tests-override/" go build ./cmd/jsonnet go build ./cmd/jsonnetfmt +GOOS=js GOARCH=wasm go build -o libjsonnet.wasm ./cmd/wasm export DISABLE_LIB_TESTS=true export DISABLE_ERROR_TESTS=true