@tanstack/cli 0.59.7 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+
3
+ export default defineConfig({
4
+ testDir: './tests-e2e',
5
+ fullyParallel: false,
6
+ forbidOnly: !!process.env.CI,
7
+ retries: process.env.CI ? 1 : 0,
8
+ workers: 1,
9
+ timeout: 10 * 60 * 1000,
10
+ expect: {
11
+ timeout: 20 * 1000,
12
+ },
13
+ reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
14
+ use: {
15
+ ...devices['Desktop Chrome'],
16
+ channel: 'chrome',
17
+ trace: 'on-first-retry',
18
+ screenshot: 'only-on-failure',
19
+ video: 'retain-on-failure',
20
+ },
21
+ })
package/src/bin.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { cli } from './cli.js'
3
+ import {
4
+ createReactFrameworkDefinition,
5
+ createSolidFrameworkDefinition,
6
+ } from '@tanstack/create'
3
7
 
4
8
  cli({
5
9
  name: 'tanstack',
6
10
  appName: 'TanStack',
11
+ frameworkDefinitionInitializers: [
12
+ createReactFrameworkDefinition,
13
+ createSolidFrameworkDefinition,
14
+ ],
7
15
  })
package/src/cli.ts CHANGED
@@ -9,10 +9,9 @@ import {
9
9
  SUPPORTED_PACKAGE_MANAGERS,
10
10
  addToApp,
11
11
  compileAddOn,
12
- devAddOn,
13
12
  compileStarter,
14
13
  createApp,
15
- createSerializedOptions,
14
+ devAddOn,
16
15
  getAllAddOns,
17
16
  getFrameworkByName,
18
17
  getFrameworks,
@@ -20,8 +19,6 @@ import {
20
19
  initStarter,
21
20
  } from '@tanstack/create'
22
21
 
23
- import { launchUI } from '@tanstack/create-ui'
24
-
25
22
  import { runMCPServer } from './mcp.js'
26
23
 
27
24
  import { promptForAddOns, promptForCreateOptions } from './options.js'
@@ -52,7 +49,6 @@ export function cli({
52
49
  forcedAddOns = [],
53
50
  forcedDeployment,
54
51
  defaultFramework,
55
- webBase,
56
52
  frameworkDefinitionInitializers,
57
53
  showDeploymentOptions = false,
58
54
  legacyAutoCreate = false,
@@ -63,7 +59,6 @@ export function cli({
63
59
  forcedAddOns?: Array<string>
64
60
  forcedDeployment?: string
65
61
  defaultFramework?: string
66
- webBase?: string
67
62
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
68
63
  showDeploymentOptions?: boolean
69
64
  legacyAutoCreate?: boolean
@@ -106,6 +101,91 @@ export function cli({
106
101
 
107
102
  const availableFrameworks = getFrameworks().map((f) => f.name)
108
103
 
104
+ function resolveBuiltInDevWatchPath(frameworkId: string): string {
105
+ const candidates = [
106
+ resolve(process.cwd(), 'packages/create/src/frameworks', frameworkId),
107
+ resolve(process.cwd(), '../create/src/frameworks', frameworkId),
108
+ ]
109
+
110
+ for (const candidate of candidates) {
111
+ if (fs.existsSync(candidate)) {
112
+ return candidate
113
+ }
114
+ }
115
+
116
+ return candidates[0]
117
+ }
118
+
119
+ async function startDevWatchMode(projectName: string, options: CliOptions) {
120
+ // Validate dev watch options
121
+ const validation = validateDevWatchOptions({ ...options, projectName })
122
+ if (!validation.valid) {
123
+ console.error(validation.error)
124
+ process.exit(1)
125
+ }
126
+
127
+ // Enter dev watch mode
128
+ if (!projectName && !options.targetDir) {
129
+ console.error('Project name/target directory is required for dev watch mode')
130
+ process.exit(1)
131
+ }
132
+
133
+ if (!options.framework) {
134
+ console.error('Failed to detect framework')
135
+ process.exit(1)
136
+ }
137
+
138
+ const framework = getFrameworkByName(options.framework)
139
+ if (!framework) {
140
+ console.error('Failed to detect framework')
141
+ process.exit(1)
142
+ }
143
+
144
+ // First, create the app normally using the standard flow
145
+ const normalizedOpts = await normalizeOptions(
146
+ {
147
+ ...options,
148
+ projectName,
149
+ framework: framework.id,
150
+ },
151
+ forcedAddOns,
152
+ )
153
+
154
+ if (!normalizedOpts) {
155
+ throw new Error('Failed to normalize options')
156
+ }
157
+
158
+ normalizedOpts.targetDir =
159
+ options.targetDir || resolve(process.cwd(), projectName)
160
+
161
+ // Create the initial app with minimal output for dev watch mode
162
+ console.log(chalk.bold('\ndev-watch'))
163
+ console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
164
+ if (normalizedOpts.install !== false) {
165
+ console.log(
166
+ chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...',
167
+ )
168
+ }
169
+ const silentEnvironment = createUIEnvironment(appName, true)
170
+ await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
171
+ await createApp(silentEnvironment, normalizedOpts)
172
+ console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
173
+
174
+ // Now start the dev watch mode
175
+ const manager = new DevWatchManager({
176
+ watchPath: options.devWatch!,
177
+ targetDir: normalizedOpts.targetDir,
178
+ framework,
179
+ cliOptions: normalizedOpts,
180
+ packageManager: normalizedOpts.packageManager,
181
+ runDevCommand: options.runDev,
182
+ environment,
183
+ frameworkDefinitionInitializers,
184
+ })
185
+
186
+ await manager.start()
187
+ }
188
+
109
189
  const toolchains = new Set<string>()
110
190
  for (const framework of getFrameworks()) {
111
191
  for (const addOn of framework.getAddOns()) {
@@ -235,72 +315,7 @@ export function cli({
235
315
  }
236
316
 
237
317
  if (options.devWatch) {
238
- // Validate dev watch options
239
- const validation = validateDevWatchOptions({ ...options, projectName })
240
- if (!validation.valid) {
241
- console.error(validation.error)
242
- process.exit(1)
243
- }
244
-
245
- // Enter dev watch mode
246
- if (!projectName && !options.targetDir) {
247
- console.error(
248
- 'Project name/target directory is required for dev watch mode',
249
- )
250
- process.exit(1)
251
- }
252
-
253
- if (!options.framework) {
254
- console.error('Failed to detect framework')
255
- process.exit(1)
256
- }
257
-
258
- const framework = getFrameworkByName(options.framework)
259
- if (!framework) {
260
- console.error('Failed to detect framework')
261
- process.exit(1)
262
- }
263
-
264
- // First, create the app normally using the standard flow
265
- const normalizedOpts = await normalizeOptions(
266
- {
267
- ...options,
268
- projectName,
269
- framework: framework.id,
270
- },
271
- forcedAddOns,
272
- )
273
-
274
- if (!normalizedOpts) {
275
- throw new Error('Failed to normalize options')
276
- }
277
-
278
- normalizedOpts.targetDir =
279
- options.targetDir || resolve(process.cwd(), projectName)
280
-
281
- // Create the initial app with minimal output for dev watch mode
282
- console.log(chalk.bold('\ndev-watch'))
283
- console.log(chalk.gray('├─') + ' ' + `creating initial ${appName} app...`)
284
- if (normalizedOpts.install !== false) {
285
- console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
286
- }
287
- const silentEnvironment = createUIEnvironment(appName, true)
288
- await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
289
- await createApp(silentEnvironment, normalizedOpts)
290
- console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
291
-
292
- // Now start the dev watch mode
293
- const manager = new DevWatchManager({
294
- watchPath: options.devWatch,
295
- targetDir: normalizedOpts.targetDir,
296
- framework,
297
- cliOptions: normalizedOpts,
298
- packageManager: normalizedOpts.packageManager,
299
- environment,
300
- frameworkDefinitionInitializers,
301
- })
302
-
303
- await manager.start()
318
+ await startDevWatchMode(projectName, options)
304
319
  return
305
320
  }
306
321
 
@@ -317,6 +332,9 @@ export function cli({
317
332
  if (
318
333
  cliOptions.routerOnly !== true &&
319
334
  cliOptions.template &&
335
+ ['file-router', 'typescript', 'tsx', 'javascript', 'js', 'jsx'].includes(
336
+ cliOptions.template.toLowerCase(),
337
+ ) &&
320
338
  cliOptions.template.toLowerCase() !== 'file-router'
321
339
  ) {
322
340
  cliOptions.routerOnly = true
@@ -337,29 +355,6 @@ export function cli({
337
355
  )
338
356
  }
339
357
 
340
- if (options.ui) {
341
- const optionsFromCLI = await normalizeOptions(
342
- cliOptions,
343
- forcedAddOns,
344
- { disableNameCheck: true, forcedDeployment },
345
- )
346
- const uiOptions = {
347
- ...createSerializedOptions(optionsFromCLI!),
348
- projectName: 'my-app',
349
- targetDir: resolve(process.cwd(), 'my-app'),
350
- }
351
- launchUI({
352
- mode: 'setup',
353
- options: uiOptions,
354
- forcedRouterMode: defaultMode,
355
- forcedAddOns,
356
- environmentFactory: () => createUIEnvironment(appName, false),
357
- webBase,
358
- showDeploymentOptions,
359
- })
360
- return
361
- }
362
-
363
358
  if (finalOptions) {
364
359
  intro(`Creating a new ${appName} app in ${projectName}...`)
365
360
  } else {
@@ -411,6 +406,10 @@ export function cli({
411
406
  '--framework <type>',
412
407
  `project framework (${availableFrameworks.join(', ')})`,
413
408
  (value) => {
409
+ if (value.toLowerCase() === 'react-cra') {
410
+ return 'react'
411
+ }
412
+
414
413
  if (
415
414
  !availableFrameworks.some(
416
415
  (f) => f.toLowerCase() === value.toLowerCase(),
@@ -428,10 +427,15 @@ export function cli({
428
427
 
429
428
  cmd
430
429
  .option(
431
- '--starter [url]',
432
- 'initialize this project from a starter URL',
430
+ '--starter [url-or-id]',
431
+ 'DEPRECATED: use --template. Initializes from a template URL or built-in id',
433
432
  false,
434
433
  )
434
+ .option('--template-id <id>', 'initialize using a built-in template id')
435
+ .option(
436
+ '--template [url-or-id]',
437
+ 'initialize this project from a template URL or built-in template id',
438
+ )
435
439
  .option('--no-install', 'skip installing dependencies')
436
440
  .option<PackageManager>(
437
441
  `--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
@@ -451,14 +455,11 @@ export function cli({
451
455
  '--dev-watch <path>',
452
456
  'Watch a framework directory for changes and auto-rebuild',
453
457
  )
458
+ .option('--run-dev', 'Run the app dev server alongside dev-watch', false)
454
459
  .option(
455
460
  '--router-only',
456
461
  'Use router-only compatibility mode (file-based routing without TanStack Start)',
457
462
  )
458
- .option(
459
- '--template <type>',
460
- 'Deprecated: compatibility flag from create-tsrouter-app',
461
- )
462
463
  .option(
463
464
  '--tailwind',
464
465
  'Deprecated: compatibility flag; Tailwind is always enabled',
@@ -530,7 +531,6 @@ export function cli({
530
531
  '--target-dir <path>',
531
532
  'the target directory for the application root',
532
533
  )
533
- .option('--ui', 'Launch the UI for project creation')
534
534
  .option(
535
535
  '--add-on-config <config>',
536
536
  'JSON string with add-on configuration options',
@@ -553,6 +553,34 @@ export function cli({
553
553
  configureCreateCommand(createCommand)
554
554
  createCommand.action(handleCreate)
555
555
 
556
+ // === DEV SUBCOMMAND ===
557
+ const devCommand = program
558
+ .command('dev')
559
+ .description(
560
+ 'Create a sandbox app and watch built-in framework templates/add-ons',
561
+ )
562
+
563
+ configureCreateCommand(devCommand)
564
+ devCommand.action(async (projectName: string, options: CliOptions) => {
565
+ const frameworkName = options.framework || defaultFramework || 'React'
566
+ const framework = getFrameworkByName(frameworkName)
567
+ if (!framework) {
568
+ console.error(`Unknown framework: ${frameworkName}`)
569
+ process.exit(1)
570
+ }
571
+
572
+ const watchPath = resolveBuiltInDevWatchPath(framework.id)
573
+ const devOptions: CliOptions = {
574
+ ...options,
575
+ framework: framework.name,
576
+ devWatch: watchPath,
577
+ runDev: true,
578
+ install: options.install ?? true,
579
+ }
580
+
581
+ await startDevWatchMode(projectName, devOptions)
582
+ })
583
+
556
584
  // === MCP SUBCOMMAND ===
557
585
  program
558
586
  .command('mcp')
@@ -647,8 +675,7 @@ Remove your node_modules directory and package lock file and re-install.`,
647
675
  'Name of the add-ons (or add-ons separated by spaces or commas)',
648
676
  )
649
677
  .option('--forced', 'Force the add-on to be added', false)
650
- .option('--ui', 'Add with the UI')
651
- .action(async (addOns: Array<string>, options: { forced: boolean; ui: boolean }) => {
678
+ .action(async (addOns: Array<string>, options: { forced: boolean }) => {
652
679
  const parsedAddOns: Array<string> = []
653
680
  for (const addOn of addOns) {
654
681
  if (addOn.includes(',') || addOn.includes(' ')) {
@@ -659,18 +686,7 @@ Remove your node_modules directory and package lock file and re-install.`,
659
686
  parsedAddOns.push(addOn.trim())
660
687
  }
661
688
  }
662
- if (options.ui) {
663
- launchUI({
664
- mode: 'add',
665
- addOns: parsedAddOns,
666
- projectPath: resolve(process.cwd()),
667
- forcedRouterMode: defaultMode,
668
- forcedAddOns,
669
- environmentFactory: () => createUIEnvironment(appName, false),
670
- webBase,
671
- showDeploymentOptions,
672
- })
673
- } else if (parsedAddOns.length < 1) {
689
+ if (parsedAddOns.length < 1) {
674
690
  const selectedAddOns = await promptForAddOns()
675
691
  if (selectedAddOns.length) {
676
692
  await addToApp(environment, selectedAddOns, resolve(process.cwd()), {
@@ -707,17 +723,32 @@ Remove your node_modules directory and package lock file and re-install.`,
707
723
  await devAddOn(environment)
708
724
  })
709
725
 
710
- // === STARTER SUBCOMMAND ===
726
+ // === TEMPLATE SUBCOMMAND ===
727
+ const templateCommand = program.command('template')
728
+ templateCommand
729
+ .command('init')
730
+ .description('Initialize a project template from the current project')
731
+ .action(async () => {
732
+ await initStarter(environment)
733
+ })
734
+ templateCommand
735
+ .command('compile')
736
+ .description('Compile the template JSON file for the current project')
737
+ .action(async () => {
738
+ await compileStarter(environment)
739
+ })
740
+
741
+ // Legacy alias for template command
711
742
  const starterCommand = program.command('starter')
712
743
  starterCommand
713
744
  .command('init')
714
- .description('Initialize a project starter from the current project')
745
+ .description('Deprecated alias: initialize a project template')
715
746
  .action(async () => {
716
747
  await initStarter(environment)
717
748
  })
718
749
  starterCommand
719
750
  .command('compile')
720
- .description('Compile the starter JSON file for the current project')
751
+ .description('Deprecated alias: compile the template JSON file')
721
752
  .action(async () => {
722
753
  await compileStarter(environment)
723
754
  })
@@ -6,6 +6,7 @@ import {
6
6
  finalizeAddOns,
7
7
  getFrameworkById,
8
8
  getPackageManager,
9
+ getRawRegistry,
9
10
  loadStarter,
10
11
  populateAddOnOptionsDefaults,
11
12
  } from '@tanstack/create'
@@ -25,15 +26,182 @@ const SUPPORTED_LEGACY_TEMPLATES = new Set([
25
26
  'tsx',
26
27
  ])
27
28
 
29
+ const LEGACY_TEMPLATE_ALIASES = new Set(['javascript', 'js', 'jsx'])
30
+
31
+ function getLegacyTemplateValue(templateValue?: string) {
32
+ if (!templateValue) {
33
+ return undefined
34
+ }
35
+
36
+ const normalized = templateValue.toLowerCase().trim()
37
+ if (
38
+ SUPPORTED_LEGACY_TEMPLATES.has(normalized) ||
39
+ LEGACY_TEMPLATE_ALIASES.has(normalized)
40
+ ) {
41
+ return normalized
42
+ }
43
+
44
+ return undefined
45
+ }
46
+
47
+ function slugifyStarterName(value: string) {
48
+ return value
49
+ .trim()
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9]+/g, '-')
52
+ .replace(/^-+|-+$/g, '')
53
+ }
54
+
55
+ function isLikelyStarterUrlOrPath(value: string) {
56
+ return (
57
+ /^https?:\/\//i.test(value) ||
58
+ /^file:\/\//i.test(value) ||
59
+ value.startsWith('./') ||
60
+ value.startsWith('../') ||
61
+ value.startsWith('/') ||
62
+ /^[a-zA-Z]:[\\/]/.test(value)
63
+ )
64
+ }
65
+
66
+ function getStarterIdsFromUrl(starterUrl: string) {
67
+ const ids = new Set<string>()
68
+
69
+ try {
70
+ const pathname = new URL(starterUrl).pathname
71
+ const parts = pathname.split('/').filter(Boolean)
72
+ const lastPart = parts.at(-1)?.toLowerCase()
73
+
74
+ if (lastPart) {
75
+ ids.add(lastPart.replace(/\.json$/i, ''))
76
+ }
77
+
78
+ if (
79
+ (lastPart === 'starter.json' || lastPart === 'template.json') &&
80
+ parts.length >= 2
81
+ ) {
82
+ ids.add(parts.at(-2)!.toLowerCase())
83
+ }
84
+ } catch {
85
+ // Ignore URL parse errors and rely on other id heuristics.
86
+ }
87
+
88
+ return ids
89
+ }
90
+
91
+ function resolveMonorepoStarterById(starterId: string) {
92
+ const normalized = starterId.toLowerCase().trim()
93
+ const idVariants = Array.from(
94
+ new Set([
95
+ normalized,
96
+ normalized.replace(/-template$/i, ''),
97
+ normalized.replace(/-starter$/i, ''),
98
+ ]),
99
+ ).filter(Boolean)
100
+
101
+ const cwd = process.cwd()
102
+ const rootCandidates = [
103
+ cwd,
104
+ resolve(cwd, '..'),
105
+ resolve(cwd, '../..'),
106
+ resolve(cwd, '../../..'),
107
+ ]
108
+
109
+ for (const root of rootCandidates) {
110
+ for (const framework of ['react', 'solid']) {
111
+ for (const id of idVariants) {
112
+ const templatePath = resolve(root, 'examples', framework, id, 'template.json')
113
+ if (fs.existsSync(templatePath)) {
114
+ return templatePath
115
+ }
116
+
117
+ const starterPath = resolve(root, 'examples', framework, id, 'starter.json')
118
+ if (fs.existsSync(starterPath)) {
119
+ return starterPath
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ return undefined
126
+ }
127
+
128
+ async function resolveStarterSpecifier(starterSpecifier: string) {
129
+ const normalized = starterSpecifier.trim()
130
+
131
+ if (!normalized || isLikelyStarterUrlOrPath(normalized)) {
132
+ return normalized
133
+ }
134
+
135
+ const registry = await getRawRegistry()
136
+ if (registry && registry.starters?.length) {
137
+ const lookup = normalized.toLowerCase()
138
+ const match = registry.starters.find((starter) => {
139
+ const candidateIds = new Set<string>()
140
+ candidateIds.add(starter.name.toLowerCase())
141
+ candidateIds.add(slugifyStarterName(starter.name))
142
+
143
+ for (const id of getStarterIdsFromUrl(starter.url)) {
144
+ candidateIds.add(id)
145
+ }
146
+
147
+ return candidateIds.has(lookup)
148
+ })
149
+
150
+ if (match) {
151
+ return match.url
152
+ }
153
+ }
154
+
155
+ const monorepoStarterPath = resolveMonorepoStarterById(normalized)
156
+ if (monorepoStarterPath) {
157
+ return monorepoStarterPath
158
+ }
159
+
160
+ if (!registry || !registry.starters?.length) {
161
+ throw new Error(
162
+ `Could not resolve template id "${normalized}" because no template registry is configured. Pass a template URL (or set CTA_REGISTRY).`,
163
+ )
164
+ }
165
+
166
+ const availableIds = Array.from(
167
+ new Set(
168
+ registry.starters.flatMap((starter) => {
169
+ const ids = [slugifyStarterName(starter.name)]
170
+ ids.push(...Array.from(getStarterIdsFromUrl(starter.url)))
171
+ return ids
172
+ }),
173
+ ),
174
+ )
175
+ .filter(Boolean)
176
+ .sort()
177
+
178
+ throw new Error(
179
+ `Unknown template id "${normalized}". Available built-in templates: ${availableIds.join(', ')}`,
180
+ )
181
+ }
182
+
28
183
  export function validateLegacyCreateFlags(cliOptions: CliOptions): {
29
184
  warnings: Array<string>
30
185
  error?: string
31
186
  } {
32
187
  const warnings: Array<string> = []
188
+ const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
189
+
190
+ if (cliOptions.starter) {
191
+ warnings.push(
192
+ 'The --starter flag is deprecated; prefer --template instead. Backward compatibility remains for now.',
193
+ )
194
+ }
195
+
196
+ if (cliOptions.starter && cliOptions.template && !legacyTemplate) {
197
+ warnings.push(
198
+ 'Both --starter and --template were provided. --template takes precedence.',
199
+ )
200
+ }
33
201
 
34
202
  if (cliOptions.routerOnly) {
35
203
  warnings.push(
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.',
204
+ 'The --router-only flag enables router-only compatibility mode. Start-dependent add-ons, deployment adapters, and templates are disabled; only the base template and optional toolchain are supported.',
37
205
  )
38
206
  }
39
207
 
@@ -50,7 +218,11 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
50
218
  }
51
219
 
52
220
  if (cliOptions.routerOnly && cliOptions.starter) {
53
- warnings.push('Ignoring --starter in router-only compatibility mode.')
221
+ warnings.push('Ignoring --starter/--template in router-only compatibility mode.')
222
+ }
223
+
224
+ if (cliOptions.routerOnly && cliOptions.templateId) {
225
+ warnings.push('Ignoring --template-id in router-only compatibility mode.')
54
226
  }
55
227
 
56
228
  if (cliOptions.tailwind === true) {
@@ -65,11 +237,11 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
65
237
  )
66
238
  }
67
239
 
68
- if (!cliOptions.template) {
240
+ if (!legacyTemplate) {
69
241
  return { warnings }
70
242
  }
71
243
 
72
- const template = cliOptions.template.toLowerCase().trim()
244
+ const template = legacyTemplate
73
245
 
74
246
  if (template === 'javascript' || template === 'js' || template === 'jsx') {
75
247
  return {
@@ -126,13 +298,27 @@ export async function normalizeOptions(
126
298
  let mode = 'file-router'
127
299
  let routerOnly = !!cliOptions.routerOnly
128
300
 
129
- const template = cliOptions.template?.toLowerCase().trim()
301
+ const legacyTemplate = getLegacyTemplateValue(cliOptions.template)
302
+
303
+ if (!cliOptions.starter) {
304
+ if (cliOptions.template && !legacyTemplate) {
305
+ cliOptions.starter = cliOptions.template
306
+ } else if (cliOptions.templateId) {
307
+ cliOptions.starter = cliOptions.templateId
308
+ }
309
+ }
310
+
311
+ const template = legacyTemplate
130
312
  if (template && template !== 'file-router') {
131
313
  routerOnly = true
132
314
  }
133
315
 
316
+ if (!cliOptions.starter && cliOptions.templateId) {
317
+ cliOptions.starter = cliOptions.templateId
318
+ }
319
+
134
320
  const starter = !routerOnly && cliOptions.starter
135
- ? await loadStarter(cliOptions.starter)
321
+ ? await loadStarter(await resolveStarterSpecifier(cliOptions.starter))
136
322
  : undefined
137
323
 
138
324
  // TypeScript and Tailwind are always enabled with TanStack Start
@@ -144,7 +330,7 @@ export async function normalizeOptions(
144
330
  mode = starter.mode
145
331
  }
146
332
 
147
- const framework = getFrameworkById(cliOptions.framework || 'react-cra')!
333
+ const framework = getFrameworkById(cliOptions.framework || 'react')!
148
334
 
149
335
  async function selectAddOns() {
150
336
  // Edge case for Windows Powershell