feat: enhance Vercel deployment process with framework detection and source file handling

- Implemented a function to detect project frameworks based on package.json and configuration files.
- Added support for including source files in the deployment request for frameworks that require building.
- Updated the action function to handle framework detection and adjust deployment configuration accordingly.
- Removed unnecessary console logs and improved error handling for file reading operations.
This commit is contained in:
xKevIsDev
2025-07-08 01:20:58 +01:00
committed by Roamin
parent 5de162eec8
commit 22cb5977be
3 changed files with 299 additions and 27 deletions

View File

@@ -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, 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
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<string, string>;
sourceFiles?: Record<string, string>;
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) {