bolt.diy/app/components/deploy/VercelDeploy.client.tsx
KevIsDev 33305c4326 feat(deploy): add deploy alert system for build and deployment status
Introduce a new `DeployAlert` interface and related components to provide visual feedback on build and deployment stages. This includes status updates for Vercel and Netlify deployments, with progress visualization and error handling. The changes enhance user experience by offering real-time updates during the deployment process.
2025-04-04 11:22:56 +01:00

203 lines
6.3 KiB
TypeScript

import { toast } from 'react-toastify';
import { useStore } from '@nanostores/react';
import { vercelConnection } from '~/lib/stores/vercel';
import { workbenchStore } from '~/lib/stores/workbench';
import { webcontainer } from '~/lib/webcontainer';
import { path } from '~/utils/path';
import { useState } from 'react';
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
import { chatId } from '~/lib/persistence/useChatHistory';
export function useVercelDeploy() {
const [isDeploying, setIsDeploying] = useState(false);
const vercelConn = useStore(vercelConnection);
const currentChatId = useStore(chatId);
const handleVercelDeploy = async () => {
if (!vercelConn.user || !vercelConn.token) {
toast.error('Please connect to Vercel first in the settings tab!');
return false;
}
if (!currentChatId) {
toast.error('No active chat found');
return false;
}
try {
setIsDeploying(true);
const artifact = workbenchStore.firstArtifact;
if (!artifact) {
throw new Error('No active project found');
}
// Create a deployment artifact for visual feedback
const deploymentId = `deploy-vercel-project`;
workbenchStore.addArtifact({
id: deploymentId,
messageId: deploymentId,
title: 'Vercel Deployment',
type: 'standalone',
});
const deployArtifact = workbenchStore.artifacts.get()[deploymentId];
// Notify that build is starting
deployArtifact.runner.handleDeployAction('building', 'running', { source: 'vercel' });
const actionId = 'build-' + Date.now();
const actionData: ActionCallbackData = {
messageId: 'vercel build',
artifactId: artifact.id,
actionId,
action: {
type: 'build' as const,
content: 'npm run build',
},
};
// Add the action first
artifact.runner.addAction(actionData);
// Then run it
await artifact.runner.runAction(actionData);
if (!artifact.runner.buildOutput) {
// Notify that build failed
deployArtifact.runner.handleDeployAction('building', 'failed', {
error: 'Build failed. Check the terminal for details.',
source: 'vercel',
});
throw new Error('Build failed');
}
// Notify that build succeeded and deployment is starting
deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'vercel' });
// Get the build files
const container = await webcontainer;
// Remove /home/project from buildPath if it exists
const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
// Check if the build path exists
let finalBuildPath = buildPath;
// List of common output directories to check if the specified build path doesn't exist
const commonOutputDirs = [buildPath, '/dist', '/build', '/out', '/output', '/.next', '/public'];
// Verify the build path exists, or try to find an alternative
let buildPathExists = false;
for (const dir of commonOutputDirs) {
try {
await container.fs.readdir(dir);
finalBuildPath = dir;
buildPathExists = true;
console.log(`Using build directory: ${finalBuildPath}`);
break;
} catch (error) {
console.log(`Directory ${dir} doesn't exist, trying next option. ${error}`);
// Directory doesn't exist, try the next one
continue;
}
}
if (!buildPathExists) {
throw new Error('Could not find build output directory. Please check your build configuration.');
}
// Get all files recursively
async function getAllFiles(dirPath: string): Promise<Record<string, string>> {
const files: Record<string, string> = {};
const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isFile()) {
const content = await container.fs.readFile(fullPath, 'utf-8');
// Remove build path prefix from the path
const deployPath = fullPath.replace(finalBuildPath, '');
files[deployPath] = content;
} else if (entry.isDirectory()) {
const subFiles = await getAllFiles(fullPath);
Object.assign(files, subFiles);
}
}
return files;
}
const fileContents = await getAllFiles(finalBuildPath);
// Use chatId instead of artifact.id
const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`);
const response = await fetch('/api/vercel-deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
projectId: existingProjectId || undefined,
files: fileContents,
token: vercelConn.token,
chatId: currentChatId,
}),
});
const data = (await response.json()) as any;
if (!response.ok || !data.deploy || !data.project) {
console.error('Invalid deploy response:', data);
// Notify that deployment failed
deployArtifact.runner.handleDeployAction('deploying', 'failed', {
error: data.error || 'Invalid deployment response',
source: 'vercel',
});
throw new Error(data.error || 'Invalid deployment response');
}
if (data.project) {
localStorage.setItem(`vercel-project-${currentChatId}`, data.project.id);
}
// Notify that deployment completed successfully
deployArtifact.runner.handleDeployAction('complete', 'complete', {
url: data.deploy.url,
source: 'vercel',
});
toast.success(
<div>
Deployed successfully to Vercel!{' '}
<a href={data.deploy.url} target="_blank" rel="noopener noreferrer" className="underline">
View site
</a>
</div>,
);
return true;
} catch (err) {
console.error('Vercel deploy error:', err);
toast.error(err instanceof Error ? err.message : 'Vercel deployment failed');
return false;
} finally {
setIsDeploying(false);
}
};
return {
isDeploying,
handleVercelDeploy,
isConnected: !!vercelConn.user,
};
}