mirror of
https://github.com/LukeHagar/wasm-overhead-research.git
synced 2025-12-06 04:22:06 +00:00
chore: tcr analysis
This commit is contained in:
469
.mise.toml
Normal file
469
.mise.toml
Normal file
@@ -0,0 +1,469 @@
|
||||
[tools]
|
||||
go = "1.24"
|
||||
"rust" = { version = "1.83.0" }
|
||||
|
||||
# ============================================================================
|
||||
# Individual Implementation Build Tasks
|
||||
# ============================================================================
|
||||
|
||||
# Basic implementation - Go
|
||||
[tasks."build:basic:go"]
|
||||
description = "Build basic implementation with Go"
|
||||
env.GOOS = "js"
|
||||
env.GOARCH = "wasm"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building basic implementation with Go..."
|
||||
mkdir -p assets/wasm
|
||||
go mod tidy > /dev/null 2>&1
|
||||
go build -trimpath -o main.wasm implementations/basic/main.go
|
||||
gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz
|
||||
rm main.wasm
|
||||
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
echo "✅ Basic Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# Basic implementation - TinyGo
|
||||
[tasks."build:basic:tinygo"]
|
||||
description = "Build basic implementation with TinyGo"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building basic implementation with TinyGo..."
|
||||
if ! command -v tinygo >/dev/null 2>&1; then
|
||||
echo "❌ Error: TinyGo is not installed"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p assets/wasm
|
||||
go mod tidy > /dev/null 2>&1
|
||||
tinygo build -target wasm -o main.wasm implementations/basic/main.go
|
||||
gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz
|
||||
rm main.wasm
|
||||
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
echo "✅ Basic TinyGo build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# Goja implementation - Go only (doesn't work with TinyGo)
|
||||
[tasks."build:goja:go"]
|
||||
description = "Build goja implementation with Go"
|
||||
env.GOOS = "js"
|
||||
env.GOARCH = "wasm"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building goja implementation with Go..."
|
||||
mkdir -p assets/wasm
|
||||
go mod tidy > /dev/null 2>&1
|
||||
go build -trimpath -o main.wasm implementations/goja/main.go
|
||||
gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz
|
||||
rm main.wasm
|
||||
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
echo "✅ Goja Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# Javy implementation
|
||||
[tasks."build:javy"]
|
||||
description = "Build Javy implementation (JavaScript to WASM)"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building Javy implementation..."
|
||||
if ! command -v javy >/dev/null 2>&1; then
|
||||
echo "❌ Error: Javy CLI is not installed or not on PATH"
|
||||
echo "Please install the Javy CLI from https://github.com/bytecodealliance/javy"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p assets/wasm
|
||||
cd implementations/javy
|
||||
javy emit-plugin -o plugin.wasm
|
||||
javy build -C dynamic -C plugin=plugin.wasm -o transform_dynamic.wasm transform.js
|
||||
cd ../..
|
||||
gzip -9 -c implementations/javy/transform_dynamic.wasm > assets/wasm/lib.wasm.gz
|
||||
echo "✅ Javy build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# Porffor implementation
|
||||
[tasks."build:porffor"]
|
||||
description = "Build Porffor implementation (AOT JavaScript to WASM)"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building Porffor implementation..."
|
||||
mkdir -p assets/wasm
|
||||
cd implementations/porffor
|
||||
npx porf wasm transform.js transform.wasm
|
||||
cd ../..
|
||||
gzip -9 -c implementations/porffor/transform.wasm > assets/wasm/lib.wasm.gz
|
||||
echo "✅ Porffor build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# QuickJS implementation
|
||||
[tasks."build:quickjs"]
|
||||
description = "Build QuickJS implementation (Rust + QuickJS)"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building QuickJS implementation..."
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
echo "❌ Error: Rust/Cargo is not installed"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p assets/wasm
|
||||
cd implementations/quickjs
|
||||
cargo build --target wasm32-wasip1 --release
|
||||
cd ../..
|
||||
gzip -9 -c implementations/quickjs/target/wasm32-wasip1/release/quickjs_transform.wasm > assets/wasm/lib.wasm.gz
|
||||
echo "✅ QuickJS build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# AssemblyScript implementation
|
||||
[tasks."build:assemblyscript"]
|
||||
description = "Build AssemblyScript implementation with json-as"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building AssemblyScript implementation..."
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "❌ Error: npm is not installed"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p assets/wasm
|
||||
cd implementations/assemblyscript
|
||||
npm install --legacy-peer-deps > /dev/null 2>&1
|
||||
npm run asbuild > /dev/null 2>&1
|
||||
cd ../..
|
||||
gzip -9 -c implementations/assemblyscript/build/release.wasm > assets/wasm/lib.wasm.gz
|
||||
echo "✅ AssemblyScript build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# AssemblyScript optimized implementation
|
||||
[tasks."build:assemblyscript:optimized"]
|
||||
description = "Build optimized AssemblyScript implementation"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building optimized AssemblyScript implementation..."
|
||||
mkdir -p assets/wasm
|
||||
cd implementations/assemblyscript
|
||||
npm install --legacy-peer-deps > /dev/null 2>&1
|
||||
npm run asbuild > /dev/null 2>&1
|
||||
if command -v wasm-opt >/dev/null 2>&1; then
|
||||
wasm-opt -Oz --enable-bulk-memory --strip-debug --strip-dwarf --strip-producers --converge build/release.wasm -o build/release_opt.wasm
|
||||
mv build/release_opt.wasm build/release.wasm
|
||||
fi
|
||||
cd ../..
|
||||
gzip -9 -c implementations/assemblyscript/build/release.wasm > assets/wasm/lib.wasm.gz
|
||||
echo "✅ AssemblyScript optimized build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Optimized Build Tasks
|
||||
# ============================================================================
|
||||
|
||||
[tasks."build:basic:go:optimized"]
|
||||
description = "Build basic implementation with optimized Go"
|
||||
env.GOOS = "js"
|
||||
env.GOARCH = "wasm"
|
||||
env.CGO_ENABLED = "0"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building basic implementation with optimized Go..."
|
||||
mkdir -p assets/wasm
|
||||
go mod tidy > /dev/null 2>&1
|
||||
go build -ldflags="-s -w -buildid=" -gcflags="-l=4 -B -C" -trimpath -o main.wasm implementations/basic/main.go
|
||||
if command -v wasm-opt >/dev/null 2>&1; then
|
||||
wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --converge main.wasm -o main_opt.wasm
|
||||
mv main_opt.wasm main.wasm
|
||||
fi
|
||||
gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz
|
||||
rm main.wasm
|
||||
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
echo "✅ Basic optimized Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
[tasks."build:basic:tinygo:optimized"]
|
||||
description = "Build basic implementation with optimized TinyGo"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building basic implementation with optimized TinyGo..."
|
||||
if ! command -v tinygo >/dev/null 2>&1; then
|
||||
echo "❌ Error: TinyGo is not installed"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p assets/wasm
|
||||
go mod tidy > /dev/null 2>&1
|
||||
tinygo build -target wasm -no-debug -gc=leaking -opt=z -size=full -panic=trap -o main.wasm implementations/basic/main.go
|
||||
if command -v wasm-opt >/dev/null 2>&1; then
|
||||
wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --enable-mutable-globals --converge --all-features main.wasm -o main_opt.wasm
|
||||
mv main_opt.wasm main.wasm
|
||||
fi
|
||||
gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz
|
||||
rm main.wasm
|
||||
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
echo "✅ Basic optimized TinyGo build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
[tasks."build:goja:go:optimized"]
|
||||
description = "Build goja implementation with optimized Go"
|
||||
env.GOOS = "js"
|
||||
env.GOARCH = "wasm"
|
||||
env.CGO_ENABLED = "0"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building goja implementation with optimized Go..."
|
||||
mkdir -p assets/wasm
|
||||
go mod tidy > /dev/null 2>&1
|
||||
go build -ldflags="-s -w -buildid=" -gcflags="-l=4 -B -C" -trimpath -o main.wasm implementations/goja/main.go
|
||||
if command -v wasm-opt >/dev/null 2>&1; then
|
||||
wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --converge main.wasm -o main_opt.wasm
|
||||
mv main_opt.wasm main.wasm
|
||||
fi
|
||||
gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz
|
||||
rm main.wasm
|
||||
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
echo "✅ Goja optimized Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
|
||||
[tasks."build:porffor:optimized"]
|
||||
description = "Build Porffor implementation with optimization"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building optimized Porffor implementation..."
|
||||
mkdir -p assets/wasm
|
||||
cd implementations/porffor
|
||||
npx porf wasm -O3 transform.js transform.wasm
|
||||
cd ../..
|
||||
gzip -9 -c implementations/porffor/transform.wasm > assets/wasm/lib.wasm.gz
|
||||
echo "✅ Porffor optimized build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# Individual Test Tasks (with dependencies on builds)
|
||||
# ============================================================================
|
||||
|
||||
[tasks."test:basic:go"]
|
||||
description = "Test basic implementation with Go build"
|
||||
depends = ["build:basic:go"]
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Testing basic Go implementation..."
|
||||
npm install > /dev/null 2>&1
|
||||
npx vitest run --reporter=verbose
|
||||
"""
|
||||
|
||||
[tasks."test:basic:tinygo"]
|
||||
description = "Test basic implementation with TinyGo build"
|
||||
depends = ["build:basic:tinygo"]
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Testing basic TinyGo implementation..."
|
||||
npm install > /dev/null 2>&1
|
||||
npx vitest run --reporter=verbose
|
||||
"""
|
||||
|
||||
[tasks."test:goja:go"]
|
||||
description = "Test goja implementation with Go build"
|
||||
depends = ["build:goja:go"]
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Testing goja Go implementation..."
|
||||
npm install > /dev/null 2>&1
|
||||
npx vitest run --reporter=verbose
|
||||
"""
|
||||
|
||||
|
||||
[tasks."test:javy"]
|
||||
description = "Test Javy implementation"
|
||||
depends = ["build:javy"]
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Testing Javy implementation..."
|
||||
node -e "import('./implementations/javy/javy-adapter.js').then(async (javy) => {
|
||||
console.log('🚀 Javy adapter loaded');
|
||||
try {
|
||||
const health = await javy.healthCheck();
|
||||
console.log('💓 Health check:', JSON.parse(health).status);
|
||||
const result = await javy.transformData('{\"name\":\"test\",\"value\":42}');
|
||||
const parsed = JSON.parse(result);
|
||||
console.log('🔄 Transform test:', parsed.engine === 'javy' ? 'PASS' : 'FAIL');
|
||||
console.log('✅ Javy tests passed!');
|
||||
} catch (error) {
|
||||
console.error('❌ Javy test failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}).catch(console.error);"
|
||||
"""
|
||||
|
||||
[tasks."test:porffor"]
|
||||
description = "Test Porffor implementation"
|
||||
depends = ["build:porffor"]
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Testing Porffor implementation..."
|
||||
if command -v wasmer >/dev/null 2>&1; then
|
||||
cd implementations/porffor
|
||||
if [ -f porffor-wasmer-test.sh ]; then
|
||||
./porffor-wasmer-test.sh
|
||||
else
|
||||
echo "⚠️ Porffor test script not found"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Wasmer not installed, skipping Porffor WASI test"
|
||||
fi
|
||||
"""
|
||||
|
||||
[tasks."test:quickjs"]
|
||||
description = "Test QuickJS implementation"
|
||||
depends = ["build:quickjs"]
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Testing QuickJS implementation..."
|
||||
if [ -f implementations/quickjs/quickjs-wasi-test.js ]; then
|
||||
node implementations/quickjs/quickjs-wasi-test.js
|
||||
elif command -v wasmer >/dev/null 2>&1; then
|
||||
cd implementations/quickjs
|
||||
if [ -f quickjs-wasmer-test.sh ]; then
|
||||
./quickjs-wasmer-test.sh
|
||||
else
|
||||
echo "⚠️ QuickJS test script not found"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ No QuickJS test available"
|
||||
fi
|
||||
"""
|
||||
|
||||
[tasks."test:assemblyscript"]
|
||||
description = "Test AssemblyScript implementation"
|
||||
depends = ["build:assemblyscript"]
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Testing AssemblyScript implementation..."
|
||||
cd implementations/assemblyscript
|
||||
node test.js
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# Master Test Task - depends on all individual tests
|
||||
# ============================================================================
|
||||
|
||||
[tasks.test]
|
||||
description = "Run all working implementation tests"
|
||||
depends = [
|
||||
"test:basic:go",
|
||||
"test:basic:tinygo",
|
||||
"test:goja:go",
|
||||
"test:porffor",
|
||||
"test:quickjs",
|
||||
"test:assemblyscript"
|
||||
]
|
||||
run = """
|
||||
echo "✅ All working tests completed successfully!"
|
||||
echo ""
|
||||
echo "Note: Skipped implementations:"
|
||||
echo " - javy (CLI not available via npm package)"
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# Build All Task - builds all implementations
|
||||
# ============================================================================
|
||||
|
||||
[tasks."build:all"]
|
||||
description = "Build all implementations"
|
||||
depends = [
|
||||
"build:basic:go",
|
||||
"build:basic:tinygo",
|
||||
"build:goja:go",
|
||||
"build:javy",
|
||||
"build:porffor",
|
||||
"build:quickjs",
|
||||
"build:assemblyscript"
|
||||
]
|
||||
run = """
|
||||
echo "✅ All builds completed successfully!"
|
||||
"""
|
||||
|
||||
[tasks."build:all:optimized"]
|
||||
description = "Build all implementations with optimization"
|
||||
depends = [
|
||||
"build:basic:go:optimized",
|
||||
"build:basic:tinygo:optimized",
|
||||
"build:goja:go:optimized",
|
||||
"build:porffor:optimized",
|
||||
"build:quickjs",
|
||||
"build:assemblyscript:optimized"
|
||||
]
|
||||
run = """
|
||||
echo "✅ All optimized builds completed successfully!"
|
||||
"""
|
||||
|
||||
# ============================================================================
|
||||
# Utility Tasks
|
||||
# ============================================================================
|
||||
|
||||
[tasks.clean]
|
||||
description = "Clean build artifacts and node_modules"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Cleaning build artifacts..."
|
||||
rm -rf assets/
|
||||
rm -f main.wasm main.wasm.gz
|
||||
rm -rf node_modules/
|
||||
rm -f implementations/javy/*.wasm
|
||||
rm -f implementations/porffor/*.wasm
|
||||
rm -rf implementations/quickjs/target/
|
||||
echo "✅ Clean complete"
|
||||
"""
|
||||
|
||||
[tasks."size:compare"]
|
||||
description = "Compare sizes of all implementations"
|
||||
run = """
|
||||
set -euo pipefail
|
||||
echo "Building and comparing all implementations..."
|
||||
echo ""
|
||||
echo "| Implementation | Compiler | Size (gzipped) |"
|
||||
echo "|----------------|----------|----------------|"
|
||||
|
||||
# Basic - Go
|
||||
mise run build:basic:go > /dev/null 2>&1
|
||||
printf "| Basic | Go | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# Basic - Go Optimized
|
||||
mise run build:basic:go:optimized > /dev/null 2>&1
|
||||
printf "| Basic | Go (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# Basic - TinyGo
|
||||
mise run build:basic:tinygo > /dev/null 2>&1
|
||||
printf "| Basic | TinyGo | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# Basic - TinyGo Optimized
|
||||
mise run build:basic:tinygo:optimized > /dev/null 2>&1
|
||||
printf "| Basic | TinyGo (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# Goja - Go
|
||||
mise run build:goja:go > /dev/null 2>&1
|
||||
printf "| Goja | Go | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# Goja - Go Optimized
|
||||
mise run build:goja:go:optimized > /dev/null 2>&1
|
||||
printf "| Goja | Go (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# Javy
|
||||
if command -v javy >/dev/null 2>&1; then
|
||||
mise run build:javy > /dev/null 2>&1
|
||||
printf "| Javy | Dynamic | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
fi
|
||||
|
||||
# Porffor
|
||||
mise run build:porffor > /dev/null 2>&1
|
||||
printf "| Porffor | AOT | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
mise run build:porffor:optimized > /dev/null 2>&1
|
||||
printf "| Porffor | AOT (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# QuickJS
|
||||
mise run build:quickjs > /dev/null 2>&1
|
||||
printf "| QuickJS | Rust | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
# AssemblyScript
|
||||
mise run build:assemblyscript > /dev/null 2>&1
|
||||
printf "| AssemblyScript | AssemblyScript | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)"
|
||||
|
||||
echo ""
|
||||
echo "✅ Size comparison complete!"
|
||||
"""
|
||||
|
||||
373
Makefile
373
Makefile
@@ -1,387 +1,120 @@
|
||||
.PHONY: build build-go build-tinygo build-optimized build-go-optimized build-tinygo-optimized test test-go test-tinygo clean watch help size-comparison
|
||||
|
||||
# Default implementation
|
||||
IMPL ?= basic
|
||||
# Default implementation (deprecated): use mise task arguments instead
|
||||
|
||||
# Pass-through args to mise: use "make <target> <args...>"
|
||||
ARGS := $(filter-out $@,$(MAKECMDGOALS))
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build the WASM binary with Go (default)"
|
||||
@echo " build-go - Build the WASM binary with Go"
|
||||
@echo " build-tinygo - Build the WASM binary with TinyGo (smaller size)"
|
||||
@echo " build-optimized - Build with maximum Go optimization + wasm-opt"
|
||||
@echo " build-go-optimized - Build with maximum Go optimization + wasm-opt"
|
||||
@echo " build-tinygo-optimized - Build with maximum TinyGo optimization + wasm-opt"
|
||||
@echo " build-javy - Build with Javy (JavaScript-to-WASM)"
|
||||
@echo " build-porffor - Build with Porffor (AOT JavaScript)"
|
||||
@echo " build-quickjs - Build with QuickJS (Rust + JavaScript engine)"
|
||||
@echo " test - Run tests with Go build (default)"
|
||||
@echo " test-go - Run tests with Go build"
|
||||
@echo " test-tinygo - Run tests with TinyGo build"
|
||||
@echo " test-wasmer - Test WASI-compatible implementations with Wasmer"
|
||||
@echo " test-quickjs-wasmer - Test QuickJS implementation with Wasmer"
|
||||
@echo " test-porffor-wasmer - Test Porffor implementation with Wasmer"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " watch - Build in watch mode with Go"
|
||||
@echo " size-comparison - Compare binary sizes for all implementations"
|
||||
@echo " help - Show this help message"
|
||||
@echo "This project uses mise for toolchain and tasks."
|
||||
@echo ""
|
||||
@echo "Available implementations:"
|
||||
@ls -1 implementations/ 2>/dev/null || echo " No implementations found"
|
||||
@echo "Common commands:"
|
||||
@echo " mise install # Install tools (Go 1.23)"
|
||||
@echo " mise tasks # List available tasks"
|
||||
@echo " mise run build <impl> # Build with Go (alias for build-go)"
|
||||
@echo " mise run build-go-optimized <impl> # Build with Go (optimized)"
|
||||
@echo " mise run build-tinygo <impl> # Build with TinyGo"
|
||||
@echo " mise run test <impl> # Test (builds first)"
|
||||
@echo " mise run clean # Clean artifacts"
|
||||
@echo " mise run watch <impl> # Watch build (requires fswatch)"
|
||||
@echo " make build <impl> # Convenience wrapper to mise"
|
||||
@echo " make test <impl> # Convenience wrapper to mise"
|
||||
@echo ""
|
||||
@echo "Usage: make build IMPL=<implementation>"
|
||||
@echo "Example: make build IMPL=basic"
|
||||
@echo "Example: make build-optimized IMPL=goja"
|
||||
@echo "Note: Makefile targets are retained for compatibility but logic has moved to mise."
|
||||
|
||||
# Default build uses Go
|
||||
build: build-go
|
||||
# Default build uses mise task (accepts positional args: make build <impl>)
|
||||
build:
|
||||
@mise run build $(ARGS)
|
||||
|
||||
# Build the WASM binary with Go
|
||||
build-go:
|
||||
@echo "Building WASM binary with Go ($(IMPL) implementation)..."
|
||||
@if [ ! -f "implementations/$(IMPL)/main.go" ]; then \
|
||||
echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Installing Go dependencies..."
|
||||
@go mod tidy > /dev/null 2>&1
|
||||
@echo "Compiling to WASM with Go..."
|
||||
@GOOS=js GOARCH=wasm go build -trimpath -o main.wasm implementations/$(IMPL)/main.go
|
||||
@echo "Compressing WASM binary..."
|
||||
@gzip -9 -c main.wasm > main.wasm.gz
|
||||
@rm main.wasm
|
||||
@mv main.wasm.gz assets/wasm/lib.wasm.gz
|
||||
@cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
@echo "✅ Go build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-go $(ARGS)
|
||||
|
||||
# Build the WASM binary with TinyGo
|
||||
build-tinygo:
|
||||
@echo "Building WASM binary with TinyGo ($(IMPL) implementation)..."
|
||||
@if [ ! -f "implementations/$(IMPL)/main.go" ]; then \
|
||||
echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Installing Go dependencies..."
|
||||
@go mod tidy > /dev/null 2>&1
|
||||
@echo "Compiling to WASM with TinyGo..."
|
||||
@if ! command -v tinygo >/dev/null 2>&1; then \
|
||||
echo "❌ Error: TinyGo is not installed. Please install it from https://tinygo.org/getting-started/install/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@tinygo build -target wasm -o main.wasm implementations/$(IMPL)/main.go
|
||||
@echo "Compressing WASM binary..."
|
||||
@gzip -9 -c main.wasm > main.wasm.gz
|
||||
@rm main.wasm
|
||||
@mv main.wasm.gz assets/wasm/lib.wasm.gz
|
||||
@cp "$$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
@echo "✅ TinyGo build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-tinygo $(ARGS)
|
||||
|
||||
# Default optimized build uses Go
|
||||
build-optimized: build-go-optimized
|
||||
# Default optimized build uses Go (mise wrapper)
|
||||
build-optimized:
|
||||
@mise run build-optimized $(ARGS)
|
||||
|
||||
# Build the WASM binary with maximum Go optimization + wasm-opt
|
||||
build-go-optimized:
|
||||
@echo "Building WASM binary with maximum Go optimization ($(IMPL) implementation)..."
|
||||
@if [ ! -f "implementations/$(IMPL)/main.go" ]; then \
|
||||
echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Installing Go dependencies..."
|
||||
@go mod tidy > /dev/null 2>&1
|
||||
@echo "Compiling to WASM with maximum Go optimization..."
|
||||
@CGO_ENABLED=0 GOOS=js GOARCH=wasm go build \
|
||||
-ldflags="-s -w -buildid=" \
|
||||
-gcflags="-l=4 -B -C" \
|
||||
-trimpath \
|
||||
-o main_raw.wasm implementations/$(IMPL)/main.go
|
||||
@echo "Raw size: $$(du -h main_raw.wasm | cut -f1)"
|
||||
@echo "Optimizing with wasm-opt..."
|
||||
@if command -v wasm-opt >/dev/null 2>&1; then \
|
||||
wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --converge main_raw.wasm -o main.wasm; \
|
||||
echo "Optimized size: $$(du -h main.wasm | cut -f1)"; \
|
||||
else \
|
||||
echo "⚠️ wasm-opt not found, skipping post-build optimization"; \
|
||||
mv main_raw.wasm main.wasm; \
|
||||
fi
|
||||
@echo "Compressing WASM binary..."
|
||||
@gzip -9 -c main.wasm > main.wasm.gz
|
||||
@rm main.wasm main_raw.wasm 2>/dev/null || true
|
||||
@mv main.wasm.gz assets/wasm/lib.wasm.gz
|
||||
@cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
@echo "✅ Optimized Go build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-go-optimized $(ARGS)
|
||||
|
||||
# Build the WASM binary with maximum TinyGo optimization + wasm-opt
|
||||
build-tinygo-optimized:
|
||||
@echo "Building WASM binary with maximum TinyGo optimization ($(IMPL) implementation)..."
|
||||
@if [ ! -f "implementations/$(IMPL)/main.go" ]; then \
|
||||
echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Installing Go dependencies..."
|
||||
@go mod tidy > /dev/null 2>&1
|
||||
@echo "Compiling to WASM with maximum TinyGo optimization..."
|
||||
@if ! command -v tinygo >/dev/null 2>&1; then \
|
||||
echo "❌ Error: TinyGo is not installed. Please install it from https://tinygo.org/getting-started/install/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@tinygo build \
|
||||
-o main_raw.wasm \
|
||||
-target wasm \
|
||||
-no-debug \
|
||||
-gc=leaking \
|
||||
-opt=z \
|
||||
-size=full \
|
||||
-panic=trap \
|
||||
implementations/$(IMPL)/main.go
|
||||
@echo "Raw size: $$(du -h main_raw.wasm | cut -f1)"
|
||||
@echo "Optimizing with wasm-opt..."
|
||||
@if command -v wasm-opt >/dev/null 2>&1; then \
|
||||
wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --enable-mutable-globals --converge --all-features main_raw.wasm -o main.wasm; \
|
||||
echo "Optimized size: $$(du -h main.wasm | cut -f1)"; \
|
||||
else \
|
||||
echo "⚠️ wasm-opt not found, skipping post-build optimization"; \
|
||||
mv main_raw.wasm main.wasm; \
|
||||
fi
|
||||
@echo "Compressing WASM binary..."
|
||||
@gzip -9 -c main.wasm > main.wasm.gz
|
||||
@rm main.wasm main_raw.wasm 2>/dev/null || true
|
||||
@mv main.wasm.gz assets/wasm/lib.wasm.gz
|
||||
@cp "$$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js
|
||||
@echo "✅ Optimized TinyGo build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-tinygo-optimized $(ARGS)
|
||||
|
||||
# Build WASM binary with Javy (JavaScript to WASM) using dynamic linking
|
||||
build-javy:
|
||||
@if [ "$(IMPL)" != "javy" ]; then \
|
||||
echo "❌ Error: Javy build only supports IMPL=javy"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Building WASM binary with Javy ($(IMPL) implementation)..."
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Creating Javy plugin..."
|
||||
@if ! command -v javy >/dev/null 2>&1; then \
|
||||
echo "❌ Error: Javy is not installed. Please install it from https://github.com/bytecodealliance/javy"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@cd implementations/$(IMPL) && javy emit-plugin -o plugin.wasm
|
||||
@echo "Compiling JavaScript to WASM with dynamic linking..."
|
||||
@cd implementations/$(IMPL) && javy build -C dynamic -C plugin=plugin.wasm -o transform_dynamic.wasm transform.js
|
||||
@echo "Plugin size: $$(du -h implementations/$(IMPL)/plugin.wasm | cut -f1)"
|
||||
@echo "Dynamic module size: $$(du -h implementations/$(IMPL)/transform_dynamic.wasm | cut -f1)"
|
||||
@echo "Total size: $$(du -ch implementations/$(IMPL)/plugin.wasm implementations/$(IMPL)/transform_dynamic.wasm | tail -1 | cut -f1)"
|
||||
@echo "Compressing dynamic module..."
|
||||
@gzip -c implementations/$(IMPL)/transform_dynamic.wasm > assets/wasm/lib.wasm.gz
|
||||
@echo "✅ Javy build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-javy $(ARGS)
|
||||
|
||||
# Build WASM binary with Javy optimization (dynamic linking is already optimized)
|
||||
build-javy-optimized:
|
||||
@if [ "$(IMPL)" != "javy" ]; then \
|
||||
echo "❌ Error: Javy build only supports IMPL=javy"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Building WASM binary with optimized Javy ($(IMPL) implementation)..."
|
||||
@echo "Note: Javy dynamic linking is already highly optimized (4KB modules)"
|
||||
@echo "Note: wasm-opt corrupts Javy plugins, so using standard dynamic build"
|
||||
@$(MAKE) build-javy IMPL=$(IMPL)
|
||||
@echo "✅ Optimized Javy build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-javy-optimized $(ARGS)
|
||||
|
||||
# Build WASM binary with Porffor (AOT JavaScript to WASM)
|
||||
build-porffor:
|
||||
@if [ "$(IMPL)" != "porffor" ]; then \
|
||||
echo "❌ Error: Porffor build only supports IMPL=porffor"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Building WASM binary with Porffor ($(IMPL) implementation)..."
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Compiling JavaScript to WASM with Porffor AOT compiler..."
|
||||
@if ! command -v porf >/dev/null 2>&1; then \
|
||||
echo "❌ Error: Porffor is not installed. Please install it with: npm install -g porffor@latest"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@cd implementations/$(IMPL) && porf wasm transform.js transform.wasm
|
||||
@echo "Raw size: $$(du -h implementations/$(IMPL)/transform.wasm | cut -f1)"
|
||||
@echo "Compressing WASM binary..."
|
||||
@gzip -c implementations/$(IMPL)/transform.wasm > assets/wasm/lib.wasm.gz
|
||||
@echo "✅ Porffor build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-porffor $(ARGS)
|
||||
|
||||
# Build WASM binary with Porffor optimization
|
||||
build-porffor-optimized:
|
||||
@if [ "$(IMPL)" != "porffor" ]; then \
|
||||
echo "❌ Error: Porffor build only supports IMPL=porffor"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Building WASM binary with optimized Porffor ($(IMPL) implementation)..."
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Compiling JavaScript to WASM with Porffor AOT compiler (max optimization)..."
|
||||
@if ! command -v porf >/dev/null 2>&1; then \
|
||||
echo "❌ Error: Porffor is not installed. Please install it with: npm install -g porffor@latest"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@cd implementations/$(IMPL) && porf wasm -O3 transform.js transform_opt.wasm
|
||||
@echo "Optimized size: $$(du -h implementations/$(IMPL)/transform_opt.wasm | cut -f1)"
|
||||
@echo "Compressing WASM binary..."
|
||||
@gzip -c implementations/$(IMPL)/transform_opt.wasm > assets/wasm/lib.wasm.gz
|
||||
@echo "✅ Optimized Porffor build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-porffor-optimized $(ARGS)
|
||||
|
||||
# Default test uses Go
|
||||
test: test-go
|
||||
# Default test uses mise task (accepts positional args: make test <impl>)
|
||||
test:
|
||||
@mise run test $(ARGS)
|
||||
|
||||
# Run tests with Go build (builds first)
|
||||
test-go: build-go
|
||||
@echo "Installing Node.js dependencies..."
|
||||
@npm install > /dev/null 2>&1
|
||||
@echo "Running tests with Go build..."
|
||||
@npx vitest run
|
||||
# Run tests with Go build (mise wrapper)
|
||||
test-go:
|
||||
@mise run test-go $(ARGS)
|
||||
|
||||
# Run tests with TinyGo build (builds first)
|
||||
test-tinygo: build-tinygo
|
||||
@echo "Installing Node.js dependencies..."
|
||||
@npm install > /dev/null 2>&1
|
||||
@echo "Running tests with TinyGo build..."
|
||||
@npx vitest run
|
||||
# Run tests with TinyGo build (mise wrapper)
|
||||
test-tinygo:
|
||||
@mise run test-tinygo $(ARGS)
|
||||
|
||||
# Test Javy implementation directly
|
||||
test-javy:
|
||||
@$(MAKE) build-javy IMPL=javy > /dev/null 2>&1
|
||||
@echo "Testing Javy implementation..."
|
||||
@node -e "import('./implementations/javy/javy-adapter.js').then(async (javy) => { \
|
||||
console.log('🚀 Javy adapter loaded'); \
|
||||
try { \
|
||||
const health = await javy.healthCheck(); \
|
||||
console.log('💓 Health check:', JSON.parse(health).status); \
|
||||
const result = await javy.transformData('{\"name\":\"test\",\"value\":42}'); \
|
||||
const parsed = JSON.parse(result); \
|
||||
console.log('🔄 Transform test:', parsed.engine === 'javy' ? 'PASS' : 'FAIL'); \
|
||||
console.log('✅ All Javy tests passed!'); \
|
||||
} catch (error) { \
|
||||
console.error('❌ Javy test failed:', error.message); \
|
||||
process.exit(1); \
|
||||
} \
|
||||
}).catch(console.error);" 2>/dev/null
|
||||
@mise run test-javy $(ARGS)
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf assets/
|
||||
@rm -f main.wasm main.wasm.gz
|
||||
@rm -rf node_modules/
|
||||
@echo "✅ Clean complete"
|
||||
@mise run clean
|
||||
|
||||
# Build in watch mode with Go (requires fswatch)
|
||||
watch:
|
||||
@if ! command -v fswatch >/dev/null 2>&1; then \
|
||||
echo "❌ Error: fswatch is not installed. Install with: brew install fswatch (macOS) or apt-get install fswatch (Linux)"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "🚀 Starting watch mode with Go ($(IMPL)). Press Ctrl+C to stop."
|
||||
@$(MAKE) build-go IMPL=$(IMPL)
|
||||
@echo "👀 Watching for changes to *.go files..."
|
||||
@fswatch -r -e ".*" -i "\\.go$$" implementations/$(IMPL)/ | while read file; do \
|
||||
echo "🔄 Change detected in $$file"; \
|
||||
$(MAKE) build-go IMPL=$(IMPL); \
|
||||
done
|
||||
@mise run watch $(ARGS)
|
||||
|
||||
# Compare binary sizes for all implementations
|
||||
size-comparison:
|
||||
@echo "=== Binary Size Comparison ==="
|
||||
@echo "| Implementation | Go (KB) | Go Opt (KB) | TinyGo (KB) | TinyGo Opt (KB) | Javy (KB) | Javy Opt (KB) | Best Reduction |"
|
||||
@echo "|---------------|---------|-------------|-------------|-----------------|-----------|---------------|----------------|"
|
||||
@for impl in $$(ls implementations/); do \
|
||||
if [ -f "implementations/$$impl/main.go" ]; then \
|
||||
echo -n "| $$impl | "; \
|
||||
$(MAKE) build-go IMPL=$$impl > /dev/null 2>&1; \
|
||||
go_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \
|
||||
echo -n "$$go_size | "; \
|
||||
$(MAKE) build-go-optimized IMPL=$$impl > /dev/null 2>&1; \
|
||||
go_opt_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \
|
||||
echo -n "$$go_opt_size | "; \
|
||||
if [ "$$impl" = "goja" ]; then \
|
||||
echo -n "N/A* | N/A* | N/A | N/A | "; \
|
||||
reduction=$$(echo "scale=1; ($$go_size - $$go_opt_size) * 100 / $$go_size" | bc -l 2>/dev/null || echo "N/A"); \
|
||||
echo "$$reduction% |"; \
|
||||
else \
|
||||
$(MAKE) build-tinygo IMPL=$$impl > /dev/null 2>&1; \
|
||||
tinygo_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \
|
||||
echo -n "$$tinygo_size | "; \
|
||||
$(MAKE) build-tinygo-optimized IMPL=$$impl > /dev/null 2>&1; \
|
||||
tinygo_opt_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \
|
||||
echo -n "$$tinygo_opt_size | N/A | N/A | "; \
|
||||
reduction=$$(echo "scale=1; ($$go_size - $$tinygo_opt_size) * 100 / $$go_size" | bc -l 2>/dev/null || echo "N/A"); \
|
||||
echo "$$reduction% |"; \
|
||||
fi \
|
||||
elif [ -f "implementations/$$impl/transform.js" ]; then \
|
||||
echo -n "| $$impl | N/A | N/A | N/A | N/A | "; \
|
||||
$(MAKE) build-javy IMPL=$$impl > /dev/null 2>&1; \
|
||||
javy_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \
|
||||
echo -n "$$javy_size | "; \
|
||||
$(MAKE) build-javy-optimized IMPL=$$impl > /dev/null 2>&1; \
|
||||
javy_opt_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \
|
||||
echo -n "$$javy_opt_size | "; \
|
||||
reduction=$$(echo "scale=1; ($$javy_size - $$javy_opt_size) * 100 / $$javy_size" | bc -l 2>/dev/null || echo "N/A"); \
|
||||
echo "$$reduction% |"; \
|
||||
fi \
|
||||
done
|
||||
@echo ""
|
||||
@echo "*Goja implementation doesn't compile with TinyGo due to dependency complexity"
|
||||
@mise run size-comparison
|
||||
gzipped-sizes:
|
||||
@echo "<22><> Measuring gzipped sizes of all WASM binaries..."
|
||||
@node measure-gzipped-sizes.js
|
||||
@mise run gzipped-sizes
|
||||
|
||||
# Build WASM binary with QuickJS (Rust + QuickJS JavaScript engine)
|
||||
build-quickjs:
|
||||
@if [ "$(IMPL)" != "quickjs" ]; then \
|
||||
echo "❌ Error: QuickJS build only supports IMPL=quickjs"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Building WASM binary with QuickJS ($(IMPL) implementation)..."
|
||||
@mkdir -p assets/wasm
|
||||
@echo "Compiling Rust + QuickJS to WASM with WASI target..."
|
||||
@if ! command -v cargo >/dev/null 2>&1; then \
|
||||
echo "❌ Error: Rust/Cargo is not installed. Please install it from https://rustup.rs/"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@cd implementations/$(IMPL) && cargo build --target wasm32-wasip1 --release
|
||||
@echo "Raw size: $$(du -h implementations/$(IMPL)/target/wasm32-wasip1/release/quickjs_transform.wasm | cut -f1)"
|
||||
@echo "Compressing WASM binary..."
|
||||
@gzip -c implementations/$(IMPL)/target/wasm32-wasip1/release/quickjs_transform.wasm > assets/wasm/lib.wasm.gz
|
||||
@echo "✅ QuickJS build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-quickjs $(ARGS)
|
||||
|
||||
# Build WASM binary with QuickJS optimization (already optimized with --release)
|
||||
build-quickjs-optimized:
|
||||
@if [ "$(IMPL)" != "quickjs" ]; then \
|
||||
echo "❌ Error: QuickJS build only supports IMPL=quickjs"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "Building WASM binary with optimized QuickJS ($(IMPL) implementation)..."
|
||||
@echo "Note: QuickJS Rust build is already optimized with --release profile"
|
||||
@$(MAKE) build-quickjs IMPL=$(IMPL)
|
||||
@echo "✅ Optimized QuickJS build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))"
|
||||
@mise run build-quickjs-optimized $(ARGS)
|
||||
|
||||
# Test QuickJS implementation directly
|
||||
test-quickjs:
|
||||
@$(MAKE) build-quickjs IMPL=quickjs > /dev/null 2>&1
|
||||
@echo "Testing QuickJS implementation..."
|
||||
@node implementations/quickjs/quickjs-wasi-test.js 2>/dev/null
|
||||
@mise run test-quickjs $(ARGS)
|
||||
|
||||
# Test WASI-compatible implementations with Wasmer
|
||||
test-wasmer:
|
||||
@echo "🧪 Testing WASI-compatible implementations with Wasmer..."
|
||||
@./test-wasmer.sh
|
||||
@mise run test-wasmer
|
||||
|
||||
# Test individual implementations with Wasmer
|
||||
test-quickjs-wasmer:
|
||||
@$(MAKE) build-quickjs IMPL=quickjs > /dev/null 2>&1
|
||||
@echo "Testing QuickJS with Wasmer..."
|
||||
@cd implementations/quickjs && ./quickjs-wasmer-test.sh
|
||||
@mise run test-quickjs-wasmer
|
||||
|
||||
test-porffor-wasmer:
|
||||
@$(MAKE) build-porffor IMPL=porffor > /dev/null 2>&1
|
||||
@echo "Testing Porffor with Wasmer..."
|
||||
@cd implementations/porffor && ./porffor-wasmer-test.sh
|
||||
@mise run test-porffor-wasmer
|
||||
|
||||
measure-all: size-comparison gzipped-sizes
|
||||
@echo ""
|
||||
@echo "✅ Complete size analysis finished!"
|
||||
measure-all:
|
||||
@mise run measure-all
|
||||
|
||||
@@ -1,53 +1,26 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// This file has been modified for use by the TinyGo compiler.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
// Map multiple JavaScript environments to a single common API,
|
||||
// preferring web standards over Node.js API.
|
||||
//
|
||||
// Environments considered:
|
||||
// - Browsers
|
||||
// - Node.js
|
||||
// - Electron
|
||||
// - Parcel
|
||||
|
||||
if (typeof global !== "undefined") {
|
||||
// global already exists
|
||||
} else if (typeof window !== "undefined") {
|
||||
window.global = window;
|
||||
} else if (typeof self !== "undefined") {
|
||||
self.global = self;
|
||||
} else {
|
||||
throw new Error("cannot export Go (neither global, window nor self is defined)");
|
||||
}
|
||||
|
||||
if (!global.require && typeof require !== "undefined") {
|
||||
global.require = require;
|
||||
}
|
||||
|
||||
if (!global.fs && global.require) {
|
||||
global.fs = require("node:fs");
|
||||
}
|
||||
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!global.fs) {
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
global.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substr(0, nl));
|
||||
outputBuf = outputBuf.substr(nl + 1);
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
@@ -85,8 +58,8 @@
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.process) {
|
||||
global.process = {
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
@@ -100,53 +73,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!global.crypto) {
|
||||
const nodeCrypto = require("node:crypto");
|
||||
global.crypto = {
|
||||
getRandomValues(b) {
|
||||
nodeCrypto.randomFillSync(b);
|
||||
},
|
||||
};
|
||||
if (!globalThis.path) {
|
||||
globalThis.path = {
|
||||
resolve(...pathSegments) {
|
||||
return pathSegments.join("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!global.performance) {
|
||||
global.performance = {
|
||||
now() {
|
||||
const [sec, nsec] = process.hrtime();
|
||||
return sec * 1000 + nsec / 1000000;
|
||||
},
|
||||
};
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!global.TextEncoder) {
|
||||
global.TextEncoder = require("node:util").TextEncoder;
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!global.TextDecoder) {
|
||||
global.TextDecoder = require("node:util").TextDecoder;
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
// End of polyfills for common API.
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let reinterpretBuf = new DataView(new ArrayBuffer(8));
|
||||
var logLine = [];
|
||||
const wasmExit = {}; // thrown to exit via proc_exit (not an error)
|
||||
|
||||
global.Go = class {
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this._callbackTimeouts = new Map();
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const mem = () => {
|
||||
// The buffer may change when requesting more memory.
|
||||
return new DataView(this._inst.exports.memory.buffer);
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const unboxValue = (v_ref) => {
|
||||
reinterpretBuf.setBigInt64(0, v_ref, true);
|
||||
const f = reinterpretBuf.getFloat64(0, true);
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -154,77 +140,69 @@
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = v_ref & 0xffffffffn;
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
const loadValue = (addr) => {
|
||||
let v_ref = mem().getBigUint64(addr, true);
|
||||
return unboxValue(v_ref);
|
||||
}
|
||||
|
||||
const boxValue = (v) => {
|
||||
const nanHead = 0x7FF80000n;
|
||||
|
||||
if (typeof v === "number") {
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
return nanHead << 32n;
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
if (v === 0) {
|
||||
return (nanHead << 32n) | 1n;
|
||||
}
|
||||
reinterpretBuf.setFloat64(0, v, true);
|
||||
return reinterpretBuf.getBigInt64(0, true);
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (v) {
|
||||
case undefined:
|
||||
return 0n;
|
||||
case null:
|
||||
return (nanHead << 32n) | 2n;
|
||||
case true:
|
||||
return (nanHead << 32n) | 3n;
|
||||
case false:
|
||||
return (nanHead << 32n) | 4n;
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = BigInt(this._values.length);
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 1n;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2n;
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3n;
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4n;
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
return id | ((nanHead | typeFlag) << 32n);
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
let v_ref = boxValue(v);
|
||||
mem().setBigUint64(addr, v_ref, true);
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSlice = (array, len, cap) => {
|
||||
return new Uint8Array(this._inst.exports.memory.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (array, len, cap) => {
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
@@ -232,81 +210,109 @@
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (ptr, len) => {
|
||||
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const testCallExport = (a, b) => {
|
||||
this._inst.exports.testExport0();
|
||||
return this._inst.exports.testExport(a, b);
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
wasi_snapshot_preview1: {
|
||||
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
|
||||
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
|
||||
let nwritten = 0;
|
||||
if (fd == 1) {
|
||||
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
|
||||
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
|
||||
let ptr = mem().getUint32(iov_ptr + 0, true);
|
||||
let len = mem().getUint32(iov_ptr + 4, true);
|
||||
nwritten += len;
|
||||
for (let i=0; i<len; i++) {
|
||||
let c = mem().getUint8(ptr+i);
|
||||
if (c == 13) { // CR
|
||||
// ignore
|
||||
} else if (c == 10) { // LF
|
||||
// write line
|
||||
let line = decoder.decode(new Uint8Array(logLine));
|
||||
logLine = [];
|
||||
console.log(line);
|
||||
} else {
|
||||
logLine.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('invalid file descriptor:', fd);
|
||||
}
|
||||
mem().setUint32(nwritten_ptr, nwritten, true);
|
||||
return 0;
|
||||
},
|
||||
fd_close: () => 0, // dummy
|
||||
fd_fdstat_get: () => 0, // dummy
|
||||
fd_seek: () => 0, // dummy
|
||||
proc_exit: (code) => {
|
||||
this.exited = true;
|
||||
this.exitCode = code;
|
||||
this._resolveExitPromise();
|
||||
throw wasmExit;
|
||||
},
|
||||
random_get: (bufPtr, bufLen) => {
|
||||
crypto.getRandomValues(loadSlice(bufPtr, bufLen));
|
||||
return 0;
|
||||
},
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
callExport: testCallExport,
|
||||
},
|
||||
gojs: {
|
||||
// func ticks() float64
|
||||
"runtime.ticks": () => {
|
||||
return timeOrigin + performance.now();
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func sleepTicks(timeout float64)
|
||||
"runtime.sleepTicks": (timeout) => {
|
||||
// Do not sleep, only reactivate scheduler after the given timeout.
|
||||
setTimeout(() => {
|
||||
if (this.exited) return;
|
||||
try {
|
||||
this._inst.exports.go_scheduler();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
}, timeout);
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (v_ref) => {
|
||||
// Note: TinyGo does not support finalizers so this is only called
|
||||
// for one specific case, by js.go:jsString. and can/might leak memory.
|
||||
const id = v_ref & 0xffffffffn;
|
||||
if (this._goRefCounts?.[id] !== undefined) {
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
@@ -314,205 +320,243 @@
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
} else {
|
||||
console.error("syscall/js.finalizeRef: unknown id", id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (value_ptr, value_len) => {
|
||||
value_ptr >>>= 0;
|
||||
const s = loadString(value_ptr, value_len);
|
||||
return boxValue(s);
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => {
|
||||
let prop = loadString(p_ptr, p_len);
|
||||
let v = unboxValue(v_ref);
|
||||
let result = Reflect.get(v, prop);
|
||||
return boxValue(result);
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
const x = unboxValue(x_ref);
|
||||
Reflect.set(v, p, x);
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const p = loadString(p_ptr, p_len);
|
||||
Reflect.deleteProperty(v, p);
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (v_ref, i) => {
|
||||
return boxValue(Reflect.get(unboxValue(v_ref), i));
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => {
|
||||
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref));
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const name = loadString(m_ptr, m_len);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const m = Reflect.get(v, name);
|
||||
storeValue(ret_addr, Reflect.apply(m, v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
storeValue(ret_addr, Reflect.apply(v, undefined, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr + 8, 0);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => {
|
||||
const v = unboxValue(v_ref);
|
||||
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
storeValue(ret_addr, Reflect.construct(v, args));
|
||||
mem().setUint8(ret_addr + 8, 1);
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
storeValue(ret_addr, err);
|
||||
mem().setUint8(ret_addr+ 8, 0);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (v_ref) => {
|
||||
return unboxValue(v_ref).length;
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (ret_addr, v_ref) => {
|
||||
const s = String(unboxValue(v_ref));
|
||||
const str = encoder.encode(s);
|
||||
storeValue(ret_addr, str);
|
||||
mem().setInt32(ret_addr + 8, str.length, true);
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => {
|
||||
const str = unboxValue(v_ref);
|
||||
loadSlice(slice_ptr, slice_len, slice_cap).set(str);
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (v_ref, t_ref) => {
|
||||
return unboxValue(v_ref) instanceof unboxValue(t_ref);
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = loadSlice(dest_addr, dest_len);
|
||||
const src = unboxValue(src_ref);
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
// Originally copied from upstream Go project, then modified:
|
||||
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416
|
||||
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => {
|
||||
let num_bytes_copied_addr = ret_addr;
|
||||
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable
|
||||
|
||||
const dst = unboxValue(dst_ref);
|
||||
const src = loadSlice(src_addr, src_len);
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
mem().setUint8(returned_status_addr, 0); // Return "not ok" status
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
mem().setUint32(num_bytes_copied_addr, toCopy.length, true);
|
||||
mem().setUint8(returned_status_addr, 1); // Return "ok" status
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Go 1.20 uses 'env'. Go 1.21 uses 'gojs'.
|
||||
// For compatibility, we use both as long as Go 1.20 is supported.
|
||||
this.importObject.env = this.importObject.gojs;
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
global,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map(); // mapping from JS values to reference ids
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
this.exitCode = 0;
|
||||
|
||||
if (this._inst.exports._start) {
|
||||
let exitPromise = new Promise((resolve, reject) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// Run program, but catch the wasmExit exception that's thrown
|
||||
// to return back here.
|
||||
try {
|
||||
this._inst.exports._start();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
await exitPromise;
|
||||
return this.exitCode;
|
||||
} else {
|
||||
this._inst.exports._initialize();
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
try {
|
||||
this._inst.exports.resume();
|
||||
} catch (e) {
|
||||
if (e !== wasmExit) throw e;
|
||||
}
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
@@ -528,26 +572,4 @@
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
global.require &&
|
||||
global.require.main === module &&
|
||||
global.process &&
|
||||
global.process.versions &&
|
||||
!global.process.versions.electron
|
||||
) {
|
||||
if (process.argv.length != 3) {
|
||||
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => {
|
||||
let exitCode = await go.run(result.instance);
|
||||
process.exit(exitCode);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
76
implementations/assemblyscript/adapter.js
Normal file
76
implementations/assemblyscript/adapter.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { instantiate } from "@assemblyscript/loader";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Load and instantiate the WASM module
|
||||
export async function loadWasm() {
|
||||
const wasmPath = path.join(__dirname, "build", "release.wasm");
|
||||
const wasmBuffer = await fs.readFile(wasmPath);
|
||||
|
||||
const { exports } = await instantiate(wasmBuffer, {
|
||||
env: {
|
||||
"console.log": (msgPtr) => {
|
||||
console.log(exports.__getString(msgPtr));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
memory,
|
||||
__newString,
|
||||
__getString,
|
||||
transformDataWithTimestamp,
|
||||
healthCheck
|
||||
} = exports;
|
||||
|
||||
return {
|
||||
// Promise-based API for compatibility with tests
|
||||
async transformData(jsonString) {
|
||||
try {
|
||||
// Generate ISO timestamp in JavaScript
|
||||
const isoTimestamp = new Date().toISOString();
|
||||
|
||||
// Marshal strings to/from WASM
|
||||
const jsonPtr = __newString(jsonString);
|
||||
const timestampPtr = __newString(isoTimestamp);
|
||||
const resultPtr = transformDataWithTimestamp(jsonPtr, timestampPtr);
|
||||
const result = __getString(resultPtr);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Match Go behavior on parse errors
|
||||
throw new Error(`failed to parse input JSON: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async healthCheck() {
|
||||
try {
|
||||
const resultPtr = healthCheck();
|
||||
const result = __getString(resultPtr);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`health check failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Export individual functions for testing
|
||||
let wasmInstance = null;
|
||||
|
||||
export async function transformData(jsonString) {
|
||||
if (!wasmInstance) {
|
||||
wasmInstance = await loadWasm();
|
||||
}
|
||||
return wasmInstance.transformData(jsonString);
|
||||
}
|
||||
|
||||
export async function healthCheck() {
|
||||
if (!wasmInstance) {
|
||||
wasmInstance = await loadWasm();
|
||||
}
|
||||
return wasmInstance.healthCheck();
|
||||
}
|
||||
17
implementations/assemblyscript/asconfig.json
Normal file
17
implementations/assemblyscript/asconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"targets": {
|
||||
"release": {
|
||||
"outFile": "build/release.wasm",
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 2,
|
||||
"converge": true,
|
||||
"noAssert": true,
|
||||
"runtime": "stub",
|
||||
"transform": ["json-as/transform"]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"exportRuntime": false,
|
||||
"bindings": "raw"
|
||||
}
|
||||
}
|
||||
42
implementations/assemblyscript/assembly/index.ts
Normal file
42
implementations/assemblyscript/assembly/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { JSON } from "json-as/assembly";
|
||||
|
||||
// Define data structures with @json decorator
|
||||
@json
|
||||
class HealthResponse {
|
||||
status!: string;
|
||||
message!: string;
|
||||
}
|
||||
|
||||
// Health check function
|
||||
export function healthCheck(): string {
|
||||
const response = new HealthResponse();
|
||||
response.status = "healthy";
|
||||
response.message = "AssemblyScript WASM module is running";
|
||||
return JSON.stringify<HealthResponse>(response);
|
||||
}
|
||||
|
||||
// Transform data function with timestamp passed from JS
|
||||
export function transformDataWithTimestamp(jsonStr: string, isoTimestamp: string): string {
|
||||
// Parse arbitrary JSON into a JSON.Value
|
||||
let original: JSON.Value;
|
||||
original = JSON.parse<JSON.Value>(jsonStr);
|
||||
|
||||
// Create wrapper object
|
||||
const wrapper = new JSON.Obj();
|
||||
|
||||
// Populate fields
|
||||
wrapper.set("original", original);
|
||||
wrapper.set("transformed", true);
|
||||
wrapper.set("timestamp", isoTimestamp);
|
||||
wrapper.set("message", "Data has been processed by AssemblyScript WASM");
|
||||
|
||||
// Stringify the complete object
|
||||
return wrapper.toString();
|
||||
}
|
||||
|
||||
// Export with the standard name for compatibility
|
||||
export function transformData(jsonStr: string): string {
|
||||
// For backward compatibility, but this shouldn't be used directly
|
||||
// The JS adapter will use transformDataWithTimestamp
|
||||
return transformDataWithTimestamp(jsonStr, "");
|
||||
}
|
||||
20
implementations/assemblyscript/package.json
Normal file
20
implementations/assemblyscript/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "assemblyscript-json-transform",
|
||||
"version": "1.0.0",
|
||||
"description": "AssemblyScript JSON transformation using json-as",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"asbuild:release": "asc assembly/index.ts --target release --transform json-as/transform",
|
||||
"asbuild": "npm run asbuild:release",
|
||||
"build": "npm run asbuild && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers -o build/module.opt.wasm build/release.wasm",
|
||||
"test": "node test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@assemblyscript/loader": "^0.28.4",
|
||||
"assemblyscript": "^0.28.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"json-as": "^1.1.21"
|
||||
}
|
||||
}
|
||||
59
implementations/assemblyscript/test.js
Normal file
59
implementations/assemblyscript/test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { transformData, healthCheck } from './adapter.js';
|
||||
|
||||
async function runTests() {
|
||||
console.log('🧪 Testing AssemblyScript implementation...\n');
|
||||
|
||||
try {
|
||||
// Test 1: Health check
|
||||
console.log('Test 1: Health check');
|
||||
const health = await healthCheck();
|
||||
const healthObj = JSON.parse(health);
|
||||
console.log('✅ Health:', healthObj);
|
||||
if (healthObj.status !== 'healthy') {
|
||||
throw new Error('Health check failed');
|
||||
}
|
||||
|
||||
// Test 2: Simple object transformation
|
||||
console.log('\nTest 2: Simple object');
|
||||
const simple = await transformData('{"name":"test","value":42}');
|
||||
const simpleObj = JSON.parse(simple);
|
||||
console.log('✅ Simple:', simpleObj);
|
||||
if (simpleObj.original.name !== 'test' || simpleObj.original.value !== 42) {
|
||||
throw new Error('Simple object test failed');
|
||||
}
|
||||
|
||||
// Test 3: Array transformation
|
||||
console.log('\nTest 3: Array');
|
||||
const array = await transformData('[1,2,3,"test"]');
|
||||
const arrayObj = JSON.parse(array);
|
||||
console.log('✅ Array:', arrayObj);
|
||||
if (!Array.isArray(arrayObj.original) || arrayObj.original.length !== 4) {
|
||||
throw new Error('Array test failed');
|
||||
}
|
||||
|
||||
// Test 4: Nested object
|
||||
console.log('\nTest 4: Nested object');
|
||||
const nested = await transformData('{"user":{"id":1,"name":"John"},"meta":{"version":"1.0"}}');
|
||||
const nestedObj = JSON.parse(nested);
|
||||
console.log('✅ Nested:', nestedObj);
|
||||
if (nestedObj.original.user.name !== 'John') {
|
||||
throw new Error('Nested object test failed');
|
||||
}
|
||||
|
||||
// Test 5: Invalid JSON
|
||||
console.log('\nTest 5: Invalid JSON');
|
||||
try {
|
||||
await transformData('invalid json {');
|
||||
throw new Error('Should have thrown on invalid JSON');
|
||||
} catch (error) {
|
||||
console.log('✅ Invalid JSON correctly rejected:', error.message);
|
||||
}
|
||||
|
||||
console.log('\n✅ All tests passed!');
|
||||
} catch (error) {
|
||||
console.error('\n❌ Test failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTests().catch(console.error);
|
||||
@@ -48,21 +48,36 @@ func promisify(fn func([]js.Value) (string, error)) js.Func {
|
||||
})
|
||||
}
|
||||
|
||||
// transformData takes a JSON string and JavaScript code, processes it using Goja JavaScript engine, and returns a JSON string
|
||||
// transformData takes a JSON string, processes it using Goja JavaScript engine with a default transform, and returns a JSON string
|
||||
func transformData(args []js.Value) (string, error) {
|
||||
js.Global().Get("console").Call("log", "🔄 transformData called with", len(args), "arguments")
|
||||
|
||||
if len(args) < 2 {
|
||||
return "", fmt.Errorf("expected two arguments: JSON string and JavaScript code")
|
||||
if len(args) < 1 {
|
||||
return "", fmt.Errorf("expected at least one argument (JSON string)")
|
||||
}
|
||||
|
||||
// Get the input JSON string
|
||||
inputJSON := args[0].String()
|
||||
js.Global().Get("console").Call("log", "📥 Input JSON:", inputJSON)
|
||||
|
||||
// Get the JavaScript transformation code
|
||||
transformJS := args[1].String()
|
||||
// Use a default transformation if no JavaScript code is provided
|
||||
var transformJS string
|
||||
if len(args) >= 2 {
|
||||
transformJS = args[1].String()
|
||||
js.Global().Get("console").Call("log", "📜 JavaScript code length:", len(transformJS), "characters")
|
||||
} else {
|
||||
// Default transformation that matches the test expectations
|
||||
transformJS = `
|
||||
function transform(data) {
|
||||
return {
|
||||
original: data,
|
||||
transformed: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: "Data has been processed by Go WASM"
|
||||
};
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
// Parse the input JSON
|
||||
var inputData interface{}
|
||||
@@ -118,7 +133,7 @@ func main() {
|
||||
// Add a simple health check function
|
||||
js.Global().Set("healthCheck", promisify(func(args []js.Value) (string, error) {
|
||||
js.Global().Get("console").Call("log", "💓 Health check called")
|
||||
return `{"status": "healthy", "message": "Go WASM module with Goja is running"}`, nil
|
||||
return `{"status": "healthy", "message": "Go WASM module is running"}`, nil
|
||||
}))
|
||||
|
||||
js.Global().Get("console").Call("log", "✅ Functions exposed to JavaScript:")
|
||||
|
||||
@@ -3,9 +3,9 @@ name = "quickjs-transform"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
name = "quickjs_transform"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
rquickjs = { version = "0.6", default-features = false, features = ["bindgen"] }
|
||||
|
||||
@@ -29,8 +29,9 @@ const importObject = {
|
||||
const wasmModule = await WebAssembly.instantiate(wasmBytes, importObject);
|
||||
const wasmInstance = wasmModule.instance;
|
||||
|
||||
// Initialize WASI - now that we have a _start function, we can call start
|
||||
wasi.start(wasmInstance);
|
||||
// Initialize WASI even though we don't have _start
|
||||
// This is needed for memory operations to work properly
|
||||
wasi.initialize(wasmInstance);
|
||||
|
||||
// Helper functions to work with C strings
|
||||
function allocateString(wasmInstance, str) {
|
||||
|
||||
96
implementations/quickjs/src/lib.rs
Normal file
96
implementations/quickjs/src/lib.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use rquickjs::{Context, Runtime, Value};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn execute_js_internal(js_code: &str, input_data: &str) -> Result<String, String> {
|
||||
let rt = Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?;
|
||||
let ctx = Context::full(&rt).map_err(|e| format!("Failed to create context: {}", e))?;
|
||||
|
||||
ctx.with(|ctx| {
|
||||
// Set up the input data as a global variable
|
||||
let setup_code = format!("const inputData = `{}`;", input_data.replace('`', r#"\`"#));
|
||||
if let Err(e) = ctx.eval::<(), _>(setup_code.as_bytes()) {
|
||||
return Err(format!("Failed to set up input data: {:?}", e));
|
||||
}
|
||||
|
||||
// Execute the user's JavaScript code
|
||||
let result: Result<Value, _> = ctx.eval(js_code.as_bytes());
|
||||
|
||||
match result {
|
||||
Ok(value) => {
|
||||
if let Some(s) = value.as_string() {
|
||||
Ok(s.to_string().unwrap_or_default())
|
||||
} else if let Some(n) = value.as_number() {
|
||||
Ok(n.to_string())
|
||||
} else if let Some(b) = value.as_bool() {
|
||||
Ok(b.to_string())
|
||||
} else if value.is_null() {
|
||||
Ok("null".to_string())
|
||||
} else if value.is_undefined() {
|
||||
Ok("undefined".to_string())
|
||||
} else {
|
||||
Ok(format!("{:?}", value))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!("JavaScript execution error: {:?}", e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// transform_data that accepts a null-terminated C string.
|
||||
/// Returns a newly allocated C string (must be freed with free_string).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn transform_data(input_ptr: *const c_char) -> *mut c_char {
|
||||
if input_ptr.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
// Safety: input_ptr must point to a valid C string.
|
||||
let input = unsafe { CStr::from_ptr(input_ptr) }.to_string_lossy().into_owned();
|
||||
// Default transform: JSON parse then stringify (no-op normalize)
|
||||
let js_code = "JSON.stringify(JSON.parse(inputData))";
|
||||
match execute_js_internal(js_code, &input) {
|
||||
Ok(s) => CString::new(s).map(|c| c.into_raw()).unwrap_or(std::ptr::null_mut()),
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// transform_data that accepts (ptr, len) input.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn transform_data_len(ptr: *const u8, len: usize) -> *mut c_char {
|
||||
if ptr.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
// Safety: ptr must point to a valid buffer of length len.
|
||||
let input = unsafe { std::slice::from_raw_parts(ptr, len) };
|
||||
let input = String::from_utf8_lossy(input).into_owned();
|
||||
let js_code = "JSON.stringify(JSON.parse(inputData))";
|
||||
match execute_js_internal(js_code, &input) {
|
||||
Ok(s) => CString::new(s).map(|c| c.into_raw()).unwrap_or(std::ptr::null_mut()),
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute arbitrary JS with inputData available as a global.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn execute_js(js_ptr: *const c_char, input_ptr: *const c_char) -> *mut c_char {
|
||||
if js_ptr.is_null() || input_ptr.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
let js = unsafe { CStr::from_ptr(js_ptr) }.to_string_lossy().into_owned();
|
||||
let input = unsafe { CStr::from_ptr(input_ptr) }.to_string_lossy().into_owned();
|
||||
match execute_js_internal(&js, &input) {
|
||||
Ok(s) => CString::new(s).map(|c| c.into_raw()).unwrap_or(std::ptr::null_mut()),
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Free a C string previously returned by this module.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn free_string(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
// Safety: must only be called on pointers returned by our functions.
|
||||
unsafe { let _ = CString::from_raw(ptr); }
|
||||
}
|
||||
}
|
||||
|
||||
// Note: malloc is already provided by the libc runtime, so we don't define our own
|
||||
@@ -1,67 +0,0 @@
|
||||
use rquickjs::{Runtime, Context, Value};
|
||||
use std::env;
|
||||
use std::io::{self, Read};
|
||||
|
||||
fn execute_js(js_code: &str, input_data: &str) -> Result<String, String> {
|
||||
let rt = Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?;
|
||||
let ctx = Context::full(&rt).map_err(|e| format!("Failed to create context: {}", e))?;
|
||||
|
||||
ctx.with(|ctx| {
|
||||
// Set up the input data as a global variable
|
||||
let setup_code = format!("const inputData = `{}`;", input_data.replace('`', r#"\`"#));
|
||||
if ctx.eval::<(), _>(setup_code.as_bytes()).is_err() {
|
||||
return Err("Failed to set up input data".to_string());
|
||||
}
|
||||
|
||||
// Execute the user's JavaScript code
|
||||
let result: Result<Value, _> = ctx.eval(js_code.as_bytes());
|
||||
|
||||
match result {
|
||||
Ok(value) => {
|
||||
if let Some(s) = value.as_string() {
|
||||
Ok(s.to_string().unwrap_or_default())
|
||||
} else if let Some(n) = value.as_number() {
|
||||
Ok(n.to_string())
|
||||
} else if let Some(b) = value.as_bool() {
|
||||
Ok(b.to_string())
|
||||
} else if value.is_null() {
|
||||
Ok("null".to_string())
|
||||
} else if value.is_undefined() {
|
||||
Ok("undefined".to_string())
|
||||
} else {
|
||||
Ok(format!("{:?}", value))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!("JavaScript execution error: {:?}", e))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
// Require JavaScript code as argument
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: wasmer run quickjs.wasm 'JavaScript code' [input_data]");
|
||||
eprintln!("Example: echo '{{\"test\": 123}}' | wasmer run quickjs.wasm 'JSON.stringify({{result: JSON.parse(inputData)}})'");
|
||||
return;
|
||||
}
|
||||
|
||||
let js_code = args[1].clone();
|
||||
|
||||
// Read input data from stdin
|
||||
let mut input_data = String::new();
|
||||
if let Err(e) = io::stdin().read_to_string(&mut input_data) {
|
||||
eprintln!("Error reading input: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trim whitespace from input
|
||||
input_data = input_data.trim().to_string();
|
||||
|
||||
// Execute JavaScript with the input data
|
||||
match execute_js(&js_code, &input_data) {
|
||||
Ok(result) => println!("{}", result),
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^11.0.3",
|
||||
"javy": "^0.1.2",
|
||||
"porffor": "^0.60.6",
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -13,24 +13,25 @@ export function callGoFunction(fn: any, ...args: any[]): Promise<string> {
|
||||
// Check if current build is using Javy
|
||||
async function isJavyBuild(): Promise<boolean> {
|
||||
try {
|
||||
// Check if javy-adapter.js exists in the current implementation
|
||||
const javyAdapterPath = path.join(
|
||||
// Check if the Javy dynamic WASM exists in the expected location
|
||||
// This is only present when Javy build was run
|
||||
const javyDynamicPath = path.join(
|
||||
__dirname,
|
||||
"implementations/javy/javy-adapter.js"
|
||||
"implementations/javy/transform_dynamic.wasm"
|
||||
);
|
||||
await access(javyAdapterPath);
|
||||
await access(javyDynamicPath);
|
||||
|
||||
// Additionally check if the current lib.wasm.gz was built from Javy
|
||||
// by checking if plugin.wasm exists (only created by Javy build)
|
||||
const javyPluginPath = path.join(
|
||||
__dirname,
|
||||
"implementations/javy/plugin.wasm"
|
||||
);
|
||||
await access(javyPluginPath);
|
||||
|
||||
// Also check if the current symlink points to javy implementation
|
||||
const mainGoPath = path.join(__dirname, "main.go");
|
||||
try {
|
||||
const mainGoContent = await readFile(mainGoPath, "utf8");
|
||||
// This is a simple heuristic - in a real implementation you might check the symlink target
|
||||
return false; // Go implementations will have main.go
|
||||
} catch {
|
||||
// If main.go doesn't exist, might be a Javy build
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// If Javy artifacts don't exist, this is not a Javy build
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user