Skip to content

Commit 68aaf2f

Browse files
committed
Updated playground
1 parent 5b036b2 commit 68aaf2f

50 files changed

Lines changed: 7048 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ Thumbs.db
1818
vscode-extension/node_modules/
1919
vscode-extension/out/
2020
vscode-extension/*.vsix
21+
22+
# Playground
23+
playground/frontend/node_modules/
24+
playground/frontend/dist/
25+
playground/frontend/src/wasm-pkg/
26+
playground/frontend/.vite/

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ members = [
88
"crates/solscript-cli",
99
"crates/solscript-lsp",
1010
"crates/solscript-bpf",
11+
"playground/wasm",
12+
"playground/backend",
1113
]
1214

1315
[workspace.package]

playground/Dockerfile

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Stage 1: Build WASM
2+
FROM rust:1.83-bookworm AS wasm-builder
3+
RUN cargo install wasm-pack
4+
WORKDIR /app
5+
COPY Cargo.toml Cargo.lock ./
6+
COPY crates/ crates/
7+
COPY playground/wasm/ playground/wasm/
8+
RUN cd playground/wasm && wasm-pack build --target web --release --out-dir ../frontend/src/wasm-pkg
9+
10+
# Stage 2: Build Vue frontend
11+
FROM node:20-alpine AS frontend-builder
12+
WORKDIR /app
13+
COPY playground/frontend/package*.json ./
14+
RUN npm ci
15+
COPY playground/frontend/ ./
16+
COPY --from=wasm-builder /app/playground/frontend/src/wasm-pkg/ ./src/wasm-pkg/
17+
RUN npm run build
18+
19+
# Stage 3: Build Axum backend
20+
FROM rust:1.83-bookworm AS backend-builder
21+
WORKDIR /app
22+
COPY Cargo.toml Cargo.lock ./
23+
COPY crates/ crates/
24+
COPY playground/backend/ playground/backend/
25+
COPY playground/wasm/ playground/wasm/
26+
RUN cargo build --release -p solscript-playground
27+
28+
# Stage 4: Runtime
29+
FROM debian:bookworm-slim AS runtime
30+
31+
RUN apt-get update && apt-get install -y \
32+
ca-certificates \
33+
curl \
34+
build-essential \
35+
pkg-config \
36+
libssl-dev \
37+
&& rm -rf /var/lib/apt/lists/*
38+
39+
# Install Solana CLI
40+
RUN sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" && \
41+
echo 'export PATH="/root/.local/share/solana/install/active_release/bin:$PATH"' >> /root/.bashrc
42+
ENV PATH="/root/.local/share/solana/install/active_release/bin:${PATH}"
43+
44+
# Install Rust toolchain for cargo-build-sbf
45+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.83.0
46+
ENV PATH="/root/.cargo/bin:${PATH}"
47+
48+
# Copy backend binary
49+
COPY --from=backend-builder /app/target/release/solscript-playground /usr/local/bin/
50+
51+
# Copy frontend static files
52+
COPY --from=frontend-builder /app/dist/ /app/static/
53+
54+
ENV STATIC_DIR=/app/static
55+
ENV PORT=3000
56+
ENV RUST_LOG=info
57+
58+
EXPOSE 3000
59+
60+
CMD ["solscript-playground"]

playground/backend/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "solscript-playground"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Web backend for the SolScript Playground"
6+
7+
[[bin]]
8+
name = "solscript-playground"
9+
path = "src/main.rs"
10+
11+
[dependencies]
12+
solscript-parser = { path = "../../crates/solscript-parser" }
13+
solscript-typeck = { path = "../../crates/solscript-typeck" }
14+
solscript-codegen = { path = "../../crates/solscript-codegen" }
15+
16+
axum = { version = "0.7", features = ["json"] }
17+
tokio = { version = "1", features = ["full"] }
18+
tower = { version = "0.5", features = ["limit", "timeout"] }
19+
tower-http = { version = "0.6", features = ["fs", "cors"] }
20+
serde = { version = "1.0", features = ["derive"] }
21+
serde_json = "1.0"
22+
uuid = { version = "1", features = ["v4"] }
23+
base64 = "0.22"
24+
tracing = "0.1"
25+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
26+
miette = { version = "7.0", default-features = false }

playground/backend/src/main.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
mod routes;
2+
mod sandbox;
3+
4+
use axum::{
5+
routing::{get, post},
6+
Router,
7+
};
8+
use std::net::SocketAddr;
9+
use tower_http::cors::CorsLayer;
10+
use tower_http::services::{ServeDir, ServeFile};
11+
use tracing_subscriber::EnvFilter;
12+
13+
#[tokio::main]
14+
async fn main() {
15+
tracing_subscriber::fmt()
16+
.with_env_filter(EnvFilter::from_default_env().add_directive("info".parse().unwrap()))
17+
.init();
18+
19+
let static_dir =
20+
std::env::var("STATIC_DIR").unwrap_or_else(|_| "../frontend/dist".to_string());
21+
22+
let api_routes = Router::new()
23+
.route("/api/build", post(routes::build))
24+
.route("/api/health", get(routes::health));
25+
26+
let spa_fallback = ServeFile::new(format!("{}/index.html", static_dir));
27+
28+
let app = api_routes
29+
.fallback_service(ServeDir::new(&static_dir).fallback(spa_fallback))
30+
.layer(CorsLayer::permissive());
31+
32+
let port: u16 = std::env::var("PORT")
33+
.unwrap_or_else(|_| "3000".to_string())
34+
.parse()
35+
.unwrap_or(3000);
36+
37+
let addr = SocketAddr::from(([0, 0, 0, 0], port));
38+
tracing::info!("SolScript Playground listening on {}", addr);
39+
40+
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
41+
axum::serve(listener, app).await.unwrap();
42+
}

playground/backend/src/routes.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use axum::{http::StatusCode, Json};
2+
use serde::{Deserialize, Serialize};
3+
4+
use crate::sandbox;
5+
6+
#[derive(Deserialize)]
7+
pub struct BuildRequest {
8+
pub source: String,
9+
}
10+
11+
#[derive(Serialize)]
12+
pub struct BuildResponse {
13+
pub success: bool,
14+
#[serde(skip_serializing_if = "Option::is_none")]
15+
pub bytecode: Option<String>,
16+
#[serde(skip_serializing_if = "Option::is_none")]
17+
pub idl: Option<String>,
18+
pub errors: Vec<BuildError>,
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
pub build_time_secs: Option<f64>,
21+
}
22+
23+
#[derive(Serialize)]
24+
pub struct BuildError {
25+
pub phase: String,
26+
pub message: String,
27+
#[serde(skip_serializing_if = "Option::is_none")]
28+
pub start: Option<usize>,
29+
#[serde(skip_serializing_if = "Option::is_none")]
30+
pub end: Option<usize>,
31+
}
32+
33+
#[derive(Serialize)]
34+
pub struct HealthResponse {
35+
pub status: String,
36+
pub version: String,
37+
}
38+
39+
pub async fn health() -> Json<HealthResponse> {
40+
Json(HealthResponse {
41+
status: "ok".to_string(),
42+
version: env!("CARGO_PKG_VERSION").to_string(),
43+
})
44+
}
45+
46+
pub async fn build(Json(payload): Json<BuildRequest>) -> (StatusCode, Json<BuildResponse>) {
47+
// Validate source size (100KB max)
48+
if payload.source.len() > 100_000 {
49+
return (
50+
StatusCode::BAD_REQUEST,
51+
Json(BuildResponse {
52+
success: false,
53+
bytecode: None,
54+
idl: None,
55+
errors: vec![BuildError {
56+
phase: "validation".to_string(),
57+
message: "Source code exceeds maximum size of 100KB".to_string(),
58+
start: None,
59+
end: None,
60+
}],
61+
build_time_secs: None,
62+
}),
63+
);
64+
}
65+
66+
match sandbox::compile_source(&payload.source).await {
67+
Ok(result) => (StatusCode::OK, Json(result)),
68+
Err(response) => (StatusCode::INTERNAL_SERVER_ERROR, Json(response)),
69+
}
70+
}

playground/backend/src/sandbox.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use miette::Diagnostic;
2+
use std::time::Instant;
3+
4+
use crate::routes::{BuildError, BuildResponse};
5+
6+
/// Compile SolScript source code through the full pipeline.
7+
/// Currently generates Anchor code only (no BPF compilation without Solana toolchain).
8+
/// BPF compilation will be added when running inside Docker with cargo-build-sbf available.
9+
pub async fn compile_source(source: &str) -> Result<BuildResponse, BuildResponse> {
10+
let start = Instant::now();
11+
12+
// Parse
13+
let program = match solscript_parser::parse(source) {
14+
Ok(prog) => prog,
15+
Err(err) => {
16+
let (offset, len) = if let Some(labels) = err.labels() {
17+
let labels: Vec<_> = labels.collect();
18+
if let Some(label) = labels.first() {
19+
(Some(label.offset()), Some(label.offset() + label.len()))
20+
} else {
21+
(None, None)
22+
}
23+
} else {
24+
(None, None)
25+
};
26+
27+
return Err(BuildResponse {
28+
success: false,
29+
bytecode: None,
30+
idl: None,
31+
errors: vec![BuildError {
32+
phase: "parse".to_string(),
33+
message: err.to_string(),
34+
start: offset,
35+
end: len,
36+
}],
37+
build_time_secs: Some(start.elapsed().as_secs_f64()),
38+
});
39+
}
40+
};
41+
42+
// Type check
43+
if let Err(errors) = solscript_typeck::typecheck(&program, source) {
44+
let build_errors: Vec<BuildError> = errors
45+
.iter()
46+
.map(|err| {
47+
let (offset, len) = if let Some(labels) = err.labels() {
48+
let labels: Vec<_> = labels.collect();
49+
if let Some(label) = labels.first() {
50+
(Some(label.offset()), Some(label.offset() + label.len()))
51+
} else {
52+
(None, None)
53+
}
54+
} else {
55+
(None, None)
56+
};
57+
BuildError {
58+
phase: "typecheck".to_string(),
59+
message: err.to_string(),
60+
start: offset,
61+
end: len,
62+
}
63+
})
64+
.collect();
65+
66+
return Err(BuildResponse {
67+
success: false,
68+
bytecode: None,
69+
idl: None,
70+
errors: build_errors,
71+
build_time_secs: Some(start.elapsed().as_secs_f64()),
72+
});
73+
}
74+
75+
// Generate code
76+
let project = match solscript_codegen::generate(&program) {
77+
Ok(proj) => proj,
78+
Err(err) => {
79+
return Err(BuildResponse {
80+
success: false,
81+
bytecode: None,
82+
idl: None,
83+
errors: vec![BuildError {
84+
phase: "codegen".to_string(),
85+
message: err.to_string(),
86+
start: None,
87+
end: None,
88+
}],
89+
build_time_secs: Some(start.elapsed().as_secs_f64()),
90+
});
91+
}
92+
};
93+
94+
// TODO: When running in Docker with cargo-build-sbf, write project to temp dir,
95+
// run cargo build-sbf, read the .so file, and return base64-encoded bytecode.
96+
// For now, return the IDL and generated code as confirmation of successful compilation.
97+
98+
Ok(BuildResponse {
99+
success: true,
100+
bytecode: None, // Will be populated when BPF build is available
101+
idl: Some(project.idl_json),
102+
errors: vec![],
103+
build_time_secs: Some(start.elapsed().as_secs_f64()),
104+
})
105+
}

playground/captain-definition

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"schemaVersion": 2,
3+
"dockerfilePath": "./playground/Dockerfile"
4+
}

playground/frontend/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>SolScript Playground</title>
8+
</head>
9+
<body class="bg-gray-900 text-gray-100">
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.ts"></script>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)