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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/rsbuild-nitro-deployments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@tanstack/create': minor
'@tanstack/cli': minor
---

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.
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
7 changes: 7 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 five deployment adapters through Nitro's
experimental Rsbuild integration: `nitro`, `railway`, `render`, `vercel`,
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

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.
Expand Down
19 changes: 15 additions & 4 deletions packages/cli/skills/create-app-scaffold/references/bundlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ 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`, 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`.

```bash
npx @tanstack/cli create app --build-tool rsbuild --deployment nitro -y
```

Use the build-tool-aware catalog view before constructing an Rsbuild command:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ npx @tanstack/cli create app --deployment vercel -y
```

Deployment providers are exclusive; choose one per scaffold.

## Rsbuild

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.
13 changes: 2 additions & 11 deletions packages/cli/src/command-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
}
}
}
Expand Down Expand Up @@ -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.',
)
}
}
Expand Down
56 changes: 34 additions & 22 deletions packages/cli/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand All @@ -189,6 +186,7 @@ export async function promptForCreateOptions(
options.framework,
cliOptions.deployment,
forcedDeployment,
options.bundler,
)
} else {
deployment = forcedDeployment
Expand Down Expand Up @@ -319,18 +317,24 @@ export async function promptForAddOns(): Promise<Array<string>> {
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()) ||
Expand All @@ -340,20 +344,28 @@ export async function promptForAddOns(): Promise<Array<string>> {
)
.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)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/ui-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,12 +415,12 @@ export async function selectDeployment(
framework: Framework,
deployment?: string,
forcedDeployment?: string,
bundlerId?: string,
): Promise<string | undefined> {
const deployments = new Set<AddOn>()
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',
Expand Down
32 changes: 22 additions & 10 deletions packages/cli/tests-e2e/rsbuild-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
[
Expand All @@ -40,10 +47,14 @@ 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(5)
expect(addOns).not.toContainEqual(expect.objectContaining({ id: 'cloudflare' }))
})

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'

Expand All @@ -58,8 +69,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',
Expand All @@ -75,7 +86,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 })
Expand All @@ -84,13 +95,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',
Expand Down
14 changes: 11 additions & 3 deletions packages/cli/tests/command-line.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,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 () => {
Expand Down Expand Up @@ -827,8 +827,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) => {
Expand All @@ -841,6 +840,15 @@ describe('validateLegacyCreateFlags', () => {
},
)

it.each(['nitro', 'railway', 'render', 'vercel', 'netlify'])(
'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',
Expand Down
Loading