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'); } 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) { 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', ''); 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); 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') { 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) { 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); } toast.success(
Deployed successfully!{' '} View site
, ); 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, }; }