| 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 deploymentId = `deploy-artifact`; |
| workbenchStore.addArtifact({ |
| id: deploymentId, |
| messageId: deploymentId, |
| title: 'Netlify Deployment', |
| type: 'standalone', |
| }); |
|
|
| const deployArtifact = workbenchStore.artifacts.get()[deploymentId]; |
|
|
| |
| deployArtifact.runner.handleDeployAction('building', 'running', { source: 'netlify' }); |
|
|
| |
| const actionId = 'build-' + Date.now(); |
| const actionData: ActionCallbackData = { |
| messageId: 'netlify build', |
| artifactId: artifact.id, |
| actionId, |
| action: { |
| type: 'build' as const, |
| content: 'npm run build', |
| }, |
| }; |
|
|
| |
| artifact.runner.addAction(actionData); |
|
|
| |
| await artifact.runner.runAction(actionData); |
|
|
| if (!artifact.runner.buildOutput) { |
| |
| deployArtifact.runner.handleDeployAction('building', 'failed', { |
| error: 'Build failed. Check the terminal for details.', |
| source: 'netlify', |
| }); |
| throw new Error('Build failed'); |
| } |
|
|
| |
| deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'netlify' }); |
|
|
| |
| const container = await webcontainer; |
|
|
| |
| const buildPath = artifact.runner.buildOutput.path.replace('/home/project', ''); |
|
|
| console.log('Original buildPath', buildPath); |
|
|
| |
| let finalBuildPath = buildPath; |
|
|
| |
| const commonOutputDirs = [buildPath, '/dist', '/build', '/out', '/output', '/.next', '/public']; |
|
|
| |
| 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}`); |
| 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'); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| deployArtifact.runner.handleDeployAction('deploying', 'failed', { |
| error: data.error || 'Invalid deployment response', |
| source: 'netlify', |
| }); |
| throw new Error(data.error || 'Invalid deployment response'); |
| } |
|
|
| const maxAttempts = 20; |
| 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') { |
| |
| 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) { |
| |
| deployArtifact.runner.handleDeployAction('deploying', 'failed', { |
| error: 'Deployment timed out', |
| source: 'netlify', |
| }); |
| throw new Error('Deployment timed out'); |
| } |
|
|
| |
| if (data.site) { |
| localStorage.setItem(`netlify-site-${currentChatId}`, data.site.id); |
| } |
|
|
| |
| 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, |
| }; |
| } |
|
|