From 8d9bf98e5912a21191bf8cdba919b3f77919036b Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Fri, 20 Dec 2024 17:08:19 -0800 Subject: [PATCH] refactor --- d2js/d2wasm/api.go | 70 ++++++++ d2js/d2wasm/functions.go | 161 +++++++++++++++++++ d2js/d2wasm/types.go | 28 ++++ d2js/js.go | 334 ++------------------------------------- 4 files changed, 274 insertions(+), 319 deletions(-) create mode 100644 d2js/d2wasm/api.go create mode 100644 d2js/d2wasm/functions.go create mode 100644 d2js/d2wasm/types.go diff --git a/d2js/d2wasm/api.go b/d2js/d2wasm/api.go new file mode 100644 index 0000000000..b3c9f7274c --- /dev/null +++ b/d2js/d2wasm/api.go @@ -0,0 +1,70 @@ +//go:build js && wasm + +package d2wasm + +import ( + "encoding/json" + "fmt" + "syscall/js" +) + +type D2API struct { + exports map[string]js.Func +} + +func NewD2API() *D2API { + return &D2API{ + exports: make(map[string]js.Func), + } +} + +func (api *D2API) Register(name string, fn func(args []js.Value) (interface{}, error)) { + api.exports[name] = wrapWASMCall(fn) +} + +func (api *D2API) ExportTo(target js.Value) { + d2Namespace := make(map[string]interface{}) + for name, fn := range api.exports { + d2Namespace[name] = fn + } + target.Set("d2", js.ValueOf(d2Namespace)) +} + +func wrapWASMCall(fn func(args []js.Value) (interface{}, error)) js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) (result any) { + defer func() { + if r := recover(); r != nil { + resp := WASMResponse{ + Error: &WASMError{ + Message: fmt.Sprintf("panic recovered: %v", r), + Code: 500, + }, + } + jsonResp, _ := json.Marshal(resp) + result = string(jsonResp) + } + }() + + data, err := fn(args) + if err != nil { + wasmErr, ok := err.(*WASMError) + if !ok { + wasmErr = &WASMError{ + Message: err.Error(), + Code: 500, + } + } + resp := WASMResponse{ + Error: wasmErr, + } + jsonResp, _ := json.Marshal(resp) + return string(jsonResp) + } + + resp := WASMResponse{ + Data: data, + } + jsonResp, _ := json.Marshal(resp) + return string(jsonResp) + }) +} diff --git a/d2js/d2wasm/functions.go b/d2js/d2wasm/functions.go new file mode 100644 index 0000000000..10706fb750 --- /dev/null +++ b/d2js/d2wasm/functions.go @@ -0,0 +1,161 @@ +//go:build js && wasm + +package d2wasm + +import ( + "encoding/json" + "strings" + "syscall/js" + + "oss.terrastruct.com/d2/d2ast" + "oss.terrastruct.com/d2/d2compiler" + "oss.terrastruct.com/d2/d2format" + "oss.terrastruct.com/d2/d2lsp" + "oss.terrastruct.com/d2/d2oracle" + "oss.terrastruct.com/d2/d2parser" + "oss.terrastruct.com/d2/lib/version" +) + +func GetParentID(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing id argument", Code: 400} + } + + id := args[0].String() + mk, err := d2parser.ParseMapKey(id) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 400} + } + + if len(mk.Edges) > 0 { + return "", nil + } + + if mk.Key != nil { + if len(mk.Key.Path) == 1 { + return "root", nil + } + mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1] + return strings.Join(mk.Key.StringIDA(), "."), nil + } + + return "", nil +} + +func GetObjOrder(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing dsl argument", Code: 400} + } + + dsl := args[0].String() + g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ + UTF16Pos: true, + }) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 400} + } + + objOrder, err := d2oracle.GetObjOrder(g, nil) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + return map[string]interface{}{ + "order": objOrder, + }, nil +} + +func GetRefRanges(args []js.Value) (interface{}, error) { + if len(args) < 4 { + return nil, &WASMError{Message: "missing required arguments", Code: 400} + } + + var fs map[string]string + if err := json.Unmarshal([]byte(args[0].String()), &fs); err != nil { + return nil, &WASMError{Message: "invalid fs argument", Code: 400} + } + + file := args[1].String() + key := args[2].String() + + var boardPath []string + if err := json.Unmarshal([]byte(args[3].String()), &boardPath); err != nil { + return nil, &WASMError{Message: "invalid boardPath argument", Code: 400} + } + + ranges, importRanges, err := d2lsp.GetRefRanges(file, fs, boardPath, key) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + return RefRangesResponse{ + Ranges: ranges, + ImportRanges: importRanges, + }, nil +} + +func Compile(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing script argument", Code: 400} + } + + script := args[0].String() + g, _, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ + UTF16Pos: true, + }) + if err != nil { + if pe, ok := err.(*d2parser.ParseError); ok { + return nil, &WASMError{Message: pe.Error(), Code: 400} + } + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + newScript := d2format.Format(g.AST) + if script != newScript { + return map[string]string{"result": newScript}, nil + } + + return nil, nil +} + +func GetBoardAtPosition(args []js.Value) (interface{}, error) { + if len(args) < 3 { + return nil, &WASMError{Message: "missing required arguments", Code: 400} + } + + dsl := args[0].String() + line := args[1].Int() + column := args[2].Int() + + boardPath, err := d2lsp.GetBoardAtPosition(dsl, d2ast.Position{ + Line: line, + Column: column, + }) + if err != nil { + return nil, &WASMError{Message: err.Error(), Code: 500} + } + + return BoardPositionResponse{BoardPath: boardPath}, nil +} + +func Encode(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing script argument", Code: 400} + } + + script := args[0].String() + return map[string]string{"result": script}, nil +} + +func Decode(args []js.Value) (interface{}, error) { + if len(args) < 1 { + return nil, &WASMError{Message: "missing script argument", Code: 400} + } + + script := args[0].String() + return map[string]string{"result": script}, nil +} + +func GetVersion(args []js.Value) (interface{}, error) { + return version.Version, nil +} diff --git a/d2js/d2wasm/types.go b/d2js/d2wasm/types.go new file mode 100644 index 0000000000..911c6b5ebe --- /dev/null +++ b/d2js/d2wasm/types.go @@ -0,0 +1,28 @@ +//go:build js && wasm + +package d2wasm + +import "oss.terrastruct.com/d2/d2ast" + +type WASMResponse struct { + Data interface{} `json:"data,omitempty"` + Error *WASMError `json:"error,omitempty"` +} + +type WASMError struct { + Message string `json:"message"` + Code int `json:"code"` +} + +func (e *WASMError) Error() string { + return e.Message +} + +type RefRangesResponse struct { + Ranges []d2ast.Range `json:"ranges"` + ImportRanges []d2ast.Range `json:"importRanges"` +} + +type BoardPositionResponse struct { + BoardPath []string `json:"boardPath"` +} diff --git a/d2js/js.go b/d2js/js.go index 53aeb13f3d..c1461b5df8 100644 --- a/d2js/js.go +++ b/d2js/js.go @@ -1,333 +1,29 @@ -//go:build wasm +//go:build js && wasm package main import ( - "encoding/json" - "errors" - "io" - "io/fs" - "os" - "strings" "syscall/js" - "oss.terrastruct.com/d2/d2ast" - "oss.terrastruct.com/d2/d2compiler" - "oss.terrastruct.com/d2/d2format" - "oss.terrastruct.com/d2/d2lsp" - "oss.terrastruct.com/d2/d2oracle" - "oss.terrastruct.com/d2/d2parser" - "oss.terrastruct.com/d2/lib/urlenc" - "oss.terrastruct.com/d2/lib/version" + "oss.terrastruct.com/d2/d2js/d2wasm" ) func main() { - js.Global().Set("d2GetParentID", js.FuncOf(jsGetParentID)) - js.Global().Set("d2GetObjOrder", js.FuncOf(jsGetObjOrder)) - js.Global().Set("d2GetRefRanges", js.FuncOf(jsGetRefRanges)) - js.Global().Set("d2Compile", js.FuncOf(jsCompile)) - js.Global().Set("d2GetBoardAtPosition", js.FuncOf(jsGetBoardAtPosition)) - js.Global().Set("d2Parse", js.FuncOf(jsParse)) - js.Global().Set("d2Encode", js.FuncOf(jsEncode)) - js.Global().Set("d2Decode", js.FuncOf(jsDecode)) - js.Global().Set("d2Version", js.FuncOf(jsVersion)) - initCallback := js.Global().Get("onWasmInitialized") - if !initCallback.IsUndefined() { - initCallback.Invoke() - } - select {} -} - -type jsObjOrder struct { - Order []string `json:"order"` - Error string `json:"error"` -} - -func jsGetObjOrder(this js.Value, args []js.Value) interface{} { - dsl := args[0].String() - - g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16Pos: true, - }) - if err != nil { - ret := jsObjOrder{Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - objOrder, err := d2oracle.GetObjOrder(g, nil) - if err != nil { - ret := jsObjOrder{Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - resp := jsObjOrder{ - Order: objOrder, - } - - str, _ := json.Marshal(resp) - return string(str) -} - -func jsGetParentID(this js.Value, args []js.Value) interface{} { - id := args[0].String() - - mk, _ := d2parser.ParseMapKey(id) - - if len(mk.Edges) > 0 { - return "" - } - - if mk.Key != nil { - if len(mk.Key.Path) == 1 { - return "root" - } - mk.Key.Path = mk.Key.Path[:len(mk.Key.Path)-1] - return strings.Join(mk.Key.StringIDA(), ".") - } - - return "" -} - -type jsRefRanges struct { - Ranges []d2ast.Range `json:"ranges"` - ImportRanges []d2ast.Range `json:"importRanges"` - ParseError string `json:"parseError"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` -} - -func jsGetRefRanges(this js.Value, args []js.Value) interface{} { - fsRaw := args[0].String() - file := args[1].String() - key := args[2].String() - boardPathRaw := args[3].String() - - var fs map[string]string - err := json.Unmarshal([]byte(fsRaw), &fs) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - _, err = d2parser.ParseMapKey(key) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - var boardPath []string - err = json.Unmarshal([]byte(boardPathRaw), &boardPath) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - ranges, importRanges, err := d2lsp.GetRefRanges(file, fs, boardPath, key) - if err != nil { - ret := jsRefRanges{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - resp := jsRefRanges{ - Ranges: ranges, - ImportRanges: importRanges, - } - - str, _ := json.Marshal(resp) - return string(str) -} - -type jsObject struct { - Result string `json:"result"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` -} - -type jsParseResponse struct { - DSL string `json:"dsl"` - ParseError string `json:"parseError"` - UserError string `json:"userError"` - D2Error string `json:"d2Error"` -} - -type emptyFile struct{} - -func (f *emptyFile) Stat() (os.FileInfo, error) { - return nil, nil -} - -func (f *emptyFile) Read(p []byte) (int, error) { - return 0, io.EOF -} - -func (f *emptyFile) Close() error { - return nil -} - -type detectFS struct { - importUsed bool -} - -func (detectFS *detectFS) Open(name string) (fs.File, error) { - detectFS.importUsed = true - return &emptyFile{}, nil -} - -func jsParse(this js.Value, args []js.Value) interface{} { - dsl := args[0].String() - themeID := args[1].Int() - - detectFS := detectFS{} - - g, _, err := d2compiler.Compile("", strings.NewReader(dsl), &d2compiler.CompileOptions{ - UTF16Pos: true, - FS: &detectFS, - }) - // If an import was used, client side D2 cannot reliably compile - // Defer to backend compilation - if !detectFS.importUsed { - var pe *d2parser.ParseError - if err != nil { - if errors.As(err, &pe) { - serialized, _ := json.Marshal(err) - ret := jsParseResponse{ParseError: string(serialized)} - str, _ := json.Marshal(ret) - return string(str) - } - ret := jsParseResponse{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - for _, o := range g.Objects { - if (o.Attributes.Top == nil) != (o.Attributes.Left == nil) { - ret := jsParseResponse{UserError: `keywords "top" and "left" currently must be used together`} - str, _ := json.Marshal(ret) - return string(str) - } - } - - err = g.ApplyTheme(int64(themeID)) - if err != nil { - ret := jsParseResponse{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - } - - m, err := d2parser.Parse("", strings.NewReader(dsl), &d2parser.ParseOptions{ - UTF16Pos: true, - }) - if err != nil { - return err - } - - resp := jsParseResponse{} - - newDSL := d2format.Format(m) - if dsl != newDSL { - resp.DSL = newDSL - } - - str, _ := json.Marshal(resp) - return string(str) -} - -// TODO error passing -// TODO recover panics -func jsCompile(this js.Value, args []js.Value) interface{} { - script := args[0].String() - - g, _, err := d2compiler.Compile("", strings.NewReader(script), &d2compiler.CompileOptions{ - UTF16Pos: true, - }) - var pe *d2parser.ParseError - if err != nil { - if errors.As(err, &pe) { - serialized, _ := json.Marshal(err) - ret := jsObject{UserError: string(serialized)} - str, _ := json.Marshal(ret) - return string(str) - } - ret := jsObject{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - newScript := d2format.Format(g.AST) - if script != newScript { - ret := jsObject{Result: newScript} - str, _ := json.Marshal(ret) - return string(str) - } - - return nil -} - -func jsEncode(this js.Value, args []js.Value) interface{} { - script := args[0].String() - - encoded, err := urlenc.Encode(script) - // should never happen - if err != nil { - ret := jsObject{D2Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - ret := jsObject{Result: encoded} - str, _ := json.Marshal(ret) - return string(str) -} - -func jsDecode(this js.Value, args []js.Value) interface{} { - script := args[0].String() + api := d2wasm.NewD2API() - script, err := urlenc.Decode(script) - if err != nil { - ret := jsObject{UserError: err.Error()} - str, _ := json.Marshal(ret) - return string(str) - } - - ret := jsObject{Result: script} - str, _ := json.Marshal(ret) - return string(str) -} + api.Register("getParentID", d2wasm.GetParentID) + api.Register("getObjOrder", d2wasm.GetObjOrder) + api.Register("getRefRanges", d2wasm.GetRefRanges) + api.Register("compile", d2wasm.Compile) + api.Register("getBoardAtPosition", d2wasm.GetBoardAtPosition) + api.Register("encode", d2wasm.Encode) + api.Register("decode", d2wasm.Decode) + api.Register("version", d2wasm.GetVersion) -func jsVersion(this js.Value, args []js.Value) interface{} { - return version.Version -} - -type jsBoardAtPosition struct { - BoardPath []string `json:"boardPath"` - Error string `json:"error"` -} + api.ExportTo(js.Global()) -func jsGetBoardAtPosition(this js.Value, args []js.Value) interface{} { - dsl := args[0].String() - line := args[1].Int() - column := args[2].Int() - - boardPath, err := d2lsp.GetBoardAtPosition(dsl, d2ast.Position{ - Line: line, - Column: column, - }) - - if err != nil { - ret := jsBoardAtPosition{Error: err.Error()} - str, _ := json.Marshal(ret) - return string(str) + if cb := js.Global().Get("onWasmInitialized"); !cb.IsUndefined() { + cb.Invoke() } - - resp := jsBoardAtPosition{ - BoardPath: boardPath, - } - str, _ := json.Marshal(resp) - return string(str) + select {} }