From 9d55ec4e5ca80679daa265f84da4e5e407e96c62 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 1 Mar 2025 15:30:07 -0800 Subject: [PATCH] feat(code): additional cleanup for stdout response from WebContainer --- components/webcontainer-provider.tsx | 122 +++++++++++++++++++++++++++ executor/handlers.ts | 7 +- lib/webcontainer.ts | 78 ++++++++++++----- 3 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 components/webcontainer-provider.tsx diff --git a/components/webcontainer-provider.tsx b/components/webcontainer-provider.tsx new file mode 100644 index 0000000000..6ebc4f44bc --- /dev/null +++ b/components/webcontainer-provider.tsx @@ -0,0 +1,122 @@ +'use client' + +import { createContext, useCallback, useContext, useEffect, useState } from 'react' +import { AlertTriangle } from 'lucide-react' +import { executeCode, getWebContainer } from '@/lib/webcontainer' + +// Create a context for WebContainer state and utilities +interface WebContainerContextType { + isReady: boolean + error: string | null + executeFunction: (code: string, params?: Record) => Promise +} + +const WebContainerContext = createContext({ + isReady: false, + error: null, + executeFunction: async () => ({ success: false, error: 'WebContainer not initialized' }), +}) + +// Hook to use WebContainer +export const useWebContainer = () => useContext(WebContainerContext) + +/** + * WebContainerProvider initializes the WebContainer API on the client side + * This component should be included near the root of the application + */ +export function WebContainerProvider({ children }: { children: React.ReactNode }) { + const [isInitialized, setIsInitialized] = useState(false) + const [error, setError] = useState(null) + const [isIsolated, setIsIsolated] = useState(null) + + // Function to execute code directly from client components + const executeFunction = useCallback( + async (code: string, params: Record = {}) => { + if (!isInitialized) { + return { + success: false, + output: {}, + error: 'WebContainer not initialized', + } + } + + try { + return await executeCode(code, params) + } catch (err: any) { + console.error('WebContainer execution error:', err) + return { + success: false, + output: {}, + error: err.message || 'Error executing function', + } + } + }, + [isInitialized] + ) + + useEffect(() => { + // Check if we're in a cross-origin isolated context + if (typeof window !== 'undefined') { + setIsIsolated(!!window.crossOriginIsolated) + } + + const initWebContainer = async () => { + try { + // Initialize WebContainer when the component mounts + await getWebContainer() + setIsInitialized(true) + } catch (err: any) { + console.error('Failed to initialize WebContainer:', err) + setError(err.message || 'Failed to initialize WebContainer') + } + } + + // Only try to initialize if we're properly isolated + if (isIsolated) { + initWebContainer() + } + }, [isIsolated]) + + // Provide WebContainer context + const contextValue = { + isReady: isInitialized, + error, + executeFunction, + } + + if (error || isIsolated === false) { + const needsRestart = error?.includes('restart') || !isIsolated + + return ( + +
+
+ +
+

WebContainer Error

+

+ {isIsolated === false + ? 'Cross-Origin Isolation is not enabled. WebContainers require COOP/COEP headers.' + : error} +

+ {needsRestart && ( +

+ Please restart the server for the COOP/COEP headers to take effect. +

+ )} +

+ Note: Function blocks will fall back to less secure VM-based execution until this is + resolved. +

+
+
+
+ {children} +
+ ) + } + + return ( + {children} + ) +} diff --git a/executor/handlers.ts b/executor/handlers.ts index 6136967e51..f3b03e1f3e 100644 --- a/executor/handlers.ts +++ b/executor/handlers.ts @@ -482,7 +482,12 @@ export class FunctionBlockHandler implements BlockHandler { return { response: result.output } } catch (error: any) { - console.warn('WebContainer execution failed, falling back to VM:', error.message) + console.warn('WebContainer execution failed, falling back to VM:', error) + console.error('WebContainer error details:', { + name: error.name, + message: error.message, + stack: error.stack, + }) } } diff --git a/lib/webcontainer.ts b/lib/webcontainer.ts index ba0ad08f8c..2be4a4e5c5 100644 --- a/lib/webcontainer.ts +++ b/lib/webcontainer.ts @@ -172,6 +172,8 @@ export async function executeCode( }> { const startTime = Date.now() let process: any = null + let stdout = '' + let result: any = null try { // Get or initialize WebContainer @@ -186,10 +188,22 @@ export async function executeCode( const imports = [] let remainingCode = code + // More carefully extract imports to prevent consuming the entire code let match - while ((match = importRegex.exec(code)) !== null) { - imports.push(match[0]) - remainingCode = remainingCode.replace(match[0], '') + let codeLines = code.split('\n') + let importLines = [] + + // Find import lines + for (let i = 0; i < codeLines.length; i++) { + if (codeLines[i].trim().startsWith('import ')) { + importLines.push(i) + } + } + + // Extract imports and remaining code separately + if (importLines.length > 0) { + imports.push(...importLines.map((idx) => codeLines[idx])) + remainingCode = codeLines.filter((_, idx) => !importLines.includes(idx)).join('\n') } // Create the module file with proper structure @@ -222,8 +236,6 @@ export async function executeCode( await webcontainer.fs.writeFile('/code.mjs', moduleCode) // Set up stdout capture - let stdout = '' - let result: any = null let processCompleted = false // Run the code with Node.js in ES module mode @@ -258,7 +270,7 @@ export async function executeCode( }) ) - // Listen for process exit + // Handle process exit process.exit.then((code: number) => { processCompleted = true if (code === 0) { @@ -269,25 +281,36 @@ export async function executeCode( }) }) - // Add a timeout + // Set up error handling + const errorPromise = new Promise((_, reject) => { + process.stderr.pipeTo( + new WritableStream({ + write(data) { + console.error('WebContainer executeCode - Process error:', data) + stdout += `ERROR: ${data}\n` + }, + }) + ) + }) + + // Set up timeout const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { - if (!processCompleted && process) { - process.kill() + if (!processCompleted) { + console.error('WebContainer executeCode - Process timed out after', timeout, 'ms') + reject(new Error(`Execution timed out after ${timeout}ms`)) } - reject(new Error(`Execution timed out after ${timeout}ms`)) }, timeout) }) // Wait for either completion or timeout - await Promise.race([resultPromise, timeoutPromise]) + await Promise.race([resultPromise, errorPromise, timeoutPromise]) const executionTime = Date.now() - startTime - // Clean stdout of our result markers + // Clean the stdout from our internal markers and result JSON const cleanedStdout = stdout - .replace(/(__RESULT_START__|__RESULT_END__)/g, '') - .replace(/\n{3,}/g, '\n\n') // Reduce multiple newlines + .replace(/\r?\n?__RESULT_START__\r?\n?[\s\S]*?__RESULT_END__\r?\n?/g, '') .trim() return { @@ -299,25 +322,38 @@ export async function executeCode( }, } } catch (error: any) { - // Ensure process is killed on error + console.error('WebContainer executeCode - Execution failed:', { + error: error.message, + name: error.name, + stack: error.stack, + stdout: stdout || 'No stdout', + }) + + // Try to kill the process if it's still running if (process) { try { - process.kill() + await process.kill() } catch (killError) { - console.error('Error killing process:', killError) + console.error('WebContainer executeCode - Failed to kill process:', killError) } } - const executionTime = Date.now() - startTime + // Clean stdout before returning, even in error case + let cleanedStdout = '' + if (stdout) { + cleanedStdout = stdout + .replace(/\r?\n?__RESULT_START__\r?\n?[\s\S]*?__RESULT_END__\r?\n?/g, '') + .trim() + } return { success: false, + error: error.message || 'Unknown error occurred during execution', output: { result: null, - stdout: '', - executionTime, + stdout: cleanedStdout, + executionTime: Date.now() - startTime, }, - error: error.message || 'Code execution failed', } } }