Paper2Code / lib /sandbox /providers /vercel-provider.ts
AUXteam's picture
Upload folder using huggingface_hub
d530f14 verified
import { Sandbox } from '@vercel/sandbox';
import { SandboxProvider, SandboxInfo, CommandResult } from '../types';
// SandboxProviderConfig available through parent class
export class VercelProvider extends SandboxProvider {
private existingFiles: Set<string> = new Set();
async createSandbox(): Promise<SandboxInfo> {
try {
// Kill existing sandbox if any
if (this.sandbox) {
try {
await this.sandbox.stop();
} catch (e) {
console.error('Failed to stop existing sandbox:', e);
}
this.sandbox = null;
}
// Clear existing files tracking
this.existingFiles.clear();
// Create Vercel sandbox
const sandboxConfig: any = {
timeout: 300000, // 5 minutes in ms
runtime: 'node22', // Use node22 runtime for Vercel sandboxes
ports: [5173] // Vite port
};
// Add authentication based on environment variables
if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
sandboxConfig.teamId = process.env.VERCEL_TEAM_ID;
sandboxConfig.projectId = process.env.VERCEL_PROJECT_ID;
sandboxConfig.token = process.env.VERCEL_TOKEN;
} else if (process.env.VERCEL_OIDC_TOKEN) {
sandboxConfig.oidcToken = process.env.VERCEL_OIDC_TOKEN;
}
this.sandbox = await Sandbox.create(sandboxConfig);
const sandboxId = this.sandbox.sandboxId;
// Sandbox created successfully
// Get the sandbox URL using the correct Vercel Sandbox API
const sandboxUrl = this.sandbox.domain(5173);
this.sandboxInfo = {
sandboxId,
url: sandboxUrl,
provider: 'vercel',
createdAt: new Date()
};
return this.sandboxInfo;
} catch (error) {
console.error('[VercelProvider] Error creating sandbox:', error);
throw error;
}
}
async runCommand(command: string): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
try {
// Parse command into cmd and args (matching PR syntax)
const parts = command.split(' ');
const cmd = parts[0];
const args = parts.slice(1);
// Vercel uses runCommand with cmd and args object (based on PR)
const result = await this.sandbox.runCommand({
cmd: cmd,
args: args,
cwd: '/vercel/sandbox',
env: {}
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
return {
stdout: stdout,
stderr: stderr,
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
} catch (error: any) {
return {
stdout: '',
stderr: error.message || 'Command failed',
exitCode: 1,
success: false
};
}
}
async writeFile(path: string, content: string): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Vercel sandbox default working directory is /vercel/sandbox
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
// Writing file to sandbox
// Based on Vercel SDK docs, writeFiles expects path and Buffer content
try {
const buffer = Buffer.from(content, 'utf-8');
// Writing file with buffer
await this.sandbox.writeFiles([{
path: fullPath,
content: buffer
}]);
this.existingFiles.add(path);
} catch (writeError: any) {
// Log detailed error information
console.error(`[VercelProvider] writeFiles failed for ${fullPath}:`, {
error: writeError,
message: writeError?.message,
response: writeError?.response,
statusCode: writeError?.response?.status,
responseData: writeError?.response?.data
});
// Fallback to command-based approach if writeFiles fails
// Falling back to command-based file write
// Ensure directory exists
const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
if (dir) {
const mkdirResult = await this.sandbox.runCommand({
cmd: 'mkdir',
args: ['-p', dir]
});
// Directory created
}
// Write file using echo and redirection
const escapedContent = content
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$')
.replace(/`/g, '\\`')
.replace(/\n/g, '\\n');
const writeResult = await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', `echo "${escapedContent}" > "${fullPath}"`]
});
// File written
if (writeResult.exitCode === 0) {
this.existingFiles.add(path);
} else {
throw new Error(`Failed to write file via command: ${writeResult.stderr}`);
}
}
}
async readFile(path: string): Promise<string> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Vercel sandbox default working directory is /vercel/sandbox
const fullPath = path.startsWith('/') ? path : `/vercel/sandbox/${path}`;
const result = await this.sandbox.runCommand({
cmd: 'cat',
args: [fullPath]
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
if (result.exitCode !== 0) {
throw new Error(`Failed to read file: ${stderr}`);
}
return stdout;
}
async listFiles(directory: string = '/vercel/sandbox'): Promise<string[]> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const result = await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', `find ${directory} -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/.next/*" -not -path "*/dist/*" -not -path "*/build/*" | sed "s|^${directory}/||"`],
cwd: '/'
});
// Handle stdout - it might be a function in Vercel SDK
let stdout = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
if (result.exitCode !== 0) {
return [];
}
return stdout.split('\n').filter((line: string) => line.trim() !== '');
}
async installPackages(packages: string[]): Promise<CommandResult> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
const flags = process.env.NPM_FLAGS || '';
// Installing packages
// Build args array
const args = ['install'];
if (flags) {
args.push(...flags.split(' '));
}
args.push(...packages);
const result = await this.sandbox.runCommand({
cmd: 'npm',
args: args,
cwd: '/vercel/sandbox'
});
// Handle stdout and stderr - they might be functions in Vercel SDK
let stdout = '';
let stderr = '';
try {
if (typeof result.stdout === 'function') {
stdout = await result.stdout();
} else {
stdout = result.stdout || '';
}
} catch (e) {
stdout = '';
}
try {
if (typeof result.stderr === 'function') {
stderr = await result.stderr();
} else {
stderr = result.stderr || '';
}
} catch (e) {
stderr = '';
}
// Restart Vite if configured and successful
if (result.exitCode === 0 && process.env.AUTO_RESTART_VITE === 'true') {
await this.restartViteServer();
}
return {
stdout: stdout,
stderr: stderr,
exitCode: result.exitCode || 0,
success: result.exitCode === 0
};
}
async setupViteApp(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Setting up Vite app for sandbox
// Create directory structure
const mkdirResult = await this.sandbox.runCommand({
cmd: 'mkdir',
args: ['-p', '/vercel/sandbox/src']
});
// Directory structure created
// Create package.json
const packageJson = {
name: "sandbox-app",
version: "1.0.0",
type: "module",
scripts: {
dev: "vite --host",
build: "vite build",
preview: "vite preview"
},
dependencies: {
react: "^18.2.0",
"react-dom": "^18.2.0"
},
devDependencies: {
"@vitejs/plugin-react": "^4.0.0",
vite: "^4.3.9",
tailwindcss: "^3.3.0",
postcss: "^8.4.31",
autoprefixer: "^10.4.16"
}
};
await this.writeFile('package.json', JSON.stringify(packageJson, null, 2));
// Create vite.config.js
const viteConfig = `import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
allowedHosts: [
'.vercel.run', // Allow all Vercel sandbox domains
'.e2b.dev', // Allow all E2B sandbox domains
'localhost'
],
hmr: {
clientPort: 443,
protocol: 'wss'
}
}
})`;
await this.writeFile('vite.config.js', viteConfig);
// Create tailwind.config.js
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}`;
await this.writeFile('tailwind.config.js', tailwindConfig);
// Create postcss.config.js
const postcssConfig = `export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}`;
await this.writeFile('postcss.config.js', postcssConfig);
// Create index.html
const indexHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sandbox App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>`;
await this.writeFile('index.html', indexHtml);
// Create src/main.jsx
const mainJsx = `import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)`;
await this.writeFile('src/main.jsx', mainJsx);
// Create src/App.jsx
const appJsx = `function App() {
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-4">
<div className="text-center max-w-2xl">
<p className="text-lg text-gray-400">
Vercel Sandbox Ready<br/>
Start building your React app with Vite and Tailwind CSS!
</p>
</div>
</div>
)
}
export default App`;
await this.writeFile('src/App.jsx', appJsx);
// Create src/index.css
const indexCss = `@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: rgb(17 24 39);
}`;
await this.writeFile('src/index.css', indexCss);
// Installing npm dependencies
// Install dependencies
try {
const installResult = await this.sandbox.runCommand({
cmd: 'npm',
args: ['install'],
cwd: '/vercel/sandbox'
});
// npm install completed
if (installResult.exitCode === 0) {
// Dependencies installed successfully
} else {
console.warn('[VercelProvider] npm install had issues:', installResult.stderr);
}
} catch (error: any) {
console.error('[VercelProvider] npm install error:', {
message: error?.message,
response: error?.response?.status,
responseText: error?.text
});
// Try alternative approach - run as shell command
try {
const altResult = await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'cd /vercel/sandbox && npm install'],
cwd: '/vercel/sandbox'
});
if (altResult.exitCode === 0) {
// Alternative npm install succeeded
} else {
console.warn('[VercelProvider] Alternative npm install also had issues:', altResult.stderr);
}
} catch (altError) {
console.error('[VercelProvider] Alternative npm install also failed:', altError);
console.warn('[VercelProvider] Continuing without npm install - packages may need to be installed manually');
}
}
// Start Vite dev server
// Starting Vite dev server
// Kill any existing Vite processes
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'pkill -f vite || true'],
cwd: '/'
});
// Start Vite in background
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/vercel/sandbox'
});
// Vite server started in background
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
// Track initial files
this.existingFiles.add('src/App.jsx');
this.existingFiles.add('src/main.jsx');
this.existingFiles.add('src/index.css');
this.existingFiles.add('index.html');
this.existingFiles.add('package.json');
this.existingFiles.add('vite.config.js');
this.existingFiles.add('tailwind.config.js');
this.existingFiles.add('postcss.config.js');
}
async restartViteServer(): Promise<void> {
if (!this.sandbox) {
throw new Error('No active sandbox');
}
// Restarting Vite server
// Kill existing Vite process
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'pkill -f vite || true'],
cwd: '/'
});
// Wait a moment
await new Promise(resolve => setTimeout(resolve, 2000));
// Start Vite in background
await this.sandbox.runCommand({
cmd: 'sh',
args: ['-c', 'nohup npm run dev > /tmp/vite.log 2>&1 &'],
cwd: '/vercel/sandbox'
});
// Vite server started in background
// Wait for Vite to be ready
await new Promise(resolve => setTimeout(resolve, 7000));
}
getSandboxUrl(): string | null {
return this.sandboxInfo?.url || null;
}
getSandboxInfo(): SandboxInfo | null {
return this.sandboxInfo;
}
async terminate(): Promise<void> {
if (this.sandbox) {
try {
await this.sandbox.stop();
} catch (e) {
console.error('Failed to terminate sandbox:', e);
}
this.sandbox = null;
this.sandboxInfo = null;
}
}
isAlive(): boolean {
return !!this.sandbox;
}
}