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> { const files: Record = {}; 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, }; }