mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-09 21:50:36 +00:00
feat: add netlify one-click deployment
This commit is contained in:
parent
2a8472ed17
commit
4da13d1edc
@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { logStore } from '~/lib/stores/logs';
|
import { logStore } from '~/lib/stores/logs';
|
||||||
import { classNames } from '~/utils/classNames';
|
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { GithubConnection } from './GithubConnection';
|
import { GithubConnection } from './GithubConnection';
|
||||||
@ -74,8 +73,6 @@ export default function ConnectionsTab() {
|
|||||||
tokenType: 'classic',
|
tokenType: 'classic',
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
const [isFetchingStats, setIsFetchingStats] = useState(false);
|
|
||||||
|
|
||||||
// Load saved connection on mount
|
// Load saved connection on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -101,8 +98,6 @@ export default function ConnectionsTab() {
|
|||||||
|
|
||||||
const fetchGitHubStats = async (token: string) => {
|
const fetchGitHubStats = async (token: string) => {
|
||||||
try {
|
try {
|
||||||
setIsFetchingStats(true);
|
|
||||||
|
|
||||||
// Fetch repositories - only owned by the authenticated user
|
// Fetch repositories - only owned by the authenticated user
|
||||||
const reposResponse = await fetch(
|
const reposResponse = await fetch(
|
||||||
'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
|
'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
|
||||||
@ -184,59 +179,9 @@ export default function ConnectionsTab() {
|
|||||||
logStore.logError('Failed to fetch GitHub stats', { error });
|
logStore.logError('Failed to fetch GitHub stats', { error });
|
||||||
toast.error('Failed to fetch GitHub statistics');
|
toast.error('Failed to fetch GitHub statistics');
|
||||||
} finally {
|
} finally {
|
||||||
setIsFetchingStats(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchGithubUser = async (token: string) => {
|
|
||||||
try {
|
|
||||||
setIsConnecting(true);
|
|
||||||
|
|
||||||
const response = await fetch('https://api.github.com/user', {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Invalid token or unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as GitHubUserResponse;
|
|
||||||
const newConnection: GitHubConnection = {
|
|
||||||
user: data,
|
|
||||||
token,
|
|
||||||
tokenType: connection.tokenType,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Save connection
|
|
||||||
localStorage.setItem('github_connection', JSON.stringify(newConnection));
|
|
||||||
setConnection(newConnection);
|
|
||||||
|
|
||||||
// Fetch additional stats
|
|
||||||
await fetchGitHubStats(token);
|
|
||||||
|
|
||||||
toast.success('Successfully connected to GitHub');
|
|
||||||
} catch (error) {
|
|
||||||
logStore.logError('Failed to authenticate with GitHub', { error });
|
|
||||||
toast.error('Failed to connect to GitHub');
|
|
||||||
setConnection({ user: null, token: '', tokenType: 'classic' });
|
|
||||||
} finally {
|
|
||||||
setIsConnecting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConnect = async (event: React.FormEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
await fetchGithubUser(connection.token);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
|
||||||
localStorage.removeItem('github_connection');
|
|
||||||
setConnection({ user: null, token: '', tokenType: 'classic' });
|
|
||||||
toast.success('Disconnected from GitHub');
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
@ -553,4 +553,3 @@ export function GithubConnection() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +28,7 @@ export function NetlifyConnection() {
|
|||||||
|
|
||||||
const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -37,7 +37,7 @@ export function NetlifyConnection() {
|
|||||||
throw new Error(`Failed to fetch sites: ${sitesResponse.status}`);
|
throw new Error(`Failed to fetch sites: ${sitesResponse.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sites = await sitesResponse.json() as NetlifySite[];
|
const sites = (await sitesResponse.json()) as NetlifySite[];
|
||||||
|
|
||||||
const currentState = netlifyConnection.get();
|
const currentState = netlifyConnection.get();
|
||||||
updateNetlifyConnection({
|
updateNetlifyConnection({
|
||||||
@ -63,7 +63,7 @@ export function NetlifyConnection() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('https://api.netlify.com/api/v1/user', {
|
const response = await fetch('https://api.netlify.com/api/v1/user', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${connection.token}`,
|
Authorization: `Bearer ${connection.token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -72,7 +72,7 @@ export function NetlifyConnection() {
|
|||||||
throw new Error('Invalid token or unauthorized');
|
throw new Error('Invalid token or unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userData = await response.json() as NetlifyUser;
|
const userData = (await response.json()) as NetlifyUser;
|
||||||
updateNetlifyConnection({
|
updateNetlifyConnection({
|
||||||
user: userData,
|
user: userData,
|
||||||
token: connection.token,
|
token: connection.token,
|
||||||
@ -105,7 +105,13 @@ export function NetlifyConnection() {
|
|||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img className='w-5 h-5' height="24" width="24" crossOrigin='anonymous' src="https://cdn.simpleicons.org/netlify" />
|
<img
|
||||||
|
className="w-5 h-5"
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src="https://cdn.simpleicons.org/netlify"
|
||||||
|
/>
|
||||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Netlify Connection</h3>
|
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Netlify Connection</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -113,9 +119,7 @@ export function NetlifyConnection() {
|
|||||||
{!connection.user ? (
|
{!connection.user ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-bolt-elements-textSecondary mb-2">
|
<label className="block text-sm text-bolt-elements-textSecondary mb-2">Personal Access Token</label>
|
||||||
Personal Access Token
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={connection.token}
|
value={connection.token}
|
||||||
@ -192,7 +196,7 @@ export function NetlifyConnection() {
|
|||||||
<div className="flex items-center gap-4 p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
|
<div className="flex items-center gap-4 p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
|
||||||
<img
|
<img
|
||||||
src={connection.user.avatar_url}
|
src={connection.user.avatar_url}
|
||||||
referrerPolicy='no-referrer'
|
referrerPolicy="no-referrer"
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
alt={connection.user.full_name}
|
alt={connection.user.full_name}
|
||||||
className="w-12 h-12 rounded-full border-2 border-[#00AD9F]"
|
className="w-12 h-12 rounded-full border-2 border-[#00AD9F]"
|
||||||
@ -231,7 +235,12 @@ export function NetlifyConnection() {
|
|||||||
{site.name}
|
{site.name}
|
||||||
</h5>
|
</h5>
|
||||||
<div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
|
<div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
|
||||||
<a href={site.url} target="_blank" rel="noopener noreferrer" className="hover:text-[#00AD9F]">
|
<a
|
||||||
|
href={site.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:text-[#00AD9F]"
|
||||||
|
>
|
||||||
{site.url}
|
{site.url}
|
||||||
</a>
|
</a>
|
||||||
{site.published_deploy && (
|
{site.published_deploy && (
|
||||||
|
@ -16,7 +16,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
const showWorkbench = useStore(workbenchStore.showWorkbench);
|
||||||
const { showChat } = useStore(chatStore);
|
const { showChat } = useStore(chatStore);
|
||||||
const connection = useStore(netlifyConnection);
|
const connection = useStore(netlifyConnection);
|
||||||
const [activePreviewIndex, setActivePreviewIndex] = useState(0);
|
const [activePreviewIndex] = useState(0);
|
||||||
const previews = useStore(workbenchStore.previews);
|
const previews = useStore(workbenchStore.previews);
|
||||||
const activePreview = previews[activePreviewIndex];
|
const activePreview = previews[activePreviewIndex];
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
@ -31,6 +31,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
|
|
||||||
const artifact = workbenchStore.firstArtifact;
|
const artifact = workbenchStore.firstArtifact;
|
||||||
|
|
||||||
if (!artifact) {
|
if (!artifact) {
|
||||||
@ -39,13 +40,13 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
|
|
||||||
const actionId = 'build-' + Date.now();
|
const actionId = 'build-' + Date.now();
|
||||||
const actionData: ActionCallbackData = {
|
const actionData: ActionCallbackData = {
|
||||||
messageId: "netlify build",
|
messageId: 'netlify build',
|
||||||
artifactId: artifact.id,
|
artifactId: artifact.id,
|
||||||
actionId,
|
actionId,
|
||||||
action: {
|
action: {
|
||||||
type: 'build' as const,
|
type: 'build' as const,
|
||||||
content: 'npm run build',
|
content: 'npm run build',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the action first
|
// Add the action first
|
||||||
@ -60,8 +61,10 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
|
|
||||||
// Get the build files
|
// Get the build files
|
||||||
const container = await webcontainer;
|
const container = await webcontainer;
|
||||||
|
|
||||||
// Remove /home/project from buildPath if it exists
|
// Remove /home/project from buildPath if it exists
|
||||||
const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
|
const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
|
||||||
|
|
||||||
// Get all files recursively
|
// Get all files recursively
|
||||||
async function getAllFiles(dirPath: string): Promise<Record<string, string>> {
|
async function getAllFiles(dirPath: string): Promise<Record<string, string>> {
|
||||||
const files: Record<string, string> = {};
|
const files: Record<string, string> = {};
|
||||||
@ -69,8 +72,10 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const fullPath = path.join(dirPath, entry.name);
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
|
|
||||||
if (entry.isFile()) {
|
if (entry.isFile()) {
|
||||||
const content = await container.fs.readFile(fullPath, 'utf-8');
|
const content = await container.fs.readFile(fullPath, 'utf-8');
|
||||||
|
|
||||||
// Remove /dist prefix from the path
|
// Remove /dist prefix from the path
|
||||||
const deployPath = fullPath.replace(buildPath, '');
|
const deployPath = fullPath.replace(buildPath, '');
|
||||||
files[deployPath] = content;
|
files[deployPath] = content;
|
||||||
@ -96,11 +101,11 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
siteId: existingSiteId || undefined,
|
siteId: existingSiteId || undefined,
|
||||||
files: fileContents,
|
files: fileContents,
|
||||||
token: connection.token,
|
token: connection.token,
|
||||||
chatId: artifact.id
|
chatId: artifact.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json() as any;
|
const data = (await response.json()) as any;
|
||||||
|
|
||||||
if (!response.ok || !data.deploy || !data.site) {
|
if (!response.ok || !data.deploy || !data.site) {
|
||||||
console.error('Invalid deploy response:', data);
|
console.error('Invalid deploy response:', data);
|
||||||
@ -114,13 +119,16 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
|
|
||||||
while (attempts < maxAttempts) {
|
while (attempts < maxAttempts) {
|
||||||
try {
|
try {
|
||||||
const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`, {
|
const statusResponse = await fetch(
|
||||||
|
`https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${connection.token}`,
|
Authorization: `Bearer ${connection.token}`,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
deploymentStatus = await statusResponse.json() as any;
|
deploymentStatus = (await statusResponse.json()) as any;
|
||||||
|
|
||||||
if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') {
|
if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') {
|
||||||
break;
|
break;
|
||||||
@ -131,11 +139,11 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attempts++;
|
attempts++;
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Status check error:', error);
|
console.error('Status check error:', error);
|
||||||
attempts++;
|
attempts++;
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +167,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
>
|
>
|
||||||
View site
|
View site
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Deploy error:', error);
|
console.error('Deploy error:', error);
|
||||||
@ -176,7 +184,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
|
|||||||
active
|
active
|
||||||
disabled={isDeploying || !activePreview}
|
disabled={isDeploying || !activePreview}
|
||||||
onClick={handleDeploy}
|
onClick={handleDeploy}
|
||||||
className='px-4 hover:bg-bolt-elements-item-backgroundActive'
|
className="px-4 hover:bg-bolt-elements-item-backgroundActive"
|
||||||
>
|
>
|
||||||
{isDeploying ? 'Deploying...' : 'Deploy'}
|
{isDeploying ? 'Deploying...' : 'Deploy'}
|
||||||
</Button>
|
</Button>
|
||||||
@ -222,14 +230,16 @@ interface ButtonProps {
|
|||||||
function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
|
function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classNames('flex items-center p-1.5', {
|
className={classNames(
|
||||||
|
'flex items-center p-1.5',
|
||||||
|
{
|
||||||
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
|
||||||
!active,
|
!active,
|
||||||
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
|
||||||
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
|
||||||
disabled,
|
disabled,
|
||||||
},
|
},
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
@ -159,6 +159,7 @@ export class ActionRunner {
|
|||||||
}
|
}
|
||||||
case 'build': {
|
case 'build': {
|
||||||
const buildOutput = await this.#runBuildAction(action);
|
const buildOutput = await this.#runBuildAction(action);
|
||||||
|
|
||||||
// Store build output for deployment
|
// Store build output for deployment
|
||||||
this.buildOutput = buildOutput;
|
this.buildOutput = buildOutput;
|
||||||
break;
|
break;
|
||||||
@ -318,6 +319,7 @@ export class ActionRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const webcontainer = await this.#webcontainer;
|
const webcontainer = await this.#webcontainer;
|
||||||
|
|
||||||
// Create a new terminal specifically for the build
|
// Create a new terminal specifically for the build
|
||||||
const buildProcess = await webcontainer.spawn('npm', ['run', 'build']);
|
const buildProcess = await webcontainer.spawn('npm', ['run', 'build']);
|
||||||
|
|
||||||
@ -327,7 +329,7 @@ export class ActionRunner {
|
|||||||
write(data) {
|
write(data) {
|
||||||
output += data;
|
output += data;
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const exitCode = await buildProcess.exit;
|
const exitCode = await buildProcess.exit;
|
||||||
@ -342,7 +344,7 @@ export class ActionRunner {
|
|||||||
return {
|
return {
|
||||||
path: buildDir,
|
path: buildDir,
|
||||||
exitCode,
|
exitCode,
|
||||||
output
|
output,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ interface DeployRequestBody {
|
|||||||
|
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
try {
|
try {
|
||||||
const { siteId, files, token, chatId } = await request.json() as DeployRequestBody & { token: string };
|
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return json({ error: 'Not connected to Netlify' }, { status: 401 });
|
return json({ error: 'Not connected to Netlify' }, { status: 401 });
|
||||||
}
|
}
|
||||||
@ -24,43 +25,43 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: siteName,
|
name: siteName,
|
||||||
custom_domain: null,
|
custom_domain: null,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createSiteResponse.ok) {
|
if (!createSiteResponse.ok) {
|
||||||
return json({ error: 'Failed to create site' }, { status: 400 });
|
return json({ error: 'Failed to create site' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSite = await createSiteResponse.json() as any;
|
const newSite = (await createSiteResponse.json()) as any;
|
||||||
targetSiteId = newSite.id;
|
targetSiteId = newSite.id;
|
||||||
siteInfo = {
|
siteInfo = {
|
||||||
id: newSite.id,
|
id: newSite.id,
|
||||||
name: newSite.name,
|
name: newSite.name,
|
||||||
url: newSite.url,
|
url: newSite.url,
|
||||||
chatId
|
chatId,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Get existing site info
|
// Get existing site info
|
||||||
if (targetSiteId) {
|
if (targetSiteId) {
|
||||||
const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, {
|
const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (siteResponse.ok) {
|
if (siteResponse.ok) {
|
||||||
const existingSite = await siteResponse.json() as any;
|
const existingSite = (await siteResponse.json()) as any;
|
||||||
siteInfo = {
|
siteInfo = {
|
||||||
id: existingSite.id,
|
id: existingSite.id,
|
||||||
name: existingSite.name,
|
name: existingSite.name,
|
||||||
url: existingSite.url,
|
url: existingSite.url,
|
||||||
chatId
|
chatId,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
targetSiteId = undefined;
|
targetSiteId = undefined;
|
||||||
@ -73,32 +74,33 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: siteName,
|
name: siteName,
|
||||||
custom_domain: null,
|
custom_domain: null,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createSiteResponse.ok) {
|
if (!createSiteResponse.ok) {
|
||||||
return json({ error: 'Failed to create site' }, { status: 400 });
|
return json({ error: 'Failed to create site' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSite = await createSiteResponse.json() as any;
|
const newSite = (await createSiteResponse.json()) as any;
|
||||||
targetSiteId = newSite.id;
|
targetSiteId = newSite.id;
|
||||||
siteInfo = {
|
siteInfo = {
|
||||||
id: newSite.id,
|
id: newSite.id,
|
||||||
name: newSite.name,
|
name: newSite.name,
|
||||||
url: newSite.url,
|
url: newSite.url,
|
||||||
chatId
|
chatId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file digests
|
// Create file digests
|
||||||
const fileDigests: Record<string, string> = {};
|
const fileDigests: Record<string, string> = {};
|
||||||
|
|
||||||
for (const [filePath, content] of Object.entries(files)) {
|
for (const [filePath, content] of Object.entries(files)) {
|
||||||
// Ensure file path starts with a forward slash
|
// Ensure file path starts with a forward slash
|
||||||
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
|
||||||
@ -110,7 +112,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys`, {
|
const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -120,15 +122,15 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
draft: false, // Change this to false for production deployments
|
draft: false, // Change this to false for production deployments
|
||||||
function_schedules: [],
|
function_schedules: [],
|
||||||
required: Object.keys(fileDigests), // Add this line
|
required: Object.keys(fileDigests), // Add this line
|
||||||
framework: null
|
framework: null,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deployResponse.ok) {
|
if (!deployResponse.ok) {
|
||||||
return json({ error: 'Failed to create deployment' }, { status: 400 });
|
return json({ error: 'Failed to create deployment' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const deploy = await deployResponse.json() as any;
|
const deploy = (await deployResponse.json()) as any;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 60;
|
const maxRetries = 60;
|
||||||
|
|
||||||
@ -136,11 +138,11 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, {
|
const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const status = await statusResponse.json() as any;
|
const status = (await statusResponse.json()) as any;
|
||||||
|
|
||||||
if (status.state === 'prepared' || status.state === 'uploaded') {
|
if (status.state === 'prepared' || status.state === 'uploaded') {
|
||||||
// Upload all files regardless of required array
|
// Upload all files regardless of required array
|
||||||
@ -152,25 +154,29 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
while (!uploadSuccess && uploadRetries < 3) {
|
while (!uploadSuccess && uploadRetries < 3) {
|
||||||
try {
|
try {
|
||||||
const uploadResponse = await fetch(`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`, {
|
const uploadResponse = await fetch(
|
||||||
|
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`,
|
||||||
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/octet-stream',
|
'Content-Type': 'application/octet-stream',
|
||||||
},
|
},
|
||||||
body: content,
|
body: content,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
uploadSuccess = uploadResponse.ok;
|
uploadSuccess = uploadResponse.ok;
|
||||||
|
|
||||||
if (!uploadSuccess) {
|
if (!uploadSuccess) {
|
||||||
console.error('Upload failed:', await uploadResponse.text());
|
console.error('Upload failed:', await uploadResponse.text());
|
||||||
uploadRetries++;
|
uploadRetries++;
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
uploadRetries++;
|
uploadRetries++;
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,9 +194,9 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
deploy: {
|
deploy: {
|
||||||
id: status.id,
|
id: status.id,
|
||||||
state: status.state,
|
state: status.state,
|
||||||
url: status.ssl_url || status.url
|
url: status.ssl_url || status.url,
|
||||||
},
|
},
|
||||||
site: siteInfo
|
site: siteInfo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,7 +206,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
retryCount++;
|
retryCount++;
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (retryCount >= maxRetries) {
|
if (retryCount >= maxRetries) {
|
||||||
@ -214,7 +220,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
id: deploy.id,
|
id: deploy.id,
|
||||||
state: deploy.state,
|
state: deploy.state,
|
||||||
},
|
},
|
||||||
site: siteInfo
|
site: siteInfo,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Deploy error:', error);
|
console.error('Deploy error:', error);
|
||||||
|
@ -98,9 +98,7 @@ const COLOR_PRIMITIVES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
safelist: [
|
safelist: [...Object.keys(customIconCollection[collectionName] || {}).map((x) => `i-bolt:${x}`)],
|
||||||
...Object.keys(customIconCollection[collectionName] || {}).map(x => `i-bolt:${x}`)
|
|
||||||
],
|
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
|
'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
|
||||||
'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
|
'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
|
||||||
|
Loading…
Reference in New Issue
Block a user