From 06263cc630f545c9b3746bec219a6970f5f575a2 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Tue, 10 Nov 2026 17:29:45 +0800 Subject: [PATCH 1/7] feat: support Rsbuild deployments with Nitro --- .changeset/rsbuild-nitro-deployments.md | 6 + ARCHITECTURE.md | 1 + docs/cli-reference.md | 7 + .../references/bundlers.md | 17 +- .../references/deployment-providers.md | 7 + packages/cli/src/command-line.ts | 13 +- packages/cli/src/options.ts | 56 +++-- packages/cli/src/ui-prompts.ts | 4 +- packages/cli/tests-e2e/rsbuild-smoke.spec.ts | 31 ++- packages/cli/tests/command-line.test.ts | 19 +- packages/cli/tests/options.test.ts | 106 +++++++-- packages/create/src/build-tools.ts | 27 ++- packages/create/src/edge-package-json.ts | 27 ++- .../react/hosts/cloudflare/README.md | 11 - .../react/hosts/cloudflare/README.md.ejs | 24 ++ .../cloudflare/assets/wrangler.jsonc.ejs | 3 +- .../react/hosts/cloudflare/info.json | 10 +- .../react/hosts/cloudflare/package.json.ejs | 20 ++ .../netlify/{README.md => README.md.ejs} | 8 + .../react/hosts/netlify/assets/netlify.toml | 7 - .../hosts/netlify/assets/netlify.toml.ejs | 7 + .../frameworks/react/hosts/netlify/info.json | 10 +- .../react/hosts/netlify/package.json | 10 - .../react/hosts/netlify/package.json.ejs | 21 ++ .../frameworks/react/hosts/nitro/info.json | 5 + .../frameworks/react/hosts/nitro/package.json | 5 - .../react/hosts/nitro/package.json.ejs | 5 + .../frameworks/react/hosts/railway/info.json | 10 +- .../react/hosts/railway/package.json.ejs | 2 +- .../frameworks/react/hosts/render/info.json | 10 +- .../react/hosts/render/package.json.ejs | 2 +- .../frameworks/react/hosts/vercel/README.md | 13 -- .../react/hosts/vercel/README.md.ejs | 23 ++ .../react/hosts/vercel/assets/vercel.json | 4 - .../react/hosts/vercel/assets/vercel.json.ejs | 5 + .../frameworks/react/hosts/vercel/info.json | 10 +- .../react/hosts/vercel/package.json | 5 - .../react/hosts/vercel/package.json.ejs | 8 + .../react/project/base/rsbuild.config.ts.ejs | 8 +- .../frameworks/react/project/packages.json | 13 ++ .../solid/hosts/cloudflare/README.md | 11 - .../solid/hosts/cloudflare/README.md.ejs | 24 ++ .../{wrangler.jsonc => wrangler.jsonc.ejs} | 3 +- .../solid/hosts/cloudflare/info.json | 10 +- .../solid/hosts/cloudflare/package.json.ejs | 20 ++ .../netlify/{README.md => README.md.ejs} | 8 + .../solid/hosts/netlify/assets/netlify.toml | 7 - .../hosts/netlify/assets/netlify.toml.ejs | 7 + .../frameworks/solid/hosts/netlify/info.json | 10 +- .../solid/hosts/netlify/package.json | 10 - .../solid/hosts/netlify/package.json.ejs | 21 ++ .../frameworks/solid/hosts/nitro/info.json | 5 + .../frameworks/solid/hosts/nitro/package.json | 5 - .../solid/hosts/nitro/package.json.ejs | 5 + .../frameworks/solid/hosts/railway/info.json | 10 +- .../package.json => railway/package.json.ejs} | 2 +- .../frameworks/solid/hosts/render/info.json | 10 +- .../package.json => render/package.json.ejs} | 2 +- .../frameworks/solid/hosts/vercel/README.md | 13 -- .../solid/hosts/vercel/README.md.ejs | 23 ++ .../solid/hosts/vercel/assets/vercel.json | 4 - .../solid/hosts/vercel/assets/vercel.json.ejs | 5 + .../frameworks/solid/hosts/vercel/info.json | 10 +- .../solid/hosts/vercel/package.json | 5 - .../solid/hosts/vercel/package.json.ejs | 8 + .../solid/project/base/rsbuild.config.ts.ejs | 8 +- .../frameworks/solid/project/packages.json | 13 ++ packages/create/src/package-json.ts | 20 +- packages/create/tests/bundlers.test.ts | 13 +- .../create/tests/deployment-bundlers.test.ts | 210 ++++++++++++++++++ packages/create/tests/edge-manifest.test.ts | 7 +- 71 files changed, 859 insertions(+), 230 deletions(-) create mode 100644 .changeset/rsbuild-nitro-deployments.md delete mode 100644 packages/create/src/frameworks/react/hosts/cloudflare/README.md create mode 100644 packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs rename packages/create/src/frameworks/react/hosts/netlify/{README.md => README.md.ejs} (59%) delete mode 100644 packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml create mode 100644 packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml.ejs delete mode 100644 packages/create/src/frameworks/react/hosts/netlify/package.json create mode 100644 packages/create/src/frameworks/react/hosts/netlify/package.json.ejs delete mode 100644 packages/create/src/frameworks/react/hosts/nitro/package.json create mode 100644 packages/create/src/frameworks/react/hosts/nitro/package.json.ejs delete mode 100644 packages/create/src/frameworks/react/hosts/vercel/README.md create mode 100644 packages/create/src/frameworks/react/hosts/vercel/README.md.ejs delete mode 100644 packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json create mode 100644 packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json.ejs delete mode 100644 packages/create/src/frameworks/react/hosts/vercel/package.json create mode 100644 packages/create/src/frameworks/react/hosts/vercel/package.json.ejs delete mode 100644 packages/create/src/frameworks/solid/hosts/cloudflare/README.md create mode 100644 packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs rename packages/create/src/frameworks/solid/hosts/cloudflare/assets/{wrangler.jsonc => wrangler.jsonc.ejs} (81%) rename packages/create/src/frameworks/solid/hosts/netlify/{README.md => README.md.ejs} (59%) delete mode 100644 packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml create mode 100644 packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml.ejs delete mode 100644 packages/create/src/frameworks/solid/hosts/netlify/package.json create mode 100644 packages/create/src/frameworks/solid/hosts/netlify/package.json.ejs delete mode 100644 packages/create/src/frameworks/solid/hosts/nitro/package.json create mode 100644 packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs rename packages/create/src/frameworks/solid/hosts/{render/package.json => railway/package.json.ejs} (53%) rename packages/create/src/frameworks/solid/hosts/{railway/package.json => render/package.json.ejs} (53%) delete mode 100644 packages/create/src/frameworks/solid/hosts/vercel/README.md create mode 100644 packages/create/src/frameworks/solid/hosts/vercel/README.md.ejs delete mode 100644 packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json create mode 100644 packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json.ejs delete mode 100644 packages/create/src/frameworks/solid/hosts/vercel/package.json create mode 100644 packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs create mode 100644 packages/create/tests/deployment-bundlers.test.ts diff --git a/.changeset/rsbuild-nitro-deployments.md b/.changeset/rsbuild-nitro-deployments.md new file mode 100644 index 000000000..babb89ce6 --- /dev/null +++ b/.changeset/rsbuild-nitro-deployments.md @@ -0,0 +1,6 @@ +--- +'@tanstack/create': minor +'@tanstack/cli': minor +--- + +Support Nitro, Railway, Render, Vercel, Netlify, and Cloudflare deployment adapters in React and Solid Rsbuild Start projects using Nitro's experimental Rsbuild integration. Deployment projects require Node.js >=22.22 and Rsbuild >=2.3. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 06e85322e..459dde8a7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -117,6 +117,7 @@ Templates (`.ejs` files) have access to: | `provider` | Wraps app in `__root.tsx` | Basic context providers | | `root-provider` | Wraps app, exports context | Providers with shared state | | `vite-plugin` | `vite.config.ts` | Vite plugins | +| `rsbuild-plugin` | `rsbuild.config.ts` | Rsbuild deployment adapter plugins | | `devtools` | After app in `__root.tsx` | Developer tools | | `header-user` | Header component | Auth UI, user menus | | `layout` | Layout wrapper | Dashboard layouts | diff --git a/docs/cli-reference.md b/docs/cli-reference.md index fdc1a3ae1..1f0596a88 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -43,6 +43,7 @@ tanstack create my-app --blank --deployment cloudflare -y tanstack create my-app --add-ons clerk,drizzle,tanstack-query tanstack create my-app --router-only --toolchain eslint --no-examples tanstack create my-app --build-tool rsbuild -y +tanstack create my-app --build-tool rsbuild --deployment nitro -y tanstack create my-app --framework Solid --build-tool rsbuild --router-only --blank --toolchain biome -y tanstack create my-app --template https://example.com/template.json tanstack create my-app --template ecommerce @@ -58,6 +59,12 @@ Explicit add-ons and deployment adapters can add their own required files and dependencies. Add `-y` to use defaults for every remaining option; when the target is non-empty, also pass `--force` or the command exits without writing. +Rsbuild Start projects support all six deployment adapters through Nitro's +experimental Rsbuild integration: `nitro`, `railway`, `render`, `vercel`, +`netlify`, and `cloudflare`. Generated deployment projects require Node.js +>=22.22 and Rsbuild >=2.3. Router-only projects do not support deployment +adapters. + ### Programmatic generation Use `@tanstack/create/worker` in Cloudflare Workers and other edge SSR runtimes. It does not import the generated template manifest at module startup. Instead, provide a loader for the framework and add-on chunks your Worker supports. diff --git a/packages/cli/skills/create-app-scaffold/references/bundlers.md b/packages/cli/skills/create-app-scaffold/references/bundlers.md index 15bbedb73..ffc2c187d 100644 --- a/packages/cli/skills/create-app-scaffold/references/bundlers.md +++ b/packages/cli/skills/create-app-scaffold/references/bundlers.md @@ -15,10 +15,19 @@ npx @tanstack/cli create app --framework solid --build-tool rsbuild -y ``` Rsbuild supports full TanStack Start and router-only projects, including the -standard and `--blank` presets, built-in example pages, Tailwind, ESLint, and -Biome. It does not currently support templates, deployment adapters, or -catalog business/example add-ons. `tanstack add` is limited to ESLint/Biome in -Rsbuild projects. +standard and `--blank` presets, built-in example pages, Tailwind, ESLint, +Biome, and Prettier. Templates and catalog business/example add-ons are not +supported. + +Full Start projects support `--deployment nitro`, `railway`, `render`, +`vercel`, `netlify`, and `cloudflare` through the experimental `nitro/rsbuild` +integration. These projects require Node.js >=22.22 and Rsbuild >=2.3. +Router-only projects do not support deployment adapters. Existing Rsbuild +Start projects can add one deployment adapter with `tanstack add`. + +```bash +npx @tanstack/cli create app --build-tool rsbuild --deployment nitro -y +``` Use the build-tool-aware catalog view before constructing an Rsbuild command: 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 f283df8e9..53fd8d925 100644 --- a/packages/cli/skills/create-app-scaffold/references/deployment-providers.md +++ b/packages/cli/skills/create-app-scaffold/references/deployment-providers.md @@ -20,3 +20,10 @@ npx @tanstack/cli create app --deployment vercel -y ``` Deployment providers are exclusive; choose one per scaffold. + +## Rsbuild + +The same deployment ids are available for full TanStack Start projects with +`--build-tool rsbuild`. They use Nitro's experimental Rsbuild integration +and require Node.js >=22.22 and Rsbuild >=2.3. Router-only projects do not +support deployment adapters. diff --git a/packages/cli/src/command-line.ts b/packages/cli/src/command-line.ts index a61c75872..7f5ac9593 100644 --- a/packages/cli/src/command-line.ts +++ b/packages/cli/src/command-line.ts @@ -337,17 +337,11 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): { 'Rsbuild does not currently support --starter, --template, or --template-id.', } } - if (cliOptions.deployment) { - return { - warnings, - error: 'Rsbuild does not currently support --deployment.', - } - } if (cliOptions.addOns) { return { warnings, error: - 'Rsbuild currently supports toolchains only. Use --toolchain eslint or --toolchain biome.', + 'Rsbuild does not support --add-ons. Use --toolchain or --deployment.', } } } @@ -524,12 +518,9 @@ export async function normalizeOptions( ) { throw new Error('Rsbuild does not currently support templates.') } - if (cliOptions.deployment || opts?.forcedDeployment) { - throw new Error('Rsbuild does not currently support deployments.') - } if (forcedAddOns?.length || cliOptions.addOns) { throw new Error( - 'Rsbuild currently supports toolchains only. Use --toolchain eslint or --toolchain biome.', + 'Rsbuild does not support --add-ons. Use --toolchain or --deployment.', ) } } diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index a12a2428b..223b7352d 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -89,9 +89,6 @@ export async function promptForCreateOptions( if (forcedAddOns.length > 0) { throw new Error('Rsbuild does not currently support forced add-ons.') } - if (forcedDeployment) { - throw new Error('Rsbuild does not currently support deployments.') - } } const projectLocation = resolveProjectLocation({ @@ -178,7 +175,7 @@ export async function promptForCreateOptions( // Deployment selection let deployment: string | undefined - if (routerOnly || options.bundler === 'rsbuild') { + if (routerOnly) { deployment = undefined } else if (cliOptions.deployment) { deployment = cliOptions.deployment @@ -189,6 +186,7 @@ export async function promptForCreateOptions( options.framework, cliOptions.deployment, forcedDeployment, + options.bundler, ) } else { deployment = forcedDeployment @@ -319,18 +317,24 @@ export async function promptForAddOns(): Promise> { const bundler = resolveBundler(framework, config.bundler).id if (bundler === 'rsbuild') { - const toolchains = getAllAddOns(framework, config.mode!, bundler).filter( - (addOn) => addOn.type === 'toolchain', + const supportedAddOns = getAllAddOns( + framework, + config.mode!, + bundler, + ).filter( + (addOn) => + addOn.type === 'toolchain' || + (!config.routerOnly && addOn.type === 'deployment'), ) const configuredAddOnIds = new Set( config.chosenAddOns.map((addOn) => addOn.toLowerCase()), ) const configuredExclusiveGroups = new Set( - toolchains + supportedAddOns .filter((addOn) => configuredAddOnIds.has(addOn.id.toLowerCase())) .flatMap((addOn) => addOn.exclusive ?? []), ) - const unavailableToolchains = toolchains + const unavailableAddOns = supportedAddOns .filter( (addOn) => configuredAddOnIds.has(addOn.id.toLowerCase()) || @@ -340,20 +344,28 @@ export async function promptForAddOns(): Promise> { ) .map((addOn) => addOn.id) - if (unavailableToolchains.length === toolchains.length) { - return [] - } - - for (const addOn of await selectAddOns( - framework, - config.mode!, - 'toolchain', - 'Which toolchain would you like to add?', - unavailableToolchains, - false, - bundler, - )) { - addOns.add(addOn) + for (const type of ['toolchain', 'deployment']) { + if ( + !supportedAddOns.some( + (addOn) => + addOn.type === type && !unavailableAddOns.includes(addOn.id), + ) + ) { + continue + } + for (const addOn of await selectAddOns( + framework, + config.mode!, + type, + type === 'toolchain' + ? 'Which toolchain would you like to add?' + : 'Which deployment adapter would you like to add?', + unavailableAddOns, + false, + bundler, + )) { + addOns.add(addOn) + } } return Array.from(addOns) } diff --git a/packages/cli/src/ui-prompts.ts b/packages/cli/src/ui-prompts.ts index 95857fd93..ffed0753a 100644 --- a/packages/cli/src/ui-prompts.ts +++ b/packages/cli/src/ui-prompts.ts @@ -415,12 +415,12 @@ export async function selectDeployment( framework: Framework, deployment?: string, forcedDeployment?: string, + bundlerId?: string, ): Promise { const deployments = new Set() let initialValue: string | undefined = undefined for (const addOn of orderAddOnsForPartnerPlacement( - framework - .getAddOns() + getAllAddOns(framework, 'file-router', bundlerId) .filter((addOn) => addOn.type === 'deployment') .sort((a, b) => a.name.localeCompare(b.name)), 'deployment', diff --git a/packages/cli/tests-e2e/rsbuild-smoke.spec.ts b/packages/cli/tests-e2e/rsbuild-smoke.spec.ts index bc983a0f1..891375a71 100644 --- a/packages/cli/tests-e2e/rsbuild-smoke.spec.ts +++ b/packages/cli/tests-e2e/rsbuild-smoke.spec.ts @@ -15,14 +15,21 @@ import { const execFileAsync = promisify(execFile) -const scenarios = [ +const scenarios: Array<{ + framework: 'react' | 'solid' + routerOnly: boolean + blank: boolean + deployment?: string +}> = [ { framework: 'react', routerOnly: false, blank: false }, { framework: 'solid', routerOnly: false, blank: false }, { framework: 'react', routerOnly: true, blank: true }, { framework: 'solid', routerOnly: true, blank: true }, -] as const + { framework: 'react', routerOnly: false, blank: false, deployment: 'nitro' }, + { framework: 'solid', routerOnly: false, blank: false, deployment: 'nitro' }, +] -test('@matrix lists only toolchains for Rsbuild', async () => { +test('@matrix lists toolchains and deployment adapters for Rsbuild', async () => { const { stdout } = await execFileAsync( process.execPath, [ @@ -40,10 +47,13 @@ test('@matrix lists only toolchains for Rsbuild', async () => { const addOns = JSON.parse(stdout) as Array<{ type: string }> expect(addOns.length).toBeGreaterThan(0) - expect(addOns.every((addOn) => addOn.type === 'toolchain')).toBe(true) + expect( + addOns.every((addOn) => ['toolchain', 'deployment'].includes(addOn.type)), + ).toBe(true) + expect(addOns.filter((addOn) => addOn.type === 'deployment')).toHaveLength(6) }) -test('@matrix rejects an Rsbuild deployment before creating a directory', async () => { +test('@matrix rejects an Rsbuild business add-on before creating a directory', async () => { const rootDir = await mkdtemp(join(tmpdir(), 'tanstack-rsbuild-conflict-')) const appName = 'invalid-rsbuild-app' @@ -58,8 +68,8 @@ test('@matrix rejects an Rsbuild deployment before creating a directory', async appName, '--build-tool', 'rsbuild', - '--deployment', - 'cloudflare', + '--add-ons', + 'shadcn', '--yes', '--no-install', '--no-git', @@ -75,7 +85,7 @@ test('@matrix rejects an Rsbuild deployment before creating a directory', async | undefined expect( `${commandError?.stdout ?? ''}\n${commandError?.stderr ?? ''}`, - ).toContain('Rsbuild does not currently support --deployment') + ).toContain('Rsbuild does not support --add-ons') await expect(access(join(rootDir, appName))).rejects.toThrow() } finally { await rm(rootDir, { recursive: true, force: true }) @@ -84,13 +94,14 @@ test('@matrix rejects an Rsbuild deployment before creating a directory', async test.describe('@matrix Rsbuild smoke matrix', () => { for (const scenario of scenarios) { - test(`@matrix ${scenario.framework} routerOnly=${scenario.routerOnly} blank=${scenario.blank}`, async ({ + test(`@matrix ${scenario.framework} routerOnly=${scenario.routerOnly} blank=${scenario.blank} deployment=${scenario.deployment ?? 'none'}`, async ({ page, }) => { const fixture = await createAppFixture({ - appName: `${scenario.framework}-rsbuild-${scenario.routerOnly ? 'router' : 'start'}-smoke`, + appName: `${scenario.framework}-rsbuild-${scenario.deployment ?? (scenario.routerOnly ? 'router' : 'start')}-smoke`, framework: scenario.framework, bundler: 'rsbuild', + deployment: scenario.deployment, routerOnly: scenario.routerOnly, blank: scenario.blank, packageManager: 'pnpm', diff --git a/packages/cli/tests/command-line.test.ts b/packages/cli/tests/command-line.test.ts index fb20fdaed..9422ed22b 100644 --- a/packages/cli/tests/command-line.test.ts +++ b/packages/cli/tests/command-line.test.ts @@ -5,10 +5,7 @@ import { normalizeOptions, validateLegacyCreateFlags, } from '../src/command-line.js' -import { - sanitizePackageName, - getCurrentDirectoryName, -} from '../src/utils.js' +import { sanitizePackageName, getCurrentDirectoryName } from '../src/utils.js' import { __testRegisterFramework, __testClearFrameworks, @@ -691,7 +688,7 @@ describe('normalizeOptions', () => { buildTool: 'rsbuild', addOns: ['form'], }), - ).rejects.toThrow('Rsbuild currently supports toolchains only') + ).rejects.toThrow('Rsbuild does not support --add-ons') }) it('keeps explicitly selected integrations in the blank preset', async () => { @@ -827,8 +824,7 @@ describe('validateLegacyCreateFlags', () => { [{ starter: 'blog' }, '--starter'], [{ template: 'blog' }, '--template'], [{ templateId: 'blog' }, '--template-id'], - [{ deployment: 'cloudflare' }, '--deployment'], - [{ addOns: ['form'] }, 'toolchains only'], + [{ addOns: ['form'] }, 'does not support --add-ons'], ])( 'rejects unsupported Rsbuild combinations containing %s', (flags, text) => { @@ -841,6 +837,15 @@ describe('validateLegacyCreateFlags', () => { }, ) + it.each(['nitro', 'railway', 'render', 'vercel', 'netlify', 'cloudflare'])( + 'allows Rsbuild with the %s deployment flag', + (deployment) => { + expect( + validateLegacyCreateFlags({ buildTool: 'rsbuild', deployment }).error, + ).toBeUndefined() + }, + ) + it('allows Rsbuild with a toolchain', () => { const result = validateLegacyCreateFlags({ buildTool: 'rsbuild', diff --git a/packages/cli/tests/options.test.ts b/packages/cli/tests/options.test.ts index 35de7586c..8b622dd77 100644 --- a/packages/cli/tests/options.test.ts +++ b/packages/cli/tests/options.test.ts @@ -10,10 +10,7 @@ import * as create from '@tanstack/create' import * as prompts from '../src/ui-prompts' import * as commandLine from '../src/command-line' -import { - getCurrentDirectoryName, - sanitizePackageName, -} from '../src/utils' +import { getCurrentDirectoryName, sanitizePackageName } from '../src/utils' import type { Framework } from '@tanstack/create' @@ -50,6 +47,20 @@ beforeEach(() => { type: 'add-on', modes: ['file-router'], }, + { + id: 'cloudflare', + name: 'Cloudflare', + type: 'deployment', + modes: ['file-router'], + exclusive: ['deploy'], + integrations: [ + { + type: 'rsbuild-plugin', + import: "import { nitro } from 'nitro/rsbuild'", + code: "nitro({ preset: 'cloudflare-module' })", + }, + ], + }, { id: 'biome', type: 'toolchain', @@ -94,14 +105,13 @@ const baseCliOptions: CliOptions = { function setBasicSpies() { vi.spyOn(prompts, 'selectBundler').mockImplementation(async () => 'vite') - vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation(async () => []) - vi - .spyOn(commandLine, 'resolveStarterSpecifier') - .mockImplementation(async (value) => - value === 'blog' - ? 'https://example.com/react/blog/starter.json' - : value, - ) + vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation( + async () => [], + ) + vi.spyOn(commandLine, 'resolveStarterSpecifier').mockImplementation( + async (value) => + value === 'blog' ? 'https://example.com/react/blog/starter.json' : value, + ) vi.spyOn(create, 'loadStarter').mockImplementation( async (id) => ({ @@ -194,14 +204,16 @@ describe('promptForCreateOptions', () => { it('prompts for templates when none was provided', async () => { setBasicSpies() - vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation(async () => [ - { - id: 'blog', - name: 'Blog', - description: 'Blog template', - framework: 'react', - }, - ]) + vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation( + async () => [ + { + id: 'blog', + name: 'Blog', + description: 'Blog template', + framework: 'react', + }, + ], + ) await promptForCreateOptions(baseCliOptions, {}) @@ -232,14 +244,18 @@ describe('promptForCreateOptions', () => { expect(options?.bundler).toBe('rsbuild') expect(options?.chosenAddOns.map((addOn) => addOn.id)).toEqual(['biome']) expect(prompts.selectTemplate).not.toHaveBeenCalled() - expect(prompts.selectDeployment).not.toHaveBeenCalled() + expect(prompts.selectDeployment).toHaveBeenCalledWith( + expect.anything(), + undefined, + undefined, + 'rsbuild', + ) expect(prompts.selectAddOns).not.toHaveBeenCalled() }) it.each([ [{ template: 'blog' }, '--starter, --template, or --template-id'], - [{ deployment: 'cloudflare' }, '--deployment'], - [{ addOns: ['react-query'] }, 'toolchains only'], + [{ addOns: ['react-query'] }, 'does not support --add-ons'], ])( 'rejects explicit %s after Rsbuild is selected interactively', async (flags, error) => { @@ -285,6 +301,25 @@ describe('promptForCreateOptions', () => { }, ) + it.each([false, true])( + 'keeps an Rsbuild deployment (forced=%s)', + async (forced) => { + setBasicSpies() + vi.mocked(prompts.selectBundler).mockResolvedValue('rsbuild') + const options = await promptForCreateOptions( + { + ...baseCliOptions, + blank: true, + deployment: forced ? undefined : 'cloudflare', + }, + forced ? { forcedDeployment: 'cloudflare' } : {}, + ) + expect(options?.chosenAddOns.map((addOn) => addOn.id)).toEqual([ + 'cloudflare', + ]) + }, + ) + it('keeps toolchain selection available for blank Rsbuild projects', async () => { setBasicSpies() vi.mocked(prompts.selectBundler).mockResolvedValue('rsbuild') @@ -438,6 +473,29 @@ describe('promptForCreateOptions', () => { }) describe('promptForAddOns', () => { + it('offers a deployment to an existing Rsbuild Start app', async () => { + vi.mocked(prompts.selectAddOns).mockReset() + vi.mocked(prompts.selectAddOns).mockResolvedValue(['cloudflare']) + vi.spyOn(create, 'readConfigFile').mockResolvedValue({ + projectName: 'test', + framework: 'react', + bundler: 'rsbuild', + mode: 'file-router', + chosenAddOns: ['eslint'], + version: 1, + }) + await expect(promptForAddOns()).resolves.toEqual(['cloudflare']) + expect(prompts.selectAddOns).toHaveBeenCalledWith( + expect.anything(), + 'file-router', + 'deployment', + expect.any(String), + ['biome', 'eslint'], + false, + 'rsbuild', + ) + }) + it('does not offer a second exclusive Rsbuild toolchain', async () => { vi.mocked(prompts.selectAddOns).mockClear() vi.spyOn(create, 'readConfigFile').mockResolvedValue({ @@ -445,7 +503,7 @@ describe('promptForAddOns', () => { framework: 'react', bundler: 'rsbuild', mode: 'file-router', - chosenAddOns: ['eslint'], + chosenAddOns: ['eslint', 'cloudflare'], version: 1, }) diff --git a/packages/create/src/build-tools.ts b/packages/create/src/build-tools.ts index 880967e57..1a32919c2 100644 --- a/packages/create/src/build-tools.ts +++ b/packages/create/src/build-tools.ts @@ -39,19 +39,26 @@ export function resolveBundler( } export function isAddOnSupportedByBundler( - addOn: Pick, + addOn: Pick, bundlerId: string, ) { - return bundlerId !== 'rsbuild' || addOn.type === 'toolchain' + return ( + bundlerId !== 'rsbuild' || + addOn.type === 'toolchain' || + (addOn.type === 'deployment' && + addOn.integrations?.some( + (integration) => integration.type === 'rsbuild-plugin', + ) === true) + ) } export function assertAddOnSupportedByBundler( - addOn: Pick, + addOn: Pick, bundlerId: string, ) { if (!isAddOnSupportedByBundler(addOn, bundlerId)) { throw new Error( - `Rsbuild currently supports toolchain add-ons only. "${addOn.id}" (${addOn.name}) is a ${addOn.type} add-on.`, + `Rsbuild supports toolchains and deployment adapters with an Rsbuild integration only. "${addOn.id}" (${addOn.name}) is not supported.`, ) } } @@ -67,6 +74,18 @@ export function normalizeAndValidateBundlerOptions(options: Options): Options { assertAddOnSupportedByBundler(addOn, bundler) } + if (bundler === 'rsbuild') { + const deployments = options.chosenAddOns.filter( + (addOn) => addOn.type === 'deployment', + ) + if (deployments.length > 1) { + throw new Error('Only one deployment adapter can be selected.') + } + if (options.routerOnly && deployments.length > 0) { + throw new Error('Rsbuild deployment adapters require TanStack Start.') + } + } + return { ...options, bundler, diff --git a/packages/create/src/edge-package-json.ts b/packages/create/src/edge-package-json.ts index 6929cc2de..4b1bbe905 100644 --- a/packages/create/src/edge-package-json.ts +++ b/packages/create/src/edge-package-json.ts @@ -73,10 +73,7 @@ export function mergePackageJSON( : [] const onlyBuiltDependencies = [ - ...new Set([ - ...baseOnlyBuiltDependencies, - ...overlayOnlyBuiltDependencies, - ]), + ...new Set([...baseOnlyBuiltDependencies, ...overlayOnlyBuiltDependencies]), ] if (packagePnpm || overlayPnpm) { @@ -110,19 +107,25 @@ export function createPackageJSON(options: Options) { pkg: string, args: Array = [], ) { - return formatCommand(getPackageManagerExecuteCommand(packageManager, pkg, args)) + return formatCommand( + getPackageManagerExecuteCommand(packageManager, pkg, args), + ) } let packageJSON: Record = { - ...(JSON.parse( - JSON.stringify(options.framework.basePackageJSON), - ) as Record), + ...(JSON.parse(JSON.stringify(options.framework.basePackageJSON)) as Record< + string, + unknown + >), name: options.projectName, } const additions: Array | undefined> = [ options.framework.optionalPackages.typescript, options.framework.optionalPackages.bundlers?.[bundler], + options.chosenAddOns.some((addOn) => addOn.type === 'deployment') + ? options.framework.optionalPackages.bundlerDeployment?.[bundler] + : undefined, includeDevtools ? options.framework.optionalPackages.devtools : undefined, includeDevtools ? options.framework.optionalPackages.bundlerDevtools?.[bundler] @@ -212,6 +215,14 @@ export function createPackageJSON(options: Options) { | undefined const scripts = packageJSON.scripts as Record | undefined + if ( + bundler === 'rsbuild' && + options.chosenAddOns.some((addOn) => addOn.type === 'deployment') + ) { + delete (packageJSON.dependencies as Record | undefined) + ?.srvx + } + if (options.routerOnly) { delete dependencies?.srvx delete scripts?.start diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/README.md b/packages/create/src/frameworks/react/hosts/cloudflare/README.md deleted file mode 100644 index 125545475..000000000 --- a/packages/create/src/frameworks/react/hosts/cloudflare/README.md +++ /dev/null @@ -1,11 +0,0 @@ -## Deploy to Cloudflare Workers - -This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: - -1. Install Wrangler: `npm install -g wrangler` -2. Authenticate: `wrangler login` -3. Deploy: `npx wrangler deploy` - -For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. - -KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs new file mode 100644 index 000000000..da5d8f178 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs @@ -0,0 +1,24 @@ +<% if (bundler === 'rsbuild') { -%> +## Deploy to Cloudflare Workers + +This project uses the experimental Nitro Rsbuild integration with the Cloudflare Workers preset. + +1. Authenticate with `wrangler login`. +2. Run `<%= packageManager %> run deploy` to build and deploy the Worker. + +Edit `wrangler.jsonc` to configure your Worker name and KV, D1, R2, or Durable Object bindings. Nitro generates `.output/server/wrangler.json` with the built entry and assets directory; the deploy script uses that generated configuration. + +Set production secrets with `wrangler secret put MY_VAR`. +<% } else { -%> +## Deploy to Cloudflare Workers + +This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: + +1. Install Wrangler: `npm install -g wrangler` +2. Authenticate: `wrangler login` +3. Deploy: `npx wrangler deploy` + +For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. + +KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. +<% } -%> diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs index 8122145a5..2a0e61d7e 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs +++ b/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs @@ -2,6 +2,7 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "tanstack-start-app", "compatibility_date": "2025-09-02", - "compatibility_flags": ["nodejs_compat"], + "compatibility_flags": ["nodejs_compat"]<% if (bundler !== 'rsbuild') { %>, "main": "<%= addOnEnabled.sentry ? './src/worker.ts' : '@tanstack/react-start/server-entry' %>" + <% } -%> } diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/info.json b/packages/create/src/frameworks/react/hosts/cloudflare/info.json index 60defee9d..d8442bcd9 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/info.json +++ b/packages/create/src/frameworks/react/hosts/cloudflare/info.json @@ -9,12 +9,20 @@ "exclusive": ["deploy"], "color": "#F38020", "priority": 200, - "partner": { "id": "cloudflare", "tier": "gold" }, + "partner": { + "id": "cloudflare", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import { cloudflare } from '@cloudflare/vite-plugin'", "code": "cloudflare({ viteEnvironment: { name: 'ssr' } })" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'cloudflare-module' })" } ] } diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs index 328265303..bdbfcb095 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs @@ -1,3 +1,22 @@ +<% if (bundler === 'rsbuild') { -%> +{ + "scripts": { + "start": "rsbuild preview", + "deploy": "<%= packageManager %> run build && wrangler deploy --config .output/server/wrangler.json" + }, + "dependencies": { + "nitro": "3.0.261103-rc" + }, + "devDependencies": { + "wrangler": "^4.70.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "workerd" + ] + } +} +<% } else { -%> { "scripts": { "deploy": "<%= packageManager %> run build && wrangler deploy" @@ -10,3 +29,4 @@ "onlyBuiltDependencies": ["sharp", "workerd"] } } +<% } -%> diff --git a/packages/create/src/frameworks/react/hosts/netlify/README.md b/packages/create/src/frameworks/react/hosts/netlify/README.md.ejs similarity index 59% rename from packages/create/src/frameworks/react/hosts/netlify/README.md rename to packages/create/src/frameworks/react/hosts/netlify/README.md.ejs index a00ad80fb..33663e31e 100644 --- a/packages/create/src/frameworks/react/hosts/netlify/README.md +++ b/packages/create/src/frameworks/react/hosts/netlify/README.md.ejs @@ -1,3 +1,10 @@ +<% if (bundler === 'rsbuild') { -%> +## Deploy to Netlify + +This project uses the experimental Nitro Rsbuild integration with the Netlify preset. + +Connect your repository to Netlify. Build with `<%= packageManager %> run build` and publish `dist`, as configured in `netlify.toml`. Nitro emits the server functions and Netlify deployment configuration in `.netlify/`. +<% } else { -%> ## Deploy to Netlify This project ships with `netlify.toml` configured for a Netlify site: @@ -9,3 +16,4 @@ This project ships with `netlify.toml` configured for a Netlify site: 5. Trigger the first deploy Server functions and API routes run on Netlify Functions. For lower-latency request handling, see Netlify Edge Functions: https://docs.netlify.com/edge-functions/overview. +<% } -%> diff --git a/packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml b/packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml deleted file mode 100644 index 0e944a365..000000000 --- a/packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml +++ /dev/null @@ -1,7 +0,0 @@ -[build] - command = "vite build" - publish = "dist/client" -[dev] - command = "npm run dev" - targetPort = 3000 - port = 8888 \ No newline at end of file diff --git a/packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml.ejs b/packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml.ejs new file mode 100644 index 000000000..e0f913a6a --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/netlify/assets/netlify.toml.ejs @@ -0,0 +1,7 @@ +[build] + command = "<%= bundler %> build" + publish = "<%= bundler === 'rsbuild' ? 'dist' : 'dist/client' %>" +[dev] + command = "npm run dev" + targetPort = 3000 + port = 8888 \ No newline at end of file diff --git a/packages/create/src/frameworks/react/hosts/netlify/info.json b/packages/create/src/frameworks/react/hosts/netlify/info.json index 611ce371f..7e96f7a04 100644 --- a/packages/create/src/frameworks/react/hosts/netlify/info.json +++ b/packages/create/src/frameworks/react/hosts/netlify/info.json @@ -9,12 +9,20 @@ "exclusive": ["deploy"], "color": "#00C7B7", "priority": 180, - "partner": { "id": "netlify", "tier": "gold" }, + "partner": { + "id": "netlify", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import netlify from '@netlify/vite-plugin-tanstack-start'", "code": "netlify()" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'netlify' })" } ] } diff --git a/packages/create/src/frameworks/react/hosts/netlify/package.json b/packages/create/src/frameworks/react/hosts/netlify/package.json deleted file mode 100644 index 70a27e562..000000000 --- a/packages/create/src/frameworks/react/hosts/netlify/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "devDependencies": { - "@netlify/vite-plugin-tanstack-start": "^1.2.15" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "sharp" - ] - } -} diff --git a/packages/create/src/frameworks/react/hosts/netlify/package.json.ejs b/packages/create/src/frameworks/react/hosts/netlify/package.json.ejs new file mode 100644 index 000000000..38b3fb736 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/netlify/package.json.ejs @@ -0,0 +1,21 @@ +<% if (bundler === 'rsbuild') { -%> +{ + "scripts": { + "start": "rsbuild preview" + }, + "dependencies": { + "nitro": "3.0.261103-rc" + } +} +<% } else { -%> +{ + "devDependencies": { + "@netlify/vite-plugin-tanstack-start": "^1.2.15" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp" + ] + } +} +<% } -%> diff --git a/packages/create/src/frameworks/react/hosts/nitro/info.json b/packages/create/src/frameworks/react/hosts/nitro/info.json index 685cf4e9f..663810158 100644 --- a/packages/create/src/frameworks/react/hosts/nitro/info.json +++ b/packages/create/src/frameworks/react/hosts/nitro/info.json @@ -14,6 +14,11 @@ "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro({ rollupConfig: { external: [/^@sentry\\//] } })" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'node-server' })" } ], "default": true diff --git a/packages/create/src/frameworks/react/hosts/nitro/package.json b/packages/create/src/frameworks/react/hosts/nitro/package.json deleted file mode 100644 index 56e871bc8..000000000 --- a/packages/create/src/frameworks/react/hosts/nitro/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "nitro": "3.0.260610-beta" - } -} diff --git a/packages/create/src/frameworks/react/hosts/nitro/package.json.ejs b/packages/create/src/frameworks/react/hosts/nitro/package.json.ejs new file mode 100644 index 000000000..51be2a91d --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/nitro/package.json.ejs @@ -0,0 +1,5 @@ +{ + "dependencies": { + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + } +} diff --git a/packages/create/src/frameworks/react/hosts/railway/info.json b/packages/create/src/frameworks/react/hosts/railway/info.json index 4ddf6b96e..bf9babec9 100644 --- a/packages/create/src/frameworks/react/hosts/railway/info.json +++ b/packages/create/src/frameworks/react/hosts/railway/info.json @@ -9,13 +9,21 @@ "exclusive": ["deploy"], "color": "#9B4DCA", "priority": 160, - "partner": { "id": "railway", "tier": "gold" }, + "partner": { + "id": "railway", + "tier": "gold" + }, "deletedFiles": ["nixpacks.toml"], "integrations": [ { "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro({ rollupConfig: { external: [/^@sentry\\//] } })" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'node-server' })" } ] } diff --git a/packages/create/src/frameworks/react/hosts/railway/package.json.ejs b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs index 909ab021f..3c6dc339d 100644 --- a/packages/create/src/frameworks/react/hosts/railway/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs @@ -3,6 +3,6 @@ "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" + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" } } diff --git a/packages/create/src/frameworks/react/hosts/render/info.json b/packages/create/src/frameworks/react/hosts/render/info.json index e7cfabae6..496cc2e19 100644 --- a/packages/create/src/frameworks/react/hosts/render/info.json +++ b/packages/create/src/frameworks/react/hosts/render/info.json @@ -10,12 +10,20 @@ "exclusive": ["deploy"], "color": "#46E3B7", "priority": 150, - "partner": { "id": "render", "tier": "gold" }, + "partner": { + "id": "render", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro({ rollupConfig: { external: [/^@sentry\\//] } })" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'render-com' })" } ] } diff --git a/packages/create/src/frameworks/react/hosts/render/package.json.ejs b/packages/create/src/frameworks/react/hosts/render/package.json.ejs index 909ab021f..3c6dc339d 100644 --- a/packages/create/src/frameworks/react/hosts/render/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/render/package.json.ejs @@ -3,6 +3,6 @@ "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" + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '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 deleted file mode 100644 index 641cbfc34..000000000 --- a/packages/create/src/frameworks/react/hosts/vercel/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## 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/README.md.ejs b/packages/create/src/frameworks/react/hosts/vercel/README.md.ejs new file mode 100644 index 000000000..858b6c3fd --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/README.md.ejs @@ -0,0 +1,23 @@ +<% if (bundler === 'rsbuild') { -%> +## Deploy to Vercel + +This project uses the experimental Nitro Rsbuild integration with the Vercel preset, which generates `.vercel/output` for the Vercel Build Output API. + +Import your repository using Add New > Project in Vercel. `vercel.json` disables framework auto-detection and sets the build command to `<%= getPackageManagerRunScript('build') %>`. For a local prebuilt deployment, run `<%= getPackageManagerRunScript('build') %>` followed by `vercel deploy --prebuilt`. + +Configure production secrets in the Vercel project settings. +<% } else { -%> +## 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/vercel.json b/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json deleted file mode 100644 index 935d5d78a..000000000 --- a/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://openapi.vercel.sh/vercel.json", - "framework": "tanstack-start" -} diff --git a/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json.ejs b/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json.ejs new file mode 100644 index 000000000..8301a9c8d --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/assets/vercel.json.ejs @@ -0,0 +1,5 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": <%- JSON.stringify(bundler === 'rsbuild' ? null : 'tanstack-start') %><% if (bundler === 'rsbuild') { %>, + "buildCommand": "<%= getPackageManagerRunScript('build') %>"<% } %> +} diff --git a/packages/create/src/frameworks/react/hosts/vercel/info.json b/packages/create/src/frameworks/react/hosts/vercel/info.json index eca28cc05..64d0868b8 100644 --- a/packages/create/src/frameworks/react/hosts/vercel/info.json +++ b/packages/create/src/frameworks/react/hosts/vercel/info.json @@ -10,12 +10,20 @@ "exclusive": ["deploy"], "color": "#000000", "priority": 140, - "partner": { "id": "vercel", "tier": "gold" }, + "partner": { + "id": "vercel", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro()" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'vercel' })" } ] } diff --git a/packages/create/src/frameworks/react/hosts/vercel/package.json b/packages/create/src/frameworks/react/hosts/vercel/package.json deleted file mode 100644 index 56e871bc8..000000000 --- a/packages/create/src/frameworks/react/hosts/vercel/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "nitro": "3.0.260610-beta" - } -} diff --git a/packages/create/src/frameworks/react/hosts/vercel/package.json.ejs b/packages/create/src/frameworks/react/hosts/vercel/package.json.ejs new file mode 100644 index 000000000..22f9e05fc --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/vercel/package.json.ejs @@ -0,0 +1,8 @@ +{ + <% if (bundler === 'rsbuild') { -%> + "scripts": { "start": "rsbuild preview" }, + <% } -%> + "dependencies": { + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + } +} diff --git a/packages/create/src/frameworks/react/project/base/rsbuild.config.ts.ejs b/packages/create/src/frameworks/react/project/base/rsbuild.config.ts.ejs index 02289a5fd..9f94084b6 100644 --- a/packages/create/src/frameworks/react/project/base/rsbuild.config.ts.ejs +++ b/packages/create/src/frameworks/react/project/base/rsbuild.config.ts.ejs @@ -1,4 +1,5 @@ <% if (bundler !== 'rsbuild') { ignoreFile() } -%> +<% const deploymentPlugins = integrations.filter(i => i.type === 'rsbuild-plugin') -%> import { defineConfig } from '@rsbuild/core' import { pluginReact } from '@rsbuild/plugin-react' <% if (tailwind) { -%> @@ -10,6 +11,10 @@ import { tanstackRouter } from '@tanstack/router-plugin/rspack' import { tanstackStart } from '@tanstack/react-start/plugin/rsbuild' <% } -%> +<% for (const integration of deploymentPlugins) { -%> +<%- integrationImportContent(integration) %> +<% } -%> + export default defineConfig({ server: { port: 3000, @@ -28,7 +33,8 @@ export default defineConfig({ plugins: [ pluginReact(), <% if (tailwind) { %>pluginTailwindcss(),<% } %> - <% if (!routerOnly) { %>tanstackStart(),<% } %> + <% if (!routerOnly) { %>tanstackStart(<% if (deploymentPlugins.length) { %>{ rsbuild: { installDevServerMiddleware: false } }<% } %>),<% } %> + <% for (const integration of deploymentPlugins) { %><%- integrationImportCode(integration) %>,<% } %> ], <% if (routerOnly) { -%> tools: { diff --git a/packages/create/src/frameworks/react/project/packages.json b/packages/create/src/frameworks/react/project/packages.json index 679581fa5..d596565c9 100644 --- a/packages/create/src/frameworks/react/project/packages.json +++ b/packages/create/src/frameworks/react/project/packages.json @@ -85,5 +85,18 @@ "@tanstack/router-cli": "^1.132.0" } } + }, + "bundlerDeployment": { + "rsbuild": { + "engines": { + "node": ">=22.22.0" + }, + "devDependencies": { + "@rsbuild/core": "^2.3.0" + }, + "scripts": { + "start": "node .output/server/index.mjs" + } + } } } diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/README.md b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md deleted file mode 100644 index 125545475..000000000 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/README.md +++ /dev/null @@ -1,11 +0,0 @@ -## Deploy to Cloudflare Workers - -This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: - -1. Install Wrangler: `npm install -g wrangler` -2. Authenticate: `wrangler login` -3. Deploy: `npx wrangler deploy` - -For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. - -KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs new file mode 100644 index 000000000..da5d8f178 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs @@ -0,0 +1,24 @@ +<% if (bundler === 'rsbuild') { -%> +## Deploy to Cloudflare Workers + +This project uses the experimental Nitro Rsbuild integration with the Cloudflare Workers preset. + +1. Authenticate with `wrangler login`. +2. Run `<%= packageManager %> run deploy` to build and deploy the Worker. + +Edit `wrangler.jsonc` to configure your Worker name and KV, D1, R2, or Durable Object bindings. Nitro generates `.output/server/wrangler.json` with the built entry and assets directory; the deploy script uses that generated configuration. + +Set production secrets with `wrangler secret put MY_VAR`. +<% } else { -%> +## Deploy to Cloudflare Workers + +This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: + +1. Install Wrangler: `npm install -g wrangler` +2. Authenticate: `wrangler login` +3. Deploy: `npx wrangler deploy` + +For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. + +KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. +<% } -%> diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc b/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc.ejs similarity index 81% rename from packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc rename to packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc.ejs index 810654d3b..22d4a9b29 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc.ejs @@ -4,6 +4,7 @@ "compatibility_date": "2025-09-02", "compatibility_flags": [ "nodejs_compat" - ], + ]<% if (bundler !== 'rsbuild') { %>, "main": "@tanstack/solid-start/server-entry" + <% } -%> } \ No newline at end of file diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/info.json b/packages/create/src/frameworks/solid/hosts/cloudflare/info.json index 60defee9d..d8442bcd9 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/info.json +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/info.json @@ -9,12 +9,20 @@ "exclusive": ["deploy"], "color": "#F38020", "priority": 200, - "partner": { "id": "cloudflare", "tier": "gold" }, + "partner": { + "id": "cloudflare", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import { cloudflare } from '@cloudflare/vite-plugin'", "code": "cloudflare({ viteEnvironment: { name: 'ssr' } })" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'cloudflare-module' })" } ] } diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs b/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs index 129dd2993..261e2be13 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs @@ -1,3 +1,22 @@ +<% if (bundler === 'rsbuild') { -%> +{ + "scripts": { + "start": "rsbuild preview", + "deploy": "<%= packageManager %> run build && wrangler deploy --config .output/server/wrangler.json" + }, + "dependencies": { + "nitro": "3.0.261103-rc" + }, + "devDependencies": { + "wrangler": "^4.70.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "workerd" + ] + } +} +<% } else { -%> { "scripts": { "deploy": "<%= packageManager %> run build && wrangler deploy" @@ -12,3 +31,4 @@ "onlyBuiltDependencies": ["sharp", "workerd"] } } +<% } -%> diff --git a/packages/create/src/frameworks/solid/hosts/netlify/README.md b/packages/create/src/frameworks/solid/hosts/netlify/README.md.ejs similarity index 59% rename from packages/create/src/frameworks/solid/hosts/netlify/README.md rename to packages/create/src/frameworks/solid/hosts/netlify/README.md.ejs index a00ad80fb..33663e31e 100644 --- a/packages/create/src/frameworks/solid/hosts/netlify/README.md +++ b/packages/create/src/frameworks/solid/hosts/netlify/README.md.ejs @@ -1,3 +1,10 @@ +<% if (bundler === 'rsbuild') { -%> +## Deploy to Netlify + +This project uses the experimental Nitro Rsbuild integration with the Netlify preset. + +Connect your repository to Netlify. Build with `<%= packageManager %> run build` and publish `dist`, as configured in `netlify.toml`. Nitro emits the server functions and Netlify deployment configuration in `.netlify/`. +<% } else { -%> ## Deploy to Netlify This project ships with `netlify.toml` configured for a Netlify site: @@ -9,3 +16,4 @@ This project ships with `netlify.toml` configured for a Netlify site: 5. Trigger the first deploy Server functions and API routes run on Netlify Functions. For lower-latency request handling, see Netlify Edge Functions: https://docs.netlify.com/edge-functions/overview. +<% } -%> diff --git a/packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml b/packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml deleted file mode 100644 index 0b48ff85f..000000000 --- a/packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml +++ /dev/null @@ -1,7 +0,0 @@ -[build] - command = "vite build" - publish = "dist/client" -[dev] - command = "npm run dev" - targetPort = 3000 - port = 8888 diff --git a/packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml.ejs b/packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml.ejs new file mode 100644 index 000000000..2f3b0f7db --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/netlify/assets/netlify.toml.ejs @@ -0,0 +1,7 @@ +[build] + command = "<%= bundler %> build" + publish = "<%= bundler === 'rsbuild' ? 'dist' : 'dist/client' %>" +[dev] + command = "npm run dev" + targetPort = 3000 + port = 8888 diff --git a/packages/create/src/frameworks/solid/hosts/netlify/info.json b/packages/create/src/frameworks/solid/hosts/netlify/info.json index 611ce371f..7e96f7a04 100644 --- a/packages/create/src/frameworks/solid/hosts/netlify/info.json +++ b/packages/create/src/frameworks/solid/hosts/netlify/info.json @@ -9,12 +9,20 @@ "exclusive": ["deploy"], "color": "#00C7B7", "priority": 180, - "partner": { "id": "netlify", "tier": "gold" }, + "partner": { + "id": "netlify", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import netlify from '@netlify/vite-plugin-tanstack-start'", "code": "netlify()" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'netlify' })" } ] } diff --git a/packages/create/src/frameworks/solid/hosts/netlify/package.json b/packages/create/src/frameworks/solid/hosts/netlify/package.json deleted file mode 100644 index 70a27e562..000000000 --- a/packages/create/src/frameworks/solid/hosts/netlify/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "devDependencies": { - "@netlify/vite-plugin-tanstack-start": "^1.2.15" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "sharp" - ] - } -} diff --git a/packages/create/src/frameworks/solid/hosts/netlify/package.json.ejs b/packages/create/src/frameworks/solid/hosts/netlify/package.json.ejs new file mode 100644 index 000000000..38b3fb736 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/netlify/package.json.ejs @@ -0,0 +1,21 @@ +<% if (bundler === 'rsbuild') { -%> +{ + "scripts": { + "start": "rsbuild preview" + }, + "dependencies": { + "nitro": "3.0.261103-rc" + } +} +<% } else { -%> +{ + "devDependencies": { + "@netlify/vite-plugin-tanstack-start": "^1.2.15" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp" + ] + } +} +<% } -%> diff --git a/packages/create/src/frameworks/solid/hosts/nitro/info.json b/packages/create/src/frameworks/solid/hosts/nitro/info.json index 1a564ec04..d5b832f55 100644 --- a/packages/create/src/frameworks/solid/hosts/nitro/info.json +++ b/packages/create/src/frameworks/solid/hosts/nitro/info.json @@ -13,6 +13,11 @@ "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro()" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'node-server' })" } ], "default": true diff --git a/packages/create/src/frameworks/solid/hosts/nitro/package.json b/packages/create/src/frameworks/solid/hosts/nitro/package.json deleted file mode 100644 index 56e871bc8..000000000 --- a/packages/create/src/frameworks/solid/hosts/nitro/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "nitro": "3.0.260610-beta" - } -} diff --git a/packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs b/packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs new file mode 100644 index 000000000..51be2a91d --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs @@ -0,0 +1,5 @@ +{ + "dependencies": { + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + } +} diff --git a/packages/create/src/frameworks/solid/hosts/railway/info.json b/packages/create/src/frameworks/solid/hosts/railway/info.json index 713a01954..cfc202c56 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/info.json +++ b/packages/create/src/frameworks/solid/hosts/railway/info.json @@ -9,13 +9,21 @@ "exclusive": ["deploy"], "color": "#9B4DCA", "priority": 160, - "partner": { "id": "railway", "tier": "gold" }, + "partner": { + "id": "railway", + "tier": "gold" + }, "deletedFiles": ["nixpacks.toml"], "integrations": [ { "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro()" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'node-server' })" } ] } diff --git a/packages/create/src/frameworks/solid/hosts/render/package.json b/packages/create/src/frameworks/solid/hosts/railway/package.json.ejs similarity index 53% rename from packages/create/src/frameworks/solid/hosts/render/package.json rename to packages/create/src/frameworks/solid/hosts/railway/package.json.ejs index a3b221161..a482f942f 100644 --- a/packages/create/src/frameworks/solid/hosts/render/package.json +++ b/packages/create/src/frameworks/solid/hosts/railway/package.json.ejs @@ -3,6 +3,6 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "nitro": "3.0.260610-beta" + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" } } diff --git a/packages/create/src/frameworks/solid/hosts/render/info.json b/packages/create/src/frameworks/solid/hosts/render/info.json index 33a2001e5..b95a00858 100644 --- a/packages/create/src/frameworks/solid/hosts/render/info.json +++ b/packages/create/src/frameworks/solid/hosts/render/info.json @@ -10,12 +10,20 @@ "exclusive": ["deploy"], "color": "#46E3B7", "priority": 150, - "partner": { "id": "render", "tier": "gold" }, + "partner": { + "id": "render", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro()" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'render-com' })" } ] } diff --git a/packages/create/src/frameworks/solid/hosts/railway/package.json b/packages/create/src/frameworks/solid/hosts/render/package.json.ejs similarity index 53% rename from packages/create/src/frameworks/solid/hosts/railway/package.json rename to packages/create/src/frameworks/solid/hosts/render/package.json.ejs index a3b221161..a482f942f 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/package.json +++ b/packages/create/src/frameworks/solid/hosts/render/package.json.ejs @@ -3,6 +3,6 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "nitro": "3.0.260610-beta" + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '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 deleted file mode 100644 index 641cbfc34..000000000 --- a/packages/create/src/frameworks/solid/hosts/vercel/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## 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/README.md.ejs b/packages/create/src/frameworks/solid/hosts/vercel/README.md.ejs new file mode 100644 index 000000000..858b6c3fd --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/vercel/README.md.ejs @@ -0,0 +1,23 @@ +<% if (bundler === 'rsbuild') { -%> +## Deploy to Vercel + +This project uses the experimental Nitro Rsbuild integration with the Vercel preset, which generates `.vercel/output` for the Vercel Build Output API. + +Import your repository using Add New > Project in Vercel. `vercel.json` disables framework auto-detection and sets the build command to `<%= getPackageManagerRunScript('build') %>`. For a local prebuilt deployment, run `<%= getPackageManagerRunScript('build') %>` followed by `vercel deploy --prebuilt`. + +Configure production secrets in the Vercel project settings. +<% } else { -%> +## 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 deleted file mode 100644 index 935d5d78a..000000000 --- a/packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://openapi.vercel.sh/vercel.json", - "framework": "tanstack-start" -} diff --git a/packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json.ejs b/packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json.ejs new file mode 100644 index 000000000..8301a9c8d --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/vercel/assets/vercel.json.ejs @@ -0,0 +1,5 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": <%- JSON.stringify(bundler === 'rsbuild' ? null : 'tanstack-start') %><% if (bundler === 'rsbuild') { %>, + "buildCommand": "<%= getPackageManagerRunScript('build') %>"<% } %> +} diff --git a/packages/create/src/frameworks/solid/hosts/vercel/info.json b/packages/create/src/frameworks/solid/hosts/vercel/info.json index eca28cc05..64d0868b8 100644 --- a/packages/create/src/frameworks/solid/hosts/vercel/info.json +++ b/packages/create/src/frameworks/solid/hosts/vercel/info.json @@ -10,12 +10,20 @@ "exclusive": ["deploy"], "color": "#000000", "priority": 140, - "partner": { "id": "vercel", "tier": "gold" }, + "partner": { + "id": "vercel", + "tier": "gold" + }, "integrations": [ { "type": "vite-plugin", "import": "import { nitro } from 'nitro/vite'", "code": "nitro()" + }, + { + "type": "rsbuild-plugin", + "import": "import { nitro } from 'nitro/rsbuild'", + "code": "nitro({ preset: 'vercel' })" } ] } diff --git a/packages/create/src/frameworks/solid/hosts/vercel/package.json b/packages/create/src/frameworks/solid/hosts/vercel/package.json deleted file mode 100644 index 56e871bc8..000000000 --- a/packages/create/src/frameworks/solid/hosts/vercel/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "nitro": "3.0.260610-beta" - } -} diff --git a/packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs b/packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs new file mode 100644 index 000000000..22f9e05fc --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs @@ -0,0 +1,8 @@ +{ + <% if (bundler === 'rsbuild') { -%> + "scripts": { "start": "rsbuild preview" }, + <% } -%> + "dependencies": { + "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + } +} diff --git a/packages/create/src/frameworks/solid/project/base/rsbuild.config.ts.ejs b/packages/create/src/frameworks/solid/project/base/rsbuild.config.ts.ejs index 126eb707e..19418d67e 100644 --- a/packages/create/src/frameworks/solid/project/base/rsbuild.config.ts.ejs +++ b/packages/create/src/frameworks/solid/project/base/rsbuild.config.ts.ejs @@ -1,4 +1,5 @@ <% if (bundler !== 'rsbuild') { ignoreFile() } -%> +<% const deploymentPlugins = integrations.filter(i => i.type === 'rsbuild-plugin') -%> import { defineConfig } from '@rsbuild/core' import { pluginBabel } from '@rsbuild/plugin-babel' import { pluginSolid } from '@rsbuild/plugin-solid' @@ -11,6 +12,10 @@ import { tanstackRouter } from '@tanstack/router-plugin/rspack' import { tanstackStart } from '@tanstack/solid-start/plugin/rsbuild' <% } -%> +<% for (const integration of deploymentPlugins) { -%> +<%- integrationImportContent(integration) %> +<% } -%> + export default defineConfig({ server: { port: 3000, @@ -32,7 +37,8 @@ export default defineConfig({ }), pluginSolid(), <% if (tailwind) { %>pluginTailwindcss(),<% } %> - <% if (!routerOnly) { %>tanstackStart(),<% } %> + <% if (!routerOnly) { %>tanstackStart(<% if (deploymentPlugins.length) { %>{ rsbuild: { installDevServerMiddleware: false } }<% } %>),<% } %> + <% for (const integration of deploymentPlugins) { %><%- integrationImportCode(integration) %>,<% } %> ], <% if (routerOnly) { -%> tools: { diff --git a/packages/create/src/frameworks/solid/project/packages.json b/packages/create/src/frameworks/solid/project/packages.json index 96494f375..2afe6f29a 100644 --- a/packages/create/src/frameworks/solid/project/packages.json +++ b/packages/create/src/frameworks/solid/project/packages.json @@ -78,5 +78,18 @@ "@tanstack/router-cli": "^1.133.21" } } + }, + "bundlerDeployment": { + "rsbuild": { + "engines": { + "node": ">=22.22.0" + }, + "devDependencies": { + "@rsbuild/core": "^2.3.0" + }, + "scripts": { + "start": "node .output/server/index.mjs" + } + } } } diff --git a/packages/create/src/package-json.ts b/packages/create/src/package-json.ts index fde5f7fd6..20fed0e46 100644 --- a/packages/create/src/package-json.ts +++ b/packages/create/src/package-json.ts @@ -36,10 +36,7 @@ export function mergePackageJSON( : [] const onlyBuiltDependencies = [ - ...new Set([ - ...baseOnlyBuiltDependencies, - ...overlayOnlyBuiltDependencies, - ]), + ...new Set([...baseOnlyBuiltDependencies, ...overlayOnlyBuiltDependencies]), ] if (packageJSON.pnpm || overlayPackageJSON?.pnpm) { @@ -72,7 +69,9 @@ export function createPackageJSON(options: Options) { pkg: string, args: Array = [], ) { - return formatCommand(getPackageManagerExecuteCommand(packageManager, pkg, args)) + return formatCommand( + getPackageManagerExecuteCommand(packageManager, pkg, args), + ) } let packageJSON = { @@ -83,6 +82,9 @@ export function createPackageJSON(options: Options) { const additions: Array | undefined> = [ options.framework.optionalPackages.typescript, options.framework.optionalPackages.bundlers?.[bundler], + options.chosenAddOns.some((addOn) => addOn.type === 'deployment') + ? options.framework.optionalPackages.bundlerDeployment?.[bundler] + : undefined, includeDevtools ? options.framework.optionalPackages.devtools : undefined, includeDevtools ? options.framework.optionalPackages.bundlerDevtools?.[bundler] @@ -161,6 +163,14 @@ export function createPackageJSON(options: Options) { ) } + if ( + bundler === 'rsbuild' && + options.chosenAddOns.some((addOn) => addOn.type === 'deployment') + ) { + delete (packageJSON.dependencies as Record | undefined) + ?.srvx + } + if (options.routerOnly) { delete packageJSON.dependencies?.srvx delete packageJSON.scripts?.start diff --git a/packages/create/tests/bundlers.test.ts b/packages/create/tests/bundlers.test.ts index 94191661f..2486b04c4 100644 --- a/packages/create/tests/bundlers.test.ts +++ b/packages/create/tests/bundlers.test.ts @@ -102,13 +102,20 @@ describe('bundler project generation', () => { ['react', createReactFrameworkDefinition, 'shadcn'], ['solid', createSolidFrameworkDefinition, 'solid-ui'], ])( - '%s filters the Rsbuild catalog to toolchains', + '%s filters the Rsbuild catalog to toolchains and compatible deployments', async (_, createDefinition, incompatibleAddOn) => { const framework = frameworkFromDefinition(createDefinition()) const addOns = getAllAddOns(framework, 'file-router', 'rsbuild') expect(addOns.length).toBeGreaterThan(0) - expect(addOns.every((addOn) => addOn.type === 'toolchain')).toBe(true) + expect( + addOns.every((addOn) => + ['toolchain', 'deployment'].includes(addOn.type), + ), + ).toBe(true) + expect( + addOns.filter((addOn) => addOn.type === 'deployment'), + ).toHaveLength(6) await expect( finalizeAddOns(framework, 'file-router', ['eslint'], 'rsbuild'), ).resolves.toEqual( @@ -121,7 +128,7 @@ describe('bundler project generation', () => { [incompatibleAddOn], 'rsbuild', ), - ).rejects.toThrow('Rsbuild currently supports toolchain add-ons only') + ).rejects.toThrow('Rsbuild supports toolchains and deployment adapters') }, ) diff --git a/packages/create/tests/deployment-bundlers.test.ts b/packages/create/tests/deployment-bundlers.test.ts new file mode 100644 index 000000000..5aac79dc7 --- /dev/null +++ b/packages/create/tests/deployment-bundlers.test.ts @@ -0,0 +1,210 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { finalizeAddOns } from '../src/add-ons.js' +import { createApp } from '../src/create-app.js' +import { createMemoryEnvironment } from '../src/environment.js' +import { isAddOnSupportedByBundler } from '../src/build-tools.js' +import { addToApp } from '../src/add-to-app.js' +import { __testClearFrameworks, registerFramework } from '../src/frameworks.js' +import { createFrameworkDefinition as react } from '../src/frameworks/react/index.js' +import { createFrameworkDefinition as solid } from '../src/frameworks/solid/index.js' + +import type { Framework, FrameworkDefinition, Options } from '../src/types.js' + +const presets = { + nitro: 'node-server', + railway: 'node-server', + render: 'render-com', + vercel: 'vercel', + netlify: 'netlify', + cloudflare: 'cloudflare-module', +} + +function frameworkFromDefinition(definition: FrameworkDefinition): Framework { + const { addOns, base, ...rest } = definition + return { + ...rest, + getFiles: async () => Object.keys(base), + getFileContents: async (path) => base[path], + getDeletedFiles: async () => [], + getAddOns: () => addOns, + } +} + +async function generate( + framework: Framework, + bundler: string, + ids: Array, + routerOnly = false, +) { + const { environment, output } = createMemoryEnvironment() + await createApp(environment, { + projectName: 'deployment-app', + targetDir: '/', + framework, + mode: 'file-router', + bundler, + typescript: true, + tailwind: false, + packageManager: 'pnpm', + git: false, + install: false, + intent: false, + chosenAddOns: await finalizeAddOns(framework, 'file-router', ids, bundler), + addOnOptions: {}, + projectPreset: 'blank', + routerOnly, + includeExamples: false, + } satisfies Options) + return Object.fromEntries( + Object.entries(output.files).map(([path, contents]) => [ + path.replace(/^\//, ''), + contents, + ]), + ) +} + +beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response(JSON.stringify({ version: '1.0.0' }))), + ) +}) + +afterEach(() => vi.unstubAllGlobals()) + +describe.each([ + ['react', react], + ['solid', solid], +] as const)('%s deployments', (_id, definition) => { + it.each(Object.entries(presets))( + 'generates the %s Nitro Rsbuild adapter', + async (id, preset) => { + const files = await generate( + frameworkFromDefinition(definition()), + 'rsbuild', + [id], + ) + const pkg = JSON.parse(files['package.json']) + const config = files['rsbuild.config.ts'] + + expect(config).toContain("import { nitro } from 'nitro/rsbuild'") + expect(config).toContain('installDevServerMiddleware: false') + expect(config).toContain(`nitro({ preset: '${preset}' })`) + expect(config.indexOf('tanstackStart({')).toBeLessThan( + config.indexOf('nitro({'), + ) + expect(files).not.toHaveProperty('vite.config.ts') + expect(pkg.dependencies.nitro).toBe('3.0.261103-rc') + expect(pkg.dependencies).not.toHaveProperty('srvx') + expect(pkg.devDependencies['@rsbuild/core']).toBe('^2.3.0') + expect(pkg.engines.node).toBe('>=22.22.0') + expect(pkg.scripts.build).toBe('rsbuild build') + expect(pkg.scripts.start).toBe( + ['nitro', 'railway', 'render'].includes(id) + ? 'node .output/server/index.mjs' + : 'rsbuild preview', + ) + expect({ + ...pkg.dependencies, + ...pkg.devDependencies, + }).not.toHaveProperty('@cloudflare/vite-plugin') + expect({ + ...pkg.dependencies, + ...pkg.devDependencies, + }).not.toHaveProperty('@netlify/vite-plugin-tanstack-start') + + if (id === 'cloudflare') { + expect(JSON.parse(files['wrangler.jsonc'])).not.toHaveProperty('main') + expect(pkg.scripts.deploy).toBe( + 'pnpm run build && wrangler deploy --config .output/server/wrangler.json', + ) + expect(pkg.devDependencies.wrangler).toBeDefined() + } else if (id === 'netlify') { + expect(files['netlify.toml']).toContain('command = "rsbuild build"') + expect(files['netlify.toml']).toContain('publish = "dist"') + } else if (id === 'vercel') { + expect(JSON.parse(files['vercel.json'])).toMatchObject({ + framework: null, + buildCommand: 'pnpm build', + }) + } else if (id === 'render') { + expect(files['render.yaml']).toContain('value: render-com') + expect(files['render.yaml']).toContain('startCommand: pnpm start') + } + }, + ) + + it.each(Object.keys(presets))('preserves the %s Vite adapter', async (id) => { + const files = await generate( + frameworkFromDefinition(definition()), + 'vite', + [id], + ) + const pkg = JSON.parse(files['package.json']) + expect(files).not.toHaveProperty('rsbuild.config.ts') + expect(files['vite.config.ts']).not.toContain('nitro/rsbuild') + expect(pkg.engines).toBeUndefined() + if (id === 'cloudflare') { + expect(JSON.parse(files['wrangler.jsonc']).main).toBe( + `@tanstack/${_id}-start/server-entry`, + ) + expect(pkg.scripts.deploy).toBe('pnpm run build && wrangler deploy') + } else if (id === 'netlify') { + expect(files['netlify.toml']).toContain('publish = "dist/client"') + expect( + pkg.devDependencies['@netlify/vite-plugin-tanstack-start'], + ).toBeDefined() + } else { + expect(pkg.dependencies.nitro).toBe('3.0.260610-beta') + } + }) + + it('rejects conflicting deployment adapters and router-only deployments', async () => { + const framework = frameworkFromDefinition(definition()) + await expect( + generate(framework, 'rsbuild', ['nitro', 'vercel']), + ).rejects.toThrow('Only one deployment adapter') + await expect( + generate(framework, 'rsbuild', ['nitro'], true), + ).rejects.toThrow('require TanStack Start') + }) + + it('adds a deployment to an existing Rsbuild app and rejects a second adapter', async () => { + __testClearFrameworks() + registerFramework(definition()) + const files = await generate( + frameworkFromDefinition(definition()), + 'rsbuild', + [], + ) + const { environment } = createMemoryEnvironment('/') + environment.startRun() + for (const [path, content] of Object.entries(files)) { + await environment.writeFile(`/${path}`, content) + } + + await addToApp(environment, ['nitro'], '/', { forced: true }) + expect(await environment.readFile('/rsbuild.config.ts')).toContain( + "nitro({ preset: 'node-server' })", + ) + expect( + JSON.parse(await environment.readFile('/package.json')).scripts.start, + ).toBe('node .output/server/index.mjs') + await expect( + addToApp(environment, ['vercel'], '/', { forced: true }), + ).rejects.toThrow('mutually exclusive') + expect( + JSON.parse(await environment.readFile('/.cta.json')).chosenAddOns, + ).toEqual(['nitro']) + }) +}) + +it('keeps Vite-only deployment adapters out of the Rsbuild catalog', () => { + expect( + isAddOnSupportedByBundler( + { type: 'deployment', integrations: [{ type: 'vite-plugin' }] }, + 'rsbuild', + ), + ).toBe(false) +}) diff --git a/packages/create/tests/edge-manifest.test.ts b/packages/create/tests/edge-manifest.test.ts index c9fee0dbf..033a3ec7f 100644 --- a/packages/create/tests/edge-manifest.test.ts +++ b/packages/create/tests/edge-manifest.test.ts @@ -95,7 +95,7 @@ describe('@tanstack/create/edge manifest', () => { ) }) - it('renders an Rsbuild app from the edge manifest', async () => { + it('renders an Rsbuild deployment from the edge manifest', async () => { try { vi.stubGlobal( 'fetch', @@ -122,13 +122,16 @@ describe('@tanstack/create/edge manifest', () => { git: false, install: false, intent: false, - chosenAddOns: [], + chosenAddOns: await finalizeAddOns(framework!, 'file-router', ['cloudflare'], 'rsbuild'), addOnOptions: {}, includeExamples: false, } satisfies Options) expect(output.files).toHaveProperty('/rsbuild-app/rsbuild.config.ts') expect(output.files).not.toHaveProperty('/rsbuild-app/vite.config.ts') + expect(output.files['/rsbuild-app/rsbuild.config.ts']).toContain("nitro({ preset: 'cloudflare-module' })") + expect(JSON.parse(output.files['/rsbuild-app/package.json']).dependencies.nitro).toBe('3.0.261103-rc') + expect(JSON.parse(output.files['/rsbuild-app/wrangler.jsonc'])).not.toHaveProperty('main') expect(JSON.parse(output.files['/rsbuild-app/.cta.json']).bundler).toBe( 'rsbuild', ) From d9b99cfae434df3465e35ef510c20da2795e12fd Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Tue, 10 Nov 2026 17:38:22 +0800 Subject: [PATCH 2/7] fix: declare Nitro development runtime dependencies --- packages/create/src/frameworks/react/project/packages.json | 4 ++++ packages/create/src/frameworks/solid/project/packages.json | 4 ++++ packages/create/tests/deployment-bundlers.test.ts | 2 ++ 3 files changed, 10 insertions(+) diff --git a/packages/create/src/frameworks/react/project/packages.json b/packages/create/src/frameworks/react/project/packages.json index d596565c9..cad750261 100644 --- a/packages/create/src/frameworks/react/project/packages.json +++ b/packages/create/src/frameworks/react/project/packages.json @@ -96,6 +96,10 @@ }, "scripts": { "start": "node .output/server/index.mjs" + }, + "dependencies": { + "db0": "^0.4.1", + "unstorage": "^2.0.0-alpha.10" } } } diff --git a/packages/create/src/frameworks/solid/project/packages.json b/packages/create/src/frameworks/solid/project/packages.json index 2afe6f29a..aedfaac3f 100644 --- a/packages/create/src/frameworks/solid/project/packages.json +++ b/packages/create/src/frameworks/solid/project/packages.json @@ -89,6 +89,10 @@ }, "scripts": { "start": "node .output/server/index.mjs" + }, + "dependencies": { + "db0": "^0.4.1", + "unstorage": "^2.0.0-alpha.10" } } } diff --git a/packages/create/tests/deployment-bundlers.test.ts b/packages/create/tests/deployment-bundlers.test.ts index 5aac79dc7..bfd84bbf0 100644 --- a/packages/create/tests/deployment-bundlers.test.ts +++ b/packages/create/tests/deployment-bundlers.test.ts @@ -96,6 +96,8 @@ describe.each([ ) expect(files).not.toHaveProperty('vite.config.ts') expect(pkg.dependencies.nitro).toBe('3.0.261103-rc') + expect(pkg.dependencies.db0).toBe('^0.4.1') + expect(pkg.dependencies.unstorage).toBe('^2.0.0-alpha.10') expect(pkg.dependencies).not.toHaveProperty('srvx') expect(pkg.devDependencies['@rsbuild/core']).toBe('^2.3.0') expect(pkg.engines.node).toBe('>=22.22.0') From 7106beb1e904f7051561eeb4b3dea310b0d5def0 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Tue, 10 Nov 2026 18:09:32 +0800 Subject: [PATCH 3/7] refactor: share Nitro version across build tools --- .changeset/rsbuild-nitro-deployments.md | 4 +++- .../create/src/frameworks/react/hosts/nitro/package.json | 8 ++++++++ .../src/frameworks/react/hosts/nitro/package.json.ejs | 5 ----- .../src/frameworks/react/hosts/railway/package.json.ejs | 3 ++- .../src/frameworks/react/hosts/render/package.json.ejs | 3 ++- .../src/frameworks/react/hosts/vercel/package.json.ejs | 3 ++- .../create/src/frameworks/solid/hosts/nitro/package.json | 8 ++++++++ .../src/frameworks/solid/hosts/nitro/package.json.ejs | 5 ----- .../hosts/railway/{package.json.ejs => package.json} | 5 ++++- .../solid/hosts/render/{package.json.ejs => package.json} | 5 ++++- .../src/frameworks/solid/hosts/vercel/package.json.ejs | 3 ++- packages/create/tests/deployment-bundlers.test.ts | 6 ++++-- packages/create/tests/partner-addons.test.ts | 6 +++--- 13 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 packages/create/src/frameworks/react/hosts/nitro/package.json delete mode 100644 packages/create/src/frameworks/react/hosts/nitro/package.json.ejs create mode 100644 packages/create/src/frameworks/solid/hosts/nitro/package.json delete mode 100644 packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs rename packages/create/src/frameworks/solid/hosts/railway/{package.json.ejs => package.json} (53%) rename packages/create/src/frameworks/solid/hosts/render/{package.json.ejs => package.json} (53%) diff --git a/.changeset/rsbuild-nitro-deployments.md b/.changeset/rsbuild-nitro-deployments.md index babb89ce6..3c7596a21 100644 --- a/.changeset/rsbuild-nitro-deployments.md +++ b/.changeset/rsbuild-nitro-deployments.md @@ -3,4 +3,6 @@ '@tanstack/cli': minor --- -Support Nitro, Railway, Render, Vercel, Netlify, and Cloudflare deployment adapters in React and Solid Rsbuild Start projects using Nitro's experimental Rsbuild integration. Deployment projects require Node.js >=22.22 and Rsbuild >=2.3. +Support Nitro, Railway, Render, Vercel, Netlify, and Cloudflare deployment adapters in React and Solid Rsbuild Start projects using Nitro's experimental Rsbuild integration. Rsbuild deployment projects require Node.js >=22.22 and Rsbuild >=2.3. + +Use Nitro 3.0.261103-rc for both Vite and Rsbuild deployment adapters. All projects using this Nitro release require Node.js >=22.22. diff --git a/packages/create/src/frameworks/react/hosts/nitro/package.json b/packages/create/src/frameworks/react/hosts/nitro/package.json new file mode 100644 index 000000000..bd46081b5 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/nitro/package.json @@ -0,0 +1,8 @@ +{ + "engines": { + "node": ">=22.22.0" + }, + "dependencies": { + "nitro": "3.0.261103-rc" + } +} diff --git a/packages/create/src/frameworks/react/hosts/nitro/package.json.ejs b/packages/create/src/frameworks/react/hosts/nitro/package.json.ejs deleted file mode 100644 index 51be2a91d..000000000 --- a/packages/create/src/frameworks/react/hosts/nitro/package.json.ejs +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" - } -} diff --git a/packages/create/src/frameworks/react/hosts/railway/package.json.ejs b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs index 3c6dc339d..b21a5f85e 100644 --- a/packages/create/src/frameworks/react/hosts/railway/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs @@ -1,8 +1,9 @@ { + "engines": { "node": ">=22.22.0" }, "scripts": { "start": "<%= addOnEnabled.sentry ? 'node --import ./.output/server/instrument.server.mjs .output/server/index.mjs' : 'node .output/server/index.mjs' %>" }, "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + "nitro": "3.0.261103-rc" } } diff --git a/packages/create/src/frameworks/react/hosts/render/package.json.ejs b/packages/create/src/frameworks/react/hosts/render/package.json.ejs index 3c6dc339d..b21a5f85e 100644 --- a/packages/create/src/frameworks/react/hosts/render/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/render/package.json.ejs @@ -1,8 +1,9 @@ { + "engines": { "node": ">=22.22.0" }, "scripts": { "start": "<%= addOnEnabled.sentry ? 'node --import ./.output/server/instrument.server.mjs .output/server/index.mjs' : 'node .output/server/index.mjs' %>" }, "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + "nitro": "3.0.261103-rc" } } diff --git a/packages/create/src/frameworks/react/hosts/vercel/package.json.ejs b/packages/create/src/frameworks/react/hosts/vercel/package.json.ejs index 22f9e05fc..925f9e5e6 100644 --- a/packages/create/src/frameworks/react/hosts/vercel/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/vercel/package.json.ejs @@ -1,8 +1,9 @@ { + "engines": { "node": ">=22.22.0" }, <% if (bundler === 'rsbuild') { -%> "scripts": { "start": "rsbuild preview" }, <% } -%> "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + "nitro": "3.0.261103-rc" } } diff --git a/packages/create/src/frameworks/solid/hosts/nitro/package.json b/packages/create/src/frameworks/solid/hosts/nitro/package.json new file mode 100644 index 000000000..bd46081b5 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/nitro/package.json @@ -0,0 +1,8 @@ +{ + "engines": { + "node": ">=22.22.0" + }, + "dependencies": { + "nitro": "3.0.261103-rc" + } +} diff --git a/packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs b/packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs deleted file mode 100644 index 51be2a91d..000000000 --- a/packages/create/src/frameworks/solid/hosts/nitro/package.json.ejs +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" - } -} diff --git a/packages/create/src/frameworks/solid/hosts/railway/package.json.ejs b/packages/create/src/frameworks/solid/hosts/railway/package.json similarity index 53% rename from packages/create/src/frameworks/solid/hosts/railway/package.json.ejs rename to packages/create/src/frameworks/solid/hosts/railway/package.json index a482f942f..a9ee531f0 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/package.json.ejs +++ b/packages/create/src/frameworks/solid/hosts/railway/package.json @@ -1,8 +1,11 @@ { + "engines": { + "node": ">=22.22.0" + }, "scripts": { "start": "node .output/server/index.mjs" }, "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + "nitro": "3.0.261103-rc" } } diff --git a/packages/create/src/frameworks/solid/hosts/render/package.json.ejs b/packages/create/src/frameworks/solid/hosts/render/package.json similarity index 53% rename from packages/create/src/frameworks/solid/hosts/render/package.json.ejs rename to packages/create/src/frameworks/solid/hosts/render/package.json index a482f942f..a9ee531f0 100644 --- a/packages/create/src/frameworks/solid/hosts/render/package.json.ejs +++ b/packages/create/src/frameworks/solid/hosts/render/package.json @@ -1,8 +1,11 @@ { + "engines": { + "node": ">=22.22.0" + }, "scripts": { "start": "node .output/server/index.mjs" }, "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + "nitro": "3.0.261103-rc" } } diff --git a/packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs b/packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs index 22f9e05fc..925f9e5e6 100644 --- a/packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs +++ b/packages/create/src/frameworks/solid/hosts/vercel/package.json.ejs @@ -1,8 +1,9 @@ { + "engines": { "node": ">=22.22.0" }, <% if (bundler === 'rsbuild') { -%> "scripts": { "start": "rsbuild preview" }, <% } -%> "dependencies": { - "nitro": "<%= bundler === 'rsbuild' ? '3.0.261103-rc' : '3.0.260610-beta' %>" + "nitro": "3.0.261103-rc" } } diff --git a/packages/create/tests/deployment-bundlers.test.ts b/packages/create/tests/deployment-bundlers.test.ts index bfd84bbf0..317b7e7d8 100644 --- a/packages/create/tests/deployment-bundlers.test.ts +++ b/packages/create/tests/deployment-bundlers.test.ts @@ -146,19 +146,21 @@ describe.each([ const pkg = JSON.parse(files['package.json']) expect(files).not.toHaveProperty('rsbuild.config.ts') expect(files['vite.config.ts']).not.toContain('nitro/rsbuild') - expect(pkg.engines).toBeUndefined() if (id === 'cloudflare') { + expect(pkg.engines).toBeUndefined() expect(JSON.parse(files['wrangler.jsonc']).main).toBe( `@tanstack/${_id}-start/server-entry`, ) expect(pkg.scripts.deploy).toBe('pnpm run build && wrangler deploy') } else if (id === 'netlify') { + expect(pkg.engines).toBeUndefined() expect(files['netlify.toml']).toContain('publish = "dist/client"') expect( pkg.devDependencies['@netlify/vite-plugin-tanstack-start'], ).toBeDefined() } else { - expect(pkg.dependencies.nitro).toBe('3.0.260610-beta') + expect(pkg.dependencies.nitro).toBe('3.0.261103-rc') + expect(pkg.engines.node).toBe('>=22.22.0') } }) diff --git a/packages/create/tests/partner-addons.test.ts b/packages/create/tests/partner-addons.test.ts index aae539450..ae2691cae 100644 --- a/packages/create/tests/partner-addons.test.ts +++ b/packages/create/tests/partner-addons.test.ts @@ -172,7 +172,7 @@ describe('partner add-on scaffolds', () => { expect(packageJSON.dependencies).toHaveProperty( 'nitro', - '3.0.260610-beta', + '3.0.261103-rc', ) expect(packageJSON.dependencies).not.toHaveProperty('nitro-nightly') expect(Object.values(packageJSON.dependencies)).not.toContain( @@ -259,7 +259,7 @@ describe('partner add-on scaffolds', () => { expect(packageJSON.dependencies).toHaveProperty( 'nitro', - '3.0.260610-beta', + '3.0.261103-rc', ) expect(packageJSON.scripts.start).toBe('node .output/server/index.mjs') expect(renderYaml).toContain('name: tanstack-start-app') @@ -327,7 +327,7 @@ describe('partner add-on scaffolds', () => { ]) expect(packageJSON.dependencies).toHaveProperty( 'nitro', - '3.0.260610-beta', + '3.0.261103-rc', ) expect(vercelJSON).toEqual({ $schema: 'https://openapi.vercel.sh/vercel.json', From 449d157b1cad395d973c6105c9e5d1e393ab1660 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Wed, 11 Nov 2026 10:03:49 +0800 Subject: [PATCH 4/7] fix: gate Cloudflare and preserve deployment engines --- .changeset/rsbuild-nitro-deployments.md | 4 +- .../references/bundlers.md | 4 +- .../references/deployment-providers.md | 6 +- packages/cli/tests-e2e/rsbuild-smoke.spec.ts | 3 +- packages/cli/tests/command-line.test.ts | 2 +- packages/cli/tests/options.test.ts | 43 +++++-- packages/create/package.json | 2 + packages/create/src/edge-package-json.ts | 7 ++ .../react/hosts/cloudflare/README.md | 11 ++ .../react/hosts/cloudflare/README.md.ejs | 24 ---- .../cloudflare/assets/wrangler.jsonc.ejs | 3 +- .../react/hosts/cloudflare/info.json | 10 +- .../react/hosts/cloudflare/package.json.ejs | 20 ---- .../solid/hosts/cloudflare/README.md | 11 ++ .../solid/hosts/cloudflare/README.md.ejs | 24 ---- .../{wrangler.jsonc.ejs => wrangler.jsonc} | 3 +- .../solid/hosts/cloudflare/info.json | 10 +- .../solid/hosts/cloudflare/package.json.ejs | 20 ---- packages/create/src/package-engines.ts | 28 +++++ packages/create/src/package-json.ts | 7 ++ packages/create/tests/bundlers.test.ts | 3 +- .../create/tests/deployment-bundlers.test.ts | 79 ++++++++----- packages/create/tests/edge-manifest.test.ts | 14 ++- packages/create/tests/package-engines.test.ts | 110 ++++++++++++++++++ pnpm-lock.yaml | 6 + 25 files changed, 294 insertions(+), 160 deletions(-) create mode 100644 packages/create/src/frameworks/react/hosts/cloudflare/README.md delete mode 100644 packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs create mode 100644 packages/create/src/frameworks/solid/hosts/cloudflare/README.md delete mode 100644 packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs rename packages/create/src/frameworks/solid/hosts/cloudflare/assets/{wrangler.jsonc.ejs => wrangler.jsonc} (81%) create mode 100644 packages/create/src/package-engines.ts create mode 100644 packages/create/tests/package-engines.test.ts diff --git a/.changeset/rsbuild-nitro-deployments.md b/.changeset/rsbuild-nitro-deployments.md index 3c7596a21..39c7eefcb 100644 --- a/.changeset/rsbuild-nitro-deployments.md +++ b/.changeset/rsbuild-nitro-deployments.md @@ -3,6 +3,8 @@ '@tanstack/cli': minor --- -Support Nitro, Railway, Render, Vercel, Netlify, and Cloudflare deployment adapters in React and Solid Rsbuild Start projects using Nitro's experimental Rsbuild integration. Rsbuild deployment projects require Node.js >=22.22 and Rsbuild >=2.3. +Support Nitro, Railway, Render, Vercel, and Netlify deployment adapters in React and Solid Rsbuild Start projects using Nitro's experimental Rsbuild integration. Rsbuild deployment projects require Node.js >=22.22 and Rsbuild >=2.3. Cloudflare remains Vite-only until its Nitro development runtime is compatible with Workerd. Use Nitro 3.0.261103-rc for both Vite and Rsbuild deployment adapters. All projects using this Nitro release require Node.js >=22.22. + +Preserve all engine requirements when merging package overlays so subsequent add-ons cannot lower a deployment adapter's Node.js minimum. diff --git a/packages/cli/skills/create-app-scaffold/references/bundlers.md b/packages/cli/skills/create-app-scaffold/references/bundlers.md index ffc2c187d..941dca431 100644 --- a/packages/cli/skills/create-app-scaffold/references/bundlers.md +++ b/packages/cli/skills/create-app-scaffold/references/bundlers.md @@ -20,8 +20,10 @@ Biome, and Prettier. Templates and catalog business/example add-ons are not supported. Full Start projects support `--deployment nitro`, `railway`, `render`, -`vercel`, `netlify`, and `cloudflare` through the experimental `nitro/rsbuild` +`vercel`, and `netlify` through the experimental `nitro/rsbuild` integration. These projects require Node.js >=22.22 and Rsbuild >=2.3. +Cloudflare remains Vite-only until Nitro's Rsbuild development runtime works +with Workerd. Router-only projects do not support deployment adapters. Existing Rsbuild Start projects can add one deployment adapter with `tanstack add`. 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 53fd8d925..f2fca6449 100644 --- a/packages/cli/skills/create-app-scaffold/references/deployment-providers.md +++ b/packages/cli/skills/create-app-scaffold/references/deployment-providers.md @@ -23,7 +23,9 @@ Deployment providers are exclusive; choose one per scaffold. ## Rsbuild -The same deployment ids are available for full TanStack Start projects with -`--build-tool rsbuild`. They use Nitro's experimental Rsbuild integration +The `nitro`, `railway`, `render`, `vercel`, and `netlify` deployment ids are +available for full TanStack Start projects with `--build-tool rsbuild`. +Cloudflare remains Vite-only until Nitro's Rsbuild development runtime works +with Workerd. Supported Rsbuild adapters use Nitro's experimental integration and require Node.js >=22.22 and Rsbuild >=2.3. Router-only projects do not support deployment adapters. diff --git a/packages/cli/tests-e2e/rsbuild-smoke.spec.ts b/packages/cli/tests-e2e/rsbuild-smoke.spec.ts index 891375a71..990c71209 100644 --- a/packages/cli/tests-e2e/rsbuild-smoke.spec.ts +++ b/packages/cli/tests-e2e/rsbuild-smoke.spec.ts @@ -50,7 +50,8 @@ test('@matrix lists toolchains and deployment adapters for Rsbuild', async () => expect( addOns.every((addOn) => ['toolchain', 'deployment'].includes(addOn.type)), ).toBe(true) - expect(addOns.filter((addOn) => addOn.type === 'deployment')).toHaveLength(6) + expect(addOns.filter((addOn) => addOn.type === 'deployment')).toHaveLength(5) + expect(addOns).not.toContainEqual(expect.objectContaining({ id: 'cloudflare' })) }) test('@matrix rejects an Rsbuild business add-on before creating a directory', async () => { diff --git a/packages/cli/tests/command-line.test.ts b/packages/cli/tests/command-line.test.ts index 9422ed22b..4bd60033f 100644 --- a/packages/cli/tests/command-line.test.ts +++ b/packages/cli/tests/command-line.test.ts @@ -837,7 +837,7 @@ describe('validateLegacyCreateFlags', () => { }, ) - it.each(['nitro', 'railway', 'render', 'vercel', 'netlify', 'cloudflare'])( + it.each(['nitro', 'railway', 'render', 'vercel', 'netlify'])( 'allows Rsbuild with the %s deployment flag', (deployment) => { expect( diff --git a/packages/cli/tests/options.test.ts b/packages/cli/tests/options.test.ts index 8b622dd77..8b5332355 100644 --- a/packages/cli/tests/options.test.ts +++ b/packages/cli/tests/options.test.ts @@ -53,11 +53,19 @@ beforeEach(() => { type: 'deployment', modes: ['file-router'], exclusive: ['deploy'], + integrations: [{ type: 'vite-plugin' }], + }, + { + id: 'nitro', + name: 'Nitro', + type: 'deployment', + modes: ['file-router'], + exclusive: ['deploy'], integrations: [ { type: 'rsbuild-plugin', import: "import { nitro } from 'nitro/rsbuild'", - code: "nitro({ preset: 'cloudflare-module' })", + code: "nitro({ preset: 'node-server' })", }, ], }, @@ -310,13 +318,30 @@ describe('promptForCreateOptions', () => { { ...baseCliOptions, blank: true, - deployment: forced ? undefined : 'cloudflare', + deployment: forced ? undefined : 'nitro', }, - forced ? { forcedDeployment: 'cloudflare' } : {}, + forced ? { forcedDeployment: 'nitro' } : {}, ) - expect(options?.chosenAddOns.map((addOn) => addOn.id)).toEqual([ - 'cloudflare', - ]) + expect(options?.chosenAddOns.map((addOn) => addOn.id)).toEqual(['nitro']) + }, + ) + + it.each([false, true])( + 'rejects the Vite-only Cloudflare deployment for Rsbuild (forced=%s)', + async (forced) => { + setBasicSpies() + vi.mocked(prompts.selectBundler).mockResolvedValue('rsbuild') + await expect( + promptForCreateOptions( + { + ...baseCliOptions, + buildTool: 'rsbuild', + blank: true, + deployment: forced ? undefined : 'cloudflare', + }, + forced ? { forcedDeployment: 'cloudflare' } : {}, + ), + ).rejects.toThrow('"cloudflare" (Cloudflare) is not supported') }, ) @@ -475,7 +500,7 @@ describe('promptForCreateOptions', () => { describe('promptForAddOns', () => { it('offers a deployment to an existing Rsbuild Start app', async () => { vi.mocked(prompts.selectAddOns).mockReset() - vi.mocked(prompts.selectAddOns).mockResolvedValue(['cloudflare']) + vi.mocked(prompts.selectAddOns).mockResolvedValue(['nitro']) vi.spyOn(create, 'readConfigFile').mockResolvedValue({ projectName: 'test', framework: 'react', @@ -484,7 +509,7 @@ describe('promptForAddOns', () => { chosenAddOns: ['eslint'], version: 1, }) - await expect(promptForAddOns()).resolves.toEqual(['cloudflare']) + await expect(promptForAddOns()).resolves.toEqual(['nitro']) expect(prompts.selectAddOns).toHaveBeenCalledWith( expect.anything(), 'file-router', @@ -503,7 +528,7 @@ describe('promptForAddOns', () => { framework: 'react', bundler: 'rsbuild', mode: 'file-router', - chosenAddOns: ['eslint', 'cloudflare'], + chosenAddOns: ['eslint', 'nitro'], version: 1, }) diff --git a/packages/create/package.json b/packages/create/package.json index 699584e6e..15f6de8ad 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -78,6 +78,7 @@ "parse-gitignore": "^2.0.0", "prettier": "^3.5.0", "rimraf": "^6.0.1", + "semver": "^7.7.3", "yaml": "^2.8.2", "zod": "^3.24.2" }, @@ -85,6 +86,7 @@ "@types/ejs": "^3.1.5", "@types/node": "^22.13.4", "@types/parse-gitignore": "^1.0.2", + "@types/semver": "^7.7.0", "@vitest/coverage-v8": "4.1.5", "eslint": "^9.20.0", "memfs": "4.17.0", diff --git a/packages/create/src/edge-package-json.ts b/packages/create/src/edge-package-json.ts index 4b1bbe905..3027b28b0 100644 --- a/packages/create/src/edge-package-json.ts +++ b/packages/create/src/edge-package-json.ts @@ -1,6 +1,7 @@ import { renderForOptions } from './edge-render.js' import { formatCommand, sortObject } from './utils.js' import { getPackageManagerExecuteCommand } from './package-manager.js' +import { mergePackageEngines } from './package-engines.js' import type { Options } from './types.js' @@ -52,6 +53,12 @@ export function mergePackageJSON( }, } + const engines = mergePackageEngines( + packageJSON.engines as Record | undefined, + overlayPackageJSON?.engines as Record | undefined, + ) + if (engines) mergedPackageJSON.engines = engines + const packagePnpm = packageJSON.pnpm && typeof packageJSON.pnpm === 'object' ? (packageJSON.pnpm as Record) diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/README.md b/packages/create/src/frameworks/react/hosts/cloudflare/README.md new file mode 100644 index 000000000..125545475 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/cloudflare/README.md @@ -0,0 +1,11 @@ +## Deploy to Cloudflare Workers + +This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: + +1. Install Wrangler: `npm install -g wrangler` +2. Authenticate: `wrangler login` +3. Deploy: `npx wrangler deploy` + +For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. + +KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs deleted file mode 100644 index da5d8f178..000000000 --- a/packages/create/src/frameworks/react/hosts/cloudflare/README.md.ejs +++ /dev/null @@ -1,24 +0,0 @@ -<% if (bundler === 'rsbuild') { -%> -## Deploy to Cloudflare Workers - -This project uses the experimental Nitro Rsbuild integration with the Cloudflare Workers preset. - -1. Authenticate with `wrangler login`. -2. Run `<%= packageManager %> run deploy` to build and deploy the Worker. - -Edit `wrangler.jsonc` to configure your Worker name and KV, D1, R2, or Durable Object bindings. Nitro generates `.output/server/wrangler.json` with the built entry and assets directory; the deploy script uses that generated configuration. - -Set production secrets with `wrangler secret put MY_VAR`. -<% } else { -%> -## Deploy to Cloudflare Workers - -This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: - -1. Install Wrangler: `npm install -g wrangler` -2. Authenticate: `wrangler login` -3. Deploy: `npx wrangler deploy` - -For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. - -KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. -<% } -%> diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs index 2a0e61d7e..8122145a5 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs +++ b/packages/create/src/frameworks/react/hosts/cloudflare/assets/wrangler.jsonc.ejs @@ -2,7 +2,6 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "tanstack-start-app", "compatibility_date": "2025-09-02", - "compatibility_flags": ["nodejs_compat"]<% if (bundler !== 'rsbuild') { %>, + "compatibility_flags": ["nodejs_compat"], "main": "<%= addOnEnabled.sentry ? './src/worker.ts' : '@tanstack/react-start/server-entry' %>" - <% } -%> } diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/info.json b/packages/create/src/frameworks/react/hosts/cloudflare/info.json index d8442bcd9..60defee9d 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/info.json +++ b/packages/create/src/frameworks/react/hosts/cloudflare/info.json @@ -9,20 +9,12 @@ "exclusive": ["deploy"], "color": "#F38020", "priority": 200, - "partner": { - "id": "cloudflare", - "tier": "gold" - }, + "partner": { "id": "cloudflare", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", "import": "import { cloudflare } from '@cloudflare/vite-plugin'", "code": "cloudflare({ viteEnvironment: { name: 'ssr' } })" - }, - { - "type": "rsbuild-plugin", - "import": "import { nitro } from 'nitro/rsbuild'", - "code": "nitro({ preset: 'cloudflare-module' })" } ] } diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs index bdbfcb095..328265303 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs @@ -1,22 +1,3 @@ -<% if (bundler === 'rsbuild') { -%> -{ - "scripts": { - "start": "rsbuild preview", - "deploy": "<%= packageManager %> run build && wrangler deploy --config .output/server/wrangler.json" - }, - "dependencies": { - "nitro": "3.0.261103-rc" - }, - "devDependencies": { - "wrangler": "^4.70.0" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "workerd" - ] - } -} -<% } else { -%> { "scripts": { "deploy": "<%= packageManager %> run build && wrangler deploy" @@ -29,4 +10,3 @@ "onlyBuiltDependencies": ["sharp", "workerd"] } } -<% } -%> diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/README.md b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md new file mode 100644 index 000000000..125545475 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md @@ -0,0 +1,11 @@ +## Deploy to Cloudflare Workers + +This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: + +1. Install Wrangler: `npm install -g wrangler` +2. Authenticate: `wrangler login` +3. Deploy: `npx wrangler deploy` + +For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. + +KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs deleted file mode 100644 index da5d8f178..000000000 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/README.md.ejs +++ /dev/null @@ -1,24 +0,0 @@ -<% if (bundler === 'rsbuild') { -%> -## Deploy to Cloudflare Workers - -This project uses the experimental Nitro Rsbuild integration with the Cloudflare Workers preset. - -1. Authenticate with `wrangler login`. -2. Run `<%= packageManager %> run deploy` to build and deploy the Worker. - -Edit `wrangler.jsonc` to configure your Worker name and KV, D1, R2, or Durable Object bindings. Nitro generates `.output/server/wrangler.json` with the built entry and assets directory; the deploy script uses that generated configuration. - -Set production secrets with `wrangler secret put MY_VAR`. -<% } else { -%> -## Deploy to Cloudflare Workers - -This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: - -1. Install Wrangler: `npm install -g wrangler` -2. Authenticate: `wrangler login` -3. Deploy: `npx wrangler deploy` - -For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. - -KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. -<% } -%> diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc.ejs b/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc similarity index 81% rename from packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc.ejs rename to packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc index 22d4a9b29..810654d3b 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc.ejs +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/assets/wrangler.jsonc @@ -4,7 +4,6 @@ "compatibility_date": "2025-09-02", "compatibility_flags": [ "nodejs_compat" - ]<% if (bundler !== 'rsbuild') { %>, + ], "main": "@tanstack/solid-start/server-entry" - <% } -%> } \ No newline at end of file diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/info.json b/packages/create/src/frameworks/solid/hosts/cloudflare/info.json index d8442bcd9..60defee9d 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/info.json +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/info.json @@ -9,20 +9,12 @@ "exclusive": ["deploy"], "color": "#F38020", "priority": 200, - "partner": { - "id": "cloudflare", - "tier": "gold" - }, + "partner": { "id": "cloudflare", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", "import": "import { cloudflare } from '@cloudflare/vite-plugin'", "code": "cloudflare({ viteEnvironment: { name: 'ssr' } })" - }, - { - "type": "rsbuild-plugin", - "import": "import { nitro } from 'nitro/rsbuild'", - "code": "nitro({ preset: 'cloudflare-module' })" } ] } diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs b/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs index 261e2be13..129dd2993 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs @@ -1,22 +1,3 @@ -<% if (bundler === 'rsbuild') { -%> -{ - "scripts": { - "start": "rsbuild preview", - "deploy": "<%= packageManager %> run build && wrangler deploy --config .output/server/wrangler.json" - }, - "dependencies": { - "nitro": "3.0.261103-rc" - }, - "devDependencies": { - "wrangler": "^4.70.0" - }, - "pnpm": { - "onlyBuiltDependencies": [ - "workerd" - ] - } -} -<% } else { -%> { "scripts": { "deploy": "<%= packageManager %> run build && wrangler deploy" @@ -31,4 +12,3 @@ "onlyBuiltDependencies": ["sharp", "workerd"] } } -<% } -%> diff --git a/packages/create/src/package-engines.ts b/packages/create/src/package-engines.ts new file mode 100644 index 000000000..6e19c9c52 --- /dev/null +++ b/packages/create/src/package-engines.ts @@ -0,0 +1,28 @@ +import { Range, subset } from 'semver' + +export function mergePackageEngines( + base?: Record, + overlay?: Record, +) { + if (!base && !overlay) return undefined + + const engines = { ...base, ...overlay } + for (const [engine, range] of Object.entries(overlay ?? {})) { + const existing = base?.[engine] + if (!existing || subset(range, existing)) continue + if (subset(existing, range)) { + engines[engine] = existing + continue + } + + // Both requirements must hold, including every alternative of OR ranges. + engines[engine] = new Range(existing).set + .flatMap((left) => + new Range(range).set.map((right) => + [...left, ...right].map((comparator) => comparator.value).join(' '), + ), + ) + .join(' || ') + } + return engines +} diff --git a/packages/create/src/package-json.ts b/packages/create/src/package-json.ts index 20fed0e46..f84f01ac1 100644 --- a/packages/create/src/package-json.ts +++ b/packages/create/src/package-json.ts @@ -1,6 +1,7 @@ import { render } from 'ejs' import { formatCommand, sortObject } from './utils.js' import { getPackageManagerExecuteCommand } from './package-manager.js' +import { mergePackageEngines } from './package-engines.js' import type { Options } from './types.js' export function mergePackageJSON( @@ -24,6 +25,12 @@ export function mergePackageJSON( }, } + const engines = mergePackageEngines( + packageJSON.engines, + overlayPackageJSON?.engines, + ) + if (engines) mergedPackageJSON.engines = engines + const baseOnlyBuiltDependencies = Array.isArray( packageJSON.pnpm?.onlyBuiltDependencies, ) diff --git a/packages/create/tests/bundlers.test.ts b/packages/create/tests/bundlers.test.ts index 2486b04c4..dc0a08f6c 100644 --- a/packages/create/tests/bundlers.test.ts +++ b/packages/create/tests/bundlers.test.ts @@ -115,7 +115,8 @@ describe('bundler project generation', () => { ).toBe(true) expect( addOns.filter((addOn) => addOn.type === 'deployment'), - ).toHaveLength(6) + ).toHaveLength(5) + expect(addOns.map((addOn) => addOn.id)).not.toContain('cloudflare') await expect( finalizeAddOns(framework, 'file-router', ['eslint'], 'rsbuild'), ).resolves.toEqual( diff --git a/packages/create/tests/deployment-bundlers.test.ts b/packages/create/tests/deployment-bundlers.test.ts index 317b7e7d8..e9e3d7ace 100644 --- a/packages/create/tests/deployment-bundlers.test.ts +++ b/packages/create/tests/deployment-bundlers.test.ts @@ -17,7 +17,6 @@ const presets = { render: 'render-com', vercel: 'vercel', netlify: 'netlify', - cloudflare: 'cloudflare-module', } function frameworkFromDefinition(definition: FrameworkDefinition): Framework { @@ -116,13 +115,7 @@ describe.each([ ...pkg.devDependencies, }).not.toHaveProperty('@netlify/vite-plugin-tanstack-start') - if (id === 'cloudflare') { - expect(JSON.parse(files['wrangler.jsonc'])).not.toHaveProperty('main') - expect(pkg.scripts.deploy).toBe( - 'pnpm run build && wrangler deploy --config .output/server/wrangler.json', - ) - expect(pkg.devDependencies.wrangler).toBeDefined() - } else if (id === 'netlify') { + if (id === 'netlify') { expect(files['netlify.toml']).toContain('command = "rsbuild build"') expect(files['netlify.toml']).toContain('publish = "dist"') } else if (id === 'vercel') { @@ -137,31 +130,55 @@ describe.each([ }, ) - it.each(Object.keys(presets))('preserves the %s Vite adapter', async (id) => { - const files = await generate( - frameworkFromDefinition(definition()), - 'vite', - [id], - ) - const pkg = JSON.parse(files['package.json']) - expect(files).not.toHaveProperty('rsbuild.config.ts') - expect(files['vite.config.ts']).not.toContain('nitro/rsbuild') - if (id === 'cloudflare') { - expect(pkg.engines).toBeUndefined() - expect(JSON.parse(files['wrangler.jsonc']).main).toBe( - `@tanstack/${_id}-start/server-entry`, + it.each([...Object.keys(presets), 'cloudflare'])( + 'preserves the %s Vite adapter', + async (id) => { + const files = await generate( + frameworkFromDefinition(definition()), + 'vite', + [id], ) - expect(pkg.scripts.deploy).toBe('pnpm run build && wrangler deploy') - } else if (id === 'netlify') { - expect(pkg.engines).toBeUndefined() - expect(files['netlify.toml']).toContain('publish = "dist/client"') - expect( - pkg.devDependencies['@netlify/vite-plugin-tanstack-start'], - ).toBeDefined() - } else { - expect(pkg.dependencies.nitro).toBe('3.0.261103-rc') - expect(pkg.engines.node).toBe('>=22.22.0') + const pkg = JSON.parse(files['package.json']) + expect(files).not.toHaveProperty('rsbuild.config.ts') + expect(files['vite.config.ts']).not.toContain('nitro/rsbuild') + if (id === 'cloudflare') { + expect(pkg.engines).toBeUndefined() + expect(JSON.parse(files['wrangler.jsonc']).main).toBe( + `@tanstack/${_id}-start/server-entry`, + ) + expect(pkg.scripts.deploy).toBe('pnpm run build && wrangler deploy') + } else if (id === 'netlify') { + expect(pkg.engines).toBeUndefined() + expect(files['netlify.toml']).toContain('publish = "dist/client"') + expect( + pkg.devDependencies['@netlify/vite-plugin-tanstack-start'], + ).toBeDefined() + } else { + expect(pkg.dependencies.nitro).toBe('3.0.261103-rc') + expect(pkg.engines.node).toBe('>=22.22.0') + } + }, + ) + + it('rejects Cloudflare when creating or extending an Rsbuild app', async () => { + const framework = frameworkFromDefinition(definition()) + await expect( + generate(framework, 'rsbuild', ['cloudflare']), + ).rejects.toThrow('"cloudflare" (Cloudflare) is not supported') + + __testClearFrameworks() + registerFramework(definition()) + const files = await generate(framework, 'rsbuild', []) + const { environment, output } = createMemoryEnvironment('/') + environment.startRun() + for (const [path, content] of Object.entries(files)) { + await environment.writeFile(`/${path}`, content) } + const before = { ...output.files } + await expect( + addToApp(environment, ['cloudflare'], '/', { forced: true }), + ).rejects.toThrow('"cloudflare" (Cloudflare) is not supported') + expect(output.files).toEqual(before) }) it('rejects conflicting deployment adapters and router-only deployments', async () => { diff --git a/packages/create/tests/edge-manifest.test.ts b/packages/create/tests/edge-manifest.test.ts index 033a3ec7f..51cfbebe5 100644 --- a/packages/create/tests/edge-manifest.test.ts +++ b/packages/create/tests/edge-manifest.test.ts @@ -122,16 +122,16 @@ describe('@tanstack/create/edge manifest', () => { git: false, install: false, intent: false, - chosenAddOns: await finalizeAddOns(framework!, 'file-router', ['cloudflare'], 'rsbuild'), + chosenAddOns: await finalizeAddOns(framework!, 'file-router', ['netlify'], 'rsbuild'), addOnOptions: {}, includeExamples: false, } satisfies Options) expect(output.files).toHaveProperty('/rsbuild-app/rsbuild.config.ts') expect(output.files).not.toHaveProperty('/rsbuild-app/vite.config.ts') - expect(output.files['/rsbuild-app/rsbuild.config.ts']).toContain("nitro({ preset: 'cloudflare-module' })") + expect(output.files['/rsbuild-app/rsbuild.config.ts']).toContain("nitro({ preset: 'netlify' })") expect(JSON.parse(output.files['/rsbuild-app/package.json']).dependencies.nitro).toBe('3.0.261103-rc') - expect(JSON.parse(output.files['/rsbuild-app/wrangler.jsonc'])).not.toHaveProperty('main') + expect(output.files['/rsbuild-app/netlify.toml']).toContain('publish = "dist"') expect(JSON.parse(output.files['/rsbuild-app/.cta.json']).bundler).toBe( 'rsbuild', ) @@ -140,6 +140,14 @@ describe('@tanstack/create/edge manifest', () => { } }) + it.each(['react', 'solid'])('keeps Cloudflare out of the %s Rsbuild edge catalog', async (id) => { + const framework = getFrameworkById(id)! + expect(getAllAddOns(framework, 'file-router', 'rsbuild').map((addOn) => addOn.id)) + .not.toContain('cloudflare') + await expect(finalizeAddOns(framework, 'file-router', ['cloudflare'], 'rsbuild')) + .rejects.toThrow('is not supported') + }) + it('returns the same React add-ons as the Node filesystem-backed path', () => { const nodeFramework = frameworkFromDefinition(createReactFrameworkDefinition()) const edgeFramework = getFrameworkById('react') diff --git a/packages/create/tests/package-engines.test.ts b/packages/create/tests/package-engines.test.ts new file mode 100644 index 000000000..55a00ace9 --- /dev/null +++ b/packages/create/tests/package-engines.test.ts @@ -0,0 +1,110 @@ +import { satisfies } from 'semver' +import { describe, expect, it } from 'vitest' + +import { finalizeAddOns } from '../src/add-ons.js' +import { createPackageJSON, mergePackageJSON } from '../src/package-json.js' +import { + createPackageJSON as createEdgePackageJSON, + mergePackageJSON as mergeEdgePackageJSON, +} from '../src/edge-package-json.js' +import { getFrameworkById } from '../src/edge.js' +import { createFrameworkDefinition } from '../src/frameworks/react/index.js' + +import type { Framework, Options } from '../src/types.js' + +describe.each([ + ['Node', mergePackageJSON], + ['edge', mergeEdgePackageJSON], +] as const)('%s package engines', (_runtime, merge) => { + it.each([false, true])( + 'preserves the higher minimum in either overlay order (reverse=%s)', + (reverse) => { + const packages = [ + { engines: { node: '>=22.22.0', npm: '>=10' } }, + { engines: { node: '>=22.11.0', pnpm: '>=9' } }, + ] + if (reverse) packages.reverse() + const engines = merge(packages[0], packages[1]).engines as Record< + string, + string + > + expect(engines).toEqual({ node: '>=22.22.0', npm: '>=10', pnpm: '>=9' }) + expect(satisfies('22.21.0', engines.node)).toBe(false) + expect(satisfies('22.22.0', engines.node)).toBe(true) + }, + ) + + it('intersects upper bounds and alternative major ranges', () => { + const engines = merge( + { engines: { node: '^20.19.0 || ^22.12.0 || ^24.0.0' } }, + { engines: { node: '>=22.22.0 <25' } }, + ).engines as Record + for (const version of [ + '20.19.0', + '21.0.0', + '22.21.0', + '23.0.0', + '25.0.0', + ]) { + expect(satisfies(version, engines.node), version).toBe(false) + } + for (const version of ['22.22.0', '24.0.0']) { + expect(satisfies(version, engines.node), version).toBe(true) + } + }) + + it('does not relax mutually incompatible requirements', () => { + const engines = merge( + { engines: { node: '<22' } }, + { engines: { node: '>=22.22.0' } }, + ).engines as Record + for (const version of ['20.19.0', '22.21.0', '22.22.0', '24.0.0']) { + expect(satisfies(version, engines.node), version).toBe(false) + } + }) +}) + +describe.each([ + ['Node', createPackageJSON], + ['edge', createEdgePackageJSON], +] as const)('%s Nitro and WorkOS scaffolds', (runtime, create) => { + it.each( + ['nitro', 'railway', 'render', 'vercel'].flatMap((deployment) => + [false, true].map((workosFirst) => [deployment, workosFirst] as const), + ), + )( + 'preserves Nitro engines with %s (WorkOS first=%s)', + async (deployment, workosFirst) => { + const { addOns, base, ...definition } = createFrameworkDefinition() + const framework: Framework = + runtime === 'edge' + ? getFrameworkById('react')! + : { + ...definition, + getAddOns: () => addOns, + getFiles: async () => Object.keys(base), + getFileContents: async (path) => base[path], + getDeletedFiles: async () => [], + } + const ids = workosFirst ? ['workos', deployment] : [deployment, 'workos'] + const pkg = create({ + framework, + projectName: 'nitro-workos', + targetDir: '/', + bundler: 'vite', + mode: 'file-router', + typescript: true, + tailwind: false, + packageManager: 'pnpm', + chosenAddOns: await finalizeAddOns(framework, 'file-router', ids), + addOnOptions: {}, + projectPreset: 'blank', + } satisfies Options) + expect(pkg.engines).toEqual({ node: '>=22.22.0' }) + expect(pkg.dependencies).toMatchObject({ + nitro: '3.0.261103-rc', + '@workos-inc/node': '^10.8.0', + }) + }, + ) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 766689b5a..c68254914 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,9 @@ importers: rimraf: specifier: ^6.0.1 version: 6.0.1 + semver: + specifier: ^7.7.3 + version: 7.7.3 yaml: specifier: ^2.8.2 version: 2.8.2 @@ -282,6 +285,9 @@ importers: '@types/parse-gitignore': specifier: ^1.0.2 version: 1.0.2 + '@types/semver': + specifier: ^7.7.0 + version: 7.7.0 '@vitest/coverage-v8': specifier: 4.1.5 version: 4.1.5(vitest@4.1.5) From 69277650a924a24e2a7c669df82cfa486f5ceb39 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Wed, 11 Nov 2026 10:27:34 +0800 Subject: [PATCH 5/7] docs: correct supported Rsbuild deployment targets --- docs/cli-reference.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 1f0596a88..4d9d185a1 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -59,11 +59,11 @@ Explicit add-ons and deployment adapters can add their own required files and dependencies. Add `-y` to use defaults for every remaining option; when the target is non-empty, also pass `--force` or the command exits without writing. -Rsbuild Start projects support all six deployment adapters through Nitro's +Rsbuild Start projects support five deployment adapters through Nitro's experimental Rsbuild integration: `nitro`, `railway`, `render`, `vercel`, -`netlify`, and `cloudflare`. Generated deployment projects require Node.js ->=22.22 and Rsbuild >=2.3. Router-only projects do not support deployment -adapters. +and `netlify`. Cloudflare remains Vite-only until Nitro's Rsbuild development +runtime works with Workerd. Generated deployment projects require Node.js +>=22.22 and Rsbuild >=2.3. Router-only projects do not support deployment adapters. ### Programmatic generation From da2b0ce0f28e4eb68c4fbed1e9855ec6f377e8d8 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Wed, 11 Nov 2026 13:28:33 +0800 Subject: [PATCH 6/7] test: remove deployment and engine test files --- .../create/tests/deployment-bundlers.test.ts | 231 ------------------ packages/create/tests/package-engines.test.ts | 110 --------- 2 files changed, 341 deletions(-) delete mode 100644 packages/create/tests/deployment-bundlers.test.ts delete mode 100644 packages/create/tests/package-engines.test.ts diff --git a/packages/create/tests/deployment-bundlers.test.ts b/packages/create/tests/deployment-bundlers.test.ts deleted file mode 100644 index e9e3d7ace..000000000 --- a/packages/create/tests/deployment-bundlers.test.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import { finalizeAddOns } from '../src/add-ons.js' -import { createApp } from '../src/create-app.js' -import { createMemoryEnvironment } from '../src/environment.js' -import { isAddOnSupportedByBundler } from '../src/build-tools.js' -import { addToApp } from '../src/add-to-app.js' -import { __testClearFrameworks, registerFramework } from '../src/frameworks.js' -import { createFrameworkDefinition as react } from '../src/frameworks/react/index.js' -import { createFrameworkDefinition as solid } from '../src/frameworks/solid/index.js' - -import type { Framework, FrameworkDefinition, Options } from '../src/types.js' - -const presets = { - nitro: 'node-server', - railway: 'node-server', - render: 'render-com', - vercel: 'vercel', - netlify: 'netlify', -} - -function frameworkFromDefinition(definition: FrameworkDefinition): Framework { - const { addOns, base, ...rest } = definition - return { - ...rest, - getFiles: async () => Object.keys(base), - getFileContents: async (path) => base[path], - getDeletedFiles: async () => [], - getAddOns: () => addOns, - } -} - -async function generate( - framework: Framework, - bundler: string, - ids: Array, - routerOnly = false, -) { - const { environment, output } = createMemoryEnvironment() - await createApp(environment, { - projectName: 'deployment-app', - targetDir: '/', - framework, - mode: 'file-router', - bundler, - typescript: true, - tailwind: false, - packageManager: 'pnpm', - git: false, - install: false, - intent: false, - chosenAddOns: await finalizeAddOns(framework, 'file-router', ids, bundler), - addOnOptions: {}, - projectPreset: 'blank', - routerOnly, - includeExamples: false, - } satisfies Options) - return Object.fromEntries( - Object.entries(output.files).map(([path, contents]) => [ - path.replace(/^\//, ''), - contents, - ]), - ) -} - -beforeEach(() => { - vi.stubGlobal( - 'fetch', - vi.fn(async () => new Response(JSON.stringify({ version: '1.0.0' }))), - ) -}) - -afterEach(() => vi.unstubAllGlobals()) - -describe.each([ - ['react', react], - ['solid', solid], -] as const)('%s deployments', (_id, definition) => { - it.each(Object.entries(presets))( - 'generates the %s Nitro Rsbuild adapter', - async (id, preset) => { - const files = await generate( - frameworkFromDefinition(definition()), - 'rsbuild', - [id], - ) - const pkg = JSON.parse(files['package.json']) - const config = files['rsbuild.config.ts'] - - expect(config).toContain("import { nitro } from 'nitro/rsbuild'") - expect(config).toContain('installDevServerMiddleware: false') - expect(config).toContain(`nitro({ preset: '${preset}' })`) - expect(config.indexOf('tanstackStart({')).toBeLessThan( - config.indexOf('nitro({'), - ) - expect(files).not.toHaveProperty('vite.config.ts') - expect(pkg.dependencies.nitro).toBe('3.0.261103-rc') - expect(pkg.dependencies.db0).toBe('^0.4.1') - expect(pkg.dependencies.unstorage).toBe('^2.0.0-alpha.10') - expect(pkg.dependencies).not.toHaveProperty('srvx') - expect(pkg.devDependencies['@rsbuild/core']).toBe('^2.3.0') - expect(pkg.engines.node).toBe('>=22.22.0') - expect(pkg.scripts.build).toBe('rsbuild build') - expect(pkg.scripts.start).toBe( - ['nitro', 'railway', 'render'].includes(id) - ? 'node .output/server/index.mjs' - : 'rsbuild preview', - ) - expect({ - ...pkg.dependencies, - ...pkg.devDependencies, - }).not.toHaveProperty('@cloudflare/vite-plugin') - expect({ - ...pkg.dependencies, - ...pkg.devDependencies, - }).not.toHaveProperty('@netlify/vite-plugin-tanstack-start') - - if (id === 'netlify') { - expect(files['netlify.toml']).toContain('command = "rsbuild build"') - expect(files['netlify.toml']).toContain('publish = "dist"') - } else if (id === 'vercel') { - expect(JSON.parse(files['vercel.json'])).toMatchObject({ - framework: null, - buildCommand: 'pnpm build', - }) - } else if (id === 'render') { - expect(files['render.yaml']).toContain('value: render-com') - expect(files['render.yaml']).toContain('startCommand: pnpm start') - } - }, - ) - - it.each([...Object.keys(presets), 'cloudflare'])( - 'preserves the %s Vite adapter', - async (id) => { - const files = await generate( - frameworkFromDefinition(definition()), - 'vite', - [id], - ) - const pkg = JSON.parse(files['package.json']) - expect(files).not.toHaveProperty('rsbuild.config.ts') - expect(files['vite.config.ts']).not.toContain('nitro/rsbuild') - if (id === 'cloudflare') { - expect(pkg.engines).toBeUndefined() - expect(JSON.parse(files['wrangler.jsonc']).main).toBe( - `@tanstack/${_id}-start/server-entry`, - ) - expect(pkg.scripts.deploy).toBe('pnpm run build && wrangler deploy') - } else if (id === 'netlify') { - expect(pkg.engines).toBeUndefined() - expect(files['netlify.toml']).toContain('publish = "dist/client"') - expect( - pkg.devDependencies['@netlify/vite-plugin-tanstack-start'], - ).toBeDefined() - } else { - expect(pkg.dependencies.nitro).toBe('3.0.261103-rc') - expect(pkg.engines.node).toBe('>=22.22.0') - } - }, - ) - - it('rejects Cloudflare when creating or extending an Rsbuild app', async () => { - const framework = frameworkFromDefinition(definition()) - await expect( - generate(framework, 'rsbuild', ['cloudflare']), - ).rejects.toThrow('"cloudflare" (Cloudflare) is not supported') - - __testClearFrameworks() - registerFramework(definition()) - const files = await generate(framework, 'rsbuild', []) - const { environment, output } = createMemoryEnvironment('/') - environment.startRun() - for (const [path, content] of Object.entries(files)) { - await environment.writeFile(`/${path}`, content) - } - const before = { ...output.files } - await expect( - addToApp(environment, ['cloudflare'], '/', { forced: true }), - ).rejects.toThrow('"cloudflare" (Cloudflare) is not supported') - expect(output.files).toEqual(before) - }) - - it('rejects conflicting deployment adapters and router-only deployments', async () => { - const framework = frameworkFromDefinition(definition()) - await expect( - generate(framework, 'rsbuild', ['nitro', 'vercel']), - ).rejects.toThrow('Only one deployment adapter') - await expect( - generate(framework, 'rsbuild', ['nitro'], true), - ).rejects.toThrow('require TanStack Start') - }) - - it('adds a deployment to an existing Rsbuild app and rejects a second adapter', async () => { - __testClearFrameworks() - registerFramework(definition()) - const files = await generate( - frameworkFromDefinition(definition()), - 'rsbuild', - [], - ) - const { environment } = createMemoryEnvironment('/') - environment.startRun() - for (const [path, content] of Object.entries(files)) { - await environment.writeFile(`/${path}`, content) - } - - await addToApp(environment, ['nitro'], '/', { forced: true }) - expect(await environment.readFile('/rsbuild.config.ts')).toContain( - "nitro({ preset: 'node-server' })", - ) - expect( - JSON.parse(await environment.readFile('/package.json')).scripts.start, - ).toBe('node .output/server/index.mjs') - await expect( - addToApp(environment, ['vercel'], '/', { forced: true }), - ).rejects.toThrow('mutually exclusive') - expect( - JSON.parse(await environment.readFile('/.cta.json')).chosenAddOns, - ).toEqual(['nitro']) - }) -}) - -it('keeps Vite-only deployment adapters out of the Rsbuild catalog', () => { - expect( - isAddOnSupportedByBundler( - { type: 'deployment', integrations: [{ type: 'vite-plugin' }] }, - 'rsbuild', - ), - ).toBe(false) -}) diff --git a/packages/create/tests/package-engines.test.ts b/packages/create/tests/package-engines.test.ts deleted file mode 100644 index 55a00ace9..000000000 --- a/packages/create/tests/package-engines.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { satisfies } from 'semver' -import { describe, expect, it } from 'vitest' - -import { finalizeAddOns } from '../src/add-ons.js' -import { createPackageJSON, mergePackageJSON } from '../src/package-json.js' -import { - createPackageJSON as createEdgePackageJSON, - mergePackageJSON as mergeEdgePackageJSON, -} from '../src/edge-package-json.js' -import { getFrameworkById } from '../src/edge.js' -import { createFrameworkDefinition } from '../src/frameworks/react/index.js' - -import type { Framework, Options } from '../src/types.js' - -describe.each([ - ['Node', mergePackageJSON], - ['edge', mergeEdgePackageJSON], -] as const)('%s package engines', (_runtime, merge) => { - it.each([false, true])( - 'preserves the higher minimum in either overlay order (reverse=%s)', - (reverse) => { - const packages = [ - { engines: { node: '>=22.22.0', npm: '>=10' } }, - { engines: { node: '>=22.11.0', pnpm: '>=9' } }, - ] - if (reverse) packages.reverse() - const engines = merge(packages[0], packages[1]).engines as Record< - string, - string - > - expect(engines).toEqual({ node: '>=22.22.0', npm: '>=10', pnpm: '>=9' }) - expect(satisfies('22.21.0', engines.node)).toBe(false) - expect(satisfies('22.22.0', engines.node)).toBe(true) - }, - ) - - it('intersects upper bounds and alternative major ranges', () => { - const engines = merge( - { engines: { node: '^20.19.0 || ^22.12.0 || ^24.0.0' } }, - { engines: { node: '>=22.22.0 <25' } }, - ).engines as Record - for (const version of [ - '20.19.0', - '21.0.0', - '22.21.0', - '23.0.0', - '25.0.0', - ]) { - expect(satisfies(version, engines.node), version).toBe(false) - } - for (const version of ['22.22.0', '24.0.0']) { - expect(satisfies(version, engines.node), version).toBe(true) - } - }) - - it('does not relax mutually incompatible requirements', () => { - const engines = merge( - { engines: { node: '<22' } }, - { engines: { node: '>=22.22.0' } }, - ).engines as Record - for (const version of ['20.19.0', '22.21.0', '22.22.0', '24.0.0']) { - expect(satisfies(version, engines.node), version).toBe(false) - } - }) -}) - -describe.each([ - ['Node', createPackageJSON], - ['edge', createEdgePackageJSON], -] as const)('%s Nitro and WorkOS scaffolds', (runtime, create) => { - it.each( - ['nitro', 'railway', 'render', 'vercel'].flatMap((deployment) => - [false, true].map((workosFirst) => [deployment, workosFirst] as const), - ), - )( - 'preserves Nitro engines with %s (WorkOS first=%s)', - async (deployment, workosFirst) => { - const { addOns, base, ...definition } = createFrameworkDefinition() - const framework: Framework = - runtime === 'edge' - ? getFrameworkById('react')! - : { - ...definition, - getAddOns: () => addOns, - getFiles: async () => Object.keys(base), - getFileContents: async (path) => base[path], - getDeletedFiles: async () => [], - } - const ids = workosFirst ? ['workos', deployment] : [deployment, 'workos'] - const pkg = create({ - framework, - projectName: 'nitro-workos', - targetDir: '/', - bundler: 'vite', - mode: 'file-router', - typescript: true, - tailwind: false, - packageManager: 'pnpm', - chosenAddOns: await finalizeAddOns(framework, 'file-router', ids), - addOnOptions: {}, - projectPreset: 'blank', - } satisfies Options) - expect(pkg.engines).toEqual({ node: '>=22.22.0' }) - expect(pkg.dependencies).toMatchObject({ - nitro: '3.0.261103-rc', - '@workos-inc/node': '^10.8.0', - }) - }, - ) -}) From 71c1f794145083a8a8accc855ffa490e86caae02 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Thu, 12 Nov 2026 10:06:54 +0800 Subject: [PATCH 7/7] style: remove unrelated formatting changes from deployment diff --- packages/cli/tests/command-line.test.ts | 5 ++- packages/cli/tests/options.test.ts | 38 ++++++++++--------- packages/create/src/edge-package-json.ts | 16 ++++---- .../frameworks/react/hosts/netlify/info.json | 5 +-- .../frameworks/react/hosts/railway/info.json | 5 +-- .../frameworks/react/hosts/render/info.json | 5 +-- .../frameworks/react/hosts/vercel/info.json | 5 +-- .../frameworks/solid/hosts/netlify/info.json | 5 +-- .../frameworks/solid/hosts/railway/info.json | 5 +-- .../frameworks/solid/hosts/render/info.json | 5 +-- .../frameworks/solid/hosts/vercel/info.json | 5 +-- packages/create/src/package-json.ts | 9 +++-- 12 files changed, 45 insertions(+), 63 deletions(-) diff --git a/packages/cli/tests/command-line.test.ts b/packages/cli/tests/command-line.test.ts index 4bd60033f..444579fa4 100644 --- a/packages/cli/tests/command-line.test.ts +++ b/packages/cli/tests/command-line.test.ts @@ -5,7 +5,10 @@ import { normalizeOptions, validateLegacyCreateFlags, } from '../src/command-line.js' -import { sanitizePackageName, getCurrentDirectoryName } from '../src/utils.js' +import { + sanitizePackageName, + getCurrentDirectoryName, +} from '../src/utils.js' import { __testRegisterFramework, __testClearFrameworks, diff --git a/packages/cli/tests/options.test.ts b/packages/cli/tests/options.test.ts index 8b5332355..2bd3b572b 100644 --- a/packages/cli/tests/options.test.ts +++ b/packages/cli/tests/options.test.ts @@ -10,7 +10,10 @@ import * as create from '@tanstack/create' import * as prompts from '../src/ui-prompts' import * as commandLine from '../src/command-line' -import { getCurrentDirectoryName, sanitizePackageName } from '../src/utils' +import { + getCurrentDirectoryName, + sanitizePackageName, +} from '../src/utils' import type { Framework } from '@tanstack/create' @@ -113,13 +116,14 @@ const baseCliOptions: CliOptions = { function setBasicSpies() { vi.spyOn(prompts, 'selectBundler').mockImplementation(async () => 'vite') - vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation( - async () => [], - ) - vi.spyOn(commandLine, 'resolveStarterSpecifier').mockImplementation( - async (value) => - value === 'blog' ? 'https://example.com/react/blog/starter.json' : value, - ) + vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation(async () => []) + vi + .spyOn(commandLine, 'resolveStarterSpecifier') + .mockImplementation(async (value) => + value === 'blog' + ? 'https://example.com/react/blog/starter.json' + : value, + ) vi.spyOn(create, 'loadStarter').mockImplementation( async (id) => ({ @@ -212,16 +216,14 @@ describe('promptForCreateOptions', () => { it('prompts for templates when none was provided', async () => { setBasicSpies() - vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation( - async () => [ - { - id: 'blog', - name: 'Blog', - description: 'Blog template', - framework: 'react', - }, - ], - ) + vi.spyOn(commandLine, 'listTemplateChoices').mockImplementation(async () => [ + { + id: 'blog', + name: 'Blog', + description: 'Blog template', + framework: 'react', + }, + ]) await promptForCreateOptions(baseCliOptions, {}) diff --git a/packages/create/src/edge-package-json.ts b/packages/create/src/edge-package-json.ts index 3027b28b0..8bfab5cb5 100644 --- a/packages/create/src/edge-package-json.ts +++ b/packages/create/src/edge-package-json.ts @@ -80,7 +80,10 @@ export function mergePackageJSON( : [] const onlyBuiltDependencies = [ - ...new Set([...baseOnlyBuiltDependencies, ...overlayOnlyBuiltDependencies]), + ...new Set([ + ...baseOnlyBuiltDependencies, + ...overlayOnlyBuiltDependencies, + ]), ] if (packagePnpm || overlayPnpm) { @@ -114,16 +117,13 @@ export function createPackageJSON(options: Options) { pkg: string, args: Array = [], ) { - return formatCommand( - getPackageManagerExecuteCommand(packageManager, pkg, args), - ) + return formatCommand(getPackageManagerExecuteCommand(packageManager, pkg, args)) } let packageJSON: Record = { - ...(JSON.parse(JSON.stringify(options.framework.basePackageJSON)) as Record< - string, - unknown - >), + ...(JSON.parse( + JSON.stringify(options.framework.basePackageJSON), + ) as Record), name: options.projectName, } diff --git a/packages/create/src/frameworks/react/hosts/netlify/info.json b/packages/create/src/frameworks/react/hosts/netlify/info.json index 7e96f7a04..d2e2c00a7 100644 --- a/packages/create/src/frameworks/react/hosts/netlify/info.json +++ b/packages/create/src/frameworks/react/hosts/netlify/info.json @@ -9,10 +9,7 @@ "exclusive": ["deploy"], "color": "#00C7B7", "priority": 180, - "partner": { - "id": "netlify", - "tier": "gold" - }, + "partner": { "id": "netlify", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/frameworks/react/hosts/railway/info.json b/packages/create/src/frameworks/react/hosts/railway/info.json index bf9babec9..d7a04c5f2 100644 --- a/packages/create/src/frameworks/react/hosts/railway/info.json +++ b/packages/create/src/frameworks/react/hosts/railway/info.json @@ -9,10 +9,7 @@ "exclusive": ["deploy"], "color": "#9B4DCA", "priority": 160, - "partner": { - "id": "railway", - "tier": "gold" - }, + "partner": { "id": "railway", "tier": "gold" }, "deletedFiles": ["nixpacks.toml"], "integrations": [ { diff --git a/packages/create/src/frameworks/react/hosts/render/info.json b/packages/create/src/frameworks/react/hosts/render/info.json index 496cc2e19..6f58768dd 100644 --- a/packages/create/src/frameworks/react/hosts/render/info.json +++ b/packages/create/src/frameworks/react/hosts/render/info.json @@ -10,10 +10,7 @@ "exclusive": ["deploy"], "color": "#46E3B7", "priority": 150, - "partner": { - "id": "render", - "tier": "gold" - }, + "partner": { "id": "render", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/frameworks/react/hosts/vercel/info.json b/packages/create/src/frameworks/react/hosts/vercel/info.json index 64d0868b8..a4ac40701 100644 --- a/packages/create/src/frameworks/react/hosts/vercel/info.json +++ b/packages/create/src/frameworks/react/hosts/vercel/info.json @@ -10,10 +10,7 @@ "exclusive": ["deploy"], "color": "#000000", "priority": 140, - "partner": { - "id": "vercel", - "tier": "gold" - }, + "partner": { "id": "vercel", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/frameworks/solid/hosts/netlify/info.json b/packages/create/src/frameworks/solid/hosts/netlify/info.json index 7e96f7a04..d2e2c00a7 100644 --- a/packages/create/src/frameworks/solid/hosts/netlify/info.json +++ b/packages/create/src/frameworks/solid/hosts/netlify/info.json @@ -9,10 +9,7 @@ "exclusive": ["deploy"], "color": "#00C7B7", "priority": 180, - "partner": { - "id": "netlify", - "tier": "gold" - }, + "partner": { "id": "netlify", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/frameworks/solid/hosts/railway/info.json b/packages/create/src/frameworks/solid/hosts/railway/info.json index cfc202c56..042b8efe4 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/info.json +++ b/packages/create/src/frameworks/solid/hosts/railway/info.json @@ -9,10 +9,7 @@ "exclusive": ["deploy"], "color": "#9B4DCA", "priority": 160, - "partner": { - "id": "railway", - "tier": "gold" - }, + "partner": { "id": "railway", "tier": "gold" }, "deletedFiles": ["nixpacks.toml"], "integrations": [ { diff --git a/packages/create/src/frameworks/solid/hosts/render/info.json b/packages/create/src/frameworks/solid/hosts/render/info.json index b95a00858..97f4d4664 100644 --- a/packages/create/src/frameworks/solid/hosts/render/info.json +++ b/packages/create/src/frameworks/solid/hosts/render/info.json @@ -10,10 +10,7 @@ "exclusive": ["deploy"], "color": "#46E3B7", "priority": 150, - "partner": { - "id": "render", - "tier": "gold" - }, + "partner": { "id": "render", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/frameworks/solid/hosts/vercel/info.json b/packages/create/src/frameworks/solid/hosts/vercel/info.json index 64d0868b8..a4ac40701 100644 --- a/packages/create/src/frameworks/solid/hosts/vercel/info.json +++ b/packages/create/src/frameworks/solid/hosts/vercel/info.json @@ -10,10 +10,7 @@ "exclusive": ["deploy"], "color": "#000000", "priority": 140, - "partner": { - "id": "vercel", - "tier": "gold" - }, + "partner": { "id": "vercel", "tier": "gold" }, "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/package-json.ts b/packages/create/src/package-json.ts index f84f01ac1..6712b0475 100644 --- a/packages/create/src/package-json.ts +++ b/packages/create/src/package-json.ts @@ -43,7 +43,10 @@ export function mergePackageJSON( : [] const onlyBuiltDependencies = [ - ...new Set([...baseOnlyBuiltDependencies, ...overlayOnlyBuiltDependencies]), + ...new Set([ + ...baseOnlyBuiltDependencies, + ...overlayOnlyBuiltDependencies, + ]), ] if (packageJSON.pnpm || overlayPackageJSON?.pnpm) { @@ -76,9 +79,7 @@ export function createPackageJSON(options: Options) { pkg: string, args: Array = [], ) { - return formatCommand( - getPackageManagerExecuteCommand(packageManager, pkg, args), - ) + return formatCommand(getPackageManagerExecuteCommand(packageManager, pkg, args)) } let packageJSON = {