Skip to content

Commit e49369d

Browse files
committed
feat(cli): restore router-only file-router compatibility mode
1 parent 23e4e62 commit e49369d

16 files changed

Lines changed: 275 additions & 22 deletions

File tree

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
#!/usr/bin/env node
2-
console.warn('\x1b[33m%s\x1b[0m', 'Warning: create-tsrouter-app is deprecated. Use "tanstack create" or "npx @tanstack/cli create" instead.')
3-
console.warn('\x1b[33m%s\x1b[0m', ' This will now create a TanStack Start app (with SSR). See: https://tanstack.com/start/latest/docs/framework/react/quick-start\n')
2+
console.warn('\x1b[33m%s\x1b[0m', 'Warning: create-tsrouter-app is deprecated. Use "tanstack create --router-only" or "npx @tanstack/cli create --router-only" instead.')
3+
console.warn('\x1b[33m%s\x1b[0m', ' This defaults to router-only compatibility mode (file-based routing, no Start-specific add-ons).\n')
44

55
import { cli } from '@tanstack/cli'
66

77
cli({
88
name: 'create-tsrouter-app',
9-
appName: 'TanStack Start',
9+
appName: 'TanStack',
1010
legacyAutoCreate: true,
11+
defaultRouterOnly: true,
1112
})

packages/cli/src/cli.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function cli({
5555
frameworkDefinitionInitializers,
5656
showDeploymentOptions = false,
5757
legacyAutoCreate = false,
58+
defaultRouterOnly = false,
5859
}: {
5960
name: string
6061
appName: string
@@ -65,6 +66,7 @@ export function cli({
6566
frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
6667
showDeploymentOptions?: boolean
6768
legacyAutoCreate?: boolean
69+
defaultRouterOnly?: boolean
6870
}) {
6971
const environment = createUIEnvironment(appName, false)
7072

@@ -307,6 +309,18 @@ export function cli({
307309
...options,
308310
} as CliOptions
309311

312+
if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
313+
cliOptions.routerOnly = true
314+
}
315+
316+
if (
317+
cliOptions.routerOnly !== true &&
318+
cliOptions.template &&
319+
cliOptions.template.toLowerCase() !== 'file-router'
320+
) {
321+
cliOptions.routerOnly = true
322+
}
323+
310324
cliOptions.framework = getFrameworkByName(
311325
options.framework || defaultFramework || 'React',
312326
)!.id
@@ -359,6 +373,9 @@ export function cli({
359373
throw new Error('No options were provided')
360374
}
361375

376+
;(finalOptions as Options & { routerOnly?: boolean }).routerOnly =
377+
!!cliOptions.routerOnly
378+
362379
// Determine target directory:
363380
// 1. Use --target-dir if provided
364381
// 2. Use targetDir from normalizeOptions if set (handles "." case)
@@ -435,7 +452,7 @@ export function cli({
435452
)
436453
.option(
437454
'--router-only',
438-
'Deprecated: compatibility flag from create-tsrouter-app',
455+
'Use router-only compatibility mode (file-based routing without TanStack Start)',
439456
)
440457
.option(
441458
'--template <type>',

packages/cli/src/command-line.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,26 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
3333

3434
if (cliOptions.routerOnly) {
3535
warnings.push(
36-
'The --router-only flag is deprecated and ignored. `tanstack create` already creates router-based apps.',
36+
'The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and starters are disabled; only the base template and optional toolchain are supported.',
3737
)
3838
}
3939

40+
if (cliOptions.routerOnly && cliOptions.addOns) {
41+
warnings.push(
42+
'Ignoring --add-ons in router-only compatibility mode.',
43+
)
44+
}
45+
46+
if (cliOptions.routerOnly && cliOptions.deployment) {
47+
warnings.push(
48+
'Ignoring --deployment in router-only compatibility mode.',
49+
)
50+
}
51+
52+
if (cliOptions.routerOnly && cliOptions.starter) {
53+
warnings.push('Ignoring --starter in router-only compatibility mode.')
54+
}
55+
4056
if (cliOptions.tailwind === true) {
4157
warnings.push(
4258
'The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.',
@@ -70,9 +86,7 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
7086
}
7187
}
7288

73-
warnings.push(
74-
'The --template flag is deprecated. TypeScript/TSX is the default and only supported template.',
75-
)
89+
warnings.push('The --template flag is deprecated and mapped for compatibility.')
7690

7791
return { warnings }
7892
}
@@ -110,8 +124,14 @@ export async function normalizeOptions(
110124

111125
// Mode is always file-router (TanStack Start)
112126
let mode = 'file-router'
127+
let routerOnly = !!cliOptions.routerOnly
128+
129+
const template = cliOptions.template?.toLowerCase().trim()
130+
if (template && template !== 'file-router') {
131+
routerOnly = true
132+
}
113133

114-
const starter = cliOptions.starter
134+
const starter = !routerOnly && cliOptions.starter
115135
? await loadStarter(cliOptions.starter)
116136
: undefined
117137

@@ -143,10 +163,10 @@ export async function normalizeOptions(
143163
cliOptions.deployment
144164
) {
145165
const selectedAddOns = new Set<string>([
146-
...(starter?.dependsOn || []),
147-
...(forcedAddOns || []),
166+
...(routerOnly ? [] : (starter?.dependsOn || [])),
167+
...(routerOnly ? [] : (forcedAddOns || [])),
148168
])
149-
if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
169+
if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
150170
for (const a of cliOptions.addOns) {
151171
if (a.toLowerCase() === 'start') {
152172
continue
@@ -157,11 +177,11 @@ export async function normalizeOptions(
157177
if (cliOptions.toolchain) {
158178
selectedAddOns.add(cliOptions.toolchain)
159179
}
160-
if (cliOptions.deployment) {
180+
if (!routerOnly && cliOptions.deployment) {
161181
selectedAddOns.add(cliOptions.deployment)
162182
}
163183

164-
if (!cliOptions.deployment && opts?.forcedDeployment) {
184+
if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
165185
selectedAddOns.add(opts.forcedDeployment)
166186
}
167187

packages/cli/src/options.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export async function promptForCreateOptions(
6060

6161
// Mode is always file-router (TanStack Start)
6262
options.mode = 'file-router'
63+
const template = cliOptions.template?.toLowerCase().trim()
64+
const routerOnly =
65+
!!cliOptions.routerOnly || (template ? template !== 'file-router' : false)
6366

6467
// TypeScript is always enabled with file-router
6568
options.typescript = true
@@ -81,7 +84,9 @@ export async function promptForCreateOptions(
8184

8285
// Deployment selection
8386
const deployment = showDeploymentOptions
84-
? await selectDeployment(options.framework, cliOptions.deployment)
87+
? routerOnly
88+
? undefined
89+
: await selectDeployment(options.framework, cliOptions.deployment)
8590
: undefined
8691

8792
// Add-ons selection
@@ -94,18 +99,20 @@ export async function promptForCreateOptions(
9499
addOns.add(deployment)
95100
}
96101

97-
for (const addOn of forcedAddOns) {
98-
addOns.add(addOn)
102+
if (!routerOnly) {
103+
for (const addOn of forcedAddOns) {
104+
addOns.add(addOn)
105+
}
99106
}
100107

101-
if (Array.isArray(cliOptions.addOns)) {
108+
if (!routerOnly && Array.isArray(cliOptions.addOns)) {
102109
for (const addOn of cliOptions.addOns) {
103110
if (addOn.toLowerCase() === 'start') {
104111
continue
105112
}
106113
addOns.add(addOn)
107114
}
108-
} else {
115+
} else if (!routerOnly) {
109116
for (const addOn of await selectAddOns(
110117
options.framework,
111118
options.mode,

packages/cli/tests/command-line.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,56 @@ describe('normalizeOptions', () => {
291291
expect(options?.typescript).toBe(true)
292292
})
293293

294+
it('should keep file-router mode in router-only compatibility mode', async () => {
295+
const options = await normalizeOptions({
296+
projectName: 'test',
297+
routerOnly: true,
298+
})
299+
300+
expect(options?.mode).toBe('file-router')
301+
})
302+
303+
it('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => {
304+
__testRegisterFramework({
305+
id: 'react-cra',
306+
name: 'react',
307+
getAddOns: () => [
308+
{
309+
id: 'form',
310+
name: 'Form',
311+
modes: ['file-router'],
312+
},
313+
{
314+
id: 'nitro',
315+
name: 'nitro',
316+
modes: ['file-router'],
317+
type: 'deployment',
318+
},
319+
{
320+
id: 'biome',
321+
name: 'Biome',
322+
modes: ['file-router'],
323+
type: 'toolchain',
324+
},
325+
],
326+
})
327+
328+
const options = await normalizeOptions(
329+
{
330+
projectName: 'test',
331+
framework: 'react-cra',
332+
routerOnly: true,
333+
addOns: ['form'],
334+
deployment: 'nitro',
335+
toolchain: 'biome',
336+
},
337+
['form'],
338+
{ forcedDeployment: 'nitro' },
339+
)
340+
341+
expect(options?.chosenAddOns.map((a) => a.id)).toEqual(['biome'])
342+
})
343+
294344
it('should handle the funky Windows edge case with CLI parsing', async () => {
295345
__testRegisterFramework({
296346
id: 'react-cra',
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<% if (!routerOnly) { ignoreFile() } %>
2+
<!doctype html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title><%= projectName %></title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<% if (!routerOnly) { ignoreFile() } %>
2+
import React from 'react'
3+
import ReactDOM from 'react-dom/client'
4+
import { RouterProvider, createRouter } from '@tanstack/react-router'
5+
import { routeTree } from './routeTree.gen'
6+
7+
const router = createRouter({
8+
routeTree,
9+
defaultPreload: 'intent',
10+
scrollRestoration: true,
11+
})
12+
13+
declare module '@tanstack/react-router' {
14+
interface Register {
15+
router: typeof router
16+
}
17+
}
18+
19+
const rootElement = document.getElementById('app')!
20+
21+
if (!rootElement.innerHTML) {
22+
const root = ReactDOM.createRoot(rootElement)
23+
root.render(<RouterProvider router={router} />)
24+
}

packages/create/src/frameworks/react/project/base/src/routes/__root.tsx.ejs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
<% if (routerOnly) { %>
2+
import { Outlet, createRootRoute } from '@tanstack/react-router'
3+
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
4+
import { TanStackDevtools } from '@tanstack/react-devtools'
5+
6+
import '../styles.css'
7+
8+
export const Route = createRootRoute({
9+
component: RootComponent,
10+
})
11+
12+
function RootComponent() {
13+
return (
14+
<>
15+
<Outlet />
16+
<TanStackDevtools
17+
config={{
18+
position: 'bottom-right',
19+
}}
20+
plugins={[
21+
{
22+
name: 'TanStack Router',
23+
render: <TanStackRouterDevtoolsPanel />,
24+
},
25+
]}
26+
/>
27+
</>
28+
)
29+
}
30+
<% } else { %>
131
<% let hasContext = addOnEnabled["apollo-client"] || addOnEnabled["tanstack-query"]; %>
232
import {
333
HeadContent, Scripts, <% if (hasContext) { %>createRootRouteWithContext<% } else { %>createRootRoute<% } %> } from '@tanstack/react-router'
@@ -93,3 +123,4 @@ function RootDocument({ children }: { children: React.ReactNode }) {
93123
</html>
94124
)
95125
}
126+
<% } %>

packages/create/src/frameworks/react/project/base/vite.config.ts.ejs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import tsconfigPaths from 'vite-tsconfig-paths'
44
<% if (addOnEnabled.paraglide) { -%>
55
import { paraglideVitePlugin } from "@inlang/paraglide-js"
66
<% } -%>
7+
<% if (routerOnly) { %>
8+
import { tanstackRouter } from '@tanstack/router-plugin/vite'
9+
<% } else { %>
710
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
11+
<% } %>
812
import viteReact from '@vitejs/plugin-react'
913
import tailwindcss from "@tailwindcss/vite"
1014
<% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportContent(integration) %>
@@ -18,7 +22,7 @@ const config = defineConfig({
1822
}), <% } %><% for(const integration of integrations.filter(i => i.type === 'vite-plugin')) { %><%- integrationImportCode(integration) %>,<% } %>
1923
tsconfigPaths({ projects: ['./tsconfig.json'] }),
2024
tailwindcss(),
21-
tanstackStart(),
25+
<% if (routerOnly) { %>tanstackRouter({ target: 'react', autoCodeSplitting: true }),<% } else { %>tanstackStart(),<% } %>
2226
viteReact(<% if (addOnEnabled.compiler) { %>{
2327
babel: {
2428
plugins: ["babel-plugin-react-compiler"],
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<% if (!routerOnly) { ignoreFile() } %>
2+
<!doctype html>
3+
<html lang="en">
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title><%= projectName %></title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)