import * as webcontainer from '@tempfix/webcontainer__api'; import type { IExecutionEnvironment, IFileEntry, IFileWatcher, 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 { // Static shared state - WebContainer only allows ONE boot per page private static sharedContainer: webcontainer.WebContainer | null = null; private static bootPromise: Promise | null = null; private _ready: boolean = false; public readonly type = 'webcontainer' as const; public get ready(): boolean { return this._ready; } private get container(): webcontainer.WebContainer | null { return WebContainerEnvironment.sharedContainer; } // ============ Lifecycle ============ public async init(): Promise { // Already initialized (this instance) if (this._ready && WebContainerEnvironment.sharedContainer) { return; } // If boot is in progress (by any instance), wait for it if (WebContainerEnvironment.bootPromise) { await WebContainerEnvironment.bootPromise; this._ready = true; return; } // If already booted by another instance, just mark ready if (WebContainerEnvironment.sharedContainer) { this._ready = true; return; } // 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' ); } // Start boot process WebContainerEnvironment.bootPromise = webcontainer.WebContainer.boot(); try { WebContainerEnvironment.sharedContainer = await WebContainerEnvironment.bootPromise; this._ready = true; } catch (error) { // Reset promise on failure so retry is possible WebContainerEnvironment.bootPromise = null; throw error; } } public async destroy(): Promise { if (WebContainerEnvironment.sharedContainer) { WebContainerEnvironment.sharedContainer.teardown(); WebContainerEnvironment.sharedContainer = null; WebContainerEnvironment.bootPromise = 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; } } } public watch( path: string, callback: (event: 'rename' | 'change', filename: string | null) => void, options?: { recursive?: boolean } ): IFileWatcher { this.ensureReady(); const watcher = this.container!.fs.watch( path, { recursive: options?.recursive ?? false }, callback ); return { stop: () => watcher.close(), }; } // ============ 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.'); } } }