bolt.diy/app/routes/api.vercel-deploy.ts
KevIsDev 4d3222ee96 refactor: reorganize project structure by moving files to a more dev friendly setup
- Move stores/utils/types to their relative directories (i.e chat stores in chat directory)
- Move utility files to shared/utils
- Move component files to shared/components
- Move type definitions to shared/types
- Move stores to shared/stores
- Update import paths across the project
2025-06-16 15:33:59 +01:00

251 lines
7.6 KiB
TypeScript

import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/cloudflare';
import type { VercelProjectInfo } from '~/shared/types/vercel';
// Add loader function to handle GET requests
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const projectId = url.searchParams.get('projectId');
const token = url.searchParams.get('token');
if (!projectId || !token) {
return json({ error: 'Missing projectId or token' }, { status: 400 });
}
try {
// Get project info
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${projectId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!projectResponse.ok) {
return json({ error: 'Failed to fetch project' }, { status: 400 });
}
const projectData = (await projectResponse.json()) as any;
// Get latest deployment
const deploymentsResponse = await fetch(`https://api.vercel.com/v6/deployments?projectId=${projectId}&limit=1`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!deploymentsResponse.ok) {
return json({ error: 'Failed to fetch deployments' }, { status: 400 });
}
const deploymentsData = (await deploymentsResponse.json()) as any;
const latestDeployment = deploymentsData.deployments?.[0];
return json({
project: {
id: projectData.id,
name: projectData.name,
url: `https://${projectData.name}.vercel.app`,
},
deploy: latestDeployment
? {
id: latestDeployment.id,
state: latestDeployment.state,
url: latestDeployment.url ? `https://${latestDeployment.url}` : `https://${projectData.name}.vercel.app`,
}
: null,
});
} catch (error) {
console.error('Error fetching Vercel deployment:', error);
return json({ error: 'Failed to fetch deployment' }, { status: 500 });
}
}
interface DeployRequestBody {
projectId?: string;
files: Record<string, string>;
chatId: string;
}
// Existing action function for POST requests
export async function action({ request }: ActionFunctionArgs) {
try {
const { projectId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
if (!token) {
return json({ error: 'Not connected to Vercel' }, { status: 401 });
}
let targetProjectId = projectId;
let projectInfo: VercelProjectInfo | undefined;
// If no projectId provided, create a new project
if (!targetProjectId) {
const projectName = `bolt-diy-${chatId}-${Date.now()}`;
const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: projectName,
framework: null,
}),
});
if (!createProjectResponse.ok) {
const errorData = (await createProjectResponse.json()) as any;
return json(
{ error: `Failed to create project: ${errorData.error?.message || 'Unknown error'}` },
{ status: 400 },
);
}
const newProject = (await createProjectResponse.json()) as any;
targetProjectId = newProject.id;
projectInfo = {
id: newProject.id,
name: newProject.name,
url: `https://${newProject.name}.vercel.app`,
chatId,
};
} else {
// Get existing project info
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${targetProjectId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (projectResponse.ok) {
const existingProject = (await projectResponse.json()) as any;
projectInfo = {
id: existingProject.id,
name: existingProject.name,
url: `https://${existingProject.name}.vercel.app`,
chatId,
};
} else {
// If project doesn't exist, create a new one
const projectName = `bolt-diy-${chatId}-${Date.now()}`;
const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: projectName,
framework: null,
}),
});
if (!createProjectResponse.ok) {
const errorData = (await createProjectResponse.json()) as any;
return json(
{ error: `Failed to create project: ${errorData.error?.message || 'Unknown error'}` },
{ status: 400 },
);
}
const newProject = (await createProjectResponse.json()) as any;
targetProjectId = newProject.id;
projectInfo = {
id: newProject.id,
name: newProject.name,
url: `https://${newProject.name}.vercel.app`,
chatId,
};
}
}
// Prepare files for deployment
const deploymentFiles = [];
for (const [filePath, content] of Object.entries(files)) {
// Ensure file path doesn't start with a slash for Vercel
const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
deploymentFiles.push({
file: normalizedPath,
data: content,
});
}
// Create a new deployment
const deployResponse = await fetch(`https://api.vercel.com/v13/deployments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: projectInfo.name,
project: targetProjectId,
target: 'production',
files: deploymentFiles,
routes: [{ src: '/(.*)', dest: '/$1' }],
}),
});
if (!deployResponse.ok) {
const errorData = (await deployResponse.json()) as any;
return json(
{ error: `Failed to create deployment: ${errorData.error?.message || 'Unknown error'}` },
{ status: 400 },
);
}
const deployData = (await deployResponse.json()) as any;
// Poll for deployment status
let retryCount = 0;
const maxRetries = 60;
let deploymentUrl = '';
let deploymentState = '';
while (retryCount < maxRetries) {
const statusResponse = await fetch(`https://api.vercel.com/v13/deployments/${deployData.id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (statusResponse.ok) {
const status = (await statusResponse.json()) as any;
deploymentState = status.readyState;
deploymentUrl = status.url ? `https://${status.url}` : '';
if (status.readyState === 'READY' || status.readyState === 'ERROR') {
break;
}
}
retryCount++;
await new Promise((resolve) => setTimeout(resolve, 2000));
}
if (deploymentState === 'ERROR') {
return json({ error: 'Deployment failed' }, { status: 500 });
}
if (retryCount >= maxRetries) {
return json({ error: 'Deployment timed out' }, { status: 500 });
}
return json({
success: true,
deploy: {
id: deployData.id,
state: deploymentState,
// Return public domain as deploy URL and private domain as fallback.
url: projectInfo.url || deploymentUrl,
},
project: projectInfo,
});
} catch (error) {
console.error('Vercel deploy error:', error);
return json({ error: 'Deployment failed' }, { status: 500 });
}
}