Add browser Python editor with Pyodide, user auth, and workspace API
- FastAPI serves static UI, file CRUD under code/ and read-only lib/ - Pyodide worker runs Python and Jedi completions in the browser - SQLite accounts: login/register, session cookies, superuser user management - Optional EDITOR_API_KEY, AUTH_* env vars, .env.example - Pipenv, pytest, Selenium smoke test, README Made-with: Cursor
This commit is contained in:
122
src/static/pyodide-worker.js
Normal file
122
src/static/pyodide-worker.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/* global importScripts, loadPyodide, self */
|
||||
importScripts('https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js');
|
||||
|
||||
const PYODIDE_INDEX_URL = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/';
|
||||
|
||||
let pyodide = null;
|
||||
let loadingPromise = null;
|
||||
|
||||
async function ensurePyodide() {
|
||||
if (pyodide) {
|
||||
return pyodide;
|
||||
}
|
||||
if (!loadingPromise) {
|
||||
loadingPromise = (async () => {
|
||||
const p = await loadPyodide({ indexURL: PYODIDE_INDEX_URL });
|
||||
p.setStdout({
|
||||
batched: (txt) => self.postMessage({ type: 'io', stream: 'stdout', text: txt }),
|
||||
});
|
||||
p.setStderr({
|
||||
batched: (txt) => self.postMessage({ type: 'io', stream: 'stderr', text: txt }),
|
||||
});
|
||||
await p.loadPackage('micropip');
|
||||
await p.runPythonAsync(`
|
||||
import micropip
|
||||
await micropip.install("jedi")
|
||||
`);
|
||||
return p;
|
||||
})();
|
||||
}
|
||||
pyodide = await loadingPromise;
|
||||
return pyodide;
|
||||
}
|
||||
|
||||
self.onmessage = async (event) => {
|
||||
const { id, type, payload } = event.data || {};
|
||||
try {
|
||||
if (type === 'init') {
|
||||
await ensurePyodide();
|
||||
self.postMessage({ id, type: 'init', ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await ensurePyodide();
|
||||
|
||||
if (type === 'complete') {
|
||||
const rel = String(payload.path || 'scratch.py').replace(/^\/+/, '');
|
||||
const vpath = `/workspace/${rel}`;
|
||||
p.globals.set('__cm_code', String(payload.content ?? ''));
|
||||
p.globals.set('__cm_path', vpath);
|
||||
p.globals.set('__cm_line', Number(payload.line) || 1);
|
||||
p.globals.set('__cm_col', Number(payload.column) || 0);
|
||||
p.globals.set('__cm_max', Math.min(100, Math.max(1, Number(payload.max_results) || 20)));
|
||||
p.globals.set('__cm_extra_json', JSON.stringify(payload.extra_files || {}));
|
||||
const raw = p.runPython(`
|
||||
import json, os
|
||||
import jedi
|
||||
|
||||
extra = json.loads(__cm_extra_json)
|
||||
os.makedirs("/workspace", exist_ok=True)
|
||||
for rel_path, body in extra.items():
|
||||
rel_path = str(rel_path).lstrip("/")
|
||||
full = os.path.join("/workspace", rel_path)
|
||||
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||
with open(full, "w", encoding="utf-8") as fh:
|
||||
fh.write(str(body))
|
||||
os.makedirs(os.path.dirname(__cm_path), exist_ok=True)
|
||||
with open(__cm_path, "w", encoding="utf-8") as fh:
|
||||
fh.write(__cm_code)
|
||||
proj = jedi.Project("/workspace")
|
||||
s = jedi.Script(code=__cm_code, path=__cm_path, project=proj)
|
||||
items = s.complete(line=__cm_line, column=__cm_col)
|
||||
out = [{"name": i.name, "type": i.type, "complete": i.complete} for i in items[:__cm_max]]
|
||||
json.dumps(out)
|
||||
`);
|
||||
const completions = JSON.parse(String(raw));
|
||||
self.postMessage({ id, type: 'complete', ok: true, completions });
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'run') {
|
||||
const files = payload.files && typeof payload.files === 'object' ? payload.files : {};
|
||||
const mainRel = String(payload.mainPath || '').replace(/^\/+/, '');
|
||||
const argsList = Array.isArray(payload.args) ? payload.args.map(String) : [];
|
||||
p.globals.set('__run_files_json', JSON.stringify(files));
|
||||
p.globals.set('__run_main', `/workspace/${mainRel}`);
|
||||
p.globals.set('__run_args', p.toPy(argsList));
|
||||
await p.runPythonAsync(`
|
||||
import json, os, shutil, sys, runpy
|
||||
|
||||
files = json.loads(__run_files_json)
|
||||
shutil.rmtree('/workspace', ignore_errors=True)
|
||||
os.makedirs('/workspace', exist_ok=True)
|
||||
for rel, body in files.items():
|
||||
rel = str(rel).lstrip("/")
|
||||
full = os.path.join("/workspace", rel)
|
||||
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||
with open(full, "w", encoding="utf-8") as fh:
|
||||
fh.write(str(body))
|
||||
|
||||
for entry in ("/workspace/lib", "/workspace"):
|
||||
if entry not in sys.path:
|
||||
sys.path.insert(0, entry)
|
||||
|
||||
os.chdir("/workspace")
|
||||
main = __run_main
|
||||
sys.argv = [main] + list(__run_args)
|
||||
runpy.run_path(main, run_name="__main__")
|
||||
`);
|
||||
self.postMessage({ id, type: 'run', ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
self.postMessage({ id, type, ok: false, error: `Unknown message type: ${type}` });
|
||||
} catch (err) {
|
||||
self.postMessage({
|
||||
id,
|
||||
type,
|
||||
ok: false,
|
||||
error: err && err.message ? String(err.message) : String(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user