Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement offline SPX project compilation and web-based execution #6

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@

# Dependency directories (remove the comment below to include it)
# vendor/

.vscode/
.idea/
main.wasm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.wasm

6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ generate token <https://github.com/settings/tokens>

```
$ ispx -ghtoken your_github_api_token github.com/goplus/FlappyCalf
```
```

### run spx offline demo

see [Offline SPX Project](offline_spx/README.md)
54 changes: 54 additions & 0 deletions offline_spx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Offline SPX Project

## Introduction

This project allows users to compile SPX projects offline by uploading folders and then run and view them on the Web.
Users can upload the entire project folder, view the file structure and content, and run the project directly in the
browser.

index.html is as follows:

![index](./image/index.png)

## Features

- Folder Upload: Users can upload the entire SPX project folder.
- File Structure Viewing: After uploading, users can browse the tree structure of the folder.
- File Content Viewing: Users can view the content of each file uploaded.
- Online Execution: Users can run the SPX project in the web browser and view the results.

## How to Use

### Folder Upload

![folder_upload](./image/folder_upload.png)

- Click the "Select Folder" button.
- Choose your SPX project folder and upload it.

### View File Structure and Content

- After a successful upload, the file structure will be displayed on the page in a tree format.

![show_file_structure](./image/show_file_structure.png)
- Enter the file's project path (starting with the project name) to view the content in the respective area.

![file_content](./image/file_content.png)

### Run Project Online

![online_game](./image/online_game.png)

- Ensure that your project files include all necessary files for execution.
- Input the project name
- Click the "play project" button to start the project in the browser.
- The execution results will be displayed in the designated output area.

## Installation and Running

```sh
./build.sh
cp $GOROOT/misc/wasm/wasm_exec.js ./
```

run http server
2 changes: 2 additions & 0 deletions offline_spx/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
GOOS=js GOARCH=wasm go build -tags canvas -o main.wasm main.go
190 changes: 190 additions & 0 deletions offline_spx/ifs/indexdbDB_with_js.go
Copy link
Member

@visualfc visualfc Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_js 本身就代表了明确的在 GOOS=js 上工作,所以这里不需要 with.
如果 ifs 本身不需要其他的 filesytem 实现,那么文件名可以简单的命名为 ifs_js.go, 不建议使用 indexdbDB 这种大小写的文件名用法。

Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package ifs

import (
"errors"
"fmt"
"log"
"strconv"
"syscall/js"
"time"
)

func getFilesStartingWith(dirname string) ([]string, error) {
log.Println("getFilesStartingWith dirname:", dirname)
defer func() {
if r := recover(); r != nil {
log.Println("getFilesStartingWith panic:", r)
}
}()

// Get JavaScript global object
jsGlobal := js.Global()

// Get the JavaScript function we want to call
jsFunc := jsGlobal.Get("getFilesStartingWith")
// Check if function is defined
if jsFunc.Type() == js.TypeUndefined {
log.Panicln("getFilesStartingWith function is not defined.")
}
// Call a JavaScript function, passing dirname as argument
promise := jsFunc.Invoke(dirname)
// Prepare a channel for receiving results
done := make(chan []string)
var files []string

// Define success callback function
onSuccess := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Get the results returned by JavaScript
jsResult := args[0]
length := jsResult.Length()
files = make([]string, length)
for i := 0; i < length; i++ {
files[i] = jsResult.Index(i).String()
}

// Send results through channel
done <- files
return nil
})

// Define failure callback function
onError := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
log.Println("Error calling getFilesStartingWith:", args[0])
done <- nil
return nil
})

// Bind callback function to Promise
promise.Call("then", onSuccess)
promise.Call("catch", onError)
// Wait for Promise to resolve
result := <-done

// Clean up callbacks
onSuccess.Release()
onError.Release()
if result == nil {
return nil, fmt.Errorf("error reading directory from IndexedDB")
}
return result, nil
}

func readFileFromIndexedDB(filename string) ([]byte, error) {
defer func() {
if r := recover(); r != nil {
log.Println("readFileFromIndexedDB panic", r)
}
}()

// Get JavaScript global object
jsGlobal := js.Global()

// Get the JavaScript function we want to call
jsFunc := jsGlobal.Get("readFileFromIndexedDB")

// Call a JavaScript function, passing filename as argument
promise := jsFunc.Invoke(filename)

// Prepare a channel for receiving results
done := make(chan []byte)

// Define success callback function
onSuccess := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Handle the returned ArrayBuffer
jsArrayBuffer := args[0]
length := jsArrayBuffer.Get("byteLength").Int()
fileContent := make([]byte, length)
js.CopyBytesToGo(fileContent, jsArrayBuffer)

done <- fileContent
return nil
})

// Define failure callback function
onError := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
fmt.Println("Error calling readFileFromIndexedDB:", args[0])
done <- nil
return nil
})

// Bind callback function to Promise
promise.Call("then", onSuccess)
promise.Call("catch", onError)

// Wait for Promise to resolve
result := <-done
// Clean up callbacks
onSuccess.Release()
onError.Release()

if result == nil {
return nil, errors.New("error reading file from IndexedDB")
}
return result, nil
}

type FileProperties struct {
Size int64
LastModified time.Time
}

func getFileProperties(filename string) (FileProperties, error) {
defer func() {
if r := recover(); r != nil {
log.Println("getFileProperties panic:", r)
}
}()

// Get JavaScript global object
jsGlobal := js.Global()

// Get the JavaScript function we want to call
jsFunc := jsGlobal.Get("getFileProperties")

// Call a JavaScript function, passing filename as argument
promise := jsFunc.Invoke(filename)

// Prepare a channel for receiving results
done := make(chan FileProperties)
var fileProperties FileProperties

// Define success callback function
onSuccess := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Handle the returned object
jsResult := args[0]
size, _ := strconv.ParseInt(jsResult.Get("size").String(), 10, 64)
lastModifiedMillis, _ := strconv.ParseInt(jsResult.Get("lastModified").String(), 10, 64)
lastModified := time.Unix(0, lastModifiedMillis*int64(time.Millisecond))

fileProperties = FileProperties{
Size: size,
LastModified: lastModified,
}

done <- fileProperties
return nil
})

// Define failure callback function
onError := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
fmt.Println("Error calling getFileProperties:", args[0])
done <- FileProperties{}
return nil
})

// Bind callback function to Promise
promise.Call("then", onSuccess)
promise.Call("catch", onError)

// Wait for Promise to resolve
result := <-done

// Clean up callbacks
onSuccess.Release()
onError.Release()

if result.Size == 0 && result.LastModified.IsZero() {
return FileProperties{}, errors.New("error getting file properties")
}
return result, nil
}
43 changes: 43 additions & 0 deletions offline_spx/ifs/indexedDB_dir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ifs

import (
"bytes"
"errors"
"io"
"io/ioutil"
"log"
)

// IndexedDBDir is the implementation of a fs.Dir
type IndexedDBDir struct {
assert string
}

func NewIndexedDBDir(assert string) *IndexedDBDir {
return &IndexedDBDir{
assert: assert,
}
}

// Open opens a file
func (d *IndexedDBDir) Open(file string) (io.ReadCloser, error) {
defer func() {
if r := recover(); r != nil {
log.Printf("IndexedDBDir.Open %s panic %s\n", file, r)
}
}()

file = d.assert + "/" + file

content, err := readFileFromIndexedDB(file)
if err != nil {
return nil, errors.New("file not found")
}
return ioutil.NopCloser(bytes.NewReader(content)), nil
}

// Close closes the directory (in this implementation, this method does nothing)
func (d *IndexedDBDir) Close() error {
// In actual applications, you may need to perform cleanup operations
return nil
}
32 changes: 32 additions & 0 deletions offline_spx/ifs/indexedDB_dirEntry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ifs

import (
"io/fs"
"os"
"time"
)

// IndexedDBDirEntry is the implementation of a fs.DirEntry
type IndexedDBDirEntry struct {
Path string
IsDirectory bool
Size int64
ModTime time.Time
}

func (e IndexedDBDirEntry) Name() string { return e.Path }
func (e IndexedDBDirEntry) IsDir() bool { return e.IsDirectory }
func (e IndexedDBDirEntry) Type() fs.FileMode {
if e.IsDir() {
return os.ModeDir
}
return 0 // file
}
func (e IndexedDBDirEntry) Info() (fs.FileInfo, error) {
return IndexedDBFileInfo{
name: e.Path,
size: e.Size,
modTime: e.ModTime,
isDir: e.IsDirectory,
}, nil
}
26 changes: 26 additions & 0 deletions offline_spx/ifs/indexedDB_fileInfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ifs

import (
"os"
"time"
)

// IndexedDBFileInfo is the implementation of an os.FileInfo
type IndexedDBFileInfo struct {
name string
size int64
modTime time.Time
isDir bool
}

func (fi IndexedDBFileInfo) Name() string { return fi.name }
func (fi IndexedDBFileInfo) Size() int64 { return fi.size }
func (fi IndexedDBFileInfo) Mode() os.FileMode {
if fi.isDir {
return os.ModeDir
}
return 0
}
func (fi IndexedDBFileInfo) ModTime() time.Time { return fi.modTime }
func (fi IndexedDBFileInfo) IsDir() bool { return fi.isDir }
func (fi IndexedDBFileInfo) Sys() interface{} { return nil }
Loading