Pure client-side, in-browser code execution engine.
Run JavaScript, TypeScript, Python, HTML/CSS/JS, C#, Java, PHP, Dart, and Go entirely in the browser with zero backend.
Live Demo → yaman-cyber.github.io/clientbox
npm install clientboximport { ClientBox } from "clientbox";
const box = new ClientBox();
const result = await box.run("node", {
files: { "/index.js": 'console.log("Hello from clientbox!")' },
entryPoint: "/index.js",
});
console.log(result.stdout); // "Hello from clientbox!"
box.destroy();| Language | Key | Runtime |
|---|---|---|
| JavaScript | node |
Web Worker + virtual FS |
| TypeScript | node |
Web Worker + type-stripping |
| Python | python |
Pyodide (CDN, ~12 MB first load) |
| HTML/CSS/JS | web |
Sandboxed iframe |
| C# | csharp |
Transpiler (C#-to-JS in iframe) |
| Java | java |
CheerpJ (CDN) + transpiler fallback |
| PHP | php |
Transpiler (PHP-to-JS in iframe) |
| Dart | dart |
Transpiler (Dart-to-JS in iframe) |
| Go | go |
Transpiler (Go-to-JS in iframe) |
Multi-file with require:
const result = await box.run("node", {
files: {
"/index.js": `
const { greet } = require('./utils');
console.log(greet('world'));
`,
"/utils.js": `
module.exports = {
greet: (name) => \`Hello, \${name}!\`,
};
`,
},
entryPoint: "/index.js",
});
// stdout: "Hello, world!"ESM imports are also supported:
const result = await box.run("node", {
files: {
"/index.js": `
import { add } from './math.js';
console.log(add(2, 3));
`,
"/math.js": `
export const add = (a, b) => a + b;
`,
},
entryPoint: "/index.js",
});
// stdout: "5"const result = await box.run("python", {
files: {
"/main.py": `
import utils
for i in range(5):
print(utils.square(i))
`,
"/utils.py": `
def square(n):
return n * n
`,
},
entryPoint: "/main.py",
});
// stdout: "0\n1\n4\n9\n16"const result = await box.run("web", {
files: {
"/index.html": `
<!DOCTYPE html>
<html>
<head><link rel="stylesheet" href="style.css"></head>
<body>
<h1>Hello</h1>
<script src="app.js"></script>
</body>
</html>
`,
"/style.css": "h1 { color: blue; }",
"/app.js": 'console.log("page loaded");',
},
entryPoint: "/index.html",
});
// stdout: "page loaded"const result = await box.run("csharp", {
files: {
"/Program.cs": `
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello from C#!");
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"i = {i}");
}
}
}
`,
},
entryPoint: "/Program.cs",
});
// stdout: "Hello from C#!\ni = 0\ni = 1\ni = 2\ni = 3\ni = 4"const result = await box.run("java", {
files: {
"/Main.java": `
public class Main {
public static void main(String[] args) {
System.out.println("Hello from Java!");
for (int i = 0; i < 5; i++) {
System.out.println("i = " + i);
}
}
}
`,
},
entryPoint: "/Main.java",
});
// stdout: "Hello from Java!\ni = 0\ni = 1\ni = 2\ni = 3\ni = 4"const result = await box.run("php", {
files: {
"/index.php": `
<?php
function factorial($n) {
if ($n <= 1) return 1;
return $n * factorial($n - 1);
}
echo "Hello from PHP!\\n";
for ($i = 1; $i <= 5; $i++) {
echo "Factorial of $i = " . factorial($i) . "\\n";
}
?>
`,
},
entryPoint: "/index.php",
});
// stdout: "Hello from PHP!\nFactorial of 1 = 1\n..."const result = await box.run("dart", {
files: {
"/main.dart": `
void main() {
print('Hello from Dart!');
for (int i = 1; i <= 5; i++) {
print('Fibonacci(\$i) = \${fib(i)}');
}
}
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
`,
},
entryPoint: "/main.dart",
});
// stdout: "Hello from Dart!\nFibonacci(1) = 1\n..."const result = await box.run("go", {
files: {
"/main.go": `
package main
import "fmt"
func main() {
for i := 1; i <= 5; i++ {
fmt.Printf("%d! = %d\\n", i, factorial(i))
}
}
func factorial(n int) int {
if n <= 1 { return 1 }
return n * factorial(n-1)
}
`,
},
entryPoint: "/main.go",
});
// stdout: "1! = 1\n2! = 2\n3! = 6\n4! = 24\n5! = 120"User code can request input at runtime (e.g. Python's input(), C#'s Console.ReadLine(), Java's Scanner). Pass an onInput callback to run() and the runner will pause execution until you return a value. Pair it with onStdout / onStderr so the prompt text actually streams to your UI before the program blocks on input.
const result = await box.run("python", {
files: {
"/main.py": `
name = input("What is your name? ")
print(f"Hello, {name}!")
`,
},
entryPoint: "/main.py",
onStdout: (chunk) => process.stdout.write(chunk), // or terminal.write(chunk) in browser
onStderr: (chunk) => process.stderr.write(chunk),
onInput: (prompt) => window.prompt(prompt) ?? null, // return null to signal EOF
});onInput is invoked with the stdout produced since the last input request, which is typically the prompt text the program just wrote. Return a string (no trailing newline needed) or null for EOF.
You can still pre-feed input as a string. Pre-supplied lines are consumed first; onInput is only called once the pre-supplied input is exhausted.
await box.run("python", {
files: { "/main.py": "a = input(); b = input(); print(int(a) + int(b))" },
entryPoint: "/main.py",
stdin: "2\n3", // both inputs supplied — onInput never fires
});onStdout / onStderr work independently. Use them to render output as the program produces it, instead of waiting for the final aggregated result.stdout:
await box.run("python", {
files: {
"/main.py":
"import time\nfor i in range(5):\n print(i); time.sleep(0.2)",
},
entryPoint: "/main.py",
onStdout: (chunk) => appendToTerminal(chunk),
});The host-facing API is the same for every language — only the user code differs:
| Language | What the user code calls |
|---|---|
| Python | input(prompt) |
| Node/TS | prompt(message) (exposed as a global by the Node runner) |
| C# | Console.ReadLine() |
| Java | new Scanner(System.in).nextLine() / nextInt() / nextDouble() |
| PHP | readline($prompt) or fgets(STDIN) |
| Dart | stdin.readLineSync() |
| Go | bufio.NewReader(os.Stdin).ReadString('\n'), fmt.Scanln(&x) |
| Web | window.prompt() works natively inside the iframe — no onInput wiring needed |
Python and Node make input look synchronous to user code by blocking the runtime's Web Worker on a SharedArrayBuffer. Browsers only expose SharedArrayBuffer when the host page is cross-origin isolated. Serve the page with these response headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
(require-corp works too, but credentialless is friendlier — it doesn't force every cross-origin resource to send Cross-Origin-Resource-Policy.)
If the headers are missing, calling box.run("python" | "node", ...) with an onInput callback rejects with a clear error telling you what to fix. Iframe-based runners (C#, Java, PHP, Dart, Go) use async postMessage instead of SharedArrayBuffer and have no such requirement.
// Quick check before calling run()
if (!window.crossOriginIsolated) {
console.warn("Interactive Python/Node input requires COOP/COEP headers");
}If you're deploying to a static host you don't control (GitHub Pages, Netlify free tier, etc.), a small service worker can inject the headers client-side. Drop the coi-serviceworker.js file in this repo at your site root and register it before anything else loads:
<head>
<!-- Must come before any other script that needs SharedArrayBuffer -->
<script src="/coi-serviceworker.js"></script>
...
</head>On first visit the page reloads once to activate the worker. After that, window.crossOriginIsolated === true and Python/Node interactive input works. The bundle docs/index.html demos both use this approach.
Try the interactive playground at yaman-cyber.github.io/clientbox -- no install required, runs entirely in your browser.
Create a new execution engine instance.
interface ClientBoxConfig {
timeout?: number; // Default execution timeout (ms). Default: 30000
pyodideCdnUrl?: string; // Override Pyodide CDN base URL
cheerpjCdnUrl?: string; // Override CheerpJ loader script URL
dotnetCdnUrl?: string; // Override .NET WASM CDN base URL
onStatusChange?: (event: StatusEvent) => void;
}Execute code and return the result.
type Language =
| "node"
| "python"
| "web"
| "csharp"
| "java"
| "php"
| "dart"
| "go";
interface RunOptions {
files: Record<string, string>; // Virtual file path -> content
entryPoint: string; // File to execute
stdin?: string; // Pre-supplied stdin (consumed before onInput is called)
timeout?: number; // Override timeout for this run
// Interactive input: called when the program requests a line of input.
// Return a string for the next line, or null to signal EOF.
// The `prompt` argument is the stdout produced since the last input request.
// See the "Interactive input" section above for per-language details and the
// cross-origin isolation requirement for the python/node runners.
onInput?: (prompt: string) => string | null | Promise<string | null>;
// Streamed output, chunk by chunk, as the program produces it.
// The final aggregated values are still returned in RunResult.stdout/stderr.
onStdout?: (chunk: string) => void;
onStderr?: (chunk: string) => void;
}
interface RunResult {
stdout: string; // Captured standard output
stderr: string; // Captured standard error
error: string | null;
exitCode: number; // 0 = success
duration: number; // Wall-clock ms
}Tear down all workers and iframes. The instance cannot be reused after this.
clientbox is a zero-dependency TypeScript library that works with any framework or vanilla JS.
"use client";
import { useCallback, useRef } from "react";
import { ClientBox } from "clientbox";
export function CodeRunner() {
const boxRef = useRef<ClientBox | null>(null);
const runCode = useCallback(async () => {
if (!boxRef.current) boxRef.current = new ClientBox();
const result = await boxRef.current.run("node", {
files: { "/index.js": 'console.log("hello")' },
entryPoint: "/index.js",
});
console.log(result.stdout);
}, []);
return <button onClick={runCode}>Run</button>;
}<script setup lang="ts">
import { ref, onUnmounted } from "vue";
import { ClientBox } from "clientbox";
const box = new ClientBox();
const output = ref("");
async function run() {
const result = await box.run("python", {
files: { "/main.py": 'print("Hello from Python!")' },
entryPoint: "/main.py",
});
output.value = result.stdout;
}
onUnmounted(() => box.destroy());
</script>
<template>
<button @click="run">Run Python</button>
<pre>{{ output }}</pre>
</template><script lang="ts">
import { ClientBox } from 'clientbox';
import { onDestroy } from 'svelte';
const box = new ClientBox();
let output = '';
async function run() {
const result = await box.run('node', {
files: { '/index.js': 'console.log(2 + 2)' },
entryPoint: '/index.js',
});
output = result.stdout;
}
onDestroy(() => box.destroy());
</script>
<button on:click={run}>Run</button>
<pre>{output}</pre>| Language | Execution strategy |
|---|---|
node |
Web Worker with patched console, custom require(), ESM import, and in-memory virtual FS. TypeScript files are type-stripped before execution. |
python |
Pyodide (CPython compiled to WebAssembly) running in a Web Worker. Loaded from CDN on first use (~12 MB). |
web |
Sandboxed <iframe> with sandbox="allow-scripts". CSS/JS files are inlined via blob URLs. |
csharp |
Iframe harness that transpiles C# to JS. Handles Console.WriteLine, string interpolation, foreach, multi-file class resolution, and more. |
java |
Iframe with CheerpJ (WebAssembly JVM) from CDN + transpiler fallback. Handles System.out, typed variables, for-each, multi-class projects. |
php |
Iframe harness that transpiles PHP to JS. Supports echo, $variables, string concatenation, foreach, arrays, and common stdlib functions. |
dart |
Iframe harness that transpiles Dart to JS. Supports print, string interpolation, typed variables, collections, and function declarations. |
go |
Iframe harness that transpiles Go to JS. Supports fmt.Print/Println/Printf, for/range loops, slices, maps, multi-file packages, and stdlib stubs. |
Runtimes for Python and Java are loaded lazily from CDN only when first needed. All other runners require no external downloads.
The package can be safely imported in server-side environments (Next.js, Nuxt, SvelteKit SSR). All browser APIs are only accessed when run() is called. If run() is called outside a browser, a clear error is thrown.
MIT
