File size: 7,492 Bytes
c545baf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716b726
c545baf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/**
 * Script Worker β€” Executes Python (Pyodide) and Lua (wasmoon) scripts
 * in a Web Worker to avoid blocking the UI thread.
 *
 * Receives: { type: 'execute', payload: { runtime, entryPoint, files } }
 *           { type: 'abort' }
 * Posts:    { type: 'stdout'|'stderr'|'status'|'error'|'complete'|'output-file', ... }
 */

/* global self, importScripts, postMessage */

let pyodide = null;
let luaFactory = null;

/**
 * Post a message back to the main thread.
 */
function send(type, data) {
  self.postMessage({ type, data });
}

function sendFile(path, content) {
  self.postMessage({ type: 'output-file', path, content });
}

// ─── Python (Pyodide) ──────────────────────────────────────────────

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;
  }

  // Pre-load micropip so users can install packages
  await pyodide.loadPackage('micropip');

  send('status', 'Python runtime ready');
  return pyodide;
}

async function executePython(entryPoint, files) {
  const py = await ensurePyodide();

  // Mount VFS files into Pyodide's filesystem
  // Create /output/ directory for visual output
  try { py.FS.mkdir('/output'); } catch (_e) { /* exists */ }

  for (const [path, content] of Object.entries(files)) {
    // Skip dotfiles
    if (path.startsWith('/.')) continue;
    const dir = path.substring(0, path.lastIndexOf('/')) || '/';
    // Ensure parent directories exist
    const parts = dir.split('/').filter(Boolean);
    let current = '';
    for (const part of parts) {
      current += '/' + part;
      try { py.FS.mkdir(current); } catch (_e) { /* exists */ }
    }
    py.FS.writeFile(path, content);
  }

  // Run the entry point script
  const code = files[entryPoint];
  if (!code) {
    send('error', `Entry point not found: ${entryPoint}`);
    return { exitCode: 1 };
  }

  // Set up Python environment so module imports work:
  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) { /* best effort */ }

  try {
    await py.runPythonAsync(code);
  } catch (err) {
    send('stderr', String(err));
    return { exitCode: 1 };
  }

  // Scan /output/ for new files and send them back
  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) { /* skip unreadable files */ }
    }
  } catch (_e) { /* /output/ may not have new files */ }

  return { exitCode: 0 };
}

// ─── Lua (wasmoon) ──────────────────────────────────────────────────

async function ensureLuaFactory() {
  if (luaFactory) return luaFactory;

  send('status', 'Loading Lua runtime...');

  try {
    // Dynamic import of wasmoon from CDN
    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 {
    // Override print to capture stdout
    engine.global.set('print', function (...args) {
      send('stdout', args.map(String).join('\t'));
    });

    // Pre-load module files so require() works
    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;
      }
    }

    // Register custom searcher for VFS modules
    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)
    `);

    // Run the entry point
    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) { /* best effort */ }
    return { exitCode: 1 };
  }
}

// ─── Message handler ────────────────────────────────────────────────

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 });
    }
  }
};