Skip to content

Commit

Permalink
Minimalistic example of compiling Go code in the browser.
Browse files Browse the repository at this point in the history
Some important details:

 - Updated GopherJS version to 1.18-beta3, which is the latest release.
 - Precompiled modules must be kept in sync with the compiler version
   the playground uses, so they have been regenerated. I had to fix a
   small issue in the precompilation script to do that, which was caused
   by recent changes in the compiler code.
 - For the same reason, I changed loading packages from a CDN to a
   relative URL.
 - The playground code has been refactored to be more Go-idiomatic. See
   detailed comments in the source.
  • Loading branch information
nevkontakte committed Aug 6, 2023
1 parent 41e20db commit 5408ff2
Show file tree
Hide file tree
Showing 217 changed files with 19,721 additions and 20,263 deletions.
10 changes: 3 additions & 7 deletions playground/go.mod
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
module github.com/gopherjs/gopherjs.github.io/playground

go 1.17
go 1.18

require (
github.com/gopherjs/gopherjs v1.18.0-beta1
github.com/neelance/go-angularjs v0.0.0-20170205214111-8c6312cca6e2
github.com/gopherjs/gopherjs v1.18.0-beta3
github.com/sirupsen/logrus v1.8.1
golang.org/x/tools v0.1.12
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2
honnef.co/go/js/xhr v0.0.0-20150307031022-00e3346113ae
)

require (
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86 // indirect
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c // indirect
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/stretchr/testify v1.7.0 // indirect
github.com/visualfc/goembed v0.3.3 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
honnef.co/go/js/util v0.0.0-20150216223935-96b8dd9d1621 // indirect
)
594 changes: 4 additions & 590 deletions playground/go.sum

Large diffs are not rendered by default.

16 changes: 5 additions & 11 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html>
<head>
<title>Go Playground</title>
<script src="main.js"></script>
<script src="main.js" defer></script>
<style>
#code {
width: 100%;
Expand All @@ -21,24 +21,18 @@ <h1>Go Playground</h1>
package main

import (
"fmt"
"syscall/js"
)

func main() {
js.Global().Call("alert", "Hello, JavaScript")
// js.Global().Call("alert", "Hello, JavaScript")
js.Global().Get("console").Call("log", "Hello, JS console")
fmt.Println("test")
}
</textarea>

<button onclick="run()">Run</button>
<button id="run">Run</button>
<pre id="output"></pre>

<script>
function run() {
var code = document.getElementById('code').value;
var output = runCodeAsync(code);
document.getElementById('output').textContent = JSON.stringify(output, null, 2);
}
</script>
</body>
</html>
24 changes: 20 additions & 4 deletions playground/internal/cmd/precompile/precompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,44 @@ import (
"flag"
"fmt"
gobuild "go/build"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/gopherjs/gopherjs/build"
"github.com/gopherjs/gopherjs/compiler"
"github.com/gopherjs/gopherjs/compiler/gopherjspkg"
log "github.com/sirupsen/logrus"
)

type logLevelFlag struct{ log.Level }

func (l *logLevelFlag) Set(raw string) error { return l.UnmarshalText([]byte(raw)) }

var (
logLevel logLevelFlag = logLevelFlag{Level: log.ErrorLevel}
)
var logLevel logLevelFlag = logLevelFlag{Level: log.ErrorLevel}

func init() {
flag.Var(&logLevel, "log_level", "Default logging level.")
}

// This is a hack to tell the compiler where gopherjs internal package sources
// are located. Normally the compiler would do it in the main package via a
// go:embed directive, but we can't import that here.
func initFS() error {
pkg, err := gobuild.Import("github.com/gopherjs/gopherjs", "", gobuild.FindOnly)
if err != nil {
return fmt.Errorf("failed to find gopherjs package location: %w", err)
}
gopherjspkg.RegisterFS(http.Dir(pkg.Dir))
return nil
}

func run() error {
if err := initFS(); err != nil {
return fmt.Errorf("failed to initialize gopherjs package file system: %w", err)
}

s, err := build.NewSession(&build.Options{
Verbose: true,
Minify: true,
Expand Down Expand Up @@ -78,7 +94,7 @@ func run() error {

func writeArchive(target string, archive *compiler.Archive) error {
path := filepath.Join(target, filepath.FromSlash(archive.ImportPath)+".a.js")
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return fmt.Errorf("failed to create precompiled package directory %q: %w", filepath.Dir(path), err)
}
f, err := os.Create(path)
Expand Down
189 changes: 96 additions & 93 deletions playground/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,156 +2,159 @@ package main

import (
"bytes"
"encoding/json"
"fmt"
"go/ast"
"go/parser"
"go/scanner"
"go/token"
"go/types"
"io/ioutil"
"io"
"net/http"
"runtime"
"strings"

"github.com/gopherjs/gopherjs/compiler"
"github.com/gopherjs/gopherjs/js"
)

type Line map[string]string

var output []Line
var fileSet *token.FileSet
var pkgsToLoad map[string]struct{}
var importContext *compiler.ImportContext
var packages map[string]*compiler.Archive
var pkgsReceived int
func consoleErrorf(format string, args ...interface{}) {
js.Global.Get("console").Call("error", fmt.Sprintf(format, args...))
}

func main() {
packages = make(map[string]*compiler.Archive)
importContext = &compiler.ImportContext{
Packages: make(map[string]*types.Package),
// newImportContext creates an ImportContext instance, which downloads
// precompiled package archives.
func newImportContext() *compiler.ImportContext {
archives := make(map[string]*compiler.Archive)
packages := make(map[string]*types.Package)
baseURL := js.Global.Get("location").Get("href").String()
importContext := &compiler.ImportContext{
Packages: packages,
Import: func(path string) (*compiler.Archive, error) {
if pkg, found := packages[path]; found {
if pkg, found := archives[path]; found {
return pkg, nil
}

var respData []byte
var err error

resp, err := http.Get("https://cdn.jsdelivr.net/gh/naorzr/gopherjs-runtime@master/playground/pkg/" + path + ".a.js")
// Precompiled archives are located at "pkg/<import path>.a.js" relative
// URL, convert that to the absolute URL http.Get() needs.
url := js.Global.Get("URL").New("pkg/"+path+".a.js", baseURL).Call("toString").String()
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()

respData, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

respData, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

pkg, err := compiler.ReadArchive(path+".a", bytes.NewReader(respData))
if err != nil {
// Handle error
return nil, err
}
packages[path] = pkg
archives[path] = pkg

if err := pkg.RegisterTypes(importContext.Packages); err != nil {
js.Global.Call("eval", js.InternalObject("console.error('error registering types package: "+path+"')"))
if err := pkg.RegisterTypes(packages); err != nil {
return nil, err
}

return pkg, nil
},
}
fileSet = token.NewFileSet()
pkgsReceived = 0

setupEnvironment()
js.Global.Set("runCode", js.MakeFunc(runCode))
js.Global.Set("runCodeAsync", js.MakeFunc(runCodeAsync))

return importContext
}

func runCodeAsync(this *js.Object, arguments []*js.Object) interface{} {
resultChan := make(chan interface{})

go func() {
// Original runCode content
result := runCode(this, arguments)

resultChan <- result
close(resultChan)
}()

// Block until the goroutine sends the result, then return it
return <-resultChan
// Playground implements Go code compilation and execution within a web browser
// context.
type Playground struct {
importContext *compiler.ImportContext
}

func runCode(this *js.Object, arguments []*js.Object) interface{} {
code := arguments[0].String()
escaped, err := json.Marshal(code)
if err != nil {
fmt.Println("Error during marshalling:", err)
return nil
}

// log code
js.Global.Call("eval", js.InternalObject("console.log('code: "+string(escaped)+"')"))
// Compile the given Go source code.
//
// Returns generated JS code that can be evaluated, or an error if compilation
// fails.
func (p *Playground) Compile(code string) (string, error) {
fileSet := token.NewFileSet()

file, err := parser.ParseFile(fileSet, "prog.go", []byte(code), parser.ParseComments)
if err != nil {
if list, ok := err.(scanner.ErrorList); ok {
var outputErr []Line
for _, entry := range list {
outputErr = append(outputErr, Line{"type": "err", "content": entry.Error()})
}
return outputErr
}
return err.Error()
return "", err
}

mainPkg, err := compiler.Compile("main", []*ast.File{file}, fileSet, importContext, false)
mainPkg, err := compiler.Compile("main", []*ast.File{file}, fileSet, p.importContext, false)
if err != nil {
if list, ok := err.(compiler.ErrorList); ok {
var outputErr []Line
for _, entry := range list {
outputErr = append(outputErr, Line{"type": "err", "content": entry.Error()})
}
return outputErr
}
return err.Error()
return "", err
}

allPkgs, _ := compiler.ImportDependencies(mainPkg, importContext.Import)
allPkgs, _ := compiler.ImportDependencies(mainPkg, p.importContext.Import)

jsCode := bytes.NewBuffer(nil)
jsCode.WriteString("try{\n")
compiler.WriteProgramCode(allPkgs, &compiler.SourceMapFilter{Writer: jsCode}, runtime.Version())
jsCode.WriteString("} catch (err) {\nconsole.error(err.message);\n}\n")
js.Global.Set("$checkForDeadlock", true)
js.Global.Call("eval", js.InternalObject(jsCode.String()))

return jsCode.String()
return jsCode.String(), nil
}

func setupEnvironment() {
js.Global.Set("goPrintToConsole", js.InternalObject(func(b []byte) {
lines := strings.Split(string(b), "\n")
if len(output) == 0 || output[len(output)-1]["type"] != "out" {
output = append(output, Line{"type": "out", "content": ""})
// Run the compiled JS code.
//
// If execution throws an exception, it will be caught and returned as an error.
func (p *Playground) Run(compiled string) (returnedError error) {
defer func() {
// JS errors are converted into Go panics, so that we can recover from them.
e := recover()
if e == nil {
return
}
output[len(output)-1]["content"] += lines[0]
for i := 1; i < len(lines); i++ {
output = append(output, Line{"type": "out", "content": lines[i]})

// We got a JS error, propagate it as-is.
if err, ok := e.(*js.Error); ok {
returnedError = err
}
}))
js.Global.Set("goPanicHandler", js.InternalObject(func(msg string) {
output = append(output, Line{"type": "err", "content": "panic: " + msg})
}))

// Some other unknown kind of panic, wrap it in an error.
returnedError = fmt.Errorf("compiled code paniced: %v", e)
}()

js.Global.Call("eval", compiled)
return nil
}

func main() {
// Create a playground object. Its lifetime will be equal to the lifetime of
// the event handler on the button, which will allow it to cache precompiled
// packages between the runs.
p := Playground{
importContext: newImportContext(),
}

// Obtain references to the relevant DOM nodes.
codeEl := js.Global.Get("document").Call("getElementById", "code")
if codeEl == nil {
panic("Can't find code input field.")
}
runEl := js.Global.Get("document").Call("getElementById", "run")
if runEl == nil {
panic("Can't find 'Run' button.")
}

// Install a click handler to the button. Instead of polluting global
// namespace, we just register an anonymous function.
runEl.Call("addEventListener", "click", func(event *js.Object) {
// Because compilation may involve blocking calls (e.g. network requests),
// it must be done in a goroutine to avoid blocking event loop. We kick off
// the goroutine and the event handler completed immediately.
go func() {
code := codeEl.Get("value").String()
println("Compiling the code:\n", code)
compiled, err := p.Compile(code)
if err != nil {
consoleErrorf("Failed to compile the code:\n%s", err)
}
err = p.Run(compiled)
if err != nil {
consoleErrorf("Failed to execute the code:\n%s", err)
}
println("Execution completed without errors.")
}()
})
}
Loading

0 comments on commit 5408ff2

Please sign in to comment.