mirror of
https://github.com/stackblitz-labs/bolt.diy.git
synced 2025-06-18 01:49:07 +01:00
also extract Netlify and Vercel deploy logic into separate components Move the Netlify and Vercel deployment logic from HeaderActionButtons.client.tsx into dedicated components (NetlifyDeploy.client.tsx and VercelDeploy.client.tsx) to improve code maintainability and reusability.
169 lines
5.1 KiB
TypeScript
169 lines
5.1 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');
|
|
}
|
|
|
|
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) {
|
|
throw new Error('Build failed');
|
|
}
|
|
|
|
// 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);
|
|
throw new Error(data.error || 'Invalid deployment response');
|
|
}
|
|
|
|
if (data.project) {
|
|
localStorage.setItem(`vercel-project-${currentChatId}`, data.project.id);
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|