├── server ├── pages-manifest.json ├── app-paths-manifest.json ├── interception-route-rewrite-manifest.js ├── middleware-manifest.json └── server-reference-manifest.js ├── types └── package.json ├── postcss.config.js ├── .env.local.example ├── tsconfig.json ├── .gitignore ├── package.json ├── LICENSE ├── app ├── api │ ├── projects │ │ ├── route.ts │ │ └── [projectId] │ │ │ └── route.ts │ └── convert │ │ ├── status │ │ └── route.ts │ │ └── route.ts ├── layout.tsx ├── globals.css ├── lib │ ├── vercel-api.ts │ ├── git-utils.ts │ └── conversion-pipeline.ts ├── dashboard │ └── page.tsx ├── page.tsx └── convert │ └── [projectId] │ └── page.tsx ├── README.md ├── tailwind.config.js └── debug-git-repo.js /server/pages-manifest.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /server/app-paths-manifest.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | {"type": "module"} -------------------------------------------------------------------------------- /server/interception-route-rewrite-manifest.js: -------------------------------------------------------------------------------- 1 | self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]" -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /server/middleware-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "middleware": {}, 4 | "functions": {}, 5 | "sortedMiddleware": [] 6 | } -------------------------------------------------------------------------------- /server/server-reference-manifest.js: -------------------------------------------------------------------------------- 1 | self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}" -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # Vercel OAuth Credentials 2 | VERCEL_CLIENT_ID= 3 | VERCEL_CLIENT_SECRET= 4 | VERCEL_REDIRECT_URI=http://localhost:3000/api/auth/callback 5 | 6 | # Next.js Auth 7 | NEXTAUTH_URL=http://localhost:3000 8 | NEXTAUTH_SECRET=your-secret-key 9 | 10 | # Vercel API Token 11 | # Obtain this from https://vercel.com/account/tokens 12 | VERCEL_API_TOKEN=your_api_token_here 13 | 14 | # App Settings 15 | LOCAL_STORAGE_PATH=./tmp/projects -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | .pnp 4 | .pnp.js 5 | 6 | # Testing 7 | /coverage 8 | 9 | # Next.js 10 | /.next/ 11 | /out/ 12 | next-env.d.ts 13 | 14 | # Production 15 | /build 16 | 17 | # Environment variables 18 | .env 19 | .env.* 20 | !.env.example 21 | !.env.local.example 22 | 23 | # Vercel 24 | .vercel 25 | 26 | # Debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # Local files 32 | .DS_Store 33 | *.pem 34 | tmp/ 35 | 36 | # Editor directories and files 37 | .idea/ 38 | .vscode/* 39 | !.vscode/extensions.json 40 | !.vscode/settings.json 41 | *.suo 42 | *.ntvs* 43 | *.njsproj 44 | *.sln 45 | *.sw? 46 | 47 | # Cache 48 | .eslintcache 49 | .stylelintcache 50 | 51 | # Server manifests with sensitive keys 52 | server/server-reference-manifest.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diverce", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "description": "A tool to convert Next.js projects from Vercel to Cloudflare", 16 | "dependencies": { 17 | "@octokit/rest": "^21.1.1", 18 | "@types/node": "^22.13.11", 19 | "@types/react": "^19.0.12", 20 | "@types/react-dom": "^19.0.4", 21 | "axios": "^1.8.4", 22 | "dotenv-flow": "^4.1.0", 23 | "next": "^14.2.25", 24 | "node-fetch": "^3.3.2", 25 | "react": "^18.3.1", 26 | "react-dom": "^18.3.1", 27 | "simple-git": "^3.27.0", 28 | "typescript": "^5.8.2" 29 | }, 30 | "devDependencies": { 31 | "autoprefixer": "^10.4.21", 32 | "postcss": "^8.5.3", 33 | "tailwindcss": "^3.4.17" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ygwyg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/api/projects/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDefaultVercelClient } from '../../lib/vercel-api'; 3 | import axios from 'axios'; 4 | 5 | export async function GET(request: NextRequest) { 6 | try { 7 | console.log('Fetching projects using API token...'); 8 | const client = getDefaultVercelClient(); 9 | 10 | console.log('Making request to Vercel API...'); 11 | const projects = await client.getProjects(); 12 | 13 | console.log(`Successfully fetched ${projects?.length || 0} projects`); 14 | 15 | // Filter for Next.js projects 16 | const nextJsProjects = projects?.filter(project => 17 | project?.framework === 'nextjs' || 18 | project?.framework?.toLowerCase?.()?.includes?.('next') 19 | ) || []; 20 | 21 | console.log(`Found ${nextJsProjects.length} Next.js projects`); 22 | 23 | return NextResponse.json({ projects: nextJsProjects }); 24 | } catch (error) { 25 | console.error('Error fetching Vercel projects:', error); 26 | 27 | // Extract more detailed error information 28 | let errorMessage = 'Failed to fetch projects'; 29 | let statusCode = 500; 30 | 31 | if (error instanceof Error) { 32 | errorMessage = error.message; 33 | 34 | // Check if it's an Axios error 35 | if (axios.isAxiosError(error) && error.response) { 36 | statusCode = error.response.status; 37 | errorMessage = `Vercel API Error (${statusCode}): ${ 38 | JSON.stringify(error.response.data) || errorMessage 39 | }`; 40 | } 41 | } 42 | 43 | console.error(`Returning error response: ${errorMessage}`); 44 | 45 | return NextResponse.json( 46 | { error: errorMessage }, 47 | { status: statusCode } 48 | ); 49 | } 50 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diverce 2 | 3 | A tool for converting Next.js projects from Vercel to Cloudflare. 4 | 5 | ## What it does 6 | 7 | Diverce helps you migrate your Next.js projects from Vercel to Cloudflare by: 8 | 9 | - Converting your project to use `@opennextjs/cloudflare` 10 | - Creating/updating `wrangler.jsonc` configuration 11 | - Removing any references to `@cloudflare/next-on-pages` or edge runtime 12 | - Updating `package.json` scripts for Cloudflare deployment 13 | - Adding necessary configuration files 14 | 15 | ## Prerequisites 16 | 17 | - Node.js 18+ 18 | - A Vercel account with Next.js projects 19 | - A Cloudflare account 20 | 21 | ## Setup 22 | 23 | 1. Clone this repository: 24 | ```bash 25 | git clone https://github.com/ygwyg/diverce.git 26 | cd diverce 27 | ``` 28 | 29 | 2. Install dependencies: 30 | ```bash 31 | npm install 32 | ``` 33 | 34 | 3. Set up your Vercel API token: 35 | ```bash 36 | cp .env.local.example .env.local 37 | ``` 38 | Then edit `.env.local` and add your Vercel API token, which you can create at https://vercel.com/account/tokens 39 | 40 | 4. Start the development server: 41 | ```bash 42 | npm run dev 43 | ``` 44 | 45 | 5. Visit http://localhost:3000 to use the app 46 | 47 | ## Usage 48 | 49 | 1. Visit the dashboard to see your Vercel Next.js projects 50 | 2. Select the project you want to convert 51 | 3. Configure conversion options: 52 | - Enable KV Cache (optional) 53 | - Create a new branch (recommended) 54 | - Commit and push changes (optional) 55 | 4. Start the conversion process 56 | 5. Follow the logs to monitor progress 57 | 6. Once completed, follow the instructions to deploy to Cloudflare 58 | 59 | ## Development 60 | 61 | - `npm run dev` - Start the development server 62 | - `npm run build` - Build the application for production 63 | - `npm start` - Start the production server 64 | 65 | ## License 66 | 67 | MIT -------------------------------------------------------------------------------- /app/api/convert/status/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | // Access the global store from parent 4 | declare global { 5 | var conversionStore: Record; 10 | } 11 | 12 | // If not available globally, define it here 13 | if (!global.conversionStore) { 14 | global.conversionStore = {}; 15 | } 16 | 17 | export async function GET(request: NextRequest) { 18 | // Set headers for SSE 19 | const headers = { 20 | 'Content-Type': 'text/event-stream', 21 | 'Cache-Control': 'no-cache, no-transform', 22 | 'Connection': 'keep-alive', 23 | }; 24 | 25 | const searchParams = request.nextUrl.searchParams; 26 | const projectId = searchParams.get('projectId'); 27 | 28 | if (!projectId) { 29 | return NextResponse.json( 30 | { error: 'Project ID is required' }, 31 | { status: 400 } 32 | ); 33 | } 34 | 35 | const encoder = new TextEncoder(); 36 | 37 | const stream = new ReadableStream({ 38 | start(controller) { 39 | // Function to send updates 40 | const sendUpdate = () => { 41 | const status = global.conversionStore[projectId] || { 42 | status: 'cloning', 43 | logs: ['Waiting for conversion to start...'], 44 | }; 45 | 46 | const data = `data: ${JSON.stringify(status)}\n\n`; 47 | controller.enqueue(encoder.encode(data)); 48 | 49 | // Continue sending updates until conversion is complete 50 | if (status.status !== 'success' && status.status !== 'failed') { 51 | setTimeout(sendUpdate, 1000); 52 | } else { 53 | controller.close(); 54 | } 55 | }; 56 | 57 | // Start sending updates 58 | sendUpdate(); 59 | }, 60 | }); 61 | 62 | return new Response(stream, { headers }); 63 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | // Base colors 12 | vercel: '#000000', 13 | cloudflare: '#f6821f', 14 | // Cloudflare-inspired color system 15 | background: { 16 | DEFAULT: '#F9F7F5', 17 | secondary: '#F2EFE9', 18 | }, 19 | foreground: { 20 | DEFAULT: '#2C2C31', 21 | secondary: '#4D4D59', 22 | tertiary: '#686877', 23 | }, 24 | accents: { 25 | 1: '#F2EFE9', 26 | 2: '#E9E5DD', 27 | 3: '#C5C2BB', 28 | 4: '#B7B4AD', 29 | 5: '#75727B', 30 | 6: '#595665', 31 | 7: '#44404F', 32 | 8: '#32303B', 33 | }, 34 | success: { 35 | lighter: '#CCEDE5', 36 | light: '#73D1BC', 37 | DEFAULT: '#00A88A', 38 | dark: '#007D66', 39 | }, 40 | error: { 41 | lighter: '#F7D4D6', 42 | light: '#FF6B6B', 43 | DEFAULT: '#E74C3C', 44 | dark: '#C0392B', 45 | }, 46 | warning: { 47 | lighter: '#FFF3CD', 48 | light: '#FFD166', 49 | DEFAULT: '#F6821F', 50 | dark: '#D96801', 51 | }, 52 | primary: { 53 | lighter: '#FFF3E0', 54 | light: '#FFBD4F', 55 | DEFAULT: '#F6821F', 56 | dark: '#DB6E00', 57 | }, 58 | secondary: { 59 | lighter: '#E2E6F4', 60 | light: '#A0AACB', 61 | DEFAULT: '#6E7CA0', 62 | dark: '#505B7A', 63 | }, 64 | }, 65 | fontFamily: { 66 | sans: [ 67 | 'Inter', 68 | 'system-ui', 69 | '-apple-system', 70 | 'BlinkMacSystemFont', 71 | 'Segoe UI', 72 | 'Roboto', 73 | 'Helvetica Neue', 74 | 'Arial', 75 | 'sans-serif', 76 | ], 77 | mono: [ 78 | 'Menlo', 79 | 'Monaco', 80 | 'Lucida Console', 81 | 'Liberation Mono', 82 | 'DejaVu Sans Mono', 83 | 'Bitstream Vera Sans Mono', 84 | 'Courier New', 85 | 'monospace', 86 | ], 87 | }, 88 | boxShadow: { 89 | 'small': '0 5px 10px rgba(0, 0, 0, 0.05)', 90 | 'medium': '0 8px 30px rgba(0, 0, 0, 0.08)', 91 | 'large': '0 30px 60px rgba(0, 0, 0, 0.1)', 92 | }, 93 | borderRadius: { 94 | 'cloudflare': '8px', 95 | }, 96 | }, 97 | }, 98 | plugins: [], 99 | } 100 | 101 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import type { Metadata } from 'next' 3 | import { Inter } from 'next/font/google' 4 | import Link from 'next/link' 5 | 6 | const inter = Inter({ subsets: ['latin'] }) 7 | 8 | export const metadata: Metadata = { 9 | title: 'Diverce - Convert Next.js from Vercel to Cloudflare', 10 | description: 'A tool to easily convert Next.js projects from Vercel to Cloudflare', 11 | } 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode 17 | }) { 18 | return ( 19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | Di 28 | verce 29 | 30 | 31 |
32 | 45 |
46 |
47 |
48 | 49 |
50 | {children} 51 |
52 | 53 | 67 | 68 | 69 | ) 70 | } -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 44, 44, 49; 7 | --background-rgb: 249, 247, 245; 8 | } 9 | 10 | body { 11 | color: rgb(var(--foreground-rgb)); 12 | background: rgb(var(--background-rgb)); 13 | font-feature-settings: "cv02", "cv03", "cv04", "cv11"; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | @layer base { 19 | a { 20 | @apply text-primary hover:text-primary-dark transition-colors duration-200; 21 | } 22 | 23 | h1, h2, h3, h4, h5, h6 { 24 | @apply font-sans tracking-tight; 25 | } 26 | 27 | p { 28 | @apply text-foreground-secondary; 29 | } 30 | 31 | pre { 32 | @apply font-mono bg-accents-1 p-4 rounded-cloudflare text-sm overflow-auto; 33 | } 34 | } 35 | 36 | @layer components { 37 | .btn { 38 | @apply inline-flex items-center justify-center px-4 py-2 rounded-cloudflare font-medium text-sm transition-all duration-200 focus:outline-none; 39 | } 40 | 41 | .btn-primary { 42 | @apply btn bg-primary text-white hover:bg-primary-dark border border-transparent shadow-small hover:shadow-medium; 43 | } 44 | 45 | .btn-secondary { 46 | @apply btn bg-white text-foreground hover:bg-accents-1 border border-accents-2 shadow-small hover:shadow-medium; 47 | } 48 | 49 | .btn-success { 50 | @apply btn bg-success text-white hover:bg-success-dark border border-transparent shadow-small hover:shadow-medium; 51 | } 52 | 53 | .btn-disabled { 54 | @apply btn bg-accents-1 text-accents-3 border border-accents-2 cursor-not-allowed; 55 | } 56 | 57 | .card { 58 | @apply bg-white rounded-cloudflare shadow-small border border-accents-2 p-6 transition-shadow duration-200; 59 | } 60 | 61 | .input-field { 62 | @apply w-full px-3 py-2 border border-accents-2 rounded-cloudflare focus:outline-none focus:ring-2 focus:ring-primary text-foreground bg-white transition-all duration-200; 63 | } 64 | 65 | .select-field { 66 | @apply w-full px-3 py-2 border border-accents-2 rounded-cloudflare focus:outline-none focus:ring-2 focus:ring-primary text-foreground bg-white transition-all duration-200; 67 | } 68 | 69 | .checkbox { 70 | @apply h-4 w-4 text-primary focus:ring-primary rounded border-accents-3; 71 | } 72 | 73 | .badge { 74 | @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; 75 | } 76 | 77 | .badge-success { 78 | @apply badge bg-success-lighter text-success-dark; 79 | } 80 | 81 | .badge-warning { 82 | @apply badge bg-warning-lighter text-warning-dark; 83 | } 84 | 85 | .badge-error { 86 | @apply badge bg-error-lighter text-error-dark; 87 | } 88 | 89 | .badge-primary { 90 | @apply badge bg-primary-lighter text-primary-dark; 91 | } 92 | 93 | .badge-secondary { 94 | @apply badge bg-secondary-lighter text-secondary-dark; 95 | } 96 | } -------------------------------------------------------------------------------- /debug-git-repo.js: -------------------------------------------------------------------------------- 1 | // Debug script to check Vercel API project fields related to Git repositories 2 | const axios = require('axios'); 3 | require('dotenv').config({ path: './.env.local' }); 4 | 5 | const VERCEL_API_URL = 'https://api.vercel.com'; 6 | const apiToken = process.env.VERCEL_API_TOKEN; 7 | const teamId = process.env.VERCEL_TEAM_ID; 8 | 9 | // Check if we have the required API token 10 | if (!apiToken) { 11 | console.error('ERROR: Missing VERCEL_API_TOKEN in .env.local file'); 12 | process.exit(1); 13 | } 14 | 15 | const headers = { 16 | Authorization: `Bearer ${apiToken}`, 17 | 'Content-Type': 'application/json' 18 | }; 19 | 20 | async function listProjects() { 21 | try { 22 | console.log('Fetching projects...'); 23 | const params = teamId ? { teamId } : {}; 24 | 25 | const response = await axios.get(`${VERCEL_API_URL}/v9/projects`, { 26 | headers, 27 | params 28 | }); 29 | 30 | console.log(`Found ${response.data.projects.length} projects`); 31 | 32 | // Print each project name and ID 33 | response.data.projects.forEach((project, index) => { 34 | console.log(`${index + 1}. ${project.name} (${project.id})`); 35 | }); 36 | 37 | // Ask the user to select a project to inspect 38 | const projectIndex = 0; // Just take the first project for simplicity 39 | const selectedProject = response.data.projects[projectIndex]; 40 | 41 | console.log(`\nSelected project: ${selectedProject.name} (${selectedProject.id})`); 42 | 43 | // Fetch detailed project info 44 | await getProjectDetails(selectedProject.id); 45 | 46 | } catch (error) { 47 | console.error('Error fetching projects:', error.response?.data || error.message); 48 | } 49 | } 50 | 51 | async function getProjectDetails(projectId) { 52 | try { 53 | console.log(`\nFetching details for project ID: ${projectId}...`); 54 | const params = teamId ? { teamId } : {}; 55 | 56 | const response = await axios.get(`${VERCEL_API_URL}/v9/projects/${projectId}`, { 57 | headers, 58 | params 59 | }); 60 | 61 | const project = response.data; 62 | 63 | console.log('\nProject Information:'); 64 | console.log(`Name: ${project.name}`); 65 | console.log(`ID: ${project.id}`); 66 | console.log(`Framework: ${project.framework || 'Not specified'}`); 67 | 68 | // Check for Git repository information 69 | console.log('\nGit Repository Information:'); 70 | if (project.gitRepository) { 71 | console.log('gitRepository field found:'); 72 | console.log(JSON.stringify(project.gitRepository, null, 2)); 73 | } else { 74 | console.log('No gitRepository field found'); 75 | } 76 | 77 | // Check for other Git-related fields 78 | const gitRelatedFields = {}; 79 | Object.keys(project).forEach(key => { 80 | if ( 81 | key.toLowerCase().includes('git') || 82 | key.toLowerCase().includes('repo') || 83 | key.toLowerCase().includes('source') 84 | ) { 85 | gitRelatedFields[key] = project[key]; 86 | } 87 | }); 88 | 89 | if (Object.keys(gitRelatedFields).length > 0) { 90 | console.log('\nOther Git-related fields:'); 91 | console.log(JSON.stringify(gitRelatedFields, null, 2)); 92 | } else { 93 | console.log('\nNo other Git-related fields found'); 94 | } 95 | 96 | // Print the full project object for complete inspection 97 | console.log('\nFull Project Object:'); 98 | console.log(JSON.stringify(project, null, 2)); 99 | 100 | } catch (error) { 101 | console.error('Error fetching project details:', error.response?.data || error.message); 102 | } 103 | } 104 | 105 | // Run the main function 106 | listProjects().catch(error => { 107 | console.error('Unhandled error:', error); 108 | }); -------------------------------------------------------------------------------- /app/api/projects/[projectId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDefaultVercelClient } from '@/app/lib/vercel-api'; 3 | 4 | // Define a more flexible project type to handle any keys 5 | interface VercelProject { 6 | id: string; 7 | name: string; 8 | framework?: string; 9 | gitRepository?: { 10 | type: string; 11 | repo: string; 12 | url: string; 13 | defaultBranch: string; 14 | }; 15 | link?: { 16 | type: string; 17 | repo: string; 18 | repoId: number; 19 | org: string; 20 | gitCredentialId: string; 21 | productionBranch: string; 22 | createdAt: number; 23 | updatedAt: number; 24 | deployHooks: any[]; 25 | }; 26 | [key: string]: any; // Allow any other properties 27 | } 28 | 29 | export async function GET( 30 | request: NextRequest, 31 | { params }: { params: { projectId: string } } 32 | ) { 33 | try { 34 | const vercelApiClient = getDefaultVercelClient(); 35 | 36 | console.log(`Fetching project details for: ${params.projectId}`); 37 | const project = await vercelApiClient.getProject(params.projectId) as VercelProject; 38 | 39 | console.log('Project data received:'); 40 | console.log('- Project ID:', project.id); 41 | console.log('- Project Name:', project.name); 42 | console.log('- Framework:', project.framework); 43 | 44 | // Check for Git repository information from either gitRepository or link field 45 | const hasGitRepository = !!project.gitRepository; 46 | const hasLinkRepository = !!project.link && project.link.type === 'github'; 47 | 48 | if (hasGitRepository && project.gitRepository) { 49 | console.log('Git Repository found in gitRepository field:'); 50 | console.log('- Type:', project.gitRepository.type); 51 | console.log('- Repository:', project.gitRepository.repo); 52 | console.log('- URL:', project.gitRepository.url); 53 | console.log('- Default Branch:', project.gitRepository.defaultBranch); 54 | } else if (hasLinkRepository && project.link) { 55 | console.log('Git Repository found in link field:'); 56 | console.log('- Type:', project.link.type); 57 | console.log('- Organization:', project.link.org); 58 | console.log('- Repository:', project.link.repo); 59 | console.log('- Production Branch:', project.link.productionBranch); 60 | 61 | // Create a gitRepository field with the data from link 62 | project.gitRepository = { 63 | type: project.link.type, 64 | repo: `${project.link.org}/${project.link.repo}`, 65 | url: `https://github.com/${project.link.org}/${project.link.repo}`, 66 | defaultBranch: project.link.productionBranch 67 | }; 68 | 69 | console.log('Created gitRepository field from link data:'); 70 | console.log(JSON.stringify(project.gitRepository, null, 2)); 71 | } else { 72 | console.log('No Git Repository found in project data'); 73 | console.log('Full project data for debugging:'); 74 | console.log(JSON.stringify(project, null, 2)); 75 | 76 | // Check if there are any other repository-related fields 77 | const repoKeys = Object.keys(project).filter(key => 78 | key.toLowerCase().includes('git') || key.toLowerCase().includes('repo') 79 | ); 80 | 81 | if (repoKeys.length > 0) { 82 | console.log('Found potential repository-related fields:'); 83 | repoKeys.forEach(key => { 84 | console.log(`- ${key}:`, project[key]); 85 | }); 86 | } 87 | } 88 | 89 | const environment = request.nextUrl.searchParams.get('environment') || 'development'; 90 | console.log(`Fetching environment variables for environment: ${environment}`); 91 | const environmentVariables = await vercelApiClient.getProjectEnvVars(params.projectId); 92 | 93 | return NextResponse.json({ 94 | project, 95 | environmentVariables 96 | }); 97 | } catch (error) { 98 | console.error('Error fetching project details:', error); 99 | return NextResponse.json( 100 | { error: 'Failed to fetch project details' }, 101 | { status: 500 } 102 | ); 103 | } 104 | } -------------------------------------------------------------------------------- /app/lib/vercel-api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const VERCEL_API_URL = 'https://api.vercel.com'; 4 | 5 | export interface VercelProject { 6 | id: string; 7 | name: string; 8 | framework: string; 9 | gitRepository?: { 10 | type: string; 11 | repo: string; 12 | url: string; 13 | defaultBranch: string; 14 | }; 15 | link?: { 16 | type: string; 17 | repo: string; 18 | repoId: number; 19 | org: string; 20 | gitCredentialId: string; 21 | productionBranch: string; 22 | createdAt: number; 23 | updatedAt: number; 24 | deployHooks: any[]; 25 | }; 26 | } 27 | 28 | export class VercelApiClient { 29 | private apiToken: string; 30 | private teamId?: string; 31 | 32 | constructor(apiToken: string, teamId?: string) { 33 | this.apiToken = apiToken; 34 | this.teamId = teamId; 35 | } 36 | 37 | private get headers() { 38 | return { 39 | Authorization: `Bearer ${this.apiToken}`, 40 | }; 41 | } 42 | 43 | private get baseParams() { 44 | if (this.teamId) { 45 | console.log(`Adding teamId parameter: ${this.teamId}`); 46 | return { teamId: this.teamId }; 47 | } 48 | return {}; 49 | } 50 | 51 | // Get user information 52 | async getUserInfo() { 53 | console.log('Fetching user info from Vercel API...'); 54 | try { 55 | const response = await axios.get(`${VERCEL_API_URL}/v2/user`, { 56 | headers: this.headers, 57 | }); 58 | console.log('Successfully fetched user info'); 59 | return response.data; 60 | } catch (error) { 61 | console.error('Error fetching user info:', error); 62 | throw error; 63 | } 64 | } 65 | 66 | // Get a list of all projects 67 | async getProjects() { 68 | console.log('Fetching projects list from Vercel API...'); 69 | 70 | const params = this.baseParams; 71 | console.log(`Base Params: ${JSON.stringify(params)}`); 72 | console.log(`Team ID: ${this.teamId || 'Not set'}`); 73 | console.log(`Full request URL: ${VERCEL_API_URL}/v9/projects ${params.teamId ? `with teamId=${params.teamId}` : ''}`); 74 | 75 | try { 76 | const response = await axios.get(`${VERCEL_API_URL}/v9/projects`, { 77 | headers: this.headers, 78 | params: params, 79 | }); 80 | 81 | console.log(`Successfully fetched ${response.data.projects?.length || 0} projects`); 82 | return response.data.projects as VercelProject[]; 83 | } catch (error) { 84 | console.error('Error fetching projects:', error); 85 | if (axios.isAxiosError(error)) { 86 | console.error('Response data:', error.response?.data); 87 | console.error('Response status:', error.response?.status); 88 | } 89 | throw error; 90 | } 91 | } 92 | 93 | // Get specific project details 94 | async getProject(projectId: string) { 95 | console.log(`Fetching project details for ${projectId}...`); 96 | try { 97 | const response = await axios.get(`${VERCEL_API_URL}/v9/projects/${projectId}`, { 98 | headers: this.headers, 99 | params: this.baseParams, 100 | }); 101 | console.log('Successfully fetched project details'); 102 | return response.data as VercelProject; 103 | } catch (error) { 104 | console.error(`Error fetching project ${projectId}:`, error); 105 | throw error; 106 | } 107 | } 108 | 109 | // Get project environment variables 110 | async getProjectEnvVars(projectId: string) { 111 | console.log(`Fetching environment variables for project ${projectId}...`); 112 | try { 113 | const response = await axios.get(`${VERCEL_API_URL}/v9/projects/${projectId}/env`, { 114 | headers: this.headers, 115 | params: this.baseParams, 116 | }); 117 | console.log('Successfully fetched project environment variables'); 118 | return response.data.envs; 119 | } catch (error) { 120 | console.error(`Error fetching environment variables for project ${projectId}:`, error); 121 | throw error; 122 | } 123 | } 124 | } 125 | 126 | // Get a default client instance from environment variables 127 | export function getDefaultVercelClient(): VercelApiClient { 128 | const apiToken = process.env.VERCEL_API_TOKEN; 129 | const teamId = process.env.VERCEL_TEAM_ID; 130 | 131 | if (!apiToken) { 132 | throw new Error('Missing VERCEL_API_TOKEN in environment variables'); 133 | } 134 | 135 | console.log('Creating Vercel API client...'); 136 | if (teamId) { 137 | console.log(`Using team ID: ${teamId}`); 138 | } 139 | 140 | return new VercelApiClient(apiToken, teamId || undefined); 141 | } -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { useRouter } from 'next/navigation'; 5 | import Link from 'next/link'; 6 | import { VercelProject } from '../lib/vercel-api'; 7 | 8 | export default function Dashboard() { 9 | const [projects, setProjects] = useState([]); 10 | const [loading, setLoading] = useState(true); 11 | const [error, setError] = useState(null); 12 | const router = useRouter(); 13 | 14 | useEffect(() => { 15 | async function fetchProjects() { 16 | try { 17 | const response = await fetch('/api/projects'); 18 | 19 | if (!response.ok) { 20 | throw new Error(`Failed to fetch projects: ${response.statusText}`); 21 | } 22 | 23 | const data = await response.json(); 24 | setProjects(data.projects); 25 | } catch (err) { 26 | setError(err instanceof Error ? err.message : 'An error occurred'); 27 | } finally { 28 | setLoading(false); 29 | } 30 | } 31 | 32 | fetchProjects(); 33 | }, []); 34 | 35 | if (loading) { 36 | return ( 37 |
38 |
39 |
40 |

Loading your projects...

41 |
42 |
43 | ); 44 | } 45 | 46 | if (error) { 47 | return ( 48 |
49 |
50 | 51 | 52 | 53 |

Error

54 |

{error}

55 | 56 | Return to Home 57 | 58 |
59 |
60 | ); 61 | } 62 | 63 | return ( 64 |
65 |
66 |
67 |

Projects

68 |

69 | Select a Next.js project from your Vercel account to convert to Cloudflare 70 |

71 |
72 | 73 | {projects.length === 0 ? ( 74 |
75 |

No Projects Found

76 |

77 | We couldn't find any Next.js projects in your Vercel account. Please make sure you have at least one Next.js project deployed on Vercel. 78 |

79 |
80 | ) : ( 81 |
82 | {projects.map(project => ( 83 |
84 |
85 | 86 | {project.framework} 87 | 88 |
89 | 90 |

{project.name}

91 |

ID: {project.id}

92 | 93 | {project.framework === 'nextjs' ? ( 94 | 95 | Convert to Cloudflare 96 | 97 | ) : ( 98 | 101 | )} 102 |
103 | ))} 104 |
105 | )} 106 |
107 |
108 | ); 109 | } -------------------------------------------------------------------------------- /app/lib/git-utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import simpleGit, { SimpleGit } from 'simple-git'; 4 | 5 | export interface CloneOptions { 6 | repoUrl: string; 7 | projectId: string; 8 | branch?: string; 9 | storagePath: string; 10 | auth?: GitAuthOptions; // For future GitHub authentication 11 | } 12 | 13 | // Future authentication options interface 14 | export interface GitAuthOptions { 15 | type: 'oauth' | 'token' | 'ssh'; 16 | token?: string; 17 | username?: string; 18 | password?: string; 19 | sshKeyPath?: string; 20 | } 21 | 22 | export interface CloneResult { 23 | success: boolean; 24 | path: string; 25 | message: string; 26 | error?: Error; 27 | } 28 | 29 | export async function cloneRepository(options: CloneOptions): Promise { 30 | const { repoUrl, projectId, branch, storagePath } = options; 31 | 32 | // Create a safe directory name from the project ID 33 | const projectDir = path.join(storagePath, projectId); 34 | 35 | // Ensure the storage path exists 36 | if (!fs.existsSync(storagePath)) { 37 | try { 38 | fs.mkdirSync(storagePath, { recursive: true }); 39 | } catch (error) { 40 | console.error('Failed to create storage directory:', error); 41 | return { 42 | success: false, 43 | path: projectDir, 44 | message: `Failed to create storage directory: ${error instanceof Error ? error.message : 'Unknown error'}`, 45 | error: error instanceof Error ? error : new Error('Unknown error'), 46 | }; 47 | } 48 | } 49 | 50 | // Create an instance of SimpleGit 51 | const git: SimpleGit = simpleGit(); 52 | 53 | try { 54 | // Check if directory already exists 55 | if (fs.existsSync(projectDir)) { 56 | try { 57 | // Try to pull latest changes if it's a git repo 58 | const localGit = simpleGit(projectDir); 59 | 60 | // Check if it's a valid git repository 61 | const isRepo = await localGit.checkIsRepo(); 62 | 63 | if (isRepo) { 64 | // If branch is specified, check it out 65 | if (branch) { 66 | try { 67 | await localGit.checkout(branch); 68 | } catch (error) { 69 | console.warn(`Warning: Could not checkout branch ${branch}:`, error); 70 | // Continue with current branch 71 | } 72 | } 73 | 74 | // Pull the latest changes 75 | await localGit.pull(); 76 | return { 77 | success: true, 78 | path: projectDir, 79 | message: 'Repository already exists locally. Pulled latest changes.', 80 | }; 81 | } else { 82 | // Not a git repo, remove directory and clone 83 | fs.rmSync(projectDir, { recursive: true, force: true }); 84 | } 85 | } catch (error) { 86 | console.error('Error updating existing repository:', error); 87 | // If there's an error, remove the directory and try to clone again 88 | try { 89 | fs.rmSync(projectDir, { recursive: true, force: true }); 90 | } catch (rmError) { 91 | console.error('Failed to remove existing directory:', rmError); 92 | return { 93 | success: false, 94 | path: projectDir, 95 | message: `Failed to prepare directory for cloning: ${rmError instanceof Error ? rmError.message : 'Unknown error'}`, 96 | error: rmError instanceof Error ? rmError : new Error('Unknown error'), 97 | }; 98 | } 99 | } 100 | } 101 | 102 | // Clean up URL to ensure it's properly formatted 103 | // Remove any quotes or spaces that might be in the URL 104 | const cleanRepoUrl = repoUrl.trim().replace(/['"]/g, ''); 105 | console.log(`Cloning repository from URL: ${cleanRepoUrl} to ${projectDir}`); 106 | 107 | // TODO: Add GitHub authentication when options.auth is provided 108 | // This would be implemented in the future to handle private repositories 109 | 110 | // Clone the repository 111 | // Don't pass branch in cloneOptions, use separate args to specify branch if needed 112 | if (branch) { 113 | console.log(`Cloning with branch: ${branch}`); 114 | await git.clone(cleanRepoUrl, projectDir, ['-b', branch]); 115 | } else { 116 | await git.clone(cleanRepoUrl, projectDir); 117 | } 118 | 119 | return { 120 | success: true, 121 | path: projectDir, 122 | message: 'Repository cloned successfully.', 123 | }; 124 | } catch (error) { 125 | console.error('Clone error:', error); 126 | // Provide more detailed error messages based on error type 127 | let errorMessage = error instanceof Error ? error.message : 'Unknown error'; 128 | 129 | // Check for common Git errors and provide more helpful messages 130 | if (errorMessage.includes('Authentication failed')) { 131 | errorMessage = 'Authentication failed. This may be a private repository that requires credentials.'; 132 | } else if (errorMessage.includes('not found')) { 133 | errorMessage = 'Repository not found. Please verify the URL is correct.'; 134 | } else if (errorMessage.includes('already exists')) { 135 | errorMessage = 'Directory already exists and is not empty.'; 136 | } 137 | 138 | return { 139 | success: false, 140 | path: projectDir, 141 | message: `Failed to clone repository: ${errorMessage}`, 142 | error: error instanceof Error ? error : new Error(errorMessage), 143 | }; 144 | } 145 | } 146 | 147 | export async function createBranch(projectPath: string, branchName: string): Promise { 148 | try { 149 | const git = simpleGit(projectPath); 150 | await git.checkoutLocalBranch(branchName); 151 | return true; 152 | } catch (error) { 153 | console.error('Error creating branch:', error); 154 | return false; 155 | } 156 | } 157 | 158 | export async function commitAndPush(projectPath: string, message: string): Promise { 159 | try { 160 | const git = simpleGit(projectPath); 161 | await git.add('.'); 162 | await git.commit(message); 163 | await git.push('origin', 'HEAD'); 164 | return true; 165 | } catch (error) { 166 | console.error('Error committing and pushing changes:', error); 167 | return false; 168 | } 169 | } -------------------------------------------------------------------------------- /app/api/convert/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import path from 'path'; 3 | import { getDefaultVercelClient } from '../../lib/vercel-api'; 4 | import { cloneRepository, createBranch, commitAndPush } from '../../lib/git-utils'; 5 | import { ConversionPipeline, ConversionResult } from '../../lib/conversion-pipeline'; 6 | 7 | // Define global store for conversion status 8 | declare global { 9 | var conversionStore: Record; 16 | } 17 | 18 | // Initialize the global store if not exists 19 | if (!global.conversionStore) { 20 | global.conversionStore = {}; 21 | } 22 | 23 | export async function POST(request: NextRequest) { 24 | try { 25 | const body = await request.json(); 26 | const { projectId, options } = body; 27 | 28 | if (!projectId) { 29 | return NextResponse.json( 30 | { error: 'Project ID is required' }, 31 | { status: 400 } 32 | ); 33 | } 34 | 35 | // Create an object to track conversion status 36 | global.conversionStore[projectId] = { 37 | status: 'cloning', 38 | logs: ['Initializing conversion process...'], 39 | }; 40 | 41 | // Start the conversion process in the background 42 | startConversion(projectId, options); 43 | 44 | return NextResponse.json({ 45 | success: true, 46 | message: 'Conversion process started', 47 | }); 48 | } catch (error) { 49 | console.error('Error starting conversion:', error); 50 | return NextResponse.json( 51 | { error: 'Failed to start conversion process' }, 52 | { status: 500 } 53 | ); 54 | } 55 | } 56 | 57 | // Add a safer error handling utility 58 | function safelyUpdateConversionStatus(projectId: string, status: 'cloning' | 'converting' | 'success' | 'failed', message: string, error?: any) { 59 | if (!global.conversionStore[projectId]) { 60 | global.conversionStore[projectId] = { 61 | status: 'idle', 62 | logs: [], 63 | }; 64 | } 65 | 66 | // Update the status and message 67 | global.conversionStore[projectId].status = status; 68 | global.conversionStore[projectId].message = message; 69 | 70 | // Add to logs 71 | global.conversionStore[projectId].logs.push(message); 72 | 73 | // Log any errors 74 | if (error) { 75 | console.error(`Conversion error for ${projectId}:`, error); 76 | const errorMsg = error instanceof Error ? error.message : String(error); 77 | global.conversionStore[projectId].logs.push(`Error details: ${errorMsg}`); 78 | global.conversionStore[projectId].error = errorMsg; 79 | } 80 | } 81 | 82 | async function startConversion( 83 | projectId: string, 84 | options: any 85 | ) { 86 | try { 87 | // Get project details from Vercel 88 | const client = getDefaultVercelClient(); 89 | const project = await client.getProject(projectId); 90 | 91 | // Check for Git repository information in either gitRepository or link field 92 | const hasGitRepository = !!project.gitRepository; 93 | const hasLinkRepository = !!project.link && project.link.type === 'github'; 94 | 95 | // Create gitRepository field from link if necessary 96 | if (!hasGitRepository && hasLinkRepository && project.link) { 97 | project.gitRepository = { 98 | type: project.link.type, 99 | repo: `${project.link.org}/${project.link.repo}`, 100 | url: `https://github.com/${project.link.org}/${project.link.repo}`, 101 | defaultBranch: project.link.productionBranch 102 | }; 103 | global.conversionStore[projectId].logs.push(`Detected Git repository from link field: ${project.link.org}/${project.link.repo}`); 104 | } 105 | 106 | if (!project.gitRepository) { 107 | safelyUpdateConversionStatus( 108 | projectId, 109 | 'failed', 110 | 'No git repository found for this project', 111 | new Error('Project doesn\'t have a connected Git repository') 112 | ); 113 | return; 114 | } 115 | 116 | // Update status 117 | global.conversionStore[projectId].logs.push(`Fetching project details for ${project.name}...`); 118 | global.conversionStore[projectId].logs.push(`Repository: ${project.gitRepository.repo}`); 119 | 120 | // Clone the repository 121 | const storagePath = process.env.LOCAL_STORAGE_PATH || './tmp/projects'; 122 | // Vercel uses url for the git repository URL 123 | const repoUrl = project.gitRepository.url; 124 | const defaultBranch = project.gitRepository.defaultBranch; 125 | 126 | // Add detailed logging 127 | console.log('Repository URL details:'); 128 | console.log('Raw URL from Vercel API:', repoUrl); 129 | console.log('URL type:', typeof repoUrl); 130 | console.log('Default branch:', defaultBranch); 131 | 132 | // Format URL for GitHub if needed 133 | let formattedRepoUrl = repoUrl; 134 | if (project.gitRepository.type === 'github' && !repoUrl.startsWith('https://') && !repoUrl.startsWith('git@')) { 135 | formattedRepoUrl = `https://github.com/${project.gitRepository.repo}.git`; 136 | console.log('Formatted GitHub URL:', formattedRepoUrl); 137 | global.conversionStore[projectId].logs.push(`Using formatted GitHub URL: ${formattedRepoUrl}`); 138 | } 139 | 140 | global.conversionStore[projectId].logs.push(`Cloning repository from ${formattedRepoUrl}...`); 141 | const cloneResult = await cloneRepository({ 142 | repoUrl: formattedRepoUrl, 143 | projectId, 144 | branch: defaultBranch, 145 | storagePath, 146 | }); 147 | 148 | if (!cloneResult.success) { 149 | safelyUpdateConversionStatus( 150 | projectId, 151 | 'failed', 152 | `Failed to clone repository: ${cloneResult.message}`, 153 | cloneResult.error 154 | ); 155 | return; 156 | } 157 | 158 | global.conversionStore[projectId].logs.push(cloneResult.message); 159 | global.conversionStore[projectId].status = 'converting'; 160 | 161 | // Create a new branch if requested 162 | const projectPath = cloneResult.path; 163 | if (options.createBranch && options.branchName) { 164 | global.conversionStore[projectId].logs.push(`Creating branch: ${options.branchName}...`); 165 | const branchCreated = await createBranch(projectPath, options.branchName); 166 | 167 | if (!branchCreated) { 168 | global.conversionStore[projectId].logs.push(`Warning: Failed to create branch ${options.branchName}. Continuing on current branch.`); 169 | } else { 170 | global.conversionStore[projectId].logs.push(`Created branch: ${options.branchName}`); 171 | } 172 | } 173 | 174 | // Run the conversion pipeline 175 | global.conversionStore[projectId].logs.push('Starting conversion pipeline...'); 176 | const pipeline = new ConversionPipeline({ 177 | projectPath, 178 | projectName: project.name, 179 | enableKVCache: options.enableKVCache, 180 | kvNamespaceId: options.kvNamespaceId, 181 | }); 182 | 183 | const result = await pipeline.run(); 184 | global.conversionStore[projectId].logs.push(...result.logs); 185 | 186 | // Commit and push if requested 187 | if (result.success && options.commitAndPush) { 188 | global.conversionStore[projectId].logs.push('Committing and pushing changes...'); 189 | const commitResult = await commitAndPush(projectPath, 'Convert to @opennextjs/cloudflare'); 190 | 191 | if (commitResult) { 192 | global.conversionStore[projectId].logs.push('Changes committed and pushed successfully'); 193 | } else { 194 | global.conversionStore[projectId].logs.push('Warning: Failed to commit and push changes'); 195 | } 196 | } 197 | 198 | // Update final status 199 | global.conversionStore[projectId].status = result.success ? 'success' : 'failed'; 200 | global.conversionStore[projectId].message = result.message; 201 | global.conversionStore[projectId].result = result; 202 | } catch (error) { 203 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 204 | console.error(`Error during conversion of project ${projectId}:`, error); 205 | 206 | safelyUpdateConversionStatus( 207 | projectId, 208 | 'failed', 209 | `Conversion process failed unexpectedly: ${errorMessage}`, 210 | error 211 | ); 212 | } 213 | } -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import Image from 'next/image' 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | {/* Hero Section */} 8 |
9 |
10 |

11 | Move from Vercel to Cloudflare seamlessly 12 |

13 |

14 | Diverce helps you migrate your Next.js projects from Vercel to Cloudflare with just a few clicks, no manual configuration needed. 15 |

16 |
17 | 18 | Get Started 19 | 20 | 21 | View on GitHub 22 | 23 |
24 |
25 |
26 | 27 | {/* Features Section */} 28 |
29 |
30 |
31 |

32 | Simple Migration Process 33 |

34 |

35 | We handle all the complex configuration so you don't have to. 36 |

37 |
38 | 39 |
40 | {/* Feature 1 */} 41 |
42 |
43 | 44 | 45 | 46 |
47 |

Automated Conversion

48 |

49 | Our tool automatically converts your Next.js project to use @opennextjs/cloudflare and sets up all necessary configurations. 50 |

51 |
52 | 53 | {/* Feature 2 */} 54 |
55 |
56 | 57 | 58 | 59 |
60 |

Zero Code Changes

61 |

62 | Move to Cloudflare without modifying your existing code. We handle all the configuration changes for you. 63 |

64 |
65 | 66 | {/* Feature 3 */} 67 |
68 |
69 | 70 | 71 | 72 |
73 |

Secure & Private

74 |

75 | Your code stays secure. We only perform the conversion locally and never store your source code on our servers. 76 |

77 |
78 |
79 |
80 |
81 | 82 | {/* How It Works Section */} 83 |
84 |
85 |
86 |

87 | How It Works 88 |

89 |

90 | Three simple steps to move your Next.js app from Vercel to Cloudflare 91 |

92 |
93 | 94 |
95 | {/* Step 1 */} 96 |
97 |
98 |
99 | 1 100 |
101 |

Connect Your Vercel Account

102 |

103 | Sign in with your Vercel account to access your projects. 104 |

105 |
106 |
107 | 108 | {/* Step 2 */} 109 |
110 |
111 |
112 | 2 113 |
114 |

Select a Project

115 |

116 | Choose the Next.js project you want to migrate to Cloudflare. 117 |

118 |
119 |
120 | 121 | {/* Step 3 */} 122 |
123 |
124 |
125 | 3 126 |
127 |

Start Conversion

128 |

129 | Click convert and let us handle the rest. You'll get a fully Cloudflare-compatible project. 130 |

131 |
132 |
133 |
134 | 135 |
136 | 137 | Try It Now 138 | 139 |
140 |
141 |
142 | 143 | {/* CTA Section */} 144 |
145 |
146 |

147 | Ready to migrate your Next.js projects? 148 |

149 |

150 | Diverce makes it easy to move from Vercel to Cloudflare without any hassle. 151 |

152 | 153 | Get Started Now 154 | 155 |
156 |
157 |
158 | ) 159 | } -------------------------------------------------------------------------------- /app/lib/conversion-pipeline.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { exec } from 'child_process'; 4 | import { promisify } from 'util'; 5 | import simpleGit from 'simple-git'; 6 | 7 | const execAsync = promisify(exec); 8 | 9 | export interface ConversionOptions { 10 | projectPath: string; 11 | projectName: string; 12 | enableKVCache?: boolean; 13 | kvNamespaceId?: string; 14 | } 15 | 16 | export interface ConversionResult { 17 | success: boolean; 18 | message: string; 19 | logs: string[]; 20 | } 21 | 22 | export class ConversionPipeline { 23 | private options: ConversionOptions; 24 | private logs: string[] = []; 25 | 26 | constructor(options: ConversionOptions) { 27 | this.options = options; 28 | } 29 | 30 | private log(message: string) { 31 | this.logs.push(message); 32 | console.log(message); 33 | } 34 | 35 | async run(): Promise { 36 | try { 37 | this.log('Starting conversion pipeline...'); 38 | 39 | // Step 1: Verify Next.js project and check for edge runtime 40 | await this.verifyNextJsProject(); 41 | 42 | // Step 2: Install OpenNext dependencies 43 | await this.installDependencies(); 44 | 45 | // Step 3: Generate or update open-next.config.ts 46 | await this.createOpenNextConfig(); 47 | 48 | // Step 4: Generate or update wrangler.jsonc 49 | await this.createWranglerConfig(); 50 | 51 | // Step 5: Update package.json scripts 52 | await this.updatePackageJson(); 53 | 54 | // Step 6: Remove conflicting references (@cloudflare/next-on-pages, edge runtime) 55 | await this.removeConflictingReferences(); 56 | 57 | // Step 7: Add .open-next to .gitignore 58 | await this.updateGitignore(); 59 | 60 | this.log('Conversion completed successfully! 🎉'); 61 | 62 | return { 63 | success: true, 64 | message: 'Project successfully converted to use @opennextjs/cloudflare', 65 | logs: this.logs, 66 | }; 67 | } catch (error) { 68 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 69 | this.log(`Error during conversion: ${errorMessage}`); 70 | 71 | return { 72 | success: false, 73 | message: `Conversion failed: ${errorMessage}`, 74 | logs: this.logs, 75 | }; 76 | } 77 | } 78 | 79 | private async verifyNextJsProject() { 80 | this.log('Verifying Next.js project...'); 81 | 82 | const packageJsonPath = path.join(this.options.projectPath, 'package.json'); 83 | 84 | if (!fs.existsSync(packageJsonPath)) { 85 | throw new Error('Could not find package.json in the project directory'); 86 | } 87 | 88 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 89 | 90 | // Check for Next.js dependency 91 | if (!packageJson.dependencies?.next && !packageJson.devDependencies?.next) { 92 | throw new Error('This project does not appear to be a Next.js project (next package not found)'); 93 | } 94 | 95 | this.log('Next.js project verified ✅'); 96 | 97 | // Check for edge runtime references 98 | this.log('Checking for Edge Runtime usage...'); 99 | try { 100 | const { stdout } = await execAsync('grep -r "export const runtime = \\"edge\\"" --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" .', { cwd: this.options.projectPath }); 101 | 102 | if (stdout.trim()) { 103 | this.log('⚠️ WARNING: Edge Runtime usage detected. These will be removed during conversion.'); 104 | } else { 105 | this.log('No Edge Runtime usage detected ✅'); 106 | } 107 | } catch (error) { 108 | // If grep returns no matches, it exits with code 1, which causes exec to throw 109 | // This is normal and expected when no matches are found 110 | this.log('No Edge Runtime usage detected ✅'); 111 | } 112 | } 113 | 114 | private async installDependencies() { 115 | this.log('Installing OpenNext dependencies...'); 116 | 117 | try { 118 | await execAsync('npm install --save-dev @opennextjs/cloudflare@latest wrangler@latest', { 119 | cwd: this.options.projectPath 120 | }); 121 | this.log('Dependencies installed successfully ✅'); 122 | } catch (error) { 123 | throw new Error(`Failed to install dependencies: ${error instanceof Error ? error.message : 'Unknown error'}`); 124 | } 125 | } 126 | 127 | private async createOpenNextConfig() { 128 | this.log('Creating or updating open-next.config.ts...'); 129 | 130 | const configPath = path.join(this.options.projectPath, 'open-next.config.ts'); 131 | const config = `import { defineCloudflareConfig } from "@opennextjs/cloudflare"; 132 | ${this.options.enableKVCache ? 'import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache";' : ''} 133 | 134 | export default defineCloudflareConfig({ 135 | ${this.options.enableKVCache ? ' incrementalCache: kvIncrementalCache,' : ''} 136 | }); 137 | `; 138 | 139 | fs.writeFileSync(configPath, config); 140 | this.log('open-next.config.ts created/updated ✅'); 141 | } 142 | 143 | private async createWranglerConfig() { 144 | this.log('Creating or updating wrangler.jsonc...'); 145 | 146 | const wranglerPath = path.join(this.options.projectPath, 'wrangler.jsonc'); 147 | const kvNamespaces = this.options.enableKVCache && this.options.kvNamespaceId 148 | ? `[ 149 | { 150 | "binding": "NEXT_CACHE_WORKERS_KV", 151 | "id": "${this.options.kvNamespaceId}" 152 | } 153 | ]` 154 | : '[]'; 155 | 156 | const config = `{ 157 | "$schema": "node_modules/wrangler/config-schema.json", 158 | "main": ".open-next/worker.js", 159 | "name": "${this.options.projectName}", 160 | "compatibility_date": "2024-12-30", 161 | "compatibility_flags": ["nodejs_compat"], 162 | "assets": { 163 | "directory": ".open-next/assets", 164 | "binding": "ASSETS" 165 | }, 166 | "kv_namespaces": ${kvNamespaces} 167 | } 168 | `; 169 | 170 | fs.writeFileSync(wranglerPath, config); 171 | this.log('wrangler.jsonc created/updated ✅'); 172 | } 173 | 174 | private async updatePackageJson() { 175 | this.log('Updating package.json scripts...'); 176 | 177 | const packageJsonPath = path.join(this.options.projectPath, 'package.json'); 178 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 179 | 180 | packageJson.scripts = packageJson.scripts || {}; 181 | packageJson.scripts.preview = 'opennextjs-cloudflare && wrangler dev'; 182 | packageJson.scripts.deploy = 'opennextjs-cloudflare && wrangler deploy'; 183 | packageJson.scripts['cf-typegen'] = 'wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts'; 184 | 185 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 186 | this.log('package.json scripts updated ✅'); 187 | } 188 | 189 | private async removeConflictingReferences() { 190 | this.log('Removing conflicting references...'); 191 | 192 | // Remove next-on-pages from dependencies if present 193 | const packageJsonPath = path.join(this.options.projectPath, 'package.json'); 194 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 195 | 196 | if (packageJson.dependencies?.['@cloudflare/next-on-pages']) { 197 | delete packageJson.dependencies['@cloudflare/next-on-pages']; 198 | this.log('Removed @cloudflare/next-on-pages from dependencies'); 199 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 200 | } 201 | 202 | if (packageJson.devDependencies?.['@cloudflare/next-on-pages']) { 203 | delete packageJson.devDependencies['@cloudflare/next-on-pages']; 204 | this.log('Removed @cloudflare/next-on-pages from devDependencies'); 205 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 206 | } 207 | 208 | // Replace "export const runtime = 'edge'" references 209 | try { 210 | await execAsync("find . -type f -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' | xargs -I{} sed -i '' 's/export const runtime = .edge.;/\\/\\/ Removed edge runtime declaration/g' {}", { 211 | cwd: this.options.projectPath, 212 | }); 213 | 214 | this.log('Removed edge runtime declarations from files'); 215 | } catch (error) { 216 | this.log('Note: Could not automatically remove edge runtime declarations. You may need to remove these manually.'); 217 | } 218 | 219 | this.log('Conflicting references removed ✅'); 220 | } 221 | 222 | private async updateGitignore() { 223 | this.log('Updating .gitignore...'); 224 | 225 | const gitignorePath = path.join(this.options.projectPath, '.gitignore'); 226 | let content = ''; 227 | 228 | if (fs.existsSync(gitignorePath)) { 229 | content = fs.readFileSync(gitignorePath, 'utf8'); 230 | } 231 | 232 | if (!content.includes('.open-next')) { 233 | content += '\n# OpenNext build output\n.open-next\n'; 234 | fs.writeFileSync(gitignorePath, content); 235 | this.log('Added .open-next to .gitignore ✅'); 236 | } else { 237 | this.log('.open-next already in .gitignore ✅'); 238 | } 239 | } 240 | } -------------------------------------------------------------------------------- /app/convert/[projectId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect, useRef } from 'react'; 4 | import { useParams, useRouter } from 'next/navigation'; 5 | import Link from 'next/link'; 6 | 7 | interface ProjectDetails { 8 | id: string; 9 | name: string; 10 | framework: string; 11 | gitRepository?: { 12 | type: string; 13 | repo: string; 14 | url: string; 15 | defaultBranch: string; 16 | }; 17 | link?: { 18 | type: string; 19 | repo: string; 20 | repoId: number; 21 | org: string; 22 | gitCredentialId: string; 23 | productionBranch: string; 24 | createdAt: number; 25 | updatedAt: number; 26 | deployHooks: any[]; 27 | }; 28 | [key: string]: any; // To allow for any other fields 29 | } 30 | 31 | interface ConversionOptions { 32 | enableKVCache: boolean; 33 | kvNamespaceId: string; 34 | createBranch: boolean; 35 | branchName: string; 36 | commitAndPush: boolean; 37 | } 38 | 39 | interface ConversionStatus { 40 | status: 'idle' | 'cloning' | 'converting' | 'success' | 'failed'; 41 | logs: string[]; 42 | message?: string; 43 | } 44 | 45 | export default function ConvertProject() { 46 | const params = useParams(); 47 | const router = useRouter(); 48 | const projectId = params.projectId as string; 49 | const logsEndRef = useRef(null); 50 | 51 | const [project, setProject] = useState(null); 52 | const [loading, setLoading] = useState(true); 53 | const [error, setError] = useState(null); 54 | const [debugMode, setDebugMode] = useState(false); 55 | 56 | const [options, setOptions] = useState({ 57 | enableKVCache: false, 58 | kvNamespaceId: '', 59 | createBranch: true, 60 | branchName: 'cloudflare-migration', 61 | commitAndPush: false, 62 | }); 63 | 64 | const [conversionStatus, setConversionStatus] = useState({ 65 | status: 'idle', 66 | logs: [], 67 | }); 68 | 69 | useEffect(() => { 70 | async function fetchProjectDetails() { 71 | try { 72 | const response = await fetch(`/api/projects/${projectId}`); 73 | 74 | if (!response.ok) { 75 | throw new Error(`Failed to fetch project details: ${response.statusText}`); 76 | } 77 | 78 | const data = await response.json(); 79 | console.log('Project data:', data); 80 | setProject(data.project); 81 | } catch (err) { 82 | setError(err instanceof Error ? err.message : 'An error occurred'); 83 | } finally { 84 | setLoading(false); 85 | } 86 | } 87 | 88 | fetchProjectDetails(); 89 | }, [projectId]); 90 | 91 | useEffect(() => { 92 | if (logsEndRef.current) { 93 | logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); 94 | } 95 | }, [conversionStatus.logs]); 96 | 97 | const handleInputChange = (e: React.ChangeEvent) => { 98 | const { name, value, type, checked } = e.target; 99 | setOptions(prev => ({ 100 | ...prev, 101 | [name]: type === 'checkbox' ? checked : value, 102 | })); 103 | }; 104 | 105 | const startConversion = async () => { 106 | setConversionStatus({ 107 | status: 'cloning', 108 | logs: ['Starting conversion process...', 'Cloning repository...'], 109 | }); 110 | 111 | try { 112 | const response = await fetch('/api/convert', { 113 | method: 'POST', 114 | headers: { 115 | 'Content-Type': 'application/json', 116 | }, 117 | body: JSON.stringify({ 118 | projectId, 119 | options, 120 | }), 121 | }); 122 | 123 | if (!response.ok) { 124 | throw new Error(`Conversion failed: ${response.statusText}`); 125 | } 126 | 127 | const eventSource = new EventSource(`/api/convert/status?projectId=${projectId}`); 128 | 129 | eventSource.onmessage = (event) => { 130 | const data = JSON.parse(event.data); 131 | 132 | setConversionStatus(prev => ({ 133 | ...prev, 134 | status: data.status, 135 | logs: [...data.logs], 136 | message: data.message, 137 | })); 138 | 139 | if (data.status === 'success' || data.status === 'failed') { 140 | eventSource.close(); 141 | } 142 | }; 143 | 144 | eventSource.onerror = () => { 145 | eventSource.close(); 146 | setConversionStatus(prev => ({ 147 | ...prev, 148 | status: 'failed', 149 | message: 'Lost connection to server', 150 | })); 151 | }; 152 | } catch (err) { 153 | setConversionStatus({ 154 | status: 'failed', 155 | logs: [...conversionStatus.logs, err instanceof Error ? err.message : 'An error occurred'], 156 | message: 'Conversion process failed', 157 | }); 158 | } 159 | }; 160 | 161 | if (loading) { 162 | return ( 163 |
164 |
165 |
166 |

Loading project details...

167 |
168 |
169 | ); 170 | } 171 | 172 | if (error || !project) { 173 | return ( 174 |
175 |
176 | 177 | 178 | 179 |

Error

180 |

{error || 'Failed to load project details'}

181 | 182 | Return to Dashboard 183 | 184 |
185 |
186 | ); 187 | } 188 | 189 | return ( 190 |
191 |
192 | {/* Header */} 193 |
194 |
195 | 196 | 197 | 198 | 199 | Back to Projects 200 | 201 |

202 | {project.name} 203 | {project.framework} 204 |

205 |

Migrating from Vercel to Cloudflare

206 |
207 |
208 | 214 |
215 |
216 | 217 | {/* Main content */} 218 |
219 |
220 | {/* Project Info Card */} 221 |
222 |

Project Details

223 | 224 |
225 |
226 |

Project ID

227 |

{project.id}

228 |
229 | 230 |
231 |

Framework

232 |

{project.framework}

233 |
234 |
235 | 236 | {/* Git Repository Info */} 237 | {!project.gitRepository && !project.link ? ( 238 |
239 |

No Git Repository

240 |

241 | This project doesn't have a connected Git repository. 242 | This tool requires a Git repository to clone and modify the code. 243 |

244 |
245 | ) : ( 246 |
247 |

Repository

248 | {project.gitRepository ? ( 249 |
250 |

251 | Repository: {project.gitRepository.repo} 252 |

253 |

254 | Branch: {project.gitRepository.defaultBranch} 255 |

256 |
257 | ) : project.link && project.link.type === 'github' ? ( 258 |
259 |

260 | Repository: {project.link.org}/{project.link.repo} 261 |

262 |

263 | Branch: {project.link.productionBranch} 264 |

265 |
266 | ) : null} 267 |
268 | )} 269 | 270 | {/* Debug Info */} 271 | {debugMode && ( 272 |
273 |

Raw Project Data

274 |
275 |
276 |                       {JSON.stringify(project, null, 2)}
277 |                     
278 |
279 |
280 | )} 281 |
282 | 283 | {/* Conversion Options */} 284 |
285 |

Conversion Options

286 | 287 |
288 |
289 |
290 | 298 | 301 |
302 |

303 | Use Cloudflare KV for incremental static regeneration cache 304 |

305 |
306 | 307 | {options.enableKVCache && ( 308 |
309 | 312 | 321 | Create a new KV namespace here 322 |
323 | )} 324 | 325 |
326 |

Git Options

327 | 328 |
329 |
330 |
331 | 339 | 342 |
343 |

344 | Create a new branch for the Cloudflare migration 345 |

346 |
347 | 348 | {options.createBranch && ( 349 |
350 | 353 | 362 |
363 | )} 364 | 365 |
366 |
367 | 375 | 378 |
379 |

380 | Automatically commit and push the changes 381 |

382 |
383 |
384 |
385 | 386 |
387 | {(project.gitRepository || (project.link && project.link.type === 'github')) && conversionStatus.status === 'idle' && ( 388 | 395 | )} 396 | 397 | {!project.gitRepository && !project.link && ( 398 | 405 | )} 406 |
407 |
408 |
409 |
410 | 411 |
412 | {/* Conversion Status & Logs */} 413 |
414 |
415 |

Conversion Status

416 | 417 | {conversionStatus.status !== 'idle' && ( 418 |
419 | {conversionStatus.status === 'cloning' && ( 420 | Cloning Repository 421 | )} 422 | {conversionStatus.status === 'converting' && ( 423 | Converting 424 | )} 425 | {conversionStatus.status === 'success' && ( 426 | Completed 427 | )} 428 | {conversionStatus.status === 'failed' && ( 429 | Failed 430 | )} 431 |
432 | )} 433 |
434 | 435 | {conversionStatus.status === 'idle' ? ( 436 |
437 | 438 | 439 | 440 |

Ready to Convert

441 |

442 | Configure the options and click "Start Conversion" to migrate your project from Vercel to Cloudflare 443 |

444 |
445 | ) : ( 446 |
447 |
448 | {conversionStatus.logs.map((log, index) => ( 449 |
450 | {log.startsWith('Error') || log.includes('failed') ? ( 451 | {log} 452 | ) : log.includes('✅') ? ( 453 | {log} 454 | ) : log.includes('WARNING') || log.includes('⚠️') ? ( 455 | {log} 456 | ) : ( 457 | {log} 458 | )} 459 |
460 | ))} 461 |
462 |
463 | 464 | {conversionStatus.status === 'success' && ( 465 |
466 |

Conversion Completed!

467 |

468 | Your Next.js project has been successfully converted to use Cloudflare. You can now deploy it to Cloudflare Workers. 469 |

470 | {/* Add code snippet that clones the repository and runs npm run deploy */} 471 |
472 |                         
487 |                         
488 |                           git clone {project.gitRepository.url}
489 | cd {project.gitRepository.repo.split('/').pop()}
490 | npm install
491 | npm run deploy 492 |
493 |
494 |
495 | 496 | Back to Projects 497 | 498 | 501 |
502 |
503 | )} 504 | 505 | {conversionStatus.status === 'failed' && ( 506 |
507 |

Conversion Failed

508 |

509 | {conversionStatus.message || 'There was an error during the conversion process. Please check the logs for details.'} 510 |

511 | 517 |
518 | )} 519 |
520 | )} 521 |
522 |
523 |
524 |
525 |
526 | ); 527 | } --------------------------------------------------------------------------------