Skip to content

Commit 9ce36dc

Browse files
committed
feat(builder): add Vercel as a deploy provider
TanStack Start has first-party support on Vercel via the Nitro Vite plugin, so add Vercel alongside Cloudflare, Netlify, and Railway in the builder's deploy actions. Mirrors the Railway/Nitro path with one deviation: pins the nitro dep to the stable 'latest' tag (currently a beta) instead of nitro-nightly, because today's nightly hits a 508 INFINITE_LOOP_DETECTED on every request when deployed to Vercel. Verified end-to-end with a real TanStack Start app deployed via this exact config. Refs: https://vercel.com/docs/frameworks/full-stack/tanstack-start
1 parent 1cc3b6c commit 9ce36dc

4 files changed

Lines changed: 82 additions & 3 deletions

File tree

src/components/ApplicationStarter.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,19 @@ export function ApplicationStarter({
832832
Deploy to Railway
833833
</Button>
834834

835+
<Button
836+
size="sm"
837+
type="button"
838+
onClick={() => {
839+
void openDeployDialog('vercel')
840+
}}
841+
disabled={!canUseFinalActions}
842+
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"
843+
>
844+
<Rocket className="h-4 w-4" />
845+
Deploy to Vercel
846+
</Button>
847+
835848
{!showMoreActions ? (
836849
<Button
837850
variant="secondary"

src/components/application-builder/shared.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
} from '~/utils/application-starter'
1010

1111
export type StarterTone = 'cyan' | 'emerald' | 'violet'
12-
export type StarterDeployProvider = 'cloudflare' | 'netlify' | 'railway'
12+
export type StarterDeployProvider =
13+
| 'cloudflare'
14+
| 'netlify'
15+
| 'railway'
16+
| 'vercel'
1317
export type StarterPackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
1418
export type StarterToolchain = 'biome' | 'eslint'
1519

src/components/deploy/shared.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Common types, constants, and validation for deploy dialogs.
55
*/
66

7-
export type DeployProvider = 'cloudflare' | 'netlify' | 'railway'
7+
export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' | 'vercel'
88

99
export type DeployState =
1010
| { step: 'auth-check' }
@@ -46,6 +46,12 @@ export const PROVIDER_INFO: Record<DeployProvider, ProviderInfo> = {
4646
deployUrl: () =>
4747
`https://railway.com/new/github?utm_medium=sponsor&utm_source=oss&utm_campaign=tanstack`,
4848
},
49+
vercel: {
50+
name: 'Vercel',
51+
color: '#000000',
52+
deployUrl: (owner, repo) =>
53+
`https://vercel.com/new/clone?repository-url=https://github.com/${owner}/${repo}`,
54+
},
4955
}
5056

5157
export async function checkRepoNameAvailability(

src/utils/provider-config.server.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* This enables 1-click deploys to Cloudflare, Netlify, Railway, etc.
66
*/
77

8-
export type DeployProvider = 'cloudflare' | 'netlify' | 'railway'
8+
export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' | 'vercel'
99

1010
interface ProviderConfigResult {
1111
files: Record<string, string>
@@ -47,6 +47,8 @@ export function getProviderConfig(
4747
return getNetlifyConfig()
4848
case 'railway':
4949
return getRailwayConfig()
50+
case 'vercel':
51+
return getVercelConfig()
5052
default:
5153
return { files: {}, devDependencies: {} }
5254
}
@@ -112,6 +114,20 @@ function getRailwayConfig(): ProviderConfigResult {
112114
}
113115
}
114116

117+
/**
118+
* Vercel configuration (uses Nitro)
119+
*/
120+
function getVercelConfig(): ProviderConfigResult {
121+
// Pinned to stable `nitro` (not nitro-nightly) — nightly currently
122+
// 508 loops on Vercel.
123+
return {
124+
files: {},
125+
devDependencies: {
126+
nitro: 'latest',
127+
},
128+
}
129+
}
130+
115131
/**
116132
* Sanitize project name for use in configs (lowercase, hyphens only)
117133
*/
@@ -238,6 +254,23 @@ cmd = "npx serve dist -s -l 3000"
238254
}
239255
}
240256

257+
case 'vercel': {
258+
// Vercel auto-detects Vite SPA builds via the dist/ output directory
259+
// and serves index.html for unmatched routes when configured below.
260+
const vercelJson = `{
261+
"$schema": "https://openapi.vercel.sh/vercel.json",
262+
"buildCommand": "npm run build",
263+
"outputDirectory": "dist",
264+
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
265+
}
266+
`
267+
return {
268+
files: {
269+
'vercel.json': vercelJson,
270+
},
271+
}
272+
}
273+
241274
default:
242275
return { files: {} }
243276
}
@@ -292,6 +325,12 @@ function updatePackageJson(
292325
pkg.scripts.build = 'vite build'
293326
pkg.scripts.start = 'node .output/server/index.mjs'
294327
break
328+
329+
case 'vercel':
330+
// Vercel auto-detects the build command. Nitro's Vercel preset
331+
// outputs the function bundle Vercel expects.
332+
pkg.scripts.build = pkg.scripts.build ?? 'vite build'
333+
break
295334
}
296335

297336
return JSON.stringify(pkg, null, 2)
@@ -361,6 +400,22 @@ function updateViteConfig(content: string, provider: DeployProvider): string {
361400
result = addPluginToConfig(result, 'nitro()')
362401
break
363402
}
403+
404+
case 'vercel': {
405+
// Add nitro import if not present
406+
if (!result.includes('nitro/vite')) {
407+
const lastImportIndex = findLastImportIndex(result)
408+
const importStatement = `import { nitro } from 'nitro/vite'\n`
409+
result =
410+
result.slice(0, lastImportIndex) +
411+
importStatement +
412+
result.slice(lastImportIndex)
413+
}
414+
415+
// Add nitro() to plugins array
416+
result = addPluginToConfig(result, 'nitro()')
417+
break
418+
}
364419
}
365420

366421
return result
@@ -414,6 +469,7 @@ export function generateExampleDescription(
414469
cloudflare: 'Cloudflare',
415470
netlify: 'Netlify',
416471
railway: 'Railway',
472+
vercel: 'Vercel',
417473
}
418474

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

0 commit comments

Comments
 (0)