Files
chunyu_prject_react/src/components/CodeRunner/pyodide-runner.ts
T
chunyu e90b787730 feat(C-03):代码在线运行+课程练一节
新增CodeRunner组件(JS worker沙箱+Python pyodide)与/utility/code-runner独立页;CourseLearn新增练习Tab(首代码块自动提取装载,无示例可直接练习)。
2026-09-15 15:26:44 +08:00

64 lines
2.1 KiB
TypeScript

/**
* C-03 Pyodide 懒加载运行器(浏览器内 WASM,无服务端执行)。
* 安全约束:
* - 默认断网:不 loadPackage、不拉取额外包;只运行解释器自带能力。
* - 超时由调用方(CodeRunner.tsx)经 Promise.race + runId 丢弃过期结果保证;
* Pyodide 本体不支持强制 terminate,超时后输出"执行超时"并忽略迟到结果。
*/
export const PYODIDE_VERSION = "0.26.4";
export const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
type PyodideRuntime = {
runPython: (code: string) => unknown;
setStdout: (opts: { batched: (s: string) => void }) => void;
setStderr: (opts: { batched: (s: string) => void }) => void;
};
declare global {
interface Window {
__pyodidePromise?: Promise<PyodideRuntime> | null;
}
}
function loadPyodideScript(): Promise<void> {
return new Promise((resolve, reject) => {
if (document.querySelector('script[data-pyodide="1"]')) {
resolve();
return;
}
const s = document.createElement("script");
s.dataset.pyodide = "1";
s.src = `${PYODIDE_CDN}pyodide.js`;
s.onload = () => resolve();
s.onerror = () => reject(new Error("Pyodide 运行时加载失败,请检查网络后重试"));
document.head.appendChild(s);
});
}
async function bootPyodide(
onStdout: (s: string) => void,
onStderr: (s: string) => void,
): Promise<PyodideRuntime> {
await loadPyodideScript();
const factory = (window as unknown as { loadPyodide?: (opts: { indexURL: string }) => Promise<PyodideRuntime> }).loadPyodide;
if (!factory) throw new Error("Pyodide 运行时初始化失败");
const py = await factory({ indexURL: PYODIDE_CDN });
py.setStdout({ batched: onStdout });
py.setStderr({ batched: onStderr });
return py;
}
export function getPyodide(
onStdout: (s: string) => void,
onStderr: (s: string) => void,
): Promise<PyodideRuntime> {
if (!window.__pyodidePromise) {
window.__pyodidePromise = bootPyodide(onStdout, onStderr).catch((e) => {
window.__pyodidePromise = null;
throw e;
});
}
return window.__pyodidePromise;
}