Skip to content

Commit 26a586d

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 26a586d

4 files changed

Lines changed: 80 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: 55 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,18 @@ function getRailwayConfig(): ProviderConfigResult {
112114
}
113115
}
114116

117+
/**
118+
* Vercel configuration (uses Nitro)
119+
*/
120+
function getVercelConfig(): ProviderConfigResult {
121+
return {
122+
files: {},
123+
devDependencies: {
124+
nitro: 'latest',
125+
},
126+
}
127+
}
128+
115129
/**
116130
* Sanitize project name for use in configs (lowercase, hyphens only)
117131
*/
@@ -238,6 +252,23 @@ cmd = "npx serve dist -s -l 3000"
238252
}
239253
}
240254

255+
case 'vercel': {
256+
// Vercel auto-detects Vite SPA builds via the dist/ output directory
257+
// and serves index.html for unmatched routes when configured below.
258+
const vercelJson = `{
259+
"$schema": "https://openapi.vercel.sh/vercel.json",
260+
"buildCommand": "npm run build",
261+
"outputDirectory": "dist",
262+
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
263+
}
264+
`
265+
return {
266+
files: {
267+
'vercel.json': vercelJson,
268+
},
269+
}
270+
}
271+
241272
default:
242273
return { files: {} }
243274
}
@@ -292,6 +323,12 @@ function updatePackageJson(
292323
pkg.scripts.build = 'vite build'
293324
pkg.scripts.start = 'node .output/server/index.mjs'
294325
break
326+
327+
case 'vercel':
328+
// Vercel auto-detects the build command. Nitro's Vercel preset
329+
// outputs the function bundle Vercel expects.
330+
pkg.scripts.build = pkg.scripts.build ?? 'vite build'
331+
break
295332
}
296333

297334
return JSON.stringify(pkg, null, 2)
@@ -361,6 +398,22 @@ function updateViteConfig(content: string, provider: DeployProvider): string {
361398
result = addPluginToConfig(result, 'nitro()')
362399
break
363400
}
401+
402+
case 'vercel': {
403+
// Add nitro import if not present
404+
if (!result.includes('nitro/vite')) {
405+
const lastImportIndex = findLastImportIndex(result)
406+
const importStatement = `import { nitro } from 'nitro/vite'\n`
407+
result =
408+
result.slice(0, lastImportIndex) +
409+
importStatement +
410+
result.slice(lastImportIndex)
411+
}
412+
413+
// Add nitro() to plugins array
414+
result = addPluginToConfig(result, 'nitro()')
415+
break
416+
}
364417
}
365418

366419
return result
@@ -414,6 +467,7 @@ export function generateExampleDescription(
414467
cloudflare: 'Cloudflare',
415468
netlify: 'Netlify',
416469
railway: 'Railway',
470+
vercel: 'Vercel',
417471
}
418472

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

0 commit comments

Comments
 (0)