const BOOT_FILES = { "package.json": { file: { contents: JSON.stringify( { type: "module", scripts: { demo: "node hello.js", }, dependencies: {}, devDependencies: {}, }, null, 2, ), }, }, "README.md": { file: { contents: "# Pi web sandbox\n\nThis filesystem and every spawned process run inside WebContainers in this browser tab.\n", }, }, "hello.js": { file: { contents: 'console.log("hello from the browser sandbox");\nconsole.log(2 + 2);\n', }, }, }; const MAX_OUTPUT_CHARS = 16000; const DEFAULT_TIMEOUT_MS = 10000; export function createSandbox({ onLog = () => {}, onStatus = () => {} } = {}) { let instance = null; let booting = null; let root = ""; async function boot() { if (instance) return instance; if (booting) return booting; booting = (async () => { if (!globalThis.crossOriginIsolated) { throw new Error("WebContainers need cross-origin isolation. Start this app through Vite or another server that sends COOP/COEP headers."); } onStatus("Booting WebContainer"); const { WebContainer } = await import("@webcontainer/api"); instance = await WebContainer.boot({ coep: "credentialless", workdirName: "workspace", }); root = instance.workdir; await instance.mount(BOOT_FILES); onLog(`sandbox booted at ${root}`); onStatus("Sandbox ready"); return instance; })(); try { return await booting; } finally { booting = null; } } function assertRelativePath(path) { const value = String(path || "").trim().replace(/^\/+/, ""); if (!value || value.includes("\0")) { throw new Error("Path is required."); } const segments = value.split("/").filter(Boolean); if (segments.some((segment) => segment === "." || segment === "..")) { throw new Error("Paths must stay inside the sandbox workspace."); } return segments.join("/"); } function toWorkspacePath(path) { return assertRelativePath(path); } async function reset() { const wc = await boot(); const entries = await wc.fs.readdir("."); await Promise.all( entries.map((entry) => wc.fs.rm(entry, { force: true, recursive: true, }), ), ); await wc.mount(BOOT_FILES); onLog("sandbox reset"); return "Sandbox reset to the starter project."; } async function listFiles(path = ".") { const wc = await boot(); const target = path === "." || path === "" ? "." : toWorkspacePath(path); const entries = await wc.fs.readdir(target, { withFileTypes: true }); return entries.map((entry) => `${entry.isDirectory() ? "dir " : "file"} ${entry.name}`).join("\n") || "(empty)"; } async function readFile(path) { const wc = await boot(); return await wc.fs.readFile(toWorkspacePath(path), "utf-8"); } async function writeFile(path, content) { const wc = await boot(); const relative = assertRelativePath(path); const parent = relative.split("/").slice(0, -1).join("/"); if (parent) { await wc.fs.mkdir(parent, { recursive: true }); } await wc.fs.writeFile(relative, String(content ?? "")); onLog(`wrote ${relative}`); return `Wrote ${relative}`; } async function runCommand(command, args = [], timeoutMs = DEFAULT_TIMEOUT_MS) { const wc = await boot(); const cmd = String(command || "").trim(); if (!cmd) throw new Error("Command is required."); const cleanArgs = Array.isArray(args) ? args.map((arg) => String(arg)) : []; onLog(`$ ${[cmd, ...cleanArgs].join(" ")}`); const process = await wc.spawn(cmd, cleanArgs, { terminal: { cols: 96, rows: 28, }, }); let output = ""; const reader = process.output.getReader(); const pump = (async () => { while (true) { const { done, value } = await reader.read(); if (done) break; output += value; if (output.length > MAX_OUTPUT_CHARS) { output = `${output.slice(0, MAX_OUTPUT_CHARS)}\n[output truncated]`; process.kill(); break; } } })(); const timeout = new Promise((resolve) => { setTimeout(() => { process.kill(); resolve("timeout"); }, Number(timeoutMs) || DEFAULT_TIMEOUT_MS); }); const exitCode = await Promise.race([process.exit, timeout]); await pump.catch(() => {}); const normalizedExitCode = exitCode === "timeout" ? 124 : exitCode; const result = { command: [cmd, ...cleanArgs].join(" "), exitCode: normalizedExitCode, output: output.trimEnd(), }; onLog(`exit ${normalizedExitCode}`); return result; } return { boot, reset, listFiles, readFile, writeFile, runCommand, get isReady() { return Boolean(instance); }, }; }