diff --git a/app/components/deploy/VercelDeploy.client.tsx b/app/components/deploy/VercelDeploy.client.tsx index 22ceb39..d2303cd 100644 --- a/app/components/deploy/VercelDeploy.client.tsx +++ b/app/components/deploy/VercelDeploy.client.tsx @@ -96,12 +96,9 @@ export function useVercelDeploy() { 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 + } catch { + // Directory doesn't exist, expected — just skip it continue; } } @@ -135,6 +132,47 @@ export function useVercelDeploy() { const fileContents = await getAllFiles(finalBuildPath); + // Get all source project files for framework detection + const allProjectFiles: Record = {}; + + async function getAllProjectFiles(dirPath: string): Promise { + const entries = await container.fs.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isFile()) { + try { + const content = await container.fs.readFile(fullPath, 'utf-8'); + + // Store with relative path from project root + let relativePath = fullPath; + + if (fullPath.startsWith('/home/project/')) { + relativePath = fullPath.replace('/home/project/', ''); + } else if (fullPath.startsWith('./')) { + relativePath = fullPath.replace('./', ''); + } + + allProjectFiles[relativePath] = content; + } catch (error) { + // Skip binary files or files that can't be read as text + console.log(`Skipping file ${entry.name}: ${error}`); + } + } else if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') { + await getAllProjectFiles(fullPath); + } + } + } + + // Try to read from the current directory first + try { + await getAllProjectFiles('.'); + } catch { + // Fallback to /home/project if current directory doesn't work + await getAllProjectFiles('/home/project'); + } + // Use chatId instead of artifact.id const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`); @@ -146,6 +184,7 @@ export function useVercelDeploy() { body: JSON.stringify({ projectId: existingProjectId || undefined, files: fileContents, + sourceFiles: allProjectFiles, token: vercelConn.token, chatId: currentChatId, }), diff --git a/app/lib/runtime/action-runner.ts b/app/lib/runtime/action-runner.ts index a3436a9..58de741 100644 --- a/app/lib/runtime/action-runner.ts +++ b/app/lib/runtime/action-runner.ts @@ -435,18 +435,15 @@ export class ActionRunner { 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}`); + } catch { + continue; } } // 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 { diff --git a/app/routes/api.vercel-deploy.ts b/app/routes/api.vercel-deploy.ts index 7d45b24..5600972 100644 --- a/app/routes/api.vercel-deploy.ts +++ b/app/routes/api.vercel-deploy.ts @@ -1,6 +1,177 @@ import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/cloudflare'; import type { VercelProjectInfo } from '~/types/vercel'; +// Function to detect framework from project files +const detectFramework = (files: Record): string => { + // Check for package.json first + const packageJson = files['package.json']; + + if (packageJson) { + try { + const pkg = JSON.parse(packageJson); + const dependencies = { ...pkg.dependencies, ...pkg.devDependencies }; + + // Check for specific frameworks + if (dependencies.next) { + return 'nextjs'; + } + + if (dependencies.react && dependencies['@remix-run/react']) { + return 'remix'; + } + + if (dependencies.react && dependencies.vite) { + return 'vite'; + } + + if (dependencies.react && dependencies['@vitejs/plugin-react']) { + return 'vite'; + } + + if (dependencies.react && dependencies['@nuxt/react']) { + return 'nuxt'; + } + + if (dependencies.react && dependencies['@qwik-city/qwik']) { + return 'qwik'; + } + + if (dependencies.react && dependencies['@sveltejs/kit']) { + return 'sveltekit'; + } + + if (dependencies.react && dependencies.astro) { + return 'astro'; + } + + if (dependencies.react && dependencies['@angular/core']) { + return 'angular'; + } + + if (dependencies.react && dependencies.vue) { + return 'vue'; + } + + if (dependencies.react && dependencies['@expo/react-native']) { + return 'expo'; + } + + if (dependencies.react && dependencies['react-native']) { + return 'react-native'; + } + + // Generic React app + if (dependencies.react) { + return 'react'; + } + + // Check for other frameworks + if (dependencies['@angular/core']) { + return 'angular'; + } + + if (dependencies.vue) { + return 'vue'; + } + + if (dependencies['@sveltejs/kit']) { + return 'sveltekit'; + } + + if (dependencies.astro) { + return 'astro'; + } + + if (dependencies['@nuxt/core']) { + return 'nuxt'; + } + + if (dependencies['@qwik-city/qwik']) { + return 'qwik'; + } + + if (dependencies['@expo/react-native']) { + return 'expo'; + } + + if (dependencies['react-native']) { + return 'react-native'; + } + + // Check for build tools + if (dependencies.vite) { + return 'vite'; + } + + if (dependencies.webpack) { + return 'webpack'; + } + + if (dependencies.parcel) { + return 'parcel'; + } + + if (dependencies.rollup) { + return 'rollup'; + } + + // Default to Node.js if package.json exists + return 'nodejs'; + } catch (error) { + console.error('Error parsing package.json:', error); + } + } + + // Check for other framework indicators + if (files['next.config.js'] || files['next.config.ts']) { + return 'nextjs'; + } + + if (files['remix.config.js'] || files['remix.config.ts']) { + return 'remix'; + } + + if (files['vite.config.js'] || files['vite.config.ts']) { + return 'vite'; + } + + if (files['nuxt.config.js'] || files['nuxt.config.ts']) { + return 'nuxt'; + } + + if (files['svelte.config.js'] || files['svelte.config.ts']) { + return 'sveltekit'; + } + + if (files['astro.config.js'] || files['astro.config.ts']) { + return 'astro'; + } + + if (files['angular.json']) { + return 'angular'; + } + + if (files['vue.config.js'] || files['vue.config.ts']) { + return 'vue'; + } + + if (files['app.json'] && files['app.json'].includes('expo')) { + return 'expo'; + } + + if (files['app.json'] && files['app.json'].includes('react-native')) { + return 'react-native'; + } + + // Check for static site indicators + if (files['index.html']) { + return 'static'; + } + + // Default to unknown + return 'other'; +}; + // Add loader function to handle GET requests export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); @@ -63,13 +234,17 @@ export async function loader({ request }: LoaderFunctionArgs) { interface DeployRequestBody { projectId?: string; files: Record; + sourceFiles?: Record; chatId: string; + framework?: 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 }; + const { projectId, files, sourceFiles, token, chatId, framework } = (await request.json()) as DeployRequestBody & { + token: string; + }; if (!token) { return json({ error: 'Not connected to Vercel' }, { status: 401 }); @@ -78,6 +253,14 @@ export async function action({ request }: ActionFunctionArgs) { let targetProjectId = projectId; let projectInfo: VercelProjectInfo | undefined; + // Detect framework from the source files if not provided + let detectedFramework = framework; + + if (!detectedFramework && sourceFiles) { + detectedFramework = detectFramework(sourceFiles); + console.log('Detected framework from source files:', detectedFramework); + } + // If no projectId provided, create a new project if (!targetProjectId) { const projectName = `bolt-diy-${chatId}-${Date.now()}`; @@ -89,7 +272,7 @@ export async function action({ request }: ActionFunctionArgs) { }, body: JSON.stringify({ name: projectName, - framework: null, + framework: detectedFramework || null, }), }); @@ -136,7 +319,7 @@ export async function action({ request }: ActionFunctionArgs) { }, body: JSON.stringify({ name: projectName, - framework: null, + framework: detectedFramework || null, }), }); @@ -162,13 +345,72 @@ export async function action({ request }: ActionFunctionArgs) { // 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, - }); + /* + * For frameworks that need to build on Vercel, include source files + * For static sites, only include build output + */ + const shouldIncludeSourceFiles = + detectedFramework && + ['nextjs', 'react', 'vite', 'remix', 'nuxt', 'sveltekit', 'astro', 'vue', 'angular'].includes(detectedFramework); + + if (shouldIncludeSourceFiles && sourceFiles) { + // Include source files for frameworks that need to build + for (const [filePath, content] of Object.entries(sourceFiles)) { + // 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, + }); + } + } else { + // For static sites, only include build output + 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 deployment configuration based on framework + const deploymentConfig: any = { + name: projectInfo.name, + project: targetProjectId, + target: 'production', + files: deploymentFiles, + }; + + // Add framework-specific configuration + if (detectedFramework === 'nextjs') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = '.next'; + } else if (detectedFramework === 'react' || detectedFramework === 'vite') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = 'dist'; + } else if (detectedFramework === 'remix') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = 'public'; + } else if (detectedFramework === 'nuxt') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = '.output'; + } else if (detectedFramework === 'sveltekit') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = 'build'; + } else if (detectedFramework === 'astro') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = 'dist'; + } else if (detectedFramework === 'vue') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = 'dist'; + } else if (detectedFramework === 'angular') { + deploymentConfig.buildCommand = 'npm run build'; + deploymentConfig.outputDirectory = 'dist'; + } else { + // For static sites, no build command needed + deploymentConfig.routes = [{ src: '/(.*)', dest: '/$1' }]; } // Create a new deployment @@ -178,13 +420,7 @@ export async function action({ request }: ActionFunctionArgs) { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, - body: JSON.stringify({ - name: projectInfo.name, - project: targetProjectId, - target: 'production', - files: deploymentFiles, - routes: [{ src: '/(.*)', dest: '/$1' }], - }), + body: JSON.stringify(deploymentConfig), }); if (!deployResponse.ok) {