diff --git a/.changeset/render-vercel-deployments.md b/.changeset/render-vercel-deployments.md new file mode 100644 index 00000000..c1710c45 --- /dev/null +++ b/.changeset/render-vercel-deployments.md @@ -0,0 +1,9 @@ +--- +'@tanstack/cli': minor +'@tanstack/create': minor +--- + +Add Gold partner deployment targets for Render and Vercel to React and Solid +scaffolds. Render generates package-manager-aware Blueprint commands, while +Vercel generates explicit framework detection and Nitro Build Output API +support. diff --git a/packages/cli/skills/choose-ecosystem-integrations/references/deployment-targets.md b/packages/cli/skills/choose-ecosystem-integrations/references/deployment-targets.md index d92ac335..c293892a 100644 --- a/packages/cli/skills/choose-ecosystem-integrations/references/deployment-targets.md +++ b/packages/cli/skills/choose-ecosystem-integrations/references/deployment-targets.md @@ -7,6 +7,8 @@ Map deployment intent to one supported target and include it in scaffold command - `cloudflare` - `netlify` - `railway` +- `render` +- `vercel` - `nitro` ## Selection pattern diff --git a/packages/cli/skills/create-app-scaffold/references/deployment-providers.md b/packages/cli/skills/create-app-scaffold/references/deployment-providers.md index 2cc780da..f283df8e 100644 --- a/packages/cli/skills/create-app-scaffold/references/deployment-providers.md +++ b/packages/cli/skills/create-app-scaffold/references/deployment-providers.md @@ -7,6 +7,8 @@ Targets `@tanstack/cli` v0.61.0. - `cloudflare` - `netlify` - `railway` +- `render` +- `vercel` - `nitro` ## Usage @@ -14,6 +16,7 @@ Targets `@tanstack/cli` v0.61.0. ```bash npx @tanstack/cli create app --deployment cloudflare -y npx @tanstack/cli create app --deployment netlify -y +npx @tanstack/cli create app --deployment vercel -y ``` Deployment providers are exclusive; choose one per scaffold. diff --git a/packages/cli/tests/partner-placement.test.ts b/packages/cli/tests/partner-placement.test.ts index e48ae742..d7645f77 100644 --- a/packages/cli/tests/partner-placement.test.ts +++ b/packages/cli/tests/partner-placement.test.ts @@ -60,6 +60,8 @@ describe('partner placement', () => { addOn('netlify', { id: 'netlify', tier: 'gold' }), addOn('cloudflare', { id: 'cloudflare', tier: 'gold' }), addOn('railway', { id: 'railway', tier: 'gold' }), + addOn('render', { id: 'render', tier: 'gold' }), + addOn('vercel', { id: 'vercel', tier: 'gold' }), ], 'deployment', 'test', @@ -71,6 +73,6 @@ describe('partner placement', () => { .slice(1) .map((item) => item.id) .sort(), - ).toEqual(['netlify', 'railway']) + ).toEqual(['netlify', 'railway', 'render', 'vercel']) }) }) diff --git a/packages/create/scripts/generate-manifest.mjs b/packages/create/scripts/generate-manifest.mjs index fdba7e85..cdb61f56 100644 --- a/packages/create/scripts/generate-manifest.mjs +++ b/packages/create/scripts/generate-manifest.mjs @@ -472,7 +472,9 @@ function writeWorkerManifest(manifest) { writeGeneratedModule( join(workerOutputDir, 'catalog.ts'), - `export const manifestCatalog = ${JSON.stringify( + `import type { ManifestCatalog } from '../../manifest-types.js' + +export const manifestCatalog: ManifestCatalog = ${JSON.stringify( { frameworks: manifest.map(stripFrameworkForCatalog), }, @@ -491,16 +493,18 @@ function writeWorkerManifest(manifest) { writeGeneratedModule( join(workerOutputDir, 'frameworks', `${frameworkSegment}.ts`), - `${createTemplateRendererSource( - createTemplateRenderersForFrameworkBase(framework), - )}\n\nexport const framework = ${JSON.stringify( + `import type { WorkerFrameworkManifestModule } from '../../../manifest-types.js' + +${createTemplateRendererSource( + createTemplateRenderersForFrameworkBase(framework), +)}\n\nexport const framework = ${JSON.stringify( { id: framework.id, base: framework.base, }, null, 2, - )}\n`, + )} satisfies WorkerFrameworkManifestModule['framework']\n`, ) for (const addOn of framework.addOns) { @@ -516,9 +520,11 @@ function writeWorkerManifest(manifest) { 'add-ons', `${addOnSegment}.ts`, ), - `${createTemplateRendererSource( - createTemplateRenderersForAddOn(addOn), - )}\n\nexport const addOn = ${JSON.stringify(addOn, null, 2)}\n`, + `import type { AddOnCompiled } from '../../../../../types.js' + +${createTemplateRendererSource( + createTemplateRenderersForAddOn(addOn), +)}\n\nexport const addOn = ${JSON.stringify(addOn, null, 2)} satisfies AddOnCompiled\n`, ) } } diff --git a/packages/create/src/create-app.ts b/packages/create/src/create-app.ts index 7d841202..e72647a1 100644 --- a/packages/create/src/create-app.ts +++ b/packages/create/src/create-app.ts @@ -8,6 +8,7 @@ import { intentCommand, packageManagerInstall, translateExecuteCommand, + validatePackageManagerSupport, } from './package-manager.js' import { createPackageJSON } from './package-json.js' import { resolvePackageJSONLatest } from './npm-resolver.js' @@ -475,6 +476,7 @@ Please read the README.md file for information on ${readmeDescription}${errorSta } export async function createApp(environment: Environment, options: Options) { + validatePackageManagerSupport(options.packageManager, options.chosenAddOns) const effectiveOptions = stripExamplesFromOptions(options) environment.startRun() diff --git a/packages/create/src/edge-create-app.ts b/packages/create/src/edge-create-app.ts index 3f58b864..4586262a 100644 --- a/packages/create/src/edge-create-app.ts +++ b/packages/create/src/edge-create-app.ts @@ -12,6 +12,7 @@ import { intentCommand, packageManagerInstall, translateExecuteCommand, + validatePackageManagerSupport, } from './package-manager.js' import type { Environment, FileBundleHandler, Options } from './types.js' @@ -591,6 +592,7 @@ Please read the README.md file for information on ${readmeDescription}${errorSta } export async function createApp(environment: Environment, options: Options) { + validatePackageManagerSupport(options.packageManager, options.chosenAddOns) const effectiveOptions = stripExamplesFromOptions(options) environment.startRun() diff --git a/packages/create/src/frameworks/react/add-ons/sentry/package.json b/packages/create/src/frameworks/react/add-ons/sentry/package.json deleted file mode 100644 index 7e8ac49f..00000000 --- a/packages/create/src/frameworks/react/add-ons/sentry/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "scripts": { - "build": "vite build && cp instrument.server.mjs .output/server", - "dev": "dotenv -e .env.local -- sh -c \"NODE_OPTIONS='--import ./instrument.server.mjs' vite dev --port 3000\"", - "start": "node --import ./.output/server/instrument.server.mjs .output/server/index.mjs" - }, - "dependencies": { - "@sentry/tanstackstart-react": "^10.67.0", - "dotenv-cli": "^11.0.0" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "@sentry/cli" - ] - } -} diff --git a/packages/create/src/frameworks/react/add-ons/sentry/package.json.ejs b/packages/create/src/frameworks/react/add-ons/sentry/package.json.ejs new file mode 100644 index 00000000..2890c070 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/sentry/package.json.ejs @@ -0,0 +1,16 @@ +{ + "scripts": { + "build": "<%= addOnEnabled.vercel ? 'vite build' : 'vite build && cp instrument.server.mjs .output/server' %>", + "dev": "<%= addOnEnabled.vercel ? 'dotenv -e .env.local -- vite dev --port 3000' : 'dotenv -e .env.local -- sh -c \"NODE_OPTIONS=\'--import ./instrument.server.mjs\' vite dev --port 3000\"' %>", + "start": "<%= addOnEnabled.vercel ? 'node .output/server/index.mjs' : 'node --import ./.output/server/instrument.server.mjs .output/server/index.mjs' %>" + }, + "dependencies": { + "@sentry/tanstackstart-react": "^10.67.0", + "dotenv-cli": "^11.0.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@sentry/cli" + ] + } +} diff --git a/packages/create/src/frameworks/react/hosts/render/README.md b/packages/create/src/frameworks/react/hosts/render/README.md new file mode 100644 index 00000000..843337f3 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/render/README.md @@ -0,0 +1,15 @@ +## Deploy to Render + +This project includes a Render Blueprint: + +1. Push this repo to GitHub, GitLab, or Bitbucket +2. In Render, choose **New > Blueprint** and connect this repo +3. Render reads `render.yaml` and creates the web service +4. In the Render Dashboard, add any secrets from `.env.example` under **Environment** +5. Deploy + +The Nitro server handles SSR, server functions, API routes, and static assets. +Render provides `PORT` automatically; Nitro reads it at runtime. + +Need a database? Add one from the Render Dashboard, then connect it via +environment variables. See https://render.com/docs/databases for options. diff --git a/packages/create/src/frameworks/react/hosts/render/assets/render.yaml.ejs b/packages/create/src/frameworks/react/hosts/render/assets/render.yaml.ejs new file mode 100644 index 00000000..ab9325e0 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/render/assets/render.yaml.ejs @@ -0,0 +1,15 @@ +services: + - type: web + runtime: node + name: tanstack-start-app + buildCommand: <%= packageManager %> install && <%= getPackageManagerRunScript('build') %> + startCommand: <%= getPackageManagerRunScript('start') %> + envVars: +<% if (packageManager === 'bun') { -%> + - key: BUN_VERSION + value: '1.3.14' +<% } -%> + - key: NITRO_PRESET + value: render-com + - key: HOST + value: 0.0.0.0 diff --git a/packages/create/src/frameworks/react/hosts/render/info.json b/packages/create/src/frameworks/react/hosts/render/info.json new file mode 100644 index 00000000..e7cfabae --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/render/info.json @@ -0,0 +1,21 @@ +{ + "name": "Render", + "description": "Deploy to Render (full-stack cloud platform with GitHub integration).", + "link": "https://render.com/", + "phase": "add-on", + "modes": ["file-router"], + "type": "deployment", + "supportedPackageManagers": ["npm", "yarn", "pnpm", "bun"], + "category": "deploy", + "exclusive": ["deploy"], + "color": "#46E3B7", + "priority": 150, + "partner": { "id": "render", "tier": "gold" }, + "integrations": [ + { + "type": "vite-plugin", + "import": "import { nitro } from 'nitro/vite'", + "code": "nitro({ rollupConfig: { external: [/^@sentry\\//] } })" + } + ] +} diff --git a/packages/create/src/frameworks/react/hosts/render/package.json.ejs b/packages/create/src/frameworks/react/hosts/render/package.json.ejs new file mode 100644 index 00000000..909ab021 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/render/package.json.ejs @@ -0,0 +1,8 @@ +{ + "scripts": { + "start": "<%= addOnEnabled.sentry ? 'node --import ./.output/server/instrument.server.mjs .output/server/index.mjs' : 'node .output/server/index.mjs' %>" + }, + "dependencies": { + "nitro": "3.0.260610-beta" + } +} diff --git a/packages/create/src/frameworks/react/hosts/vercel/README.md b/packages/create/src/frameworks/react/hosts/vercel/README.md new file mode 100644 index 00000000..641cbfc3 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/README.md @@ -0,0 +1,13 @@ +## Deploy to Vercel + +1. Push this repo to GitHub, GitLab, or Bitbucket +2. In Vercel, choose **Add New > Project** and import the repo +3. Keep the detected TanStack Start framework settings +4. Add production values from `.env.example` under **Settings > Environment Variables** +5. Deploy + +Vercel runs the build script and deploys Nitro's output as Vercel Functions and +static assets. The included `vercel.json` makes framework detection explicit. + +Variables prefixed with `VITE_` are included in the browser bundle. Keep secrets +unprefixed so they remain server-only. diff --git a/packages/create/src/frameworks/react/hosts/vercel/assets/src/server.ts.ejs b/packages/create/src/frameworks/react/hosts/vercel/assets/src/server.ts.ejs new file mode 100644 index 00000000..f5ad5aef --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/assets/src/server.ts.ejs @@ -0,0 +1,12 @@ +<% if (!addOnEnabled.sentry) { ignoreFile(); return; } -%> +import '../instrument.server.mjs' +import { wrapFetchWithSentry } from '@sentry/tanstackstart-react' +import handler, { createServerEntry } from '@tanstack/react-start/server-entry' + +export default createServerEntry( + wrapFetchWithSentry({ + fetch(request: Request) { + return handler.fetch(request) + }, + }), +) diff --git a/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json b/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json new file mode 100644 index 00000000..935d5d78 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "tanstack-start" +} diff --git a/packages/create/src/frameworks/react/hosts/vercel/info.json b/packages/create/src/frameworks/react/hosts/vercel/info.json new file mode 100644 index 00000000..eca28cc0 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/info.json @@ -0,0 +1,21 @@ +{ + "name": "Vercel", + "description": "Deploy to Vercel (Functions, static assets, and Git previews).", + "link": "https://vercel.com/docs/frameworks/full-stack/tanstack-start", + "phase": "add-on", + "modes": ["file-router"], + "type": "deployment", + "supportedPackageManagers": ["npm", "yarn", "pnpm", "bun"], + "category": "deploy", + "exclusive": ["deploy"], + "color": "#000000", + "priority": 140, + "partner": { "id": "vercel", "tier": "gold" }, + "integrations": [ + { + "type": "vite-plugin", + "import": "import { nitro } from 'nitro/vite'", + "code": "nitro()" + } + ] +} diff --git a/packages/create/src/frameworks/react/hosts/vercel/package.json b/packages/create/src/frameworks/react/hosts/vercel/package.json new file mode 100644 index 00000000..56e871bc --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "nitro": "3.0.260610-beta" + } +} diff --git a/packages/create/src/frameworks/solid/hosts/render/README.md b/packages/create/src/frameworks/solid/hosts/render/README.md new file mode 100644 index 00000000..843337f3 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/render/README.md @@ -0,0 +1,15 @@ +## Deploy to Render + +This project includes a Render Blueprint: + +1. Push this repo to GitHub, GitLab, or Bitbucket +2. In Render, choose **New > Blueprint** and connect this repo +3. Render reads `render.yaml` and creates the web service +4. In the Render Dashboard, add any secrets from `.env.example` under **Environment** +5. Deploy + +The Nitro server handles SSR, server functions, API routes, and static assets. +Render provides `PORT` automatically; Nitro reads it at runtime. + +Need a database? Add one from the Render Dashboard, then connect it via +environment variables. See https://render.com/docs/databases for options. diff --git a/packages/create/src/frameworks/solid/hosts/render/assets/render.yaml.ejs b/packages/create/src/frameworks/solid/hosts/render/assets/render.yaml.ejs new file mode 100644 index 00000000..ab9325e0 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/render/assets/render.yaml.ejs @@ -0,0 +1,15 @@ +services: + - type: web + runtime: node + name: tanstack-start-app + buildCommand: <%= packageManager %> install && <%= getPackageManagerRunScript('build') %> + startCommand: <%= getPackageManagerRunScript('start') %> + envVars: +<% if (packageManager === 'bun') { -%> + - key: BUN_VERSION + value: '1.3.14' +<% } -%> + - key: NITRO_PRESET + value: render-com + - key: HOST + value: 0.0.0.0 diff --git a/packages/create/src/frameworks/solid/hosts/render/info.json b/packages/create/src/frameworks/solid/hosts/render/info.json new file mode 100644 index 00000000..33a2001e --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/render/info.json @@ -0,0 +1,21 @@ +{ + "name": "Render", + "description": "Deploy to Render (full-stack cloud platform with GitHub integration).", + "link": "https://render.com/", + "phase": "add-on", + "modes": ["file-router"], + "type": "deployment", + "supportedPackageManagers": ["npm", "yarn", "pnpm", "bun"], + "category": "deploy", + "exclusive": ["deploy"], + "color": "#46E3B7", + "priority": 150, + "partner": { "id": "render", "tier": "gold" }, + "integrations": [ + { + "type": "vite-plugin", + "import": "import { nitro } from 'nitro/vite'", + "code": "nitro()" + } + ] +} diff --git a/packages/create/src/frameworks/solid/hosts/render/package.json b/packages/create/src/frameworks/solid/hosts/render/package.json new file mode 100644 index 00000000..a3b22116 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/render/package.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "start": "node .output/server/index.mjs" + }, + "dependencies": { + "nitro": "3.0.260610-beta" + } +} diff --git a/packages/create/src/frameworks/solid/hosts/vercel/README.md b/packages/create/src/frameworks/solid/hosts/vercel/README.md new file mode 100644 index 00000000..641cbfc3 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/vercel/README.md @@ -0,0 +1,13 @@ +## Deploy to Vercel + +1. Push this repo to GitHub, GitLab, or Bitbucket +2. In Vercel, choose **Add New > Project** and import the repo +3. Keep the detected TanStack Start framework settings +4. Add production values from `.env.example` under **Settings > Environment Variables** +5. Deploy + +Vercel runs the build script and deploys Nitro's output as Vercel Functions and +static assets. The included `vercel.json` makes framework detection explicit. + +Variables prefixed with `VITE_` are included in the browser bundle. Keep secrets +unprefixed so they remain server-only. diff --git a/packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json b/packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json new file mode 100644 index 00000000..935d5d78 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "tanstack-start" +} diff --git a/packages/create/src/frameworks/solid/hosts/vercel/info.json b/packages/create/src/frameworks/solid/hosts/vercel/info.json new file mode 100644 index 00000000..eca28cc0 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/vercel/info.json @@ -0,0 +1,21 @@ +{ + "name": "Vercel", + "description": "Deploy to Vercel (Functions, static assets, and Git previews).", + "link": "https://vercel.com/docs/frameworks/full-stack/tanstack-start", + "phase": "add-on", + "modes": ["file-router"], + "type": "deployment", + "supportedPackageManagers": ["npm", "yarn", "pnpm", "bun"], + "category": "deploy", + "exclusive": ["deploy"], + "color": "#000000", + "priority": 140, + "partner": { "id": "vercel", "tier": "gold" }, + "integrations": [ + { + "type": "vite-plugin", + "import": "import { nitro } from 'nitro/vite'", + "code": "nitro()" + } + ] +} diff --git a/packages/create/src/frameworks/solid/hosts/vercel/package.json b/packages/create/src/frameworks/solid/hosts/vercel/package.json new file mode 100644 index 00000000..56e871bc --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/vercel/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "nitro": "3.0.260610-beta" + } +} diff --git a/packages/create/src/package-manager.ts b/packages/create/src/package-manager.ts index 01d3af8f..c4b9eb43 100644 --- a/packages/create/src/package-manager.ts +++ b/packages/create/src/package-manager.ts @@ -10,6 +10,27 @@ export const SUPPORTED_PACKAGE_MANAGERS = [ export type PackageManager = (typeof SUPPORTED_PACKAGE_MANAGERS)[number] export const DEFAULT_PACKAGE_MANAGER: PackageManager = 'npm' +export function validatePackageManagerSupport( + packageManager: PackageManager, + addOns: Array<{ + name: string + supportedPackageManagers?: Array + }>, +) { + const unsupported = addOns.find( + (addOn) => + addOn.supportedPackageManagers && + !addOn.supportedPackageManagers.includes(packageManager), + ) + + const supportedPackageManagers = unsupported?.supportedPackageManagers + if (!unsupported || !supportedPackageManagers) return + + throw new Error( + `${unsupported.name} does not support the ${packageManager} package manager. Choose ${supportedPackageManagers.join(', ')}.`, + ) +} + export function getPackageManager(): PackageManager | undefined { const userAgent = process.env.npm_config_user_agent diff --git a/packages/create/src/types.ts b/packages/create/src/types.ts index e6458165..27e1212f 100644 --- a/packages/create/src/types.ts +++ b/packages/create/src/types.ts @@ -39,6 +39,9 @@ export const AddOnBaseSchema = z.object({ warning: z.string().optional(), tailwind: z.boolean().optional(), type: z.enum(['add-on', 'example', 'starter', 'toolchain', 'deployment']), + supportedPackageManagers: z + .array(z.enum(['npm', 'yarn', 'pnpm', 'bun', 'deno'])) + .optional(), category: z .enum([ 'tanstack', diff --git a/packages/create/tests/partner-addons.test.ts b/packages/create/tests/partner-addons.test.ts index 9ec7f446..95a61534 100644 --- a/packages/create/tests/partner-addons.test.ts +++ b/packages/create/tests/partner-addons.test.ts @@ -1,11 +1,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { finalizeAddOns, populateAddOnOptionsDefaults } from '../src/add-ons.js' +import { + finalizeAddOns, + getAllAddOns, + populateAddOnOptionsDefaults, +} from '../src/add-ons.js' import { createApp } from '../src/create-app.js' import { createMemoryEnvironment } from '../src/environment.js' import { createFrameworkDefinition as createReactFrameworkDefinition } from '../src/frameworks/react/index.js' import { createFrameworkDefinition as createSolidFrameworkDefinition } from '../src/frameworks/solid/index.js' +import type { PackageManager } from '../src/package-manager.js' import type { Framework, FrameworkDefinition, Options } from '../src/types.js' function frameworkFromDefinition(definition: FrameworkDefinition): Framework { @@ -24,6 +29,7 @@ async function generateApp( definition: FrameworkDefinition, addOnIds: Array, addOnOptions: Options['addOnOptions'] = {}, + packageManager: PackageManager = 'pnpm', ) { const framework = frameworkFromDefinition(definition) const chosenAddOns = await finalizeAddOns(framework, 'file-router', addOnIds) @@ -38,7 +44,7 @@ async function generateApp( mode: 'file-router', typescript: true, tailwind: true, - packageManager: 'pnpm', + packageManager, git: false, install: false, intent: false, @@ -181,6 +187,160 @@ describe('partner add-on scaffolds', () => { }, ) + it.each([ + ['React', createReactFrameworkDefinition], + ['Solid', createSolidFrameworkDefinition], + ])( + '%s partner deployment hosts only support file-router', + (_name, createFrameworkDefinition) => { + const definition = createFrameworkDefinition() + const framework = frameworkFromDefinition(definition) + const fileRouterIds = getAllAddOns(framework, 'file-router').map( + (addOn) => addOn.id, + ) + const codeRouterIds = getAllAddOns(framework, 'code-router').map( + (addOn) => addOn.id, + ) + + for (const addOnId of ['render', 'vercel']) { + expect( + definition.addOns.find((addOn) => addOn.id === addOnId)?.modes, + ).toEqual(['file-router']) + expect(fileRouterIds).toContain(addOnId) + expect(codeRouterIds).not.toContain(addOnId) + } + }, + ) + + it('rejects Deno for Render', async () => { + await expect( + generateApp( + createReactFrameworkDefinition(), + ['render'], + {}, + 'deno', + ), + ).rejects.toThrow( + 'Render does not support the deno package manager. Choose npm, yarn, pnpm, bun.', + ) + }) + + it('rejects Deno for Vercel', async () => { + await expect( + generateApp( + createReactFrameworkDefinition(), + ['vercel'], + {}, + 'deno', + ), + ).rejects.toThrow( + 'Vercel does not support the deno package manager. Choose npm, yarn, pnpm, bun.', + ) + }) + + it.each([ + ['React', createReactFrameworkDefinition], + ['Solid', createSolidFrameworkDefinition], + ])( + 'generates a Render Blueprint for %s', + async (_name, createFrameworkDefinition) => { + const output = await generateApp(createFrameworkDefinition(), ['render']) + const bunOutput = await generateApp( + createFrameworkDefinition(), + ['render'], + {}, + 'bun', + ) + const packageJSON = JSON.parse(output.files['package.json']) + const renderYaml = output.files['render.yaml'] + const bunRenderYaml = bunOutput.files['render.yaml'] + const readme = output.files['README.md'] + + expect(packageJSON.dependencies).toHaveProperty( + 'nitro', + '3.0.260610-beta', + ) + expect(packageJSON.scripts.start).toBe('node .output/server/index.mjs') + expect(renderYaml).toContain('name: tanstack-start-app') + expect(renderYaml).toContain('buildCommand: pnpm install && pnpm build') + expect(renderYaml).toContain('startCommand: pnpm start') + expect(renderYaml).not.toContain('key: BUN_VERSION') + expect(bunRenderYaml).toContain('key: BUN_VERSION') + expect(bunRenderYaml).toContain("value: '1.3.14'") + expect(renderYaml).toContain('value: render-com') + expect(renderYaml).toContain('value: 0.0.0.0') + expect(output.files['render.yaml.ejs']).toBeUndefined() + expect(readme).toContain('New > Blueprint') + expect(readme).not.toContain('Deploy to Render button') + expect(readme).not.toContain('render.com/deploy') + }, + ) + + it.each([ + ['npm', 'npm install && npm run build', 'npm run start'], + ['yarn', 'yarn install && yarn run build', 'yarn run start'], + ['pnpm', 'pnpm install && pnpm build', 'pnpm start'], + ['bun', 'bun install && bun --bun run build', 'bun --bun run start'], + ] as const)( + 'uses %s commands in the Render Blueprint', + async (packageManager, buildCommand, startCommand) => { + const output = await generateApp( + createReactFrameworkDefinition(), + ['render'], + {}, + packageManager, + ) + const renderYaml = output.files['render.yaml'] + + expect(renderYaml).toContain(`buildCommand: ${buildCommand}`) + expect(renderYaml).toContain(`startCommand: ${startCommand}`) + if (packageManager === 'bun') { + expect(renderYaml).toContain('key: BUN_VERSION') + expect(renderYaml).toContain("value: '1.3.14'") + } else { + expect(renderYaml).not.toContain('key: BUN_VERSION') + } + }, + ) + + it.each([ + ['React', createReactFrameworkDefinition], + ['Solid', createSolidFrameworkDefinition], + ])( + 'generates a Vercel Build Output API deployment for %s', + async (_name, createFrameworkDefinition) => { + const definition = createFrameworkDefinition() + const vercel = definition.addOns.find((addOn) => addOn.id === 'vercel') + const output = await generateApp(definition, ['vercel']) + const packageJSON = JSON.parse(output.files['package.json']) + const vercelJSON = JSON.parse(output.files['vercel.json']) + const viteConfig = output.files['vite.config.ts'] + const readme = output.files['README.md'] + + expect(vercel?.partner).toEqual({ id: 'vercel', tier: 'gold' }) + expect(vercel?.supportedPackageManagers).toEqual([ + 'npm', + 'yarn', + 'pnpm', + 'bun', + ]) + expect(packageJSON.dependencies).toHaveProperty( + 'nitro', + '3.0.260610-beta', + ) + expect(vercelJSON).toEqual({ + $schema: 'https://openapi.vercel.sh/vercel.json', + framework: 'tanstack-start', + }) + expect(viteConfig).toContain("import { nitro } from 'nitro/vite'") + expect(viteConfig).toContain('nitro()') + expect(viteConfig).not.toContain('NITRO_PRESET') + expect(output.files['src/server.ts']).toBeUndefined() + expect(readme).toContain('Add New > Project') + expect(readme).not.toContain('vercel.com/new/clone') + }, + ) + it.each([ ['React', createReactFrameworkDefinition], ['Solid', createSolidFrameworkDefinition], @@ -314,4 +474,43 @@ describe('partner add-on scaffolds', () => { expect(output.files['nixpacks.toml']).toBeUndefined() }, ) + + it('starts Render through the Sentry-aware package script', async () => { + const output = await generateApp(createReactFrameworkDefinition(), [ + 'sentry', + 'render', + ]) + const packageJSON = JSON.parse(output.files['package.json']) + + expect(packageJSON.scripts.start).toContain( + '--import ./.output/server/instrument.server.mjs', + ) + expect(output.files['render.yaml']).toContain('startCommand: pnpm start') + }) + + it.each([[['sentry', 'vercel']], [['vercel', 'sentry']]])( + 'composes Sentry with Vercel independent of selection order: %j', + async (addOnIds) => { + const output = await generateApp( + createReactFrameworkDefinition(), + addOnIds, + ) + const packageJSON = JSON.parse(output.files['package.json']) + const serverEntry = output.files['src/server.ts'] + const viteConfig = output.files['vite.config.ts'] + + expect(packageJSON.scripts.build).toBe('vite build') + expect(packageJSON.scripts.build).not.toContain('.output/server') + expect(packageJSON.scripts.dev).toBe( + 'dotenv -e .env.local -- vite dev --port 3000', + ) + expect(packageJSON.scripts.dev).not.toContain('NODE_OPTIONS') + expect(packageJSON.scripts.start).toBe('node .output/server/index.mjs') + expect(serverEntry).toContain("import '../instrument.server.mjs'") + expect(serverEntry).toContain('wrapFetchWithSentry') + expect(serverEntry).toContain('createServerEntry') + expect(viteConfig).not.toContain('rollupConfig') + expect(viteConfig).not.toContain('external:') + }, + ) })