KevIsDev commited on
Commit
4da13d1
·
1 Parent(s): 2a8472e

feat: add netlify one-click deployment

Browse files
app/components/@settings/tabs/connections/ConnectionsTab.tsx CHANGED
@@ -1,6 +1,5 @@
1
  import React, { useState, useEffect } from 'react';
2
  import { logStore } from '~/lib/stores/logs';
3
- import { classNames } from '~/utils/classNames';
4
  import { motion } from 'framer-motion';
5
  import { toast } from 'react-toastify';
6
  import { GithubConnection } from './GithubConnection';
@@ -74,8 +73,6 @@ export default function ConnectionsTab() {
74
  tokenType: 'classic',
75
  });
76
  const [isLoading, setIsLoading] = useState(true);
77
- const [isConnecting, setIsConnecting] = useState(false);
78
- const [isFetchingStats, setIsFetchingStats] = useState(false);
79
 
80
  // Load saved connection on mount
81
  useEffect(() => {
@@ -101,8 +98,6 @@ export default function ConnectionsTab() {
101
 
102
  const fetchGitHubStats = async (token: string) => {
103
  try {
104
- setIsFetchingStats(true);
105
-
106
  // Fetch repositories - only owned by the authenticated user
107
  const reposResponse = await fetch(
108
  'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
@@ -184,59 +179,9 @@ export default function ConnectionsTab() {
184
  logStore.logError('Failed to fetch GitHub stats', { error });
185
  toast.error('Failed to fetch GitHub statistics');
186
  } finally {
187
- setIsFetchingStats(false);
188
- }
189
- };
190
-
191
- const fetchGithubUser = async (token: string) => {
192
- try {
193
- setIsConnecting(true);
194
-
195
- const response = await fetch('https://api.github.com/user', {
196
- headers: {
197
- Authorization: `Bearer ${token}`,
198
- },
199
- });
200
-
201
- if (!response.ok) {
202
- throw new Error('Invalid token or unauthorized');
203
- }
204
-
205
- const data = (await response.json()) as GitHubUserResponse;
206
- const newConnection: GitHubConnection = {
207
- user: data,
208
- token,
209
- tokenType: connection.tokenType,
210
- };
211
-
212
- // Save connection
213
- localStorage.setItem('github_connection', JSON.stringify(newConnection));
214
- setConnection(newConnection);
215
-
216
- // Fetch additional stats
217
- await fetchGitHubStats(token);
218
-
219
- toast.success('Successfully connected to GitHub');
220
- } catch (error) {
221
- logStore.logError('Failed to authenticate with GitHub', { error });
222
- toast.error('Failed to connect to GitHub');
223
- setConnection({ user: null, token: '', tokenType: 'classic' });
224
- } finally {
225
- setIsConnecting(false);
226
  }
227
  };
228
 
229
- const handleConnect = async (event: React.FormEvent) => {
230
- event.preventDefault();
231
- await fetchGithubUser(connection.token);
232
- };
233
-
234
- const handleDisconnect = () => {
235
- localStorage.removeItem('github_connection');
236
- setConnection({ user: null, token: '', tokenType: 'classic' });
237
- toast.success('Disconnected from GitHub');
238
- };
239
-
240
  if (isLoading) {
241
  return <LoadingSpinner />;
242
  }
@@ -259,9 +204,9 @@ export default function ConnectionsTab() {
259
 
260
  <div className="grid grid-cols-1 gap-4">
261
  {/* GitHub Connection */}
262
- <GithubConnection/>
263
  {/* Netlify Connection */}
264
- <NetlifyConnection/>
265
  </div>
266
  </div>
267
  );
 
1
  import React, { useState, useEffect } from 'react';
2
  import { logStore } from '~/lib/stores/logs';
 
3
  import { motion } from 'framer-motion';
4
  import { toast } from 'react-toastify';
5
  import { GithubConnection } from './GithubConnection';
 
73
  tokenType: 'classic',
74
  });
75
  const [isLoading, setIsLoading] = useState(true);
 
 
76
 
77
  // Load saved connection on mount
78
  useEffect(() => {
 
98
 
99
  const fetchGitHubStats = async (token: string) => {
100
  try {
 
 
101
  // Fetch repositories - only owned by the authenticated user
102
  const reposResponse = await fetch(
103
  'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member,collaborator',
 
179
  logStore.logError('Failed to fetch GitHub stats', { error });
180
  toast.error('Failed to fetch GitHub statistics');
181
  } finally {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
  };
184
 
 
 
 
 
 
 
 
 
 
 
 
185
  if (isLoading) {
186
  return <LoadingSpinner />;
187
  }
 
204
 
205
  <div className="grid grid-cols-1 gap-4">
206
  {/* GitHub Connection */}
207
+ <GithubConnection />
208
  {/* Netlify Connection */}
209
+ <NetlifyConnection />
210
  </div>
211
  </div>
212
  );
app/components/@settings/tabs/connections/GithubConnection.tsx CHANGED
@@ -553,4 +553,3 @@ export function GithubConnection() {
553
  </motion.div>
554
  );
555
  }
556
-
 
553
  </motion.div>
554
  );
555
  }
 
app/components/@settings/tabs/connections/NetlifyConnection.tsx CHANGED
@@ -28,7 +28,7 @@ export function NetlifyConnection() {
28
 
29
  const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
30
  headers: {
31
- 'Authorization': `Bearer ${token}`,
32
  'Content-Type': 'application/json',
33
  },
34
  });
@@ -37,8 +37,8 @@ export function NetlifyConnection() {
37
  throw new Error(`Failed to fetch sites: ${sitesResponse.status}`);
38
  }
39
 
40
- const sites = await sitesResponse.json() as NetlifySite[];
41
-
42
  const currentState = netlifyConnection.get();
43
  updateNetlifyConnection({
44
  ...currentState,
@@ -63,7 +63,7 @@ export function NetlifyConnection() {
63
  try {
64
  const response = await fetch('https://api.netlify.com/api/v1/user', {
65
  headers: {
66
- 'Authorization': `Bearer ${connection.token}`,
67
  'Content-Type': 'application/json',
68
  },
69
  });
@@ -72,12 +72,12 @@ export function NetlifyConnection() {
72
  throw new Error('Invalid token or unauthorized');
73
  }
74
 
75
- const userData = await response.json() as NetlifyUser;
76
  updateNetlifyConnection({
77
  user: userData,
78
  token: connection.token,
79
  });
80
-
81
  await fetchNetlifyStats(connection.token);
82
  toast.success('Successfully connected to Netlify');
83
  } catch (error) {
@@ -105,7 +105,13 @@ export function NetlifyConnection() {
105
  <div className="p-6 space-y-6">
106
  <div className="flex items-center justify-between">
107
  <div className="flex items-center gap-2">
108
- <img className='w-5 h-5' height="24" width="24" crossOrigin='anonymous' src="https://cdn.simpleicons.org/netlify" />
 
 
 
 
 
 
109
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Netlify Connection</h3>
110
  </div>
111
  </div>
@@ -113,9 +119,7 @@ export function NetlifyConnection() {
113
  {!connection.user ? (
114
  <div className="space-y-4">
115
  <div>
116
- <label className="block text-sm text-bolt-elements-textSecondary mb-2">
117
- Personal Access Token
118
- </label>
119
  <input
120
  type="password"
121
  value={connection.token}
@@ -190,12 +194,12 @@ export function NetlifyConnection() {
190
  </div>
191
 
192
  <div className="flex items-center gap-4 p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
193
- <img
194
- src={connection.user.avatar_url}
195
- referrerPolicy='no-referrer'
196
- crossOrigin="anonymous"
197
- alt={connection.user.full_name}
198
- className="w-12 h-12 rounded-full border-2 border-[#00AD9F]"
199
  />
200
  <div>
201
  <h4 className="text-sm font-medium text-bolt-elements-textPrimary">{connection.user.full_name}</h4>
@@ -231,7 +235,12 @@ export function NetlifyConnection() {
231
  {site.name}
232
  </h5>
233
  <div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
234
- <a href={site.url} target="_blank" rel="noopener noreferrer" className="hover:text-[#00AD9F]">
 
 
 
 
 
235
  {site.url}
236
  </a>
237
  {site.published_deploy && (
@@ -270,4 +279,4 @@ export function NetlifyConnection() {
270
  </div>
271
  </motion.div>
272
  );
273
- }
 
28
 
29
  const sitesResponse = await fetch('https://api.netlify.com/api/v1/sites', {
30
  headers: {
31
+ Authorization: `Bearer ${token}`,
32
  'Content-Type': 'application/json',
33
  },
34
  });
 
37
  throw new Error(`Failed to fetch sites: ${sitesResponse.status}`);
38
  }
39
 
40
+ const sites = (await sitesResponse.json()) as NetlifySite[];
41
+
42
  const currentState = netlifyConnection.get();
43
  updateNetlifyConnection({
44
  ...currentState,
 
63
  try {
64
  const response = await fetch('https://api.netlify.com/api/v1/user', {
65
  headers: {
66
+ Authorization: `Bearer ${connection.token}`,
67
  'Content-Type': 'application/json',
68
  },
69
  });
 
72
  throw new Error('Invalid token or unauthorized');
73
  }
74
 
75
+ const userData = (await response.json()) as NetlifyUser;
76
  updateNetlifyConnection({
77
  user: userData,
78
  token: connection.token,
79
  });
80
+
81
  await fetchNetlifyStats(connection.token);
82
  toast.success('Successfully connected to Netlify');
83
  } catch (error) {
 
105
  <div className="p-6 space-y-6">
106
  <div className="flex items-center justify-between">
107
  <div className="flex items-center gap-2">
108
+ <img
109
+ className="w-5 h-5"
110
+ height="24"
111
+ width="24"
112
+ crossOrigin="anonymous"
113
+ src="https://cdn.simpleicons.org/netlify"
114
+ />
115
  <h3 className="text-base font-medium text-bolt-elements-textPrimary">Netlify Connection</h3>
116
  </div>
117
  </div>
 
119
  {!connection.user ? (
120
  <div className="space-y-4">
121
  <div>
122
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">Personal Access Token</label>
 
 
123
  <input
124
  type="password"
125
  value={connection.token}
 
194
  </div>
195
 
196
  <div className="flex items-center gap-4 p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
197
+ <img
198
+ src={connection.user.avatar_url}
199
+ referrerPolicy="no-referrer"
200
+ crossOrigin="anonymous"
201
+ alt={connection.user.full_name}
202
+ className="w-12 h-12 rounded-full border-2 border-[#00AD9F]"
203
  />
204
  <div>
205
  <h4 className="text-sm font-medium text-bolt-elements-textPrimary">{connection.user.full_name}</h4>
 
235
  {site.name}
236
  </h5>
237
  <div className="flex items-center gap-2 mt-2 text-xs text-bolt-elements-textSecondary">
238
+ <a
239
+ href={site.url}
240
+ target="_blank"
241
+ rel="noopener noreferrer"
242
+ className="hover:text-[#00AD9F]"
243
+ >
244
  {site.url}
245
  </a>
246
  {site.published_deploy && (
 
279
  </div>
280
  </motion.div>
281
  );
282
+ }
app/components/header/HeaderActionButtons.client.tsx CHANGED
@@ -16,7 +16,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
16
  const showWorkbench = useStore(workbenchStore.showWorkbench);
17
  const { showChat } = useStore(chatStore);
18
  const connection = useStore(netlifyConnection);
19
- const [activePreviewIndex, setActivePreviewIndex] = useState(0);
20
  const previews = useStore(workbenchStore.previews);
21
  const activePreview = previews[activePreviewIndex];
22
  const [isDeploying, setIsDeploying] = useState(false);
@@ -31,26 +31,27 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
31
 
32
  try {
33
  setIsDeploying(true);
 
34
  const artifact = workbenchStore.firstArtifact;
35
-
36
  if (!artifact) {
37
  throw new Error('No active project found');
38
  }
39
 
40
  const actionId = 'build-' + Date.now();
41
  const actionData: ActionCallbackData = {
42
- messageId: "netlify build",
43
  artifactId: artifact.id,
44
  actionId,
45
  action: {
46
  type: 'build' as const,
47
  content: 'npm run build',
48
- }
49
  };
50
 
51
  // Add the action first
52
  artifact.runner.addAction(actionData);
53
-
54
  // Then run it
55
  await artifact.runner.runAction(actionData);
56
 
@@ -60,17 +61,21 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
60
 
61
  // Get the build files
62
  const container = await webcontainer;
 
63
  // Remove /home/project from buildPath if it exists
64
  const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
 
65
  // Get all files recursively
66
  async function getAllFiles(dirPath: string): Promise<Record<string, string>> {
67
  const files: Record<string, string> = {};
68
  const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
69
-
70
  for (const entry of entries) {
71
  const fullPath = path.join(dirPath, entry.name);
 
72
  if (entry.isFile()) {
73
  const content = await container.fs.readFile(fullPath, 'utf-8');
 
74
  // Remove /dist prefix from the path
75
  const deployPath = fullPath.replace(buildPath, '');
76
  files[deployPath] = content;
@@ -79,7 +84,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
79
  Object.assign(files, subFiles);
80
  }
81
  }
82
-
83
  return files;
84
  }
85
 
@@ -96,12 +101,12 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
96
  siteId: existingSiteId || undefined,
97
  files: fileContents,
98
  token: connection.token,
99
- chatId: artifact.id
100
  }),
101
  });
102
 
103
- const data = await response.json() as any;
104
-
105
  if (!response.ok || !data.deploy || !data.site) {
106
  console.error('Invalid deploy response:', data);
107
  throw new Error(data.error || 'Invalid deployment response');
@@ -114,35 +119,38 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
114
 
115
  while (attempts < maxAttempts) {
116
  try {
117
- const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`, {
118
- headers: {
119
- 'Authorization': `Bearer ${connection.token}`,
 
 
 
120
  },
121
- });
122
-
123
- deploymentStatus = await statusResponse.json() as any;
124
-
125
  if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') {
126
  break;
127
  }
128
-
129
  if (deploymentStatus.state === 'error') {
130
  throw new Error('Deployment failed: ' + (deploymentStatus.error_message || 'Unknown error'));
131
  }
132
-
133
  attempts++;
134
- await new Promise(resolve => setTimeout(resolve, 1000));
135
  } catch (error) {
136
  console.error('Status check error:', error);
137
  attempts++;
138
- await new Promise(resolve => setTimeout(resolve, 2000));
139
  }
140
  }
141
 
142
  if (attempts >= maxAttempts) {
143
  throw new Error('Deployment timed out');
144
  }
145
-
146
  // Store the site ID if it's a new site
147
  if (data.site) {
148
  localStorage.setItem(`netlify-site-${artifact.id}`, data.site.id);
@@ -151,15 +159,15 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
151
  toast.success(
152
  <div>
153
  Deployed successfully!{' '}
154
- <a
155
- href={deploymentStatus.ssl_url || deploymentStatus.url}
156
- target="_blank"
157
  rel="noopener noreferrer"
158
  className="underline"
159
  >
160
  View site
161
  </a>
162
- </div>
163
  );
164
  } catch (error) {
165
  console.error('Deploy error:', error);
@@ -172,11 +180,11 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {
172
  return (
173
  <div className="flex">
174
  <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
175
- <Button
176
- active
177
  disabled={isDeploying || !activePreview}
178
  onClick={handleDeploy}
179
- className='px-4 hover:bg-bolt-elements-item-backgroundActive'
180
  >
181
  {isDeploying ? 'Deploying...' : 'Deploy'}
182
  </Button>
@@ -222,15 +230,17 @@ interface ButtonProps {
222
  function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
223
  return (
224
  <button
225
- className={classNames('flex items-center p-1.5', {
226
- 'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
227
- !active,
228
- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
229
- 'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
230
- disabled,
231
- },
232
- className
233
- )}
 
 
234
  onClick={onClick}
235
  >
236
  {children}
 
16
  const showWorkbench = useStore(workbenchStore.showWorkbench);
17
  const { showChat } = useStore(chatStore);
18
  const connection = useStore(netlifyConnection);
19
+ const [activePreviewIndex] = useState(0);
20
  const previews = useStore(workbenchStore.previews);
21
  const activePreview = previews[activePreviewIndex];
22
  const [isDeploying, setIsDeploying] = useState(false);
 
31
 
32
  try {
33
  setIsDeploying(true);
34
+
35
  const artifact = workbenchStore.firstArtifact;
36
+
37
  if (!artifact) {
38
  throw new Error('No active project found');
39
  }
40
 
41
  const actionId = 'build-' + Date.now();
42
  const actionData: ActionCallbackData = {
43
+ messageId: 'netlify build',
44
  artifactId: artifact.id,
45
  actionId,
46
  action: {
47
  type: 'build' as const,
48
  content: 'npm run build',
49
+ },
50
  };
51
 
52
  // Add the action first
53
  artifact.runner.addAction(actionData);
54
+
55
  // Then run it
56
  await artifact.runner.runAction(actionData);
57
 
 
61
 
62
  // Get the build files
63
  const container = await webcontainer;
64
+
65
  // Remove /home/project from buildPath if it exists
66
  const buildPath = artifact.runner.buildOutput.path.replace('/home/project', '');
67
+
68
  // Get all files recursively
69
  async function getAllFiles(dirPath: string): Promise<Record<string, string>> {
70
  const files: Record<string, string> = {};
71
  const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
72
+
73
  for (const entry of entries) {
74
  const fullPath = path.join(dirPath, entry.name);
75
+
76
  if (entry.isFile()) {
77
  const content = await container.fs.readFile(fullPath, 'utf-8');
78
+
79
  // Remove /dist prefix from the path
80
  const deployPath = fullPath.replace(buildPath, '');
81
  files[deployPath] = content;
 
84
  Object.assign(files, subFiles);
85
  }
86
  }
87
+
88
  return files;
89
  }
90
 
 
101
  siteId: existingSiteId || undefined,
102
  files: fileContents,
103
  token: connection.token,
104
+ chatId: artifact.id,
105
  }),
106
  });
107
 
108
+ const data = (await response.json()) as any;
109
+
110
  if (!response.ok || !data.deploy || !data.site) {
111
  console.error('Invalid deploy response:', data);
112
  throw new Error(data.error || 'Invalid deployment response');
 
119
 
120
  while (attempts < maxAttempts) {
121
  try {
122
+ const statusResponse = await fetch(
123
+ `https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`,
124
+ {
125
+ headers: {
126
+ Authorization: `Bearer ${connection.token}`,
127
+ },
128
  },
129
+ );
130
+
131
+ deploymentStatus = (await statusResponse.json()) as any;
132
+
133
  if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') {
134
  break;
135
  }
136
+
137
  if (deploymentStatus.state === 'error') {
138
  throw new Error('Deployment failed: ' + (deploymentStatus.error_message || 'Unknown error'));
139
  }
140
+
141
  attempts++;
142
+ await new Promise((resolve) => setTimeout(resolve, 1000));
143
  } catch (error) {
144
  console.error('Status check error:', error);
145
  attempts++;
146
+ await new Promise((resolve) => setTimeout(resolve, 2000));
147
  }
148
  }
149
 
150
  if (attempts >= maxAttempts) {
151
  throw new Error('Deployment timed out');
152
  }
153
+
154
  // Store the site ID if it's a new site
155
  if (data.site) {
156
  localStorage.setItem(`netlify-site-${artifact.id}`, data.site.id);
 
159
  toast.success(
160
  <div>
161
  Deployed successfully!{' '}
162
+ <a
163
+ href={deploymentStatus.ssl_url || deploymentStatus.url}
164
+ target="_blank"
165
  rel="noopener noreferrer"
166
  className="underline"
167
  >
168
  View site
169
  </a>
170
+ </div>,
171
  );
172
  } catch (error) {
173
  console.error('Deploy error:', error);
 
180
  return (
181
  <div className="flex">
182
  <div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden mr-2 text-sm">
183
+ <Button
184
+ active
185
  disabled={isDeploying || !activePreview}
186
  onClick={handleDeploy}
187
+ className="px-4 hover:bg-bolt-elements-item-backgroundActive"
188
  >
189
  {isDeploying ? 'Deploying...' : 'Deploy'}
190
  </Button>
 
230
  function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) {
231
  return (
232
  <button
233
+ className={classNames(
234
+ 'flex items-center p-1.5',
235
+ {
236
+ 'bg-bolt-elements-item-backgroundDefault hover:bg-bolt-elements-item-backgroundActive text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary':
237
+ !active,
238
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent': active && !disabled,
239
+ 'bg-bolt-elements-item-backgroundDefault text-alpha-gray-20 dark:text-alpha-white-20 cursor-not-allowed':
240
+ disabled,
241
+ },
242
+ className,
243
+ )}
244
  onClick={onClick}
245
  >
246
  {children}
app/lib/runtime/action-runner.ts CHANGED
@@ -159,6 +159,7 @@ export class ActionRunner {
159
  }
160
  case 'build': {
161
  const buildOutput = await this.#runBuildAction(action);
 
162
  // Store build output for deployment
163
  this.buildOutput = buildOutput;
164
  break;
@@ -318,16 +319,17 @@ export class ActionRunner {
318
  }
319
 
320
  const webcontainer = await this.#webcontainer;
 
321
  // Create a new terminal specifically for the build
322
  const buildProcess = await webcontainer.spawn('npm', ['run', 'build']);
323
-
324
  let output = '';
325
  buildProcess.output.pipeTo(
326
  new WritableStream({
327
  write(data) {
328
  output += data;
329
  },
330
- })
331
  );
332
 
333
  const exitCode = await buildProcess.exit;
@@ -342,7 +344,7 @@ export class ActionRunner {
342
  return {
343
  path: buildDir,
344
  exitCode,
345
- output
346
  };
347
  }
348
  }
 
159
  }
160
  case 'build': {
161
  const buildOutput = await this.#runBuildAction(action);
162
+
163
  // Store build output for deployment
164
  this.buildOutput = buildOutput;
165
  break;
 
319
  }
320
 
321
  const webcontainer = await this.#webcontainer;
322
+
323
  // Create a new terminal specifically for the build
324
  const buildProcess = await webcontainer.spawn('npm', ['run', 'build']);
325
+
326
  let output = '';
327
  buildProcess.output.pipeTo(
328
  new WritableStream({
329
  write(data) {
330
  output += data;
331
  },
332
+ }),
333
  );
334
 
335
  const exitCode = await buildProcess.exit;
 
344
  return {
345
  path: buildDir,
346
  exitCode,
347
+ output,
348
  };
349
  }
350
  }
app/lib/stores/netlify.ts CHANGED
@@ -24,4 +24,4 @@ export const updateNetlifyConnection = (updates: Partial<NetlifyConnection>) =>
24
  if (typeof window !== 'undefined') {
25
  localStorage.setItem('netlify_connection', JSON.stringify(newState));
26
  }
27
- };
 
24
  if (typeof window !== 'undefined') {
25
  localStorage.setItem('netlify_connection', JSON.stringify(newState));
26
  }
27
+ };
app/routes/api.deploy.ts CHANGED
@@ -10,7 +10,8 @@ interface DeployRequestBody {
10
 
11
  export async function action({ request }: ActionFunctionArgs) {
12
  try {
13
- const { siteId, files, token, chatId } = await request.json() as DeployRequestBody & { token: string };
 
14
  if (!token) {
15
  return json({ error: 'Not connected to Netlify' }, { status: 401 });
16
  }
@@ -24,81 +25,82 @@ export async function action({ request }: ActionFunctionArgs) {
24
  const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
25
  method: 'POST',
26
  headers: {
27
- 'Authorization': `Bearer ${token}`,
28
  'Content-Type': 'application/json',
29
  },
30
  body: JSON.stringify({
31
  name: siteName,
32
  custom_domain: null,
33
- })
34
  });
35
 
36
  if (!createSiteResponse.ok) {
37
  return json({ error: 'Failed to create site' }, { status: 400 });
38
  }
39
 
40
- const newSite = await createSiteResponse.json() as any;
41
  targetSiteId = newSite.id;
42
  siteInfo = {
43
  id: newSite.id,
44
  name: newSite.name,
45
  url: newSite.url,
46
- chatId
47
  };
48
  } else {
49
  // Get existing site info
50
  if (targetSiteId) {
51
  const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, {
52
  headers: {
53
- 'Authorization': `Bearer ${token}`,
54
  },
55
  });
56
-
57
  if (siteResponse.ok) {
58
- const existingSite = await siteResponse.json() as any;
59
  siteInfo = {
60
  id: existingSite.id,
61
  name: existingSite.name,
62
  url: existingSite.url,
63
- chatId
64
  };
65
  } else {
66
  targetSiteId = undefined;
67
  }
68
  }
69
-
70
  // If no siteId provided or site doesn't exist, create a new site
71
  if (!targetSiteId) {
72
  const siteName = `bolt-diy-${chatId}-${Date.now()}`;
73
  const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
74
  method: 'POST',
75
  headers: {
76
- 'Authorization': `Bearer ${token}`,
77
  'Content-Type': 'application/json',
78
  },
79
  body: JSON.stringify({
80
  name: siteName,
81
  custom_domain: null,
82
- })
83
  });
84
-
85
  if (!createSiteResponse.ok) {
86
  return json({ error: 'Failed to create site' }, { status: 400 });
87
  }
88
-
89
- const newSite = await createSiteResponse.json() as any;
90
  targetSiteId = newSite.id;
91
  siteInfo = {
92
  id: newSite.id,
93
  name: newSite.name,
94
  url: newSite.url,
95
- chatId
96
  };
97
  }
98
  }
99
 
100
  // Create file digests
101
  const fileDigests: Record<string, string> = {};
 
102
  for (const [filePath, content] of Object.entries(files)) {
103
  // Ensure file path starts with a forward slash
104
  const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
@@ -110,7 +112,7 @@ export async function action({ request }: ActionFunctionArgs) {
110
  const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys`, {
111
  method: 'POST',
112
  headers: {
113
- 'Authorization': `Bearer ${token}`,
114
  'Content-Type': 'application/json',
115
  },
116
  body: JSON.stringify({
@@ -120,15 +122,15 @@ export async function action({ request }: ActionFunctionArgs) {
120
  draft: false, // Change this to false for production deployments
121
  function_schedules: [],
122
  required: Object.keys(fileDigests), // Add this line
123
- framework: null
124
- })
125
  });
126
 
127
  if (!deployResponse.ok) {
128
  return json({ error: 'Failed to create deployment' }, { status: 400 });
129
  }
130
 
131
- const deploy = await deployResponse.json() as any;
132
  let retryCount = 0;
133
  const maxRetries = 60;
134
 
@@ -136,41 +138,45 @@ export async function action({ request }: ActionFunctionArgs) {
136
  while (retryCount < maxRetries) {
137
  const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, {
138
  headers: {
139
- 'Authorization': `Bearer ${token}`,
140
  },
141
  });
142
-
143
- const status = await statusResponse.json() as any;
144
-
145
  if (status.state === 'prepared' || status.state === 'uploaded') {
146
  // Upload all files regardless of required array
147
  for (const [filePath, content] of Object.entries(files)) {
148
  const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
149
-
150
  let uploadSuccess = false;
151
  let uploadRetries = 0;
152
-
153
  while (!uploadSuccess && uploadRetries < 3) {
154
  try {
155
- const uploadResponse = await fetch(`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`, {
156
- method: 'PUT',
157
- headers: {
158
- 'Authorization': `Bearer ${token}`,
159
- 'Content-Type': 'application/octet-stream',
 
 
 
 
160
  },
161
- body: content,
162
- });
163
 
164
  uploadSuccess = uploadResponse.ok;
 
165
  if (!uploadSuccess) {
166
  console.error('Upload failed:', await uploadResponse.text());
167
  uploadRetries++;
168
- await new Promise(resolve => setTimeout(resolve, 2000));
169
  }
170
  } catch (error) {
171
  console.error('Upload error:', error);
172
  uploadRetries++;
173
- await new Promise(resolve => setTimeout(resolve, 2000));
174
  }
175
  }
176
 
@@ -179,18 +185,18 @@ export async function action({ request }: ActionFunctionArgs) {
179
  }
180
  }
181
  }
182
-
183
  if (status.state === 'ready') {
184
  // Only return after files are uploaded
185
  if (Object.keys(files).length === 0 || status.summary?.status === 'ready') {
186
- return json({
187
- success: true,
188
  deploy: {
189
  id: status.id,
190
  state: status.state,
191
- url: status.ssl_url || status.url
192
  },
193
- site: siteInfo
194
  });
195
  }
196
  }
@@ -200,7 +206,7 @@ export async function action({ request }: ActionFunctionArgs) {
200
  }
201
 
202
  retryCount++;
203
- await new Promise(resolve => setTimeout(resolve, 1000));
204
  }
205
 
206
  if (retryCount >= maxRetries) {
@@ -208,16 +214,16 @@ export async function action({ request }: ActionFunctionArgs) {
208
  }
209
 
210
  // Make sure we're returning the deploy ID and site info
211
- return json({
212
- success: true,
213
  deploy: {
214
  id: deploy.id,
215
  state: deploy.state,
216
  },
217
- site: siteInfo
218
  });
219
  } catch (error) {
220
  console.error('Deploy error:', error);
221
  return json({ error: 'Deployment failed' }, { status: 500 });
222
  }
223
- }
 
10
 
11
  export async function action({ request }: ActionFunctionArgs) {
12
  try {
13
+ const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
14
+
15
  if (!token) {
16
  return json({ error: 'Not connected to Netlify' }, { status: 401 });
17
  }
 
25
  const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
26
  method: 'POST',
27
  headers: {
28
+ Authorization: `Bearer ${token}`,
29
  'Content-Type': 'application/json',
30
  },
31
  body: JSON.stringify({
32
  name: siteName,
33
  custom_domain: null,
34
+ }),
35
  });
36
 
37
  if (!createSiteResponse.ok) {
38
  return json({ error: 'Failed to create site' }, { status: 400 });
39
  }
40
 
41
+ const newSite = (await createSiteResponse.json()) as any;
42
  targetSiteId = newSite.id;
43
  siteInfo = {
44
  id: newSite.id,
45
  name: newSite.name,
46
  url: newSite.url,
47
+ chatId,
48
  };
49
  } else {
50
  // Get existing site info
51
  if (targetSiteId) {
52
  const siteResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}`, {
53
  headers: {
54
+ Authorization: `Bearer ${token}`,
55
  },
56
  });
57
+
58
  if (siteResponse.ok) {
59
+ const existingSite = (await siteResponse.json()) as any;
60
  siteInfo = {
61
  id: existingSite.id,
62
  name: existingSite.name,
63
  url: existingSite.url,
64
+ chatId,
65
  };
66
  } else {
67
  targetSiteId = undefined;
68
  }
69
  }
70
+
71
  // If no siteId provided or site doesn't exist, create a new site
72
  if (!targetSiteId) {
73
  const siteName = `bolt-diy-${chatId}-${Date.now()}`;
74
  const createSiteResponse = await fetch('https://api.netlify.com/api/v1/sites', {
75
  method: 'POST',
76
  headers: {
77
+ Authorization: `Bearer ${token}`,
78
  'Content-Type': 'application/json',
79
  },
80
  body: JSON.stringify({
81
  name: siteName,
82
  custom_domain: null,
83
+ }),
84
  });
85
+
86
  if (!createSiteResponse.ok) {
87
  return json({ error: 'Failed to create site' }, { status: 400 });
88
  }
89
+
90
+ const newSite = (await createSiteResponse.json()) as any;
91
  targetSiteId = newSite.id;
92
  siteInfo = {
93
  id: newSite.id,
94
  name: newSite.name,
95
  url: newSite.url,
96
+ chatId,
97
  };
98
  }
99
  }
100
 
101
  // Create file digests
102
  const fileDigests: Record<string, string> = {};
103
+
104
  for (const [filePath, content] of Object.entries(files)) {
105
  // Ensure file path starts with a forward slash
106
  const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
 
112
  const deployResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys`, {
113
  method: 'POST',
114
  headers: {
115
+ Authorization: `Bearer ${token}`,
116
  'Content-Type': 'application/json',
117
  },
118
  body: JSON.stringify({
 
122
  draft: false, // Change this to false for production deployments
123
  function_schedules: [],
124
  required: Object.keys(fileDigests), // Add this line
125
+ framework: null,
126
+ }),
127
  });
128
 
129
  if (!deployResponse.ok) {
130
  return json({ error: 'Failed to create deployment' }, { status: 400 });
131
  }
132
 
133
+ const deploy = (await deployResponse.json()) as any;
134
  let retryCount = 0;
135
  const maxRetries = 60;
136
 
 
138
  while (retryCount < maxRetries) {
139
  const statusResponse = await fetch(`https://api.netlify.com/api/v1/sites/${targetSiteId}/deploys/${deploy.id}`, {
140
  headers: {
141
+ Authorization: `Bearer ${token}`,
142
  },
143
  });
144
+
145
+ const status = (await statusResponse.json()) as any;
146
+
147
  if (status.state === 'prepared' || status.state === 'uploaded') {
148
  // Upload all files regardless of required array
149
  for (const [filePath, content] of Object.entries(files)) {
150
  const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
151
+
152
  let uploadSuccess = false;
153
  let uploadRetries = 0;
154
+
155
  while (!uploadSuccess && uploadRetries < 3) {
156
  try {
157
+ const uploadResponse = await fetch(
158
+ `https://api.netlify.com/api/v1/deploys/${deploy.id}/files${normalizedPath}`,
159
+ {
160
+ method: 'PUT',
161
+ headers: {
162
+ Authorization: `Bearer ${token}`,
163
+ 'Content-Type': 'application/octet-stream',
164
+ },
165
+ body: content,
166
  },
167
+ );
 
168
 
169
  uploadSuccess = uploadResponse.ok;
170
+
171
  if (!uploadSuccess) {
172
  console.error('Upload failed:', await uploadResponse.text());
173
  uploadRetries++;
174
+ await new Promise((resolve) => setTimeout(resolve, 2000));
175
  }
176
  } catch (error) {
177
  console.error('Upload error:', error);
178
  uploadRetries++;
179
+ await new Promise((resolve) => setTimeout(resolve, 2000));
180
  }
181
  }
182
 
 
185
  }
186
  }
187
  }
188
+
189
  if (status.state === 'ready') {
190
  // Only return after files are uploaded
191
  if (Object.keys(files).length === 0 || status.summary?.status === 'ready') {
192
+ return json({
193
+ success: true,
194
  deploy: {
195
  id: status.id,
196
  state: status.state,
197
+ url: status.ssl_url || status.url,
198
  },
199
+ site: siteInfo,
200
  });
201
  }
202
  }
 
206
  }
207
 
208
  retryCount++;
209
+ await new Promise((resolve) => setTimeout(resolve, 1000));
210
  }
211
 
212
  if (retryCount >= maxRetries) {
 
214
  }
215
 
216
  // Make sure we're returning the deploy ID and site info
217
+ return json({
218
+ success: true,
219
  deploy: {
220
  id: deploy.id,
221
  state: deploy.state,
222
  },
223
+ site: siteInfo,
224
  });
225
  } catch (error) {
226
  console.error('Deploy error:', error);
227
  return json({ error: 'Deployment failed' }, { status: 500 });
228
  }
229
+ }
app/types/netlify.ts CHANGED
@@ -38,4 +38,4 @@ export interface NetlifySiteInfo {
38
  name: string;
39
  url: string;
40
  chatId: string;
41
- }
 
38
  name: string;
39
  url: string;
40
  chatId: string;
41
+ }
uno.config.ts CHANGED
@@ -98,9 +98,7 @@ const COLOR_PRIMITIVES = {
98
  };
99
 
100
  export default defineConfig({
101
- safelist: [
102
- ...Object.keys(customIconCollection[collectionName] || {}).map(x => `i-bolt:${x}`)
103
- ],
104
  shortcuts: {
105
  'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
106
  'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
 
98
  };
99
 
100
  export default defineConfig({
101
+ safelist: [...Object.keys(customIconCollection[collectionName] || {}).map((x) => `i-bolt:${x}`)],
 
 
102
  shortcuts: {
103
  'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
104
  'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',