import * as webcontainer from '@webcontainer/api'; import type { IExecutionEnvironment, IFileEntry, IProcessHandle } from '../interfaces/IExecutionEnvironment.js'; /** * WebContainer-based execution environment. * Runs Node.js and shell commands in the browser using WebContainer API. */ export class WebContainerEnvironment implements IExecutionEnvironment { private container: webcontainer.WebContainer | null = null; private _ready: boolean = false; public readonly type = 'webcontainer' as const; public get ready(): boolean { return this._ready; } // ============ Lifecycle ============ public async init(): Promise { if (this._ready && this.container) { return; // Already initialized } // Check if SharedArrayBuffer is available (required for WebContainer) if (typeof SharedArrayBuffer === 'undefined') { throw new Error( 'WebContainer requires SharedArrayBuffer which is not available. ' + 'Ensure your server sends these headers:\n' + ' Cross-Origin-Opener-Policy: same-origin\n' + ' Cross-Origin-Embedder-Policy: require-corp' ); } this.container = await webcontainer.WebContainer.boot(); this._ready = true; } public async destroy(): Promise { if (this.container) { this.container.teardown(); this.container = null; this._ready = false; } } // ============ Filesystem Operations ============ public async readFile(path: string): Promise { this.ensureReady(); return await this.container!.fs.readFile(path, 'utf-8'); } public async writeFile(path: string, contents: string): Promise { this.ensureReady(); await this.container!.fs.writeFile(path, contents, 'utf-8'); } public async readDir(path: string): Promise { this.ensureReady(); const entries = await this.container!.fs.readdir(path, { withFileTypes: true }); return entries.map((entry) => ({ type: entry.isDirectory() ? 'directory' as const : 'file' as const, name: entry.name, path: path === '/' ? `/${entry.name}` : `${path}/${entry.name}`, })); } public async mkdir(path: string): Promise { this.ensureReady(); await this.container!.fs.mkdir(path, { recursive: true }); } public async rm(path: string, options?: { recursive?: boolean }): Promise { this.ensureReady(); await this.container!.fs.rm(path, { recursive: options?.recursive ?? false }); } public async exists(path: string): Promise { this.ensureReady(); try { await this.container!.fs.readFile(path); return true; } catch { try { await this.container!.fs.readdir(path); return true; } catch { return false; } } } // ============ Process Execution ============ public async spawn(command: string, args: string[] = []): Promise { this.ensureReady(); const process = await this.container!.spawn(command, args); return { output: process.output as unknown as ReadableStream, input: process.input as unknown as { getWriter(): WritableStreamDefaultWriter }, exit: process.exit, kill: () => process.kill(), }; } // ============ WebContainer-specific methods ============ /** * Mount files into the virtual filesystem. * This is a WebContainer-specific operation. * @param files - File tree structure to mount */ public async mount(files: webcontainer.FileSystemTree): Promise { this.ensureReady(); await this.container!.mount(files); } /** * Get the underlying WebContainer instance. * Use sparingly - prefer the interface methods. */ public getContainer(): webcontainer.WebContainer { this.ensureReady(); return this.container!; } // ============ Private Helpers ============ private ensureReady(): void { if (!this._ready || !this.container) { throw new Error('WebContainerEnvironment not initialized. Call init() first.'); } } }