Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion build.mk
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
TOOLS ?= ./tools
PUBLIC_DIR ?= $(UI)/public
WEBWORKER_PKG ?= ./cmd/webworker

.PHONY: clean
clean:
Expand All @@ -25,8 +27,14 @@ build-ui:
@echo "- Building UI..."
cd $(UI) && yarn build

.PHONY:build-webworker
build-webworker:
@echo "Building Go Webworker module..." && \
GOOS=js GOARCH=wasm go build -o $(PUBLIC_DIR)/worker.wasm $(WEBWORKER_PKG) && \
cp "$$(go env GOROOT)/misc/wasm/wasm_exec.js" $(PUBLIC_DIR)

.PHONY: build
build: clean preinstall collect-meta build-server build-ui
build: clean preinstall collect-meta build-server build-webworker build-ui
@echo "- Copying assets..."
cp -rf ./data $(TARGET)/data
mv $(UI)/build $(TARGET)/public
Expand Down
10 changes: 7 additions & 3 deletions build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
FROM node:13-alpine as ui-build
COPY web /tmp/web
WORKDIR /tmp/web
ARG APP_VERSION="1.0.0"
ARG GITHUB_URL="https://github.com/x1unix/go-playground"
ARG APP_VERSION=1.0.0
ARG GITHUB_URL=https://github.com/x1unix/go-playground
RUN yarn install --silent && REACT_APP_VERSION=$APP_VERSION REACT_APP_GITHUB_URL=$GITHUB_URL yarn build

FROM golang:1.13-alpine as build
Expand All @@ -11,7 +11,9 @@ COPY cmd ./cmd
COPY pkg ./pkg
COPY go.mod .
COPY go.sum .
RUN go build -o server ./cmd/playground
RUN go build -o server ./cmd/playground && \
GOOS=js GOARCH=wasm go build -o ./worker.wasm ./cmd/webworker && \
cp $(go env GOROOT)/misc/wasm/wasm_exec.js .

FROM golang:1.13-alpine as production
WORKDIR /opt/playground
Expand All @@ -21,5 +23,7 @@ ENV APP_DEBUG=false
COPY data ./data
COPY --from=ui-build /tmp/web/build ./public
COPY --from=build /tmp/playground/server .
COPY --from=build /tmp/playground/worker.wasm ./public
COPY --from=build /tmp/playground/wasm_exec.js ./public
EXPOSE 8000
ENTRYPOINT /opt/playground/server -f=/opt/playground/data/packages.json -addr=:8000 -clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG}
6 changes: 6 additions & 0 deletions cmd/webworker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# WebWorker

`webworker` binary exports collection of tools used by UI web worker to analyze Go code and report
code errors.

Package should be compiled as **WebAssembly** binary and loaded by JS WebWorker.
73 changes: 73 additions & 0 deletions cmd/webworker/webworker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"fmt"
"os"
"syscall/js"

"github.com/x1unix/go-playground/pkg/analyzer/check"

"github.com/x1unix/go-playground/pkg/worker"
)

type void = struct{}

var (
done = make(chan void, 0)
onResult js.Value
)

func main() {
entrypoint, err := getEntrypointFunction()
if err != nil {
panic(err)
}

// prepare exports object
analyzeCodeCb := worker.FuncOf(analyzeCode)
exitCb := js.FuncOf(exit)
module := map[string]interface{}{
"analyzeCode": analyzeCodeCb,
"exit": exitCb,
}

defer analyzeCodeCb.Release()
defer exitCb.Release()

entrypoint.Invoke(js.ValueOf(module))
<-done
fmt.Println("Go: exit")
}

func getEntrypointFunction() (js.Value, error) {
if len(os.Args) < 2 {
return js.Value{}, fmt.Errorf("WASM module requires at least 2 arguments: 'js' and entrypoint function name")
}

entrypointName := os.Args[1]
entrypoint := js.Global().Get(entrypointName)
switch t := entrypoint.Type(); t {
case js.TypeFunction:
return entrypoint, nil
case js.TypeUndefined:
return js.Value{}, fmt.Errorf("function %q doesn't exists on global JS scope", entrypointName)
default:
return js.Value{}, fmt.Errorf("%q should be callable JS function, but got %d instead", entrypointName, t)
}
}

func exit(this js.Value, args []js.Value) interface{} {
go func() {
done <- void{}
}()
return nil
}

func analyzeCode(this js.Value, args worker.Args) (interface{}, error) {
var code string
if err := args.Bind(&code); err != nil {
return nil, err
}

return check.Check(code)
}
22 changes: 22 additions & 0 deletions pkg/analyzer/check/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package check

import (
"go/parser"
"go/scanner"
"go/token"
)

// Check checks Go code and returns check result
func Check(src string) (*Result, error) {
fset := token.NewFileSet()
_, err := parser.ParseFile(fset, "main.go", src, parser.DeclarationErrors)
if err == nil {
return &Result{HasErrors: false}, nil
}

if errList, ok := err.(scanner.ErrorList); ok {
return &Result{HasErrors: true, Markers: errorsListToMarkers(errList)}, nil
}

return nil, err
}
46 changes: 46 additions & 0 deletions pkg/analyzer/check/marker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// package check checks provided Go code and reports syntax errors
package check

import "go/scanner"

// MarkerSeverity is equivalent for MarkerSeverity type in monaco-editor
type MarkerSeverity = int

const (
Hint = MarkerSeverity(1)
Info = MarkerSeverity(2)
Warning = MarkerSeverity(3)
Error = MarkerSeverity(8)
)

// MarkerData is a structure defining a problem/warning/etc.
// Equivalent to IMarkerData in 'monaco-editor'
type MarkerData struct {
Severity MarkerSeverity `json:"severity"`
StartLineNumber int `json:"startLineNumber"`
StartColumn int `json:"startColumn"`
EndLineNumber int `json:"endLineNumber"`
EndColumn int `json:"endColumn"`
Message string `json:"message"`
}

func errorsListToMarkers(errList scanner.ErrorList) []MarkerData {
markers := make([]MarkerData, 0, len(errList))
for _, err := range errList {
markers = append(markers, MarkerData{
Severity: Error,
Message: err.Msg,
StartLineNumber: err.Pos.Line,
EndLineNumber: err.Pos.Line,
StartColumn: err.Pos.Column - 1,
EndColumn: err.Pos.Column,
})
}

return markers
}

type Result struct {
HasErrors bool `json:"hasErrors"`
Markers []MarkerData `json:"markers"`
}
75 changes: 75 additions & 0 deletions pkg/worker/args.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package worker

import (
"fmt"
"syscall/js"
)

// NewTypeError creates a new type error
func NewTypeError(expType, gotType js.Type) error {
return fmt.Errorf("value type should be %q, but got %q", expType, gotType)
}

type ValueUnmarshaler interface {
UnmarshalValue(js.Value) error
}

// Args is collection if function call arguments
type Args []js.Value

// BindIndex binds argument at specified index to passed value
func (args Args) BindIndex(index int, dest interface{}) error {
if len(args) <= index {
return fmt.Errorf("function expects %d arguments, but %d were passed", index+1, len(args))
}

return BindValue(args[index], dest)
}

// Bind binds passed JS arguments to Go values
//
// Function supports *int, *bool, *string and ValueUnmarshaler values.
func (args Args) Bind(targets ...interface{}) error {
if len(args) != len(targets) {
return fmt.Errorf("function expects %d arguments, but %d were passed", len(targets), len(args))
}

for i, arg := range args {
if err := BindValue(arg, targets[i]); err != nil {
return fmt.Errorf("invalid argument %d type: %s", err)
}
}

return nil
}

// BindValue binds JS value to specified target
func BindValue(val js.Value, dest interface{}) error {
valType := val.Type()
switch v := dest.(type) {
case *int:
if valType != js.TypeNumber {
return NewTypeError(js.TypeNumber, valType)
}

*v = val.Int()
case *bool:
if valType != js.TypeBoolean {
return NewTypeError(js.TypeBoolean, valType)
}

*v = val.Bool()
case *string:
if valType != js.TypeString {
return NewTypeError(js.TypeString, valType)
}

*v = val.String()
case ValueUnmarshaler:
return v.UnmarshalValue(val)
default:
return fmt.Errorf("BindValue: unsupported JS type %q", valType)
}

return nil
}
25 changes: 25 additions & 0 deletions pkg/worker/callback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package worker

import "syscall/js"

// Callback is async function callback
type Callback = func(interface{}, error)

func newCallbackFromValue(val js.Value) (Callback, error) {
if typ := val.Type(); typ != js.TypeFunction {
return nil, NewTypeError(js.TypeFunction, typ)
}

return func(result interface{}, err error) {
if err != nil {
val.Invoke(js.ValueOf(NewErrorResponse(err).JSON()))
}

if result == nil {
val.Invoke()
return
}

val.Invoke(js.ValueOf(NewResponse(result, nil).JSON()))
}, nil
}
33 changes: 33 additions & 0 deletions pkg/worker/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package worker

import (
"encoding/json"
"fmt"
)

type Response struct {
Error string `json:"error,omitempty"`
Result interface{} `json:"result,omitempty"`
}

func (r Response) JSON() string {
data, err := json.Marshal(r)
if err != nil {
// Return manual JSON in case of error
return fmt.Sprintf(`{"error": %q}`, err)
}

return string(data)
}

func NewErrorResponse(err error) Response {
return Response{Error: err.Error()}
}

func NewResponse(result interface{}, err error) Response {
if err != nil {
return Response{Error: err.Error()}
}

return Response{Result: result}
}
46 changes: 46 additions & 0 deletions pkg/worker/worker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Package worker contains Go web-worker WASM module bridge methods
package worker

import (
"fmt"
"syscall/js"
)

// Func is worker handler function
type Func = func(this js.Value, args Args) (interface{}, error)

// ParseArgs parses async call arguments.
//
// Function expects the last argument to be a callable JS function
func ParseArgs(allArgs []js.Value) (Args, Callback, error) {
argLen := len(allArgs)
if argLen == 0 {
return nil, nil, fmt.Errorf("function requires at least 1 argument, but only 0 were passed")
}

lastIndex := len(allArgs) - 1
cb, err := newCallbackFromValue(allArgs[lastIndex:][0])
if err != nil {
return nil, nil, fmt.Errorf("last function argument should be callable (%s)", err)
}

return allArgs[:lastIndex], cb, nil
}

func callFunc(fn Func, this js.Value, jsArgs []js.Value) {
args, callback, err := ParseArgs(jsArgs)
if err != nil {
js.Global().Get("console").Call("error", fmt.Sprintf("go worker: %s", err))
panic(err)
}

callback(fn(this, args))
}

// FuncOf wraps function into js-compatible async function with callback
func FuncOf(fn Func) js.Func {
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go callFunc(fn, this, args)
return nil
})
}
3 changes: 2 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"react-scripts": "3.3.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"typescript": "^3.7.4"
"typescript": "^3.7.4",
"uuid": "^3.4.0"
},
"scripts": {
"start": "react-app-rewired start",
Expand Down
Loading