Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/components/ApplicationStarter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,19 @@ export function ApplicationStarter({
Deploy to Railway
</Button>

<Button
size="sm"
type="button"
onClick={() => {
void openDeployDialog('vercel')
}}
disabled={!canUseFinalActions}
className="border-black bg-black text-white hover:bg-gray-800 dark:border-white dark:bg-white dark:text-black dark:hover:bg-gray-100"
>
<Rocket className="h-4 w-4" />
Deploy to Vercel
</Button>

{!showMoreActions ? (
<Button
variant="secondary"
Expand Down
6 changes: 5 additions & 1 deletion src/components/application-builder/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
} from '~/utils/application-starter'

export type StarterTone = 'cyan' | 'emerald' | 'violet'
export type StarterDeployProvider = 'cloudflare' | 'netlify' | 'railway'
export type StarterDeployProvider =
| 'cloudflare'
| 'netlify'
| 'railway'
| 'vercel'
export type StarterPackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
export type StarterToolchain = 'biome' | 'eslint'

Expand Down
8 changes: 7 additions & 1 deletion src/components/deploy/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Common types, constants, and validation for deploy dialogs.
*/

export type DeployProvider = 'cloudflare' | 'netlify' | 'railway'
export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' | 'vercel'

export type DeployState =
| { step: 'auth-check' }
Expand Down Expand Up @@ -46,6 +46,12 @@ export const PROVIDER_INFO: Record<DeployProvider, ProviderInfo> = {
deployUrl: () =>
`https://railway.com/new/github?utm_medium=sponsor&utm_source=oss&utm_campaign=tanstack`,
},
vercel: {
name: 'Vercel',
color: '#000000',
deployUrl: (owner, repo) =>
`https://vercel.com/new/clone?repository-url=https://github.com/${owner}/${repo}`,
},
}

export async function checkRepoNameAvailability(
Expand Down
56 changes: 55 additions & 1 deletion src/utils/provider-config.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* This enables 1-click deploys to Cloudflare, Netlify, Railway, etc.
*/

export type DeployProvider = 'cloudflare' | 'netlify' | 'railway'
export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' | 'vercel'

interface ProviderConfigResult {
files: Record<string, string>
Expand Down Expand Up @@ -47,6 +47,8 @@ export function getProviderConfig(
return getNetlifyConfig()
case 'railway':
return getRailwayConfig()
case 'vercel':
return getVercelConfig()
default:
return { files: {}, devDependencies: {} }
}
Expand Down Expand Up @@ -112,6 +114,18 @@ function getRailwayConfig(): ProviderConfigResult {
}
}

/**
* Vercel configuration (uses Nitro)
*/
function getVercelConfig(): ProviderConfigResult {
return {
files: {},
devDependencies: {
nitro: 'latest',
},
}
}

/**
* Sanitize project name for use in configs (lowercase, hyphens only)
*/
Expand Down Expand Up @@ -238,6 +252,23 @@ cmd = "npx serve dist -s -l 3000"
}
}

case 'vercel': {
// Vercel auto-detects Vite SPA builds via the dist/ output directory
// and serves index.html for unmatched routes when configured below.
const vercelJson = `{
"$schema": "https://openapi.vercel.sh/vercel.json",
"buildCommand": "npm run build",
"outputDirectory": "dist",
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
`
return {
files: {
'vercel.json': vercelJson,
},
}
}

default:
return { files: {} }
}
Expand Down Expand Up @@ -292,6 +323,12 @@ function updatePackageJson(
pkg.scripts.build = 'vite build'
pkg.scripts.start = 'node .output/server/index.mjs'
break

case 'vercel':
// Vercel auto-detects the build command. Nitro's Vercel preset
// outputs the function bundle Vercel expects.
pkg.scripts.build = pkg.scripts.build ?? 'vite build'
break
}

return JSON.stringify(pkg, null, 2)
Expand Down Expand Up @@ -361,6 +398,22 @@ function updateViteConfig(content: string, provider: DeployProvider): string {
result = addPluginToConfig(result, 'nitro()')
break
}

case 'vercel': {
// Add nitro import if not present
if (!result.includes('nitro/vite')) {
const lastImportIndex = findLastImportIndex(result)
const importStatement = `import { nitro } from 'nitro/vite'\n`
result =
result.slice(0, lastImportIndex) +
importStatement +
result.slice(lastImportIndex)
}

// Add nitro() to plugins array
result = addPluginToConfig(result, 'nitro()')
break
}
}

return result
Expand Down Expand Up @@ -414,6 +467,7 @@ export function generateExampleDescription(
cloudflare: 'Cloudflare',
netlify: 'Netlify',
railway: 'Railway',
vercel: 'Vercel',
}

return `${libraryName} example: ${exampleName} (configured for ${providerNames[provider]})`
Expand Down