Merge pull request #1833 from xKevIsDev/deployment-fix
feat: enhance Vercel deployment process with framework detection and source file handling
This commit is contained in:
@@ -96,12 +96,9 @@ export function useVercelDeploy() {
|
|||||||
await container.fs.readdir(dir);
|
await container.fs.readdir(dir);
|
||||||
finalBuildPath = dir;
|
finalBuildPath = dir;
|
||||||
buildPathExists = true;
|
buildPathExists = true;
|
||||||
console.log(`Using build directory: ${finalBuildPath}`);
|
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.log(`Directory ${dir} doesn't exist, trying next option. ${error}`);
|
// Directory doesn't exist, expected — just skip it
|
||||||
|
|
||||||
// Directory doesn't exist, try the next one
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +132,47 @@ export function useVercelDeploy() {
|
|||||||
|
|
||||||
const fileContents = await getAllFiles(finalBuildPath);
|
const fileContents = await getAllFiles(finalBuildPath);
|
||||||
|
|
||||||
|
// Get all source project files for framework detection
|
||||||
|
const allProjectFiles: Record<string, string> = {};
|
||||||
|
|
||||||
|
async function getAllProjectFiles(dirPath: string): Promise<void> {
|
||||||
|
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
|
// Use chatId instead of artifact.id
|
||||||
const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`);
|
const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`);
|
||||||
|
|
||||||
@@ -146,6 +184,7 @@ export function useVercelDeploy() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
projectId: existingProjectId || undefined,
|
projectId: existingProjectId || undefined,
|
||||||
files: fileContents,
|
files: fileContents,
|
||||||
|
sourceFiles: allProjectFiles,
|
||||||
token: vercelConn.token,
|
token: vercelConn.token,
|
||||||
chatId: currentChatId,
|
chatId: currentChatId,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -435,18 +435,15 @@ export class ActionRunner {
|
|||||||
try {
|
try {
|
||||||
await webcontainer.fs.readdir(dirPath);
|
await webcontainer.fs.readdir(dirPath);
|
||||||
buildDir = dirPath;
|
buildDir = dirPath;
|
||||||
logger.debug(`Found build directory: ${buildDir}`);
|
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Directory doesn't exist, try the next one
|
continue;
|
||||||
logger.debug(`Build directory ${dir} not found, trying next option. ${error}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no build directory was found, use the default (dist)
|
// If no build directory was found, use the default (dist)
|
||||||
if (!buildDir) {
|
if (!buildDir) {
|
||||||
buildDir = nodePath.join(webcontainer.workdir, 'dist');
|
buildDir = nodePath.join(webcontainer.workdir, 'dist');
|
||||||
logger.debug(`No build directory found, defaulting to: ${buildDir}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,177 @@
|
|||||||
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/cloudflare';
|
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/cloudflare';
|
||||||
import type { VercelProjectInfo } from '~/types/vercel';
|
import type { VercelProjectInfo } from '~/types/vercel';
|
||||||
|
|
||||||
|
// Function to detect framework from project files
|
||||||
|
const detectFramework = (files: Record<string, string>): 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
|
// Add loader function to handle GET requests
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
@@ -63,13 +234,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
interface DeployRequestBody {
|
interface DeployRequestBody {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
files: Record<string, string>;
|
files: Record<string, string>;
|
||||||
|
sourceFiles?: Record<string, string>;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
framework?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing action function for POST requests
|
// Existing action function for POST requests
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
export async function action({ request }: ActionFunctionArgs) {
|
||||||
try {
|
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) {
|
if (!token) {
|
||||||
return json({ error: 'Not connected to Vercel' }, { status: 401 });
|
return json({ error: 'Not connected to Vercel' }, { status: 401 });
|
||||||
@@ -78,6 +253,14 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
let targetProjectId = projectId;
|
let targetProjectId = projectId;
|
||||||
let projectInfo: VercelProjectInfo | undefined;
|
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 no projectId provided, create a new project
|
||||||
if (!targetProjectId) {
|
if (!targetProjectId) {
|
||||||
const projectName = `bolt-diy-${chatId}-${Date.now()}`;
|
const projectName = `bolt-diy-${chatId}-${Date.now()}`;
|
||||||
@@ -89,7 +272,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: projectName,
|
name: projectName,
|
||||||
framework: null,
|
framework: detectedFramework || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,7 +319,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: projectName,
|
name: projectName,
|
||||||
framework: null,
|
framework: detectedFramework || null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,13 +345,72 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
// Prepare files for deployment
|
// Prepare files for deployment
|
||||||
const deploymentFiles = [];
|
const deploymentFiles = [];
|
||||||
|
|
||||||
for (const [filePath, content] of Object.entries(files)) {
|
/*
|
||||||
// Ensure file path doesn't start with a slash for Vercel
|
* For frameworks that need to build on Vercel, include source files
|
||||||
const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
* For static sites, only include build output
|
||||||
deploymentFiles.push({
|
*/
|
||||||
file: normalizedPath,
|
const shouldIncludeSourceFiles =
|
||||||
data: content,
|
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
|
// Create a new deployment
|
||||||
@@ -178,13 +420,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(deploymentConfig),
|
||||||
name: projectInfo.name,
|
|
||||||
project: targetProjectId,
|
|
||||||
target: 'production',
|
|
||||||
files: deploymentFiles,
|
|
||||||
routes: [{ src: '/(.*)', dest: '/$1' }],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deployResponse.ok) {
|
if (!deployResponse.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user