Spaces:
Configuration error
Configuration error
| 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); | |
| }, | |
| }; | |
| } | |