From ae0bd17b4ae84ae7aee962ae83a39396d695de28 Mon Sep 17 00:00:00 2001 From: DreamTeam Mobile Date: Thu, 19 Feb 2026 21:22:00 -0800 Subject: [PATCH 1/2] Add multi-language code execution (Python, C/C++, Go, Ruby, Lua) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand in-browser code execution from JS/TS-only to 7 languages using a persistent Web Worker architecture with lazy-loaded runtimes. New language runtimes: - Python: Pyodide (CPython compiled to WASM, ~5MB from jsDelivr CDN) - C/C++: Compiler Explorer / Godbolt API (GCC 14.2, server-side compilation) - Go: Go Playground API (play.golang.org/compile, server-side compilation) - Ruby: ruby.wasm (@ruby/3.3-wasm-wasi, ~36MB WASM from jsDelivr CDN) - Lua: Wasmoon (Lua 5.4 compiled to WASM, ~300KB) Architecture: - Persistent workers per language (loaded once, reused across executions) - Lazy loading: no WASM downloaded until user selects a language - Progress tracking for large downloads (Python, Ruby) via fetch interception - Runtime status store (Zustand) drives UI state for run button and output panel UI changes: - Run button shows progress ring while runtime downloads, error icon on failure - Output panel shows download progress percentage for WASM runtimes - Language selector triggers background preload on switch - All executable languages marked with ▶ prefix in dropdown - Lua added as 16th language option with Prism syntax highlighting Fixes: - Vite optimizeDeps.include prevents page reload on first worker dep import --- package-lock.json | 67 ++++++- package.json | 3 + src/__tests__/LanguageSelector.test.tsx | 10 +- src/components/CodeEditor/CodeEditor.tsx | 1 + .../CodeEditor/LanguageSelector.tsx | 13 +- src/components/CodeEditor/OutputPanel.tsx | 39 +++- src/components/TabBar.tsx | 54 +++++- src/hooks/useExecutionSync.ts | 47 ++++- src/services/code-editor-logic.ts | 1 + src/services/code-executor.ts | 20 +- src/services/runtimes/cpp.worker.ts | 143 ++++++++++++++ src/services/runtimes/go.worker.ts | 122 ++++++++++++ src/services/runtimes/lua.worker.ts | 96 ++++++++++ src/services/runtimes/python.worker.ts | 127 ++++++++++++ src/services/runtimes/ruby.worker.ts | 149 +++++++++++++++ src/services/wasm-runtime-manager.ts | 180 ++++++++++++++++++ src/stores/runtimeStore.ts | 72 +++++++ src/styles.css | 34 ++++ vite.config.ts | 8 + 19 files changed, 1162 insertions(+), 24 deletions(-) create mode 100644 src/services/runtimes/cpp.worker.ts create mode 100644 src/services/runtimes/go.worker.ts create mode 100644 src/services/runtimes/lua.worker.ts create mode 100644 src/services/runtimes/python.worker.ts create mode 100644 src/services/runtimes/ruby.worker.ts create mode 100644 src/services/wasm-runtime-manager.ts create mode 100644 src/stores/runtimeStore.ts diff --git a/package-lock.json b/package-lock.json index 82a1552..c101dd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,21 @@ { "name": "duocode", - "version": "2.1.0", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "duocode", - "version": "2.1.0", + "version": "2.4.0", "license": "UNLICENSED", "dependencies": { + "@ruby/wasm-wasi": "^2.8.1", "jspdf": "^2.5.2", "prismjs": "^1.29.0", + "pyodide": "^0.29.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "wasmoon": "^1.16.0", "zustand": "^5.0.11" }, "devDependencies": { @@ -470,6 +473,12 @@ "node": ">=18" } }, + "node_modules/@bjorn3/browser_wasi_shim": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.3.0.tgz", + "integrity": "sha512-FlRBYttPRLcWORzBe6g8nmYTafBkOEFeOqMYM4tAHJzFsQy4+xJA94z85a9BCs8S+Uzfh9LrkpII7DXr2iUVFg==", + "license": "MIT OR Apache-2.0" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1495,6 +1504,16 @@ "win32" ] }, + "node_modules/@ruby/wasm-wasi": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@ruby/wasm-wasi/-/wasm-wasi-2.8.1.tgz", + "integrity": "sha512-xtisFn9V2U45nE2goni9UAd8LERHAzwFEsQk3+mG6+hXf4imYfDRRXnBqX6jERR0rysCH15IDzEkxjMQmIZ3qA==", + "license": "MIT", + "dependencies": { + "@bjorn3/browser_wasi_shim": "^0.3.0", + "tslib": "^2.8.1" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1649,6 +1668,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/emscripten": { + "version": "1.41.5", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz", + "integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3684,6 +3709,19 @@ "node": ">=6" } }, + "node_modules/pyodide": { + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.29.3.tgz", + "integrity": "sha512-22UBuhOJawj7vKUnS7/F3xK+515LJdjiMAHoCfuS6/PbHiOrSQVnYwDe+2sbVwiOZ3sMMexdXICew6NqOMQGgA==", + "license": "MPL-2.0", + "dependencies": { + "@types/emscripten": "^1.41.4", + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -4267,6 +4305,12 @@ "node": ">=20" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -4539,6 +4583,24 @@ "node": ">=18" } }, + "node_modules/wasmoon": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/wasmoon/-/wasmoon-1.16.0.tgz", + "integrity": "sha512-FlRLb15WwAOz1A9OQDbf6oOKKSiefi5VK0ZRF2wgH9xk3o5SnU11tNPaOnQuAh1Ucr66cwwvVXaeVRaFdRBt5g==", + "license": "MIT", + "dependencies": { + "@types/emscripten": "1.39.10" + }, + "bin": { + "wasmoon": "bin/wasmoon" + } + }, + "node_modules/wasmoon/node_modules/@types/emscripten": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", + "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", + "license": "MIT" + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -4644,7 +4706,6 @@ "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 3056987..2eca813 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,13 @@ "author": "DreamTeam Mobile ", "license": "UNLICENSED", "dependencies": { + "@ruby/wasm-wasi": "^2.8.1", "jspdf": "^2.5.2", "prismjs": "^1.29.0", + "pyodide": "^0.29.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "wasmoon": "^1.16.0", "zustand": "^5.0.11" }, "devDependencies": { diff --git a/src/__tests__/LanguageSelector.test.tsx b/src/__tests__/LanguageSelector.test.tsx index 6a8ee72..589a8e7 100644 --- a/src/__tests__/LanguageSelector.test.tsx +++ b/src/__tests__/LanguageSelector.test.tsx @@ -15,19 +15,19 @@ describe('LanguageSelector', () => { expect(select.value).toBe('javascript'); }); - it('renders all 15 supported languages', () => { + it('renders all 16 supported languages', () => { const { container } = render(); const options = container.querySelectorAll('option'); - expect(options).toHaveLength(15); + expect(options).toHaveLength(16); }); it('displays human-readable labels', () => { const { getByText } = render(); - // Executable languages get a ▶ prefix + // Executable languages get a ▶ prefix (JS, TS, Python, C, C++, Go, Ruby, Lua) expect(getByText(/JavaScript/)).toBeInTheDocument(); expect(getByText(/TypeScript/)).toBeInTheDocument(); - expect(getByText('Python')).toBeInTheDocument(); - expect(getByText('C++')).toBeInTheDocument(); + expect(getByText(/Python/)).toBeInTheDocument(); + expect(getByText(/C\+\+/)).toBeInTheDocument(); expect(getByText('C#')).toBeInTheDocument(); }); diff --git a/src/components/CodeEditor/CodeEditor.tsx b/src/components/CodeEditor/CodeEditor.tsx index 5c37e2c..7d57dd5 100644 --- a/src/components/CodeEditor/CodeEditor.tsx +++ b/src/components/CodeEditor/CodeEditor.tsx @@ -10,6 +10,7 @@ import 'prismjs/components/prism-csharp'; import 'prismjs/components/prism-go'; import 'prismjs/components/prism-rust'; import 'prismjs/components/prism-ruby'; +import 'prismjs/components/prism-lua'; import 'prismjs/components/prism-swift'; import 'prismjs/components/prism-scala'; import 'prismjs/components/prism-markup'; diff --git a/src/components/CodeEditor/LanguageSelector.tsx b/src/components/CodeEditor/LanguageSelector.tsx index 5645788..07f8c8d 100644 --- a/src/components/CodeEditor/LanguageSelector.tsx +++ b/src/components/CodeEditor/LanguageSelector.tsx @@ -1,6 +1,6 @@ import { useEditorStore } from '../../stores/editorStore'; import { codeTemplates } from '../../services/code-editor-logic'; -import { isExecutable } from '../../services/code-executor'; +import { isExecutable, isWasmLanguage, preloadRuntime } from '../../services/code-executor'; const LANGUAGES = Object.keys(codeTemplates); @@ -16,6 +16,7 @@ const LANGUAGE_LABELS: Record = { go: 'Go', rust: 'Rust', ruby: 'Ruby', + lua: 'Lua', swift: 'Swift', scala: 'Scala', php: 'PHP', @@ -26,11 +27,19 @@ export default function LanguageSelector() { const language = useEditorStore((s) => s.language); const setLanguage = useEditorStore((s) => s.setLanguage); + const handleChange = (newLang: string) => { + setLanguage(newLang); + // Start preloading WASM runtime in background when user switches + if (isWasmLanguage(newLang)) { + preloadRuntime(newLang); + } + }; + return (