mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-04-26 09:00:43 +00:00
Remove redundant success toast messages from Vercel and Netlify deploy components. Additionally, prioritize the public domain URL over the private domain in the Vercel deploy action for consistency and clarity.
244 lines
7.8 KiB
TypeScript
244 lines
7.8 KiB
TypeScript
import { toast } from 'react-toastify';
|
|
import { useStore } from '@nanostores/react';
|
|
import { netlifyConnection } from '~/lib/stores/netlify';
|
|
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 useNetlifyDeploy() {
|
|
const [isDeploying, setIsDeploying] = useState(false);
|
|
const netlifyConn = useStore(netlifyConnection);
|
|
const currentChatId = useStore(chatId);
|
|
|
|
const handleNetlifyDeploy = async () => {
|
|
if (!netlifyConn.user || !netlifyConn.token) {
|
|
toast.error('Please connect to Netlify 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-artifact`;
|
|
workbenchStore.addArtifact({
|
|
id: deploymentId,
|
|
messageId: deploymentId,
|
|
title: 'Netlify Deployment',
|
|
type: 'standalone',
|
|
});
|
|
|
|
const deployArtifact = workbenchStore.artifacts.get()[deploymentId];
|
|
|
|
// Notify that build is starting
|
|
deployArtifact.runner.handleDeployAction('building', 'running', { source: 'netlify' });
|
|
|
|
// Set up build action
|
|
const actionId = 'build-' + Date.now();
|
|
const actionData: ActionCallbackData = {
|
|
messageId: 'netlify 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: 'netlify',
|
|
});
|
|
throw new Error('Build failed');
|
|
}
|
|
|
|
// Notify that build succeeded and deployment is starting
|
|
deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'netlify' });
|
|
|
|
// 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', '');
|
|
|
|
console.log('Original buildPath', buildPath);
|
|
|
|
// 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) {
|
|
// Directory doesn't exist, try the next one
|
|
console.log(`Directory ${dir} doesn't exist, trying next option. ${error}`);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (!buildPathExists) {
|
|
throw new Error('Could not find build output directory. Please check your build configuration.');
|
|
}
|
|
|
|
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 existingSiteId = localStorage.getItem(`netlify-site-${currentChatId}`);
|
|
|
|
const response = await fetch('/api/netlify-deploy', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
siteId: existingSiteId || undefined,
|
|
files: fileContents,
|
|
token: netlifyConn.token,
|
|
chatId: currentChatId,
|
|
}),
|
|
});
|
|
|
|
const data = (await response.json()) as any;
|
|
|
|
if (!response.ok || !data.deploy || !data.site) {
|
|
console.error('Invalid deploy response:', data);
|
|
|
|
// Notify that deployment failed
|
|
deployArtifact.runner.handleDeployAction('deploying', 'failed', {
|
|
error: data.error || 'Invalid deployment response',
|
|
source: 'netlify',
|
|
});
|
|
throw new Error(data.error || 'Invalid deployment response');
|
|
}
|
|
|
|
const maxAttempts = 20; // 2 minutes timeout
|
|
let attempts = 0;
|
|
let deploymentStatus;
|
|
|
|
while (attempts < maxAttempts) {
|
|
try {
|
|
const statusResponse = await fetch(
|
|
`https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${netlifyConn.token}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
deploymentStatus = (await statusResponse.json()) as any;
|
|
|
|
if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') {
|
|
break;
|
|
}
|
|
|
|
if (deploymentStatus.state === 'error') {
|
|
// Notify that deployment failed
|
|
deployArtifact.runner.handleDeployAction('deploying', 'failed', {
|
|
error: 'Deployment failed: ' + (deploymentStatus.error_message || 'Unknown error'),
|
|
source: 'netlify',
|
|
});
|
|
throw new Error('Deployment failed: ' + (deploymentStatus.error_message || 'Unknown error'));
|
|
}
|
|
|
|
attempts++;
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
} catch (error) {
|
|
console.error('Status check error:', error);
|
|
attempts++;
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
}
|
|
}
|
|
|
|
if (attempts >= maxAttempts) {
|
|
// Notify that deployment timed out
|
|
deployArtifact.runner.handleDeployAction('deploying', 'failed', {
|
|
error: 'Deployment timed out',
|
|
source: 'netlify',
|
|
});
|
|
throw new Error('Deployment timed out');
|
|
}
|
|
|
|
// Store the site ID if it's a new site
|
|
if (data.site) {
|
|
localStorage.setItem(`netlify-site-${currentChatId}`, data.site.id);
|
|
}
|
|
|
|
// Notify that deployment completed successfully
|
|
deployArtifact.runner.handleDeployAction('complete', 'complete', {
|
|
url: deploymentStatus.ssl_url || deploymentStatus.url,
|
|
source: 'netlify',
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Deploy error:', error);
|
|
toast.error(error instanceof Error ? error.message : 'Deployment failed');
|
|
|
|
return false;
|
|
} finally {
|
|
setIsDeploying(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
isDeploying,
|
|
handleNetlifyDeploy,
|
|
isConnected: !!netlifyConn.user,
|
|
};
|
|
}
|