Spaces:
Sleeping
Sleeping
| 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, | |
| }; | |
| } | |