| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| let pyodide = null; |
| let luaFactory = null; |
|
|
| |
| |
| |
| function send(type, data) { |
| self.postMessage({ type, data }); |
| } |
|
|
| function sendFile(path, content) { |
| self.postMessage({ type: 'output-file', path, content }); |
| } |
|
|
| |
|
|
| async function ensurePyodide() { |
| if (pyodide) return pyodide; |
|
|
| send('status', 'Loading Python runtime...'); |
|
|
| try { |
| importScripts('https://cdn.jsdelivr.net/pyodide/v0.27.4/full/pyodide.js'); |
| } catch (err) { |
| send('error', 'Failed to load Pyodide from CDN: ' + String(err)); |
| throw err; |
| } |
|
|
| try { |
| pyodide = await self.loadPyodide({ |
| stdout: (msg) => send('stdout', msg), |
| stderr: (msg) => send('stderr', msg), |
| }); |
| } catch (err) { |
| send('error', 'Failed to initialize Pyodide: ' + String(err)); |
| throw err; |
| } |
|
|
| |
| await pyodide.loadPackage('micropip'); |
|
|
| send('status', 'Python runtime ready'); |
| return pyodide; |
| } |
|
|
| async function executePython(entryPoint, files) { |
| const py = await ensurePyodide(); |
|
|
| |
| |
| try { py.FS.mkdir('/output'); } catch (_e) { } |
|
|
| for (const [path, content] of Object.entries(files)) { |
| |
| if (path.startsWith('/.')) continue; |
| const dir = path.substring(0, path.lastIndexOf('/')) || '/'; |
| |
| const parts = dir.split('/').filter(Boolean); |
| let current = ''; |
| for (const part of parts) { |
| current += '/' + part; |
| try { py.FS.mkdir(current); } catch (_e) { } |
| } |
| py.FS.writeFile(path, content); |
| } |
|
|
| |
| const code = files[entryPoint]; |
| if (!code) { |
| send('error', `Entry point not found: ${entryPoint}`); |
| return { exitCode: 1 }; |
| } |
|
|
| |
| const entryDir = entryPoint.substring(0, entryPoint.lastIndexOf('/')) || '/'; |
| try { |
| await py.runPythonAsync(` |
| import sys, os |
| os.chdir(${JSON.stringify(entryDir)}) |
| _ep_dir = ${JSON.stringify(entryDir)} |
| if _ep_dir not in sys.path: |
| sys.path.insert(0, _ep_dir) |
| if '/' not in sys.path: |
| sys.path.insert(0, '/') |
| __file__ = ${JSON.stringify(entryPoint)} |
| del _ep_dir |
| `); |
| } catch (_e) { } |
|
|
| try { |
| await py.runPythonAsync(code); |
| } catch (err) { |
| send('stderr', String(err)); |
| return { exitCode: 1 }; |
| } |
|
|
| |
| try { |
| const outputFiles = py.FS.readdir('/output').filter(f => f !== '.' && f !== '..'); |
| for (const filename of outputFiles) { |
| const filePath = '/output/' + filename; |
| try { |
| const data = py.FS.readFile(filePath); |
| const ext = filename.split('.').pop().toLowerCase(); |
| if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].includes(ext)) { |
| let binary = ''; |
| for (let i = 0; i < data.length; i++) { |
| binary += String.fromCharCode(data[i]); |
| } |
| const base64 = btoa(binary); |
| sendFile(filePath, base64); |
| } else { |
| const decoder = new TextDecoder(); |
| sendFile(filePath, decoder.decode(data)); |
| } |
| } catch (_e) { } |
| } |
| } catch (_e) { } |
|
|
| return { exitCode: 0 }; |
| } |
|
|
| |
|
|
| async function ensureLuaFactory() { |
| if (luaFactory) return luaFactory; |
|
|
| send('status', 'Loading Lua runtime...'); |
|
|
| try { |
| |
| const wasmoon = await import('https://esm.sh/wasmoon@1'); |
| luaFactory = new wasmoon.LuaFactory(); |
| } catch (err) { |
| send('error', 'Failed to load Lua runtime: ' + String(err)); |
| throw err; |
| } |
|
|
| send('status', 'Lua runtime ready'); |
| return luaFactory; |
| } |
|
|
| async function executeLua(entryPoint, files) { |
| const factory = await ensureLuaFactory(); |
| const engine = await factory.createEngine(); |
|
|
| try { |
| |
| engine.global.set('print', function (...args) { |
| send('stdout', args.map(String).join('\t')); |
| }); |
|
|
| |
| const moduleFiles = {}; |
| for (const [path, content] of Object.entries(files)) { |
| if (path.endsWith('.lua') && path !== entryPoint) { |
| const modName = path |
| .replace(/^\//, '') |
| .replace(/\.lua$/, '') |
| .replace(/\//g, '.'); |
| moduleFiles[modName] = content; |
| } |
| } |
|
|
| |
| engine.global.set('__vfs_modules', JSON.stringify(moduleFiles)); |
|
|
| await engine.doString(` |
| local vfs_modules = {} |
| local json_str = __vfs_modules |
| -- Simple JSON parse for module map (keys and string values only) |
| for key, value in json_str:gmatch('"([^"]+)":"(.-[^\\\\])"') do |
| -- Unescape basic sequences |
| value = value:gsub('\\\\n', '\\n'):gsub('\\\\t', '\\t'):gsub('\\\\"', '"'):gsub('\\\\\\\\', '\\\\') |
| vfs_modules[key] = value |
| end |
| __vfs_modules = nil |
| |
| table.insert(package.searchers, 2, function(modname) |
| local source = vfs_modules[modname] |
| if source then |
| local fn, err = load(source, "@" .. modname .. ".lua") |
| if fn then return fn |
| else return "\\n\\tload error: " .. err end |
| end |
| return "\\n\\tno VFS module '" .. modname .. "'" |
| end) |
| `); |
|
|
| |
| const code = files[entryPoint]; |
| if (!code) { |
| send('error', 'Entry point not found: ' + entryPoint); |
| engine.global.close(); |
| return { exitCode: 1 }; |
| } |
|
|
| await engine.doString(code); |
|
|
| engine.global.close(); |
| return { exitCode: 0 }; |
|
|
| } catch (err) { |
| send('stderr', String(err)); |
| try { engine.global.close(); } catch (_e) { } |
| return { exitCode: 1 }; |
| } |
| } |
|
|
| |
|
|
| self.onmessage = async function (event) { |
| const msg = event.data; |
|
|
| if (msg.type === 'execute') { |
| const { runtime, entryPoint, files } = msg.payload; |
|
|
| try { |
| let result; |
| if (runtime === 'python') { |
| result = await executePython(entryPoint, files); |
| } else if (runtime === 'lua') { |
| result = await executeLua(entryPoint, files); |
| } else { |
| send('error', 'Unknown runtime: ' + runtime); |
| self.postMessage({ type: 'complete', exitCode: 1 }); |
| return; |
| } |
|
|
| self.postMessage({ type: 'complete', exitCode: result.exitCode }); |
| } catch (err) { |
| send('error', String(err)); |
| self.postMessage({ type: 'complete', exitCode: 1 }); |
| } |
| } |
| }; |
|
|