mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-03 11:51:36 +00:00
Merge pull request #1590 from xKevIsDev/main
fix: simplify the SHA-1 hash function in netlify deploy by using the crypto module directly
This commit is contained in:
commit
6996b807d5
@ -29,7 +29,8 @@ import type { ProviderInfo } from '~/types/model';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import { toast } from 'react-toastify';
|
||||
import StarterTemplates from './StarterTemplates';
|
||||
import type { ActionAlert, SupabaseAlert } from '~/types/actions';
|
||||
import type { ActionAlert, SupabaseAlert, DeployAlert } from '~/types/actions';
|
||||
import DeployChatAlert from '~/components/deploy/DeployAlert';
|
||||
import ChatAlert from './ChatAlert';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import ProgressCompilation from './ProgressCompilation';
|
||||
@ -73,6 +74,8 @@ interface BaseChatProps {
|
||||
clearAlert?: () => void;
|
||||
supabaseAlert?: SupabaseAlert;
|
||||
clearSupabaseAlert?: () => void;
|
||||
deployAlert?: DeployAlert;
|
||||
clearDeployAlert?: () => void;
|
||||
data?: JSONValue[] | undefined;
|
||||
actionRunner?: ActionRunner;
|
||||
}
|
||||
@ -109,6 +112,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
messages,
|
||||
actionAlert,
|
||||
clearAlert,
|
||||
deployAlert,
|
||||
clearDeployAlert,
|
||||
supabaseAlert,
|
||||
clearSupabaseAlert,
|
||||
data,
|
||||
@ -349,6 +354,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
) : null;
|
||||
}}
|
||||
</ClientOnly>
|
||||
{deployAlert && (
|
||||
<DeployChatAlert
|
||||
alert={deployAlert}
|
||||
clearAlert={() => clearDeployAlert?.()}
|
||||
postMessage={(message: string | undefined) => {
|
||||
sendMessage?.({} as any, message);
|
||||
clearSupabaseAlert?.();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{supabaseAlert && (
|
||||
<SupabaseChatAlert
|
||||
alert={supabaseAlert}
|
||||
|
@ -124,6 +124,7 @@ export const ChatImpl = memo(
|
||||
const [fakeLoading, setFakeLoading] = useState(false);
|
||||
const files = useStore(workbenchStore.files);
|
||||
const actionAlert = useStore(workbenchStore.alert);
|
||||
const deployAlert = useStore(workbenchStore.deployAlert);
|
||||
const supabaseConn = useStore(supabaseConnection); // Add this line to get Supabase connection
|
||||
const selectedProject = supabaseConn.stats?.projects?.find(
|
||||
(project) => project.id === supabaseConn.selectedProjectId,
|
||||
@ -560,6 +561,8 @@ export const ChatImpl = memo(
|
||||
clearAlert={() => workbenchStore.clearAlert()}
|
||||
supabaseAlert={supabaseAlert}
|
||||
clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()}
|
||||
deployAlert={deployAlert}
|
||||
clearDeployAlert={() => workbenchStore.clearDeployAlert()}
|
||||
data={chatData}
|
||||
/>
|
||||
);
|
||||
|
197
app/components/deploy/DeployAlert.tsx
Normal file
197
app/components/deploy/DeployAlert.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import type { DeployAlert } from '~/types/actions';
|
||||
|
||||
interface DeployAlertProps {
|
||||
alert: DeployAlert;
|
||||
clearAlert: () => void;
|
||||
postMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
export default function DeployChatAlert({ alert, clearAlert, postMessage }: DeployAlertProps) {
|
||||
const { type, title, description, content, url, stage, buildStatus, deployStatus } = alert;
|
||||
|
||||
// Determine if we should show the deployment progress
|
||||
const showProgress = stage && (buildStatus || deployStatus);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4 mb-2`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
className="flex-shrink-0"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'text-xl',
|
||||
type === 'success'
|
||||
? 'i-ph:check-circle-duotone text-bolt-elements-icon-success'
|
||||
: type === 'error'
|
||||
? 'i-ph:warning-duotone text-bolt-elements-button-danger-text'
|
||||
: 'i-ph:info-duotone text-bolt-elements-loader-progress',
|
||||
)}
|
||||
></div>
|
||||
</motion.div>
|
||||
{/* Content */}
|
||||
<div className="ml-3 flex-1">
|
||||
<motion.h3
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className={`text-sm font-medium text-bolt-elements-textPrimary`}
|
||||
>
|
||||
{title}
|
||||
</motion.h3>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className={`mt-2 text-sm text-bolt-elements-textSecondary`}
|
||||
>
|
||||
<p>{description}</p>
|
||||
|
||||
{/* Deployment Progress Visualization */}
|
||||
{showProgress && (
|
||||
<div className="mt-4 mb-2">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
{/* Build Step */}
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={classNames(
|
||||
'w-6 h-6 rounded-full flex items-center justify-center',
|
||||
buildStatus === 'running'
|
||||
? 'bg-bolt-elements-loader-progress'
|
||||
: buildStatus === 'complete'
|
||||
? 'bg-bolt-elements-icon-success'
|
||||
: buildStatus === 'failed'
|
||||
? 'bg-bolt-elements-button-danger-background'
|
||||
: 'bg-bolt-elements-textTertiary',
|
||||
)}
|
||||
>
|
||||
{buildStatus === 'running' ? (
|
||||
<div className="i-svg-spinners:90-ring-with-bg text-white text-xs"></div>
|
||||
) : buildStatus === 'complete' ? (
|
||||
<div className="i-ph:check text-white text-xs"></div>
|
||||
) : buildStatus === 'failed' ? (
|
||||
<div className="i-ph:x text-white text-xs"></div>
|
||||
) : (
|
||||
<span className="text-white text-xs">1</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="ml-2">Build</span>
|
||||
</div>
|
||||
|
||||
{/* Connector Line */}
|
||||
<div
|
||||
className={classNames(
|
||||
'h-0.5 w-8',
|
||||
buildStatus === 'complete' ? 'bg-bolt-elements-icon-success' : 'bg-bolt-elements-textTertiary',
|
||||
)}
|
||||
></div>
|
||||
|
||||
{/* Deploy Step */}
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={classNames(
|
||||
'w-6 h-6 rounded-full flex items-center justify-center',
|
||||
deployStatus === 'running'
|
||||
? 'bg-bolt-elements-loader-progress'
|
||||
: deployStatus === 'complete'
|
||||
? 'bg-bolt-elements-icon-success'
|
||||
: deployStatus === 'failed'
|
||||
? 'bg-bolt-elements-button-danger-background'
|
||||
: 'bg-bolt-elements-textTertiary',
|
||||
)}
|
||||
>
|
||||
{deployStatus === 'running' ? (
|
||||
<div className="i-svg-spinners:90-ring-with-bg text-white text-xs"></div>
|
||||
) : deployStatus === 'complete' ? (
|
||||
<div className="i-ph:check text-white text-xs"></div>
|
||||
) : deployStatus === 'failed' ? (
|
||||
<div className="i-ph:x text-white text-xs"></div>
|
||||
) : (
|
||||
<span className="text-white text-xs">2</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="ml-2">Deploy</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content && (
|
||||
<div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
{url && type === 'success' && (
|
||||
<div className="mt-2">
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bolt-elements-item-contentAccent hover:underline flex items-center"
|
||||
>
|
||||
<span className="mr-1">View deployed site</span>
|
||||
<div className="i-ph:arrow-square-out"></div>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Actions */}
|
||||
<motion.div
|
||||
className="mt-4"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<div className={classNames('flex gap-2')}>
|
||||
{type === 'error' && (
|
||||
<button
|
||||
onClick={() =>
|
||||
postMessage(`*Fix this deployment error*\n\`\`\`\n${content || description}\n\`\`\`\n`)
|
||||
}
|
||||
className={classNames(
|
||||
`px-2 py-1.5 rounded-md text-sm font-medium`,
|
||||
'bg-bolt-elements-button-primary-background',
|
||||
'hover:bg-bolt-elements-button-primary-backgroundHover',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-danger-background',
|
||||
'text-bolt-elements-button-primary-text',
|
||||
'flex items-center gap-1.5',
|
||||
)}
|
||||
>
|
||||
<div className="i-ph:chat-circle-duotone"></div>
|
||||
Ask Bolt
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={clearAlert}
|
||||
className={classNames(
|
||||
`px-2 py-1.5 rounded-md text-sm font-medium`,
|
||||
'bg-bolt-elements-button-secondary-background',
|
||||
'hover:bg-bolt-elements-button-secondary-backgroundHover',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
|
||||
'text-bolt-elements-button-secondary-text',
|
||||
)}
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
257
app/components/deploy/NetlifyDeploy.client.tsx
Normal file
257
app/components/deploy/NetlifyDeploy.client.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
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',
|
||||
});
|
||||
|
||||
toast.success(
|
||||
<div>
|
||||
Deployed successfully!{' '}
|
||||
<a
|
||||
href={deploymentStatus.ssl_url || deploymentStatus.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
View site
|
||||
</a>
|
||||
</div>,
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
202
app/components/deploy/VercelDeploy.client.tsx
Normal file
202
app/components/deploy/VercelDeploy.client.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { vercelConnection } from '~/lib/stores/vercel';
|
||||
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 useVercelDeploy() {
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
const vercelConn = useStore(vercelConnection);
|
||||
const currentChatId = useStore(chatId);
|
||||
|
||||
const handleVercelDeploy = async () => {
|
||||
if (!vercelConn.user || !vercelConn.token) {
|
||||
toast.error('Please connect to Vercel 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-vercel-project`;
|
||||
workbenchStore.addArtifact({
|
||||
id: deploymentId,
|
||||
messageId: deploymentId,
|
||||
title: 'Vercel Deployment',
|
||||
type: 'standalone',
|
||||
});
|
||||
|
||||
const deployArtifact = workbenchStore.artifacts.get()[deploymentId];
|
||||
|
||||
// Notify that build is starting
|
||||
deployArtifact.runner.handleDeployAction('building', 'running', { source: 'vercel' });
|
||||
|
||||
const actionId = 'build-' + Date.now();
|
||||
const actionData: ActionCallbackData = {
|
||||
messageId: 'vercel 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: 'vercel',
|
||||
});
|
||||
throw new Error('Build failed');
|
||||
}
|
||||
|
||||
// Notify that build succeeded and deployment is starting
|
||||
deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'vercel' });
|
||||
|
||||
// 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', '');
|
||||
|
||||
// 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) {
|
||||
console.log(`Directory ${dir} doesn't exist, trying next option. ${error}`);
|
||||
|
||||
// Directory doesn't exist, try the next one
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!buildPathExists) {
|
||||
throw new Error('Could not find build output directory. Please check your build configuration.');
|
||||
}
|
||||
|
||||
// Get all files recursively
|
||||
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 existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`);
|
||||
|
||||
const response = await fetch('/api/vercel-deploy', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectId: existingProjectId || undefined,
|
||||
files: fileContents,
|
||||
token: vercelConn.token,
|
||||
chatId: currentChatId,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
if (!response.ok || !data.deploy || !data.project) {
|
||||
console.error('Invalid deploy response:', data);
|
||||
|
||||
// Notify that deployment failed
|
||||
deployArtifact.runner.handleDeployAction('deploying', 'failed', {
|
||||
error: data.error || 'Invalid deployment response',
|
||||
source: 'vercel',
|
||||
});
|
||||
throw new Error(data.error || 'Invalid deployment response');
|
||||
}
|
||||
|
||||
if (data.project) {
|
||||
localStorage.setItem(`vercel-project-${currentChatId}`, data.project.id);
|
||||
}
|
||||
|
||||
// Notify that deployment completed successfully
|
||||
deployArtifact.runner.handleDeployAction('complete', 'complete', {
|
||||
url: data.deploy.url,
|
||||
source: 'vercel',
|
||||
});
|
||||
|
||||
toast.success(
|
||||
<div>
|
||||
Deployed successfully to Vercel!{' '}
|
||||
<a href={data.deploy.url} target="_blank" rel="noopener noreferrer" className="underline">
|
||||
View site
|
||||
</a>
|
||||
</div>,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Vercel deploy error:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Vercel deployment failed');
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
setIsDeploying(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isDeploying,
|
||||
handleVercelDeploy,
|
||||
isConnected: !!vercelConn.user,
|
||||
};
|
||||
}
|
@ -1,19 +1,16 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { toast } from 'react-toastify';
|
||||
import useViewport from '~/lib/hooks';
|
||||
import { chatStore } from '~/lib/stores/chat';
|
||||
import { netlifyConnection } from '~/lib/stores/netlify';
|
||||
import { vercelConnection } from '~/lib/stores/vercel';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { webcontainer } from '~/lib/webcontainer';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { path } from '~/utils/path';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
|
||||
import { chatId } from '~/lib/persistence/useChatHistory';
|
||||
import { streamingState } from '~/lib/stores/streaming';
|
||||
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
|
||||
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
|
||||
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
|
||||
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
|
||||
|
||||
interface HeaderActionButtonsProps {}
|
||||
|
||||
@ -32,6 +29,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const isStreaming = useStore(streamingState);
|
||||
const { handleVercelDeploy } = useVercelDeploy();
|
||||
const { handleNetlifyDeploy } = useNetlifyDeploy();
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
@ -44,282 +43,24 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const currentChatId = useStore(chatId);
|
||||
|
||||
const handleNetlifyDeploy = async () => {
|
||||
if (!netlifyConn.user || !netlifyConn.token) {
|
||||
toast.error('Please connect to Netlify first in the settings tab!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentChatId) {
|
||||
toast.error('No active chat found');
|
||||
return;
|
||||
}
|
||||
const onVercelDeploy = async () => {
|
||||
setIsDeploying(true);
|
||||
setDeployingTo('vercel');
|
||||
|
||||
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', '');
|
||||
|
||||
// Get all files recursively
|
||||
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 /dist prefix from the path
|
||||
const deployPath = fullPath.replace(buildPath, '');
|
||||
files[deployPath] = content;
|
||||
} else if (entry.isDirectory()) {
|
||||
const subFiles = await getAllFiles(fullPath);
|
||||
Object.assign(files, subFiles);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const fileContents = await getAllFiles(buildPath);
|
||||
|
||||
// Use chatId instead of artifact.id
|
||||
const existingSiteId = localStorage.getItem(`netlify-site-${currentChatId}`);
|
||||
|
||||
// Deploy using the API route with file contents
|
||||
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, // Use chatId instead of artifact.id
|
||||
}),
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Poll for deployment status
|
||||
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(
|
||||
<div>
|
||||
Deployed successfully!{' '}
|
||||
<a
|
||||
href={deploymentStatus.ssl_url || deploymentStatus.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
View site
|
||||
</a>
|
||||
</div>,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Deploy error:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Deployment failed');
|
||||
await handleVercelDeploy();
|
||||
} finally {
|
||||
setIsDeploying(false);
|
||||
setDeployingTo(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVercelDeploy = async () => {
|
||||
if (!vercelConn.user || !vercelConn.token) {
|
||||
toast.error('Please connect to Vercel first in the settings tab!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentChatId) {
|
||||
toast.error('No active chat found');
|
||||
return;
|
||||
}
|
||||
const onNetlifyDeploy = async () => {
|
||||
setIsDeploying(true);
|
||||
setDeployingTo('netlify');
|
||||
|
||||
try {
|
||||
setIsDeploying(true);
|
||||
setDeployingTo('vercel');
|
||||
|
||||
const artifact = workbenchStore.firstArtifact;
|
||||
|
||||
if (!artifact) {
|
||||
throw new Error('No active project found');
|
||||
}
|
||||
|
||||
const actionId = 'build-' + Date.now();
|
||||
const actionData: ActionCallbackData = {
|
||||
messageId: 'vercel 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', '');
|
||||
|
||||
// Get all files recursively
|
||||
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 /dist prefix from the path
|
||||
const deployPath = fullPath.replace(buildPath, '');
|
||||
files[deployPath] = content;
|
||||
} else if (entry.isDirectory()) {
|
||||
const subFiles = await getAllFiles(fullPath);
|
||||
Object.assign(files, subFiles);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const fileContents = await getAllFiles(buildPath);
|
||||
|
||||
// Use chatId instead of artifact.id
|
||||
const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`);
|
||||
|
||||
// Deploy using the API route with file contents
|
||||
const response = await fetch('/api/vercel-deploy', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectId: existingProjectId || undefined,
|
||||
files: fileContents,
|
||||
token: vercelConn.token,
|
||||
chatId: currentChatId,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
if (!response.ok || !data.deploy || !data.project) {
|
||||
console.error('Invalid deploy response:', data);
|
||||
throw new Error(data.error || 'Invalid deployment response');
|
||||
}
|
||||
|
||||
// Store the project ID if it's a new project
|
||||
if (data.project) {
|
||||
localStorage.setItem(`vercel-project-${currentChatId}`, data.project.id);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
<div>
|
||||
Deployed successfully to Vercel!{' '}
|
||||
<a href={data.deploy.url} target="_blank" rel="noopener noreferrer" className="underline">
|
||||
View site
|
||||
</a>
|
||||
</div>,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Vercel deploy error:', error);
|
||||
toast.error(error instanceof Error ? error.message : 'Vercel deployment failed');
|
||||
await handleNetlifyDeploy();
|
||||
} finally {
|
||||
setIsDeploying(false);
|
||||
setDeployingTo(null);
|
||||
@ -348,7 +89,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
||||
<Button
|
||||
active
|
||||
onClick={() => {
|
||||
handleNetlifyDeploy();
|
||||
onNetlifyDeploy();
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
disabled={isDeploying || !activePreview || !netlifyConn.user}
|
||||
@ -369,7 +110,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
||||
<Button
|
||||
active
|
||||
onClick={() => {
|
||||
handleVercelDeploy();
|
||||
onVercelDeploy();
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
disabled={isDeploying || !activePreview || !vercelConn.user}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import { path as nodePath } from '~/utils/path';
|
||||
import { atom, map, type MapStore } from 'nanostores';
|
||||
import type { ActionAlert, BoltAction, FileHistory, SupabaseAction, SupabaseAlert } from '~/types/actions';
|
||||
import type { ActionAlert, BoltAction, DeployAlert, FileHistory, SupabaseAction, SupabaseAlert } from '~/types/actions';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
import type { ActionCallbackData } from './message-parser';
|
||||
@ -71,6 +71,7 @@ export class ActionRunner {
|
||||
actions: ActionsMap = map({});
|
||||
onAlert?: (alert: ActionAlert) => void;
|
||||
onSupabaseAlert?: (alert: SupabaseAlert) => void;
|
||||
onDeployAlert?: (alert: DeployAlert) => void;
|
||||
buildOutput?: { path: string; exitCode: number; output: string };
|
||||
|
||||
constructor(
|
||||
@ -78,11 +79,13 @@ export class ActionRunner {
|
||||
getShellTerminal: () => BoltShell,
|
||||
onAlert?: (alert: ActionAlert) => void,
|
||||
onSupabaseAlert?: (alert: SupabaseAlert) => void,
|
||||
onDeployAlert?: (alert: DeployAlert) => void,
|
||||
) {
|
||||
this.#webcontainer = webcontainerPromise;
|
||||
this.#shellTerminal = getShellTerminal;
|
||||
this.onAlert = onAlert;
|
||||
this.onSupabaseAlert = onSupabaseAlert;
|
||||
this.onDeployAlert = onDeployAlert;
|
||||
}
|
||||
|
||||
addAction(data: ActionCallbackData) {
|
||||
@ -366,6 +369,17 @@ export class ActionRunner {
|
||||
unreachable('Expected build action');
|
||||
}
|
||||
|
||||
// Trigger build started alert
|
||||
this.onDeployAlert?.({
|
||||
type: 'info',
|
||||
title: 'Building Application',
|
||||
description: 'Building your application...',
|
||||
stage: 'building',
|
||||
buildStatus: 'running',
|
||||
deployStatus: 'pending',
|
||||
source: 'netlify',
|
||||
});
|
||||
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
// Create a new terminal specifically for the build
|
||||
@ -383,11 +397,57 @@ export class ActionRunner {
|
||||
const exitCode = await buildProcess.exit;
|
||||
|
||||
if (exitCode !== 0) {
|
||||
// Trigger build failed alert
|
||||
this.onDeployAlert?.({
|
||||
type: 'error',
|
||||
title: 'Build Failed',
|
||||
description: 'Your application build failed',
|
||||
content: output || 'No build output available',
|
||||
stage: 'building',
|
||||
buildStatus: 'failed',
|
||||
deployStatus: 'pending',
|
||||
source: 'netlify',
|
||||
});
|
||||
|
||||
throw new ActionCommandError('Build Failed', output || 'No Output Available');
|
||||
}
|
||||
|
||||
// Get the build output directory path
|
||||
const buildDir = nodePath.join(webcontainer.workdir, 'dist');
|
||||
// Trigger build success alert
|
||||
this.onDeployAlert?.({
|
||||
type: 'success',
|
||||
title: 'Build Completed',
|
||||
description: 'Your application was built successfully',
|
||||
stage: 'deploying',
|
||||
buildStatus: 'complete',
|
||||
deployStatus: 'running',
|
||||
source: 'netlify',
|
||||
});
|
||||
|
||||
// Check for common build directories
|
||||
const commonBuildDirs = ['dist', 'build', 'out', 'output', '.next', 'public'];
|
||||
|
||||
let buildDir = '';
|
||||
|
||||
// Try to find the first existing build directory
|
||||
for (const dir of commonBuildDirs) {
|
||||
const dirPath = nodePath.join(webcontainer.workdir, dir);
|
||||
|
||||
try {
|
||||
await webcontainer.fs.readdir(dirPath);
|
||||
buildDir = dirPath;
|
||||
logger.debug(`Found build directory: ${buildDir}`);
|
||||
break;
|
||||
} catch (error) {
|
||||
// Directory doesn't exist, try the next one
|
||||
logger.debug(`Build directory ${dir} not found, trying next option. ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If no build directory was found, use the default (dist)
|
||||
if (!buildDir) {
|
||||
buildDir = nodePath.join(webcontainer.workdir, 'dist');
|
||||
logger.debug(`No build directory found, defaulting to: ${buildDir}`);
|
||||
}
|
||||
|
||||
return {
|
||||
path: buildDir,
|
||||
@ -441,4 +501,55 @@ export class ActionRunner {
|
||||
throw new Error(`Unknown operation: ${operation}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add this method declaration to the class
|
||||
handleDeployAction(
|
||||
stage: 'building' | 'deploying' | 'complete',
|
||||
status: ActionStatus,
|
||||
details?: {
|
||||
url?: string;
|
||||
error?: string;
|
||||
source?: 'netlify' | 'vercel' | 'github';
|
||||
},
|
||||
): void {
|
||||
if (!this.onDeployAlert) {
|
||||
logger.debug('No deploy alert handler registered');
|
||||
return;
|
||||
}
|
||||
|
||||
const alertType = status === 'failed' ? 'error' : status === 'complete' ? 'success' : 'info';
|
||||
|
||||
const title =
|
||||
stage === 'building'
|
||||
? 'Building Application'
|
||||
: stage === 'deploying'
|
||||
? 'Deploying Application'
|
||||
: 'Deployment Complete';
|
||||
|
||||
const description =
|
||||
status === 'failed'
|
||||
? `${stage === 'building' ? 'Build' : 'Deployment'} failed`
|
||||
: status === 'running'
|
||||
? `${stage === 'building' ? 'Building' : 'Deploying'} your application...`
|
||||
: status === 'complete'
|
||||
? `${stage === 'building' ? 'Build' : 'Deployment'} completed successfully`
|
||||
: `Preparing to ${stage === 'building' ? 'build' : 'deploy'} your application`;
|
||||
|
||||
const buildStatus =
|
||||
stage === 'building' ? status : stage === 'deploying' || stage === 'complete' ? 'complete' : 'pending';
|
||||
|
||||
const deployStatus = stage === 'building' ? 'pending' : status;
|
||||
|
||||
this.onDeployAlert({
|
||||
type: alertType,
|
||||
title,
|
||||
description,
|
||||
content: details?.error || '',
|
||||
url: details?.url,
|
||||
stage,
|
||||
buildStatus: buildStatus as any,
|
||||
deployStatus: deployStatus as any,
|
||||
source: details?.source || 'netlify',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import { extractRelativePath } from '~/utils/diff';
|
||||
import { description } from '~/lib/persistence';
|
||||
import Cookies from 'js-cookie';
|
||||
import { createSampler } from '~/utils/sampler';
|
||||
import type { ActionAlert, SupabaseAlert } from '~/types/actions';
|
||||
import type { ActionAlert, DeployAlert, SupabaseAlert } from '~/types/actions';
|
||||
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
@ -52,6 +52,8 @@ export class WorkbenchStore {
|
||||
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
|
||||
supabaseAlert: WritableAtom<SupabaseAlert | undefined> =
|
||||
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
|
||||
deployAlert: WritableAtom<DeployAlert | undefined> =
|
||||
import.meta.hot?.data.unsavedFiles ?? atom<DeployAlert | undefined>(undefined);
|
||||
modifiedFiles = new Set<string>();
|
||||
artifactIdList: string[] = [];
|
||||
#globalExecutionQueue = Promise.resolve();
|
||||
@ -63,6 +65,7 @@ export class WorkbenchStore {
|
||||
import.meta.hot.data.currentView = this.currentView;
|
||||
import.meta.hot.data.actionAlert = this.actionAlert;
|
||||
import.meta.hot.data.supabaseAlert = this.supabaseAlert;
|
||||
import.meta.hot.data.deployAlert = this.deployAlert;
|
||||
|
||||
// Ensure binary files are properly preserved across hot reloads
|
||||
const filesMap = this.files.get();
|
||||
@ -125,6 +128,14 @@ export class WorkbenchStore {
|
||||
this.supabaseAlert.set(undefined);
|
||||
}
|
||||
|
||||
get DeployAlert() {
|
||||
return this.deployAlert;
|
||||
}
|
||||
|
||||
clearDeployAlert() {
|
||||
this.deployAlert.set(undefined);
|
||||
}
|
||||
|
||||
toggleTerminal(value?: boolean) {
|
||||
this.#terminalStore.toggleTerminal(value);
|
||||
}
|
||||
@ -423,6 +434,13 @@ export class WorkbenchStore {
|
||||
|
||||
this.supabaseAlert.set(alert);
|
||||
},
|
||||
(alert) => {
|
||||
if (this.#reloadedMessages.has(messageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deployAlert.set(alert);
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { type ActionFunctionArgs, json } from '@remix-run/cloudflare';
|
||||
import crypto from 'crypto';
|
||||
import type { NetlifySiteInfo } from '~/types/netlify';
|
||||
|
||||
interface DeployRequestBody {
|
||||
@ -7,15 +8,6 @@ interface DeployRequestBody {
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
async function sha1(message: string) {
|
||||
const msgBuffer = new TextEncoder().encode(message);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
|
||||
@ -112,7 +104,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
// Ensure file path starts with a forward slash
|
||||
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
||||
const hash = await sha1(content);
|
||||
const hash = crypto.createHash('sha1').update(content).digest('hex');
|
||||
fileDigests[normalizedPath] = hash;
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,18 @@ export interface SupabaseAlert {
|
||||
source?: 'supabase';
|
||||
}
|
||||
|
||||
export interface DeployAlert {
|
||||
type: 'success' | 'error' | 'info';
|
||||
title: string;
|
||||
description: string;
|
||||
content?: string;
|
||||
url?: string;
|
||||
stage?: 'building' | 'deploying' | 'complete';
|
||||
buildStatus?: 'pending' | 'running' | 'complete' | 'failed';
|
||||
deployStatus?: 'pending' | 'running' | 'complete' | 'failed';
|
||||
source?: 'vercel' | 'netlify' | 'github';
|
||||
}
|
||||
|
||||
export interface FileHistory {
|
||||
originalContent: string;
|
||||
lastModified: number;
|
||||
|
@ -11,6 +11,7 @@ import { join } from 'path';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Get detailed git info with fallbacks
|
||||
const getGitInfo = () => {
|
||||
try {
|
||||
return {
|
||||
@ -39,6 +40,7 @@ const getGitInfo = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Read package.json with detailed dependency info
|
||||
const getPackageJson = () => {
|
||||
try {
|
||||
const pkgPath = join(process.cwd(), 'package.json');
|
||||
@ -91,32 +93,10 @@ export default defineConfig((config) => {
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: 'esm',
|
||||
},
|
||||
},
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
global: 'globalThis',
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
buffer: 'vite-plugin-node-polyfills/polyfills/buffer',
|
||||
crypto: 'crypto-browserify',
|
||||
stream: 'stream-browserify',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
nodePolyfills({
|
||||
include: ['buffer', 'process', 'util', 'stream', 'crypto'],
|
||||
include: ['buffer', 'process', 'util', 'stream'],
|
||||
globals: {
|
||||
Buffer: true,
|
||||
process: true,
|
||||
@ -193,4 +173,4 @@ function chrome129IssuePlugin() {
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user