@tanstack/cli 0.59.5 → 0.59.7

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @tanstack/cli
2
2
 
3
+ ## 0.59.7
4
+
5
+ ### Patch Changes
6
+
7
+ - Add a continuous development workflow for custom add-on authors. ([`b3cc585`](https://github.com/TanStack/cli/commit/b3cc5851d2b81613e3b024eb7981c440ee5183af))
8
+
9
+ - Add `tanstack add-on dev` to watch project files and continuously refresh `.add-on` outputs.
10
+ - Rebuild `.add-on` assets and `add-on.json` automatically when source files change.
11
+ - Document the new add-on development loop in the custom add-on guide.
12
+
13
+ - Improve scaffold customization and custom add-on authoring flow. ([`5fbf262`](https://github.com/TanStack/cli/commit/5fbf262fe3a0d070e6a78fa2f2a920b176b84480))
14
+
15
+ - Add `--examples` / `--no-examples` support to include or omit demo/example pages during app creation.
16
+ - Prompt for add-on-declared environment variables during interactive create and seed entered values into generated `.env.local`.
17
+ - Ensure custom add-on/starter metadata consistently includes a `version`, with safe backfill for older metadata files.
18
+ - Align bundled starter/example metadata and docs with current Start/file-router behavior.
19
+
20
+ - Updated dependencies [[`b3cc585`](https://github.com/TanStack/cli/commit/b3cc5851d2b81613e3b024eb7981c440ee5183af), [`5fbf262`](https://github.com/TanStack/cli/commit/5fbf262fe3a0d070e6a78fa2f2a920b176b84480)]:
21
+ - @tanstack/create@0.61.5
22
+ - @tanstack/create-ui@0.59.7
23
+
24
+ ## 0.59.6
25
+
26
+ ### Patch Changes
27
+
28
+ - Improve CLI compatibility and scaffold behavior for legacy router-first workflows. ([`2949819`](https://github.com/TanStack/cli/commit/2949819058b4d4b1760be683ef29bfd459ddb28b))
29
+
30
+ - Add safer target directory handling by warning before creating into non-empty folders.
31
+ - Support explicit git initialization control via `--git` and `--no-git`.
32
+ - Restore router-only compatibility mode with file-based routing templates (without Start-dependent add-ons/deployments/starters), while still allowing toolchains.
33
+ - Default `create-tsrouter-app` to router-only compatibility mode.
34
+ - Remove stale `count.txt` ignore entries from base templates.
35
+
36
+ Also expands starter documentation with clearer creation, maintenance, UI usage, and banner guidance.
37
+
38
+ - Updated dependencies [[`164522e`](https://github.com/TanStack/cli/commit/164522e444188e83710fc599304132de8cb379e6), [`2949819`](https://github.com/TanStack/cli/commit/2949819058b4d4b1760be683ef29bfd459ddb28b)]:
39
+ - @tanstack/create@0.61.4
40
+ - @tanstack/create-ui@0.59.6
41
+
3
42
  ## 0.59.5
4
43
 
5
44
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { Command, InvalidArgumentError } from 'commander';
4
- import { intro, log } from '@clack/prompts';
4
+ import { cancel, confirm, intro, isCancel, log } from '@clack/prompts';
5
5
  import chalk from 'chalk';
6
6
  import semver from 'semver';
7
- import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, compileStarter, createApp, createSerializedOptions, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
7
+ import { SUPPORTED_PACKAGE_MANAGERS, addToApp, compileAddOn, devAddOn, compileStarter, createApp, createSerializedOptions, getAllAddOns, getFrameworkByName, getFrameworks, initAddOn, initStarter, } from '@tanstack/create';
8
8
  import { launchUI } from '@tanstack/create-ui';
9
9
  import { runMCPServer } from './mcp.js';
10
10
  import { promptForAddOns, promptForCreateOptions } from './options.js';
@@ -15,9 +15,31 @@ import { DevWatchManager } from './dev-watch.js';
15
15
  const packageJsonPath = new URL('../package.json', import.meta.url);
16
16
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
17
17
  const VERSION = packageJson.version;
18
- export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, }) {
18
+ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions = false, legacyAutoCreate = false, defaultRouterOnly = false, }) {
19
19
  const environment = createUIEnvironment(appName, false);
20
20
  const program = new Command();
21
+ async function confirmTargetDirectorySafety(targetDir, forced) {
22
+ if (forced) {
23
+ return;
24
+ }
25
+ if (!fs.existsSync(targetDir)) {
26
+ return;
27
+ }
28
+ if (!fs.statSync(targetDir).isDirectory()) {
29
+ throw new Error(`Target path exists and is not a directory: ${targetDir}`);
30
+ }
31
+ if (fs.readdirSync(targetDir).length === 0) {
32
+ return;
33
+ }
34
+ const shouldContinue = await confirm({
35
+ message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`,
36
+ initialValue: false,
37
+ });
38
+ if (isCancel(shouldContinue) || !shouldContinue) {
39
+ cancel('Operation cancelled.');
40
+ process.exit(0);
41
+ }
42
+ }
21
43
  const availableFrameworks = getFrameworks().map((f) => f.name);
22
44
  const toolchains = new Set();
23
45
  for (const framework of getFrameworks()) {
@@ -158,6 +180,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
158
180
  console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...');
159
181
  }
160
182
  const silentEnvironment = createUIEnvironment(appName, true);
183
+ await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force);
161
184
  await createApp(silentEnvironment, normalizedOpts);
162
185
  console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`);
163
186
  // Now start the dev watch mode
@@ -178,6 +201,14 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
178
201
  projectName,
179
202
  ...options,
180
203
  };
204
+ if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
205
+ cliOptions.routerOnly = true;
206
+ }
207
+ if (cliOptions.routerOnly !== true &&
208
+ cliOptions.template &&
209
+ cliOptions.template.toLowerCase() !== 'file-router') {
210
+ cliOptions.routerOnly = true;
211
+ }
181
212
  cliOptions.framework = getFrameworkByName(options.framework || defaultFramework || 'React').id;
182
213
  let finalOptions;
183
214
  if (cliOptions.interactive || cliOptions.addOns === true) {
@@ -217,6 +248,9 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
217
248
  if (!finalOptions) {
218
249
  throw new Error('No options were provided');
219
250
  }
251
+ ;
252
+ finalOptions.routerOnly =
253
+ !!cliOptions.routerOnly;
220
254
  // Determine target directory:
221
255
  // 1. Use --target-dir if provided
222
256
  // 2. Use targetDir from normalizeOptions if set (handles "." case)
@@ -234,6 +268,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
234
268
  else {
235
269
  finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName);
236
270
  }
271
+ await confirmTargetDirectorySafety(finalOptions.targetDir, options.force);
237
272
  await createApp(environment, finalOptions);
238
273
  }
239
274
  catch (error) {
@@ -262,10 +297,12 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
262
297
  return value;
263
298
  })
264
299
  .option('--dev-watch <path>', 'Watch a framework directory for changes and auto-rebuild')
265
- .option('--router-only', 'Deprecated: compatibility flag from create-tsrouter-app')
300
+ .option('--router-only', 'Use router-only compatibility mode (file-based routing without TanStack Start)')
266
301
  .option('--template <type>', 'Deprecated: compatibility flag from create-tsrouter-app')
267
302
  .option('--tailwind', 'Deprecated: compatibility flag; Tailwind is always enabled')
268
- .option('--no-tailwind', 'Deprecated: compatibility flag; Tailwind opt-out is ignored');
303
+ .option('--no-tailwind', 'Deprecated: compatibility flag; Tailwind opt-out is ignored')
304
+ .option('--examples', 'include demo/example pages')
305
+ .option('--no-examples', 'exclude demo/example pages');
269
306
  if (deployments.size > 0) {
270
307
  cmd.option(`--deployment <${Array.from(deployments).join('|')}>`, `Explicitly tell the CLI to use this deployment adapter`, (value) => {
271
308
  if (!deployments.has(value)) {
@@ -295,6 +332,7 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
295
332
  })
296
333
  .option('--list-add-ons', 'list all available add-ons', false)
297
334
  .option('--addon-details <addon-id>', 'show detailed information about a specific add-on')
335
+ .option('--git', 'create a git repository')
298
336
  .option('--no-git', 'do not create a git repository')
299
337
  .option('--target-dir <path>', 'the target directory for the application root')
300
338
  .option('--ui', 'Launch the UI for project creation')
@@ -437,6 +475,12 @@ Remove your node_modules directory and package lock file and re-install.`);
437
475
  .action(async () => {
438
476
  await compileAddOn(environment);
439
477
  });
478
+ addOnCommand
479
+ .command('dev')
480
+ .description('Watch project files and continuously refresh .add-on and add-on.json')
481
+ .action(async () => {
482
+ await devAddOn(environment);
483
+ });
440
484
  // === STARTER SUBCOMMAND ===
441
485
  const starterCommand = program.command('starter');
442
486
  starterCommand
@@ -10,7 +10,16 @@ const SUPPORTED_LEGACY_TEMPLATES = new Set([
10
10
  export function validateLegacyCreateFlags(cliOptions) {
11
11
  const warnings = [];
12
12
  if (cliOptions.routerOnly) {
13
- warnings.push('The --router-only flag is deprecated and ignored. `tanstack create` already creates router-based apps.');
13
+ warnings.push('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.');
14
+ }
15
+ if (cliOptions.routerOnly && cliOptions.addOns) {
16
+ warnings.push('Ignoring --add-ons in router-only compatibility mode.');
17
+ }
18
+ if (cliOptions.routerOnly && cliOptions.deployment) {
19
+ warnings.push('Ignoring --deployment in router-only compatibility mode.');
20
+ }
21
+ if (cliOptions.routerOnly && cliOptions.starter) {
22
+ warnings.push('Ignoring --starter in router-only compatibility mode.');
14
23
  }
15
24
  if (cliOptions.tailwind === true) {
16
25
  warnings.push('The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.');
@@ -34,7 +43,7 @@ export function validateLegacyCreateFlags(cliOptions) {
34
43
  error: `Invalid --template value: ${cliOptions.template}. Supported values are: file-router, typescript, tsx.`,
35
44
  };
36
45
  }
37
- warnings.push('The --template flag is deprecated. TypeScript/TSX is the default and only supported template.');
46
+ warnings.push('The --template flag is deprecated and mapped for compatibility.');
38
47
  return { warnings };
39
48
  }
40
49
  export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
@@ -60,7 +69,12 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
60
69
  }
61
70
  // Mode is always file-router (TanStack Start)
62
71
  let mode = 'file-router';
63
- const starter = cliOptions.starter
72
+ let routerOnly = !!cliOptions.routerOnly;
73
+ const template = cliOptions.template?.toLowerCase().trim();
74
+ if (template && template !== 'file-router') {
75
+ routerOnly = true;
76
+ }
77
+ const starter = !routerOnly && cliOptions.starter
64
78
  ? await loadStarter(cliOptions.starter)
65
79
  : undefined;
66
80
  // TypeScript and Tailwind are always enabled with TanStack Start
@@ -85,10 +99,10 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
85
99
  cliOptions.toolchain ||
86
100
  cliOptions.deployment) {
87
101
  const selectedAddOns = new Set([
88
- ...(starter?.dependsOn || []),
89
- ...(forcedAddOns || []),
102
+ ...(routerOnly ? [] : (starter?.dependsOn || [])),
103
+ ...(routerOnly ? [] : (forcedAddOns || [])),
90
104
  ]);
91
- if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
105
+ if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
92
106
  for (const a of cliOptions.addOns) {
93
107
  if (a.toLowerCase() === 'start') {
94
108
  continue;
@@ -99,17 +113,21 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
99
113
  if (cliOptions.toolchain) {
100
114
  selectedAddOns.add(cliOptions.toolchain);
101
115
  }
102
- if (cliOptions.deployment) {
116
+ if (!routerOnly && cliOptions.deployment) {
103
117
  selectedAddOns.add(cliOptions.deployment);
104
118
  }
105
- if (!cliOptions.deployment && opts?.forcedDeployment) {
119
+ if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
106
120
  selectedAddOns.add(opts.forcedDeployment);
107
121
  }
108
122
  return await finalizeAddOns(framework, mode, Array.from(selectedAddOns));
109
123
  }
110
124
  return [];
111
125
  }
112
- const chosenAddOns = await selectAddOns();
126
+ const includeExamples = cliOptions.examples ?? !routerOnly;
127
+ const chosenAddOnsRaw = await selectAddOns();
128
+ const chosenAddOns = includeExamples
129
+ ? chosenAddOnsRaw
130
+ : chosenAddOnsRaw.filter((addOn) => addOn.type !== 'example');
113
131
  // Handle add-on configuration option
114
132
  let addOnOptionsFromCLI = {};
115
133
  if (cliOptions.addOnConfig) {
@@ -121,7 +139,7 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
121
139
  process.exit(1);
122
140
  }
123
141
  }
124
- return {
142
+ const normalized = {
125
143
  projectName: projectName,
126
144
  targetDir,
127
145
  framework,
@@ -131,7 +149,7 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
131
149
  packageManager: cliOptions.packageManager ||
132
150
  getPackageManager() ||
133
151
  DEFAULT_PACKAGE_MANAGER,
134
- git: !!cliOptions.git,
152
+ git: cliOptions.git ?? true,
135
153
  install: cliOptions.install,
136
154
  chosenAddOns,
137
155
  addOnOptions: {
@@ -140,6 +158,11 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
140
158
  },
141
159
  starter: starter,
142
160
  };
161
+ normalized.includeExamples =
162
+ includeExamples;
163
+ normalized.envVarValues =
164
+ {};
165
+ return normalized;
143
166
  }
144
167
  export function validateDevWatchOptions(cliOptions) {
145
168
  if (!cliOptions.devWatch) {
package/dist/options.js CHANGED
@@ -1,7 +1,6 @@
1
- import fs from 'node:fs';
2
- import { cancel, confirm, intro, isCancel } from '@clack/prompts';
1
+ import { intro } from '@clack/prompts';
3
2
  import { finalizeAddOns, getFrameworkById, getPackageManager, populateAddOnOptionsDefaults, readConfigFile, } from '@tanstack/create';
4
- import { getProjectName, promptForAddOnOptions, selectAddOns, selectDeployment, selectGit, selectPackageManager, selectToolchain, } from './ui-prompts.js';
3
+ import { getProjectName, promptForAddOnOptions, promptForEnvVars, selectAddOns, selectDeployment, selectExamples, selectGit, selectPackageManager, selectToolchain, } from './ui-prompts.js';
5
4
  import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
6
5
  export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], showDeploymentOptions = false, }) {
7
6
  const options = {};
@@ -24,21 +23,10 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
24
23
  else {
25
24
  options.projectName = await getProjectName();
26
25
  }
27
- // Check if target directory is empty
28
- if (!cliOptions.force &&
29
- fs.existsSync(options.projectName) &&
30
- fs.readdirSync(options.projectName).length > 0) {
31
- const shouldContinue = await confirm({
32
- message: `Target directory ${options.projectName} is not empty. Do you want to continue?`,
33
- initialValue: true,
34
- });
35
- if (isCancel(shouldContinue) || !shouldContinue) {
36
- cancel('Operation cancelled.');
37
- process.exit(0);
38
- }
39
- }
40
26
  // Mode is always file-router (TanStack Start)
41
27
  options.mode = 'file-router';
28
+ const template = cliOptions.template?.toLowerCase().trim();
29
+ const routerOnly = !!cliOptions.routerOnly || (template ? template !== 'file-router' : false);
42
30
  // TypeScript is always enabled with file-router
43
31
  options.typescript = true;
44
32
  // Package manager selection
@@ -54,20 +42,28 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
54
42
  const toolchain = await selectToolchain(options.framework, cliOptions.toolchain);
55
43
  // Deployment selection
56
44
  const deployment = showDeploymentOptions
57
- ? await selectDeployment(options.framework, cliOptions.deployment)
45
+ ? routerOnly
46
+ ? undefined
47
+ : await selectDeployment(options.framework, cliOptions.deployment)
58
48
  : undefined;
59
49
  // Add-ons selection
60
50
  const addOns = new Set();
51
+ // Examples/demo pages are enabled by default
52
+ const includeExamples = cliOptions.examples ?? (routerOnly ? false : await selectExamples());
53
+ options.includeExamples =
54
+ includeExamples;
61
55
  if (toolchain) {
62
56
  addOns.add(toolchain);
63
57
  }
64
58
  if (deployment) {
65
59
  addOns.add(deployment);
66
60
  }
67
- for (const addOn of forcedAddOns) {
68
- addOns.add(addOn);
61
+ if (!routerOnly) {
62
+ for (const addOn of forcedAddOns) {
63
+ addOns.add(addOn);
64
+ }
69
65
  }
70
- if (Array.isArray(cliOptions.addOns)) {
66
+ if (!routerOnly && Array.isArray(cliOptions.addOns)) {
71
67
  for (const addOn of cliOptions.addOns) {
72
68
  if (addOn.toLowerCase() === 'start') {
73
69
  continue;
@@ -75,15 +71,20 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
75
71
  addOns.add(addOn);
76
72
  }
77
73
  }
78
- else {
74
+ else if (!routerOnly) {
79
75
  for (const addOn of await selectAddOns(options.framework, options.mode, 'add-on', 'What add-ons would you like for your project?', forcedAddOns)) {
80
76
  addOns.add(addOn);
81
77
  }
82
- for (const addOn of await selectAddOns(options.framework, options.mode, 'example', 'Would you like an example?', forcedAddOns, false)) {
83
- addOns.add(addOn);
78
+ if (includeExamples) {
79
+ for (const addOn of await selectAddOns(options.framework, options.mode, 'example', 'Would you like an example?', forcedAddOns, false)) {
80
+ addOns.add(addOn);
81
+ }
84
82
  }
85
83
  }
86
- options.chosenAddOns = Array.from(await finalizeAddOns(options.framework, options.mode, Array.from(addOns)));
84
+ const chosenAddOns = Array.from(await finalizeAddOns(options.framework, options.mode, Array.from(addOns)));
85
+ options.chosenAddOns = includeExamples
86
+ ? chosenAddOns
87
+ : chosenAddOns.filter((addOn) => addOn.type !== 'example');
87
88
  // Tailwind is always enabled
88
89
  options.tailwind = true;
89
90
  // Prompt for add-on options in interactive mode
@@ -98,7 +99,13 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
98
99
  // Merge user options with defaults
99
100
  options.addOnOptions = { ...defaultOptions, ...userOptions };
100
101
  }
101
- options.git = cliOptions.git || (await selectGit());
102
+ // Prompt for env vars exposed by selected add-ons in interactive mode
103
+ const envVarValues = Array.isArray(cliOptions.addOns)
104
+ ? {}
105
+ : await promptForEnvVars(options.chosenAddOns);
106
+ options.envVarValues =
107
+ envVarValues;
108
+ options.git = cliOptions.git ?? (await selectGit());
102
109
  if (cliOptions.install === false) {
103
110
  options.install = false;
104
111
  }
@@ -1,5 +1,5 @@
1
1
  import type { FrameworkDefinition } from '@tanstack/create';
2
- export declare function cli({ name, appName, forcedAddOns, forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions, legacyAutoCreate, }: {
2
+ export declare function cli({ name, appName, forcedAddOns, forcedDeployment, defaultFramework, webBase, frameworkDefinitionInitializers, showDeploymentOptions, legacyAutoCreate, defaultRouterOnly, }: {
3
3
  name: string;
4
4
  appName: string;
5
5
  forcedAddOns?: Array<string>;
@@ -9,4 +9,5 @@ export declare function cli({ name, appName, forcedAddOns, forcedDeployment, def
9
9
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>;
10
10
  showDeploymentOptions?: boolean;
11
11
  legacyAutoCreate?: boolean;
12
+ defaultRouterOnly?: boolean;
12
13
  }): void;
@@ -22,4 +22,5 @@ export interface CliOptions {
22
22
  routerOnly?: boolean;
23
23
  template?: string;
24
24
  tailwind?: boolean;
25
+ examples?: boolean;
25
26
  }
@@ -1,9 +1,11 @@
1
- import type { PackageManager } from '@tanstack/create';
1
+ import type { AddOn, PackageManager } from '@tanstack/create';
2
2
  import type { Framework } from '@tanstack/create/dist/types/types.js';
3
3
  export declare function getProjectName(): Promise<string>;
4
4
  export declare function selectPackageManager(): Promise<PackageManager>;
5
5
  export declare function selectAddOns(framework: Framework, mode: string, type: string, message: string, forcedAddOns?: Array<string>, allowMultiple?: boolean): Promise<Array<string>>;
6
6
  export declare function selectGit(): Promise<boolean>;
7
+ export declare function selectExamples(): Promise<boolean>;
7
8
  export declare function selectToolchain(framework: Framework, toolchain?: string | false): Promise<string | undefined>;
8
9
  export declare function promptForAddOnOptions(addOnIds: Array<string>, framework: Framework): Promise<Record<string, Record<string, any>>>;
10
+ export declare function promptForEnvVars(addOns: Array<AddOn>): Promise<Record<string, string>>;
9
11
  export declare function selectDeployment(framework: Framework, deployment?: string): Promise<string | undefined>;
@@ -1,4 +1,4 @@
1
- import { cancel, confirm, isCancel, multiselect, note, select, text, } from '@clack/prompts';
1
+ import { cancel, confirm, isCancel, multiselect, note, password, select, text, } from '@clack/prompts';
2
2
  import { DEFAULT_PACKAGE_MANAGER, SUPPORTED_PACKAGE_MANAGERS, getAllAddOns, } from '@tanstack/create';
3
3
  import { validateProjectName } from './utils.js';
4
4
  export async function getProjectName() {
@@ -106,6 +106,17 @@ export async function selectGit() {
106
106
  }
107
107
  return git;
108
108
  }
109
+ export async function selectExamples() {
110
+ const includeExamples = await confirm({
111
+ message: 'Would you like to include demo/example pages?',
112
+ initialValue: true,
113
+ });
114
+ if (isCancel(includeExamples)) {
115
+ cancel('Operation cancelled.');
116
+ process.exit(0);
117
+ }
118
+ return includeExamples;
119
+ }
109
120
  export async function selectToolchain(framework, toolchain) {
110
121
  if (toolchain === false) {
111
122
  return undefined;
@@ -170,6 +181,48 @@ export async function promptForAddOnOptions(addOnIds, framework) {
170
181
  }
171
182
  return addOnOptions;
172
183
  }
184
+ export async function promptForEnvVars(addOns) {
185
+ const envVars = new Map();
186
+ for (const addOn of addOns) {
187
+ for (const envVar of addOn.envVars || []) {
188
+ if (!envVars.has(envVar.name)) {
189
+ envVars.set(envVar.name, envVar);
190
+ }
191
+ }
192
+ }
193
+ const result = {};
194
+ for (const envVar of envVars.values()) {
195
+ const label = envVar.description
196
+ ? `${envVar.name} (${envVar.description})`
197
+ : envVar.name;
198
+ const value = envVar.secret
199
+ ? await password({
200
+ message: `Enter ${label}`,
201
+ validate: envVar.required
202
+ ? (v) => v && v.trim().length > 0
203
+ ? undefined
204
+ : `${envVar.name} is required`
205
+ : undefined,
206
+ })
207
+ : await text({
208
+ message: `Enter ${label}`,
209
+ defaultValue: envVar.default,
210
+ validate: envVar.required
211
+ ? (v) => v && v.trim().length > 0
212
+ ? undefined
213
+ : `${envVar.name} is required`
214
+ : undefined,
215
+ });
216
+ if (isCancel(value)) {
217
+ cancel('Operation cancelled.');
218
+ process.exit(0);
219
+ }
220
+ if (value && value.trim()) {
221
+ result[envVar.name] = value.trim();
222
+ }
223
+ }
224
+ return result;
225
+ }
173
226
  export async function selectDeployment(framework, deployment) {
174
227
  const deployments = new Set();
175
228
  let initialValue = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cli",
3
- "version": "0.59.5",
3
+ "version": "0.59.7",
4
4
  "description": "TanStack CLI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,8 +38,8 @@
38
38
  "tempy": "^3.1.0",
39
39
  "validate-npm-package-name": "^7.0.0",
40
40
  "zod": "^3.24.2",
41
- "@tanstack/create": "0.61.3",
42
- "@tanstack/create-ui": "0.59.5"
41
+ "@tanstack/create": "0.61.5",
42
+ "@tanstack/create-ui": "0.59.7"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@tanstack/config": "^0.16.2",
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import { resolve } from 'node:path'
3
3
  import { Command, InvalidArgumentError } from 'commander'
4
- import { intro, log } from '@clack/prompts'
4
+ import { cancel, confirm, intro, isCancel, log } from '@clack/prompts'
5
5
  import chalk from 'chalk'
6
6
  import semver from 'semver'
7
7
 
@@ -9,6 +9,7 @@ import {
9
9
  SUPPORTED_PACKAGE_MANAGERS,
10
10
  addToApp,
11
11
  compileAddOn,
12
+ devAddOn,
12
13
  compileStarter,
13
14
  createApp,
14
15
  createSerializedOptions,
@@ -55,6 +56,7 @@ export function cli({
55
56
  frameworkDefinitionInitializers,
56
57
  showDeploymentOptions = false,
57
58
  legacyAutoCreate = false,
59
+ defaultRouterOnly = false,
58
60
  }: {
59
61
  name: string
60
62
  appName: string
@@ -65,11 +67,43 @@ export function cli({
65
67
  frameworkDefinitionInitializers?: Array<() => FrameworkDefinition>
66
68
  showDeploymentOptions?: boolean
67
69
  legacyAutoCreate?: boolean
70
+ defaultRouterOnly?: boolean
68
71
  }) {
69
72
  const environment = createUIEnvironment(appName, false)
70
73
 
71
74
  const program = new Command()
72
75
 
76
+ async function confirmTargetDirectorySafety(
77
+ targetDir: string,
78
+ forced?: boolean,
79
+ ) {
80
+ if (forced) {
81
+ return
82
+ }
83
+
84
+ if (!fs.existsSync(targetDir)) {
85
+ return
86
+ }
87
+
88
+ if (!fs.statSync(targetDir).isDirectory()) {
89
+ throw new Error(`Target path exists and is not a directory: ${targetDir}`)
90
+ }
91
+
92
+ if (fs.readdirSync(targetDir).length === 0) {
93
+ return
94
+ }
95
+
96
+ const shouldContinue = await confirm({
97
+ message: `Target directory "${targetDir}" already exists and is not empty. Continue anyway?`,
98
+ initialValue: false,
99
+ })
100
+
101
+ if (isCancel(shouldContinue) || !shouldContinue) {
102
+ cancel('Operation cancelled.')
103
+ process.exit(0)
104
+ }
105
+ }
106
+
73
107
  const availableFrameworks = getFrameworks().map((f) => f.name)
74
108
 
75
109
  const toolchains = new Set<string>()
@@ -251,6 +285,7 @@ export function cli({
251
285
  console.log(chalk.gray('├─') + ' ' + chalk.yellow('⟳') + ' installing packages...')
252
286
  }
253
287
  const silentEnvironment = createUIEnvironment(appName, true)
288
+ await confirmTargetDirectorySafety(normalizedOpts.targetDir, options.force)
254
289
  await createApp(silentEnvironment, normalizedOpts)
255
290
  console.log(chalk.gray('└─') + ' ' + chalk.green('✓') + ` app created`)
256
291
 
@@ -275,6 +310,18 @@ export function cli({
275
310
  ...options,
276
311
  } as CliOptions
277
312
 
313
+ if (defaultRouterOnly && cliOptions.routerOnly === undefined) {
314
+ cliOptions.routerOnly = true
315
+ }
316
+
317
+ if (
318
+ cliOptions.routerOnly !== true &&
319
+ cliOptions.template &&
320
+ cliOptions.template.toLowerCase() !== 'file-router'
321
+ ) {
322
+ cliOptions.routerOnly = true
323
+ }
324
+
278
325
  cliOptions.framework = getFrameworkByName(
279
326
  options.framework || defaultFramework || 'React',
280
327
  )!.id
@@ -327,6 +374,9 @@ export function cli({
327
374
  throw new Error('No options were provided')
328
375
  }
329
376
 
377
+ ;(finalOptions as Options & { routerOnly?: boolean }).routerOnly =
378
+ !!cliOptions.routerOnly
379
+
330
380
  // Determine target directory:
331
381
  // 1. Use --target-dir if provided
332
382
  // 2. Use targetDir from normalizeOptions if set (handles "." case)
@@ -342,6 +392,7 @@ export function cli({
342
392
  finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName)
343
393
  }
344
394
 
395
+ await confirmTargetDirectorySafety(finalOptions.targetDir, options.force)
345
396
  await createApp(environment, finalOptions)
346
397
  } catch (error) {
347
398
  log.error(
@@ -402,7 +453,7 @@ export function cli({
402
453
  )
403
454
  .option(
404
455
  '--router-only',
405
- 'Deprecated: compatibility flag from create-tsrouter-app',
456
+ 'Use router-only compatibility mode (file-based routing without TanStack Start)',
406
457
  )
407
458
  .option(
408
459
  '--template <type>',
@@ -416,6 +467,8 @@ export function cli({
416
467
  '--no-tailwind',
417
468
  'Deprecated: compatibility flag; Tailwind opt-out is ignored',
418
469
  )
470
+ .option('--examples', 'include demo/example pages')
471
+ .option('--no-examples', 'exclude demo/example pages')
419
472
 
420
473
  if (deployments.size > 0) {
421
474
  cmd.option<string>(
@@ -471,6 +524,7 @@ export function cli({
471
524
  '--addon-details <addon-id>',
472
525
  'show detailed information about a specific add-on',
473
526
  )
527
+ .option('--git', 'create a git repository')
474
528
  .option('--no-git', 'do not create a git repository')
475
529
  .option(
476
530
  '--target-dir <path>',
@@ -644,6 +698,14 @@ Remove your node_modules directory and package lock file and re-install.`,
644
698
  .action(async () => {
645
699
  await compileAddOn(environment)
646
700
  })
701
+ addOnCommand
702
+ .command('dev')
703
+ .description(
704
+ 'Watch project files and continuously refresh .add-on and add-on.json',
705
+ )
706
+ .action(async () => {
707
+ await devAddOn(environment)
708
+ })
647
709
 
648
710
  // === STARTER SUBCOMMAND ===
649
711
  const starterCommand = program.command('starter')
@@ -33,10 +33,26 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
33
33
 
34
34
  if (cliOptions.routerOnly) {
35
35
  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.',
37
37
  )
38
38
  }
39
39
 
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
+
40
56
  if (cliOptions.tailwind === true) {
41
57
  warnings.push(
42
58
  'The --tailwind flag is deprecated and ignored. Tailwind is always enabled in TanStack Start scaffolds.',
@@ -70,9 +86,7 @@ export function validateLegacyCreateFlags(cliOptions: CliOptions): {
70
86
  }
71
87
  }
72
88
 
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.')
76
90
 
77
91
  return { warnings }
78
92
  }
@@ -110,8 +124,14 @@ export async function normalizeOptions(
110
124
 
111
125
  // Mode is always file-router (TanStack Start)
112
126
  let mode = 'file-router'
127
+ let routerOnly = !!cliOptions.routerOnly
113
128
 
114
- const starter = cliOptions.starter
129
+ const template = cliOptions.template?.toLowerCase().trim()
130
+ if (template && template !== 'file-router') {
131
+ routerOnly = true
132
+ }
133
+
134
+ const starter = !routerOnly && cliOptions.starter
115
135
  ? await loadStarter(cliOptions.starter)
116
136
  : undefined
117
137
 
@@ -143,10 +163,10 @@ export async function normalizeOptions(
143
163
  cliOptions.deployment
144
164
  ) {
145
165
  const selectedAddOns = new Set<string>([
146
- ...(starter?.dependsOn || []),
147
- ...(forcedAddOns || []),
166
+ ...(routerOnly ? [] : (starter?.dependsOn || [])),
167
+ ...(routerOnly ? [] : (forcedAddOns || [])),
148
168
  ])
149
- if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
169
+ if (!routerOnly && cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
150
170
  for (const a of cliOptions.addOns) {
151
171
  if (a.toLowerCase() === 'start') {
152
172
  continue
@@ -157,11 +177,11 @@ export async function normalizeOptions(
157
177
  if (cliOptions.toolchain) {
158
178
  selectedAddOns.add(cliOptions.toolchain)
159
179
  }
160
- if (cliOptions.deployment) {
180
+ if (!routerOnly && cliOptions.deployment) {
161
181
  selectedAddOns.add(cliOptions.deployment)
162
182
  }
163
183
 
164
- if (!cliOptions.deployment && opts?.forcedDeployment) {
184
+ if (!routerOnly && !cliOptions.deployment && opts?.forcedDeployment) {
165
185
  selectedAddOns.add(opts.forcedDeployment)
166
186
  }
167
187
 
@@ -171,7 +191,11 @@ export async function normalizeOptions(
171
191
  return []
172
192
  }
173
193
 
174
- const chosenAddOns = await selectAddOns()
194
+ const includeExamples = cliOptions.examples ?? !routerOnly
195
+ const chosenAddOnsRaw = await selectAddOns()
196
+ const chosenAddOns = includeExamples
197
+ ? chosenAddOnsRaw
198
+ : chosenAddOnsRaw.filter((addOn) => addOn.type !== 'example')
175
199
 
176
200
  // Handle add-on configuration option
177
201
  let addOnOptionsFromCLI = {}
@@ -184,7 +208,7 @@ export async function normalizeOptions(
184
208
  }
185
209
  }
186
210
 
187
- return {
211
+ const normalized = {
188
212
  projectName: projectName,
189
213
  targetDir,
190
214
  framework,
@@ -195,7 +219,7 @@ export async function normalizeOptions(
195
219
  cliOptions.packageManager ||
196
220
  getPackageManager() ||
197
221
  DEFAULT_PACKAGE_MANAGER,
198
- git: !!cliOptions.git,
222
+ git: cliOptions.git ?? true,
199
223
  install: cliOptions.install,
200
224
  chosenAddOns,
201
225
  addOnOptions: {
@@ -204,6 +228,13 @@ export async function normalizeOptions(
204
228
  },
205
229
  starter: starter,
206
230
  }
231
+
232
+ ;(normalized as Options & { includeExamples?: boolean }).includeExamples =
233
+ includeExamples
234
+ ;(normalized as Options & { envVarValues?: Record<string, string> }).envVarValues =
235
+ {}
236
+
237
+ return normalized
207
238
  }
208
239
 
209
240
  export function validateDevWatchOptions(cliOptions: CliOptions): {
package/src/options.ts CHANGED
@@ -1,5 +1,4 @@
1
- import fs from 'node:fs'
2
- import { cancel, confirm, intro, isCancel } from '@clack/prompts'
1
+ import { intro } from '@clack/prompts'
3
2
 
4
3
  import {
5
4
  finalizeAddOns,
@@ -12,8 +11,10 @@ import {
12
11
  import {
13
12
  getProjectName,
14
13
  promptForAddOnOptions,
14
+ promptForEnvVars,
15
15
  selectAddOns,
16
16
  selectDeployment,
17
+ selectExamples,
17
18
  selectGit,
18
19
  selectPackageManager,
19
20
  selectToolchain,
@@ -59,25 +60,11 @@ export async function promptForCreateOptions(
59
60
  options.projectName = await getProjectName()
60
61
  }
61
62
 
62
- // Check if target directory is empty
63
- if (
64
- !cliOptions.force &&
65
- fs.existsSync(options.projectName) &&
66
- fs.readdirSync(options.projectName).length > 0
67
- ) {
68
- const shouldContinue = await confirm({
69
- message: `Target directory ${options.projectName} is not empty. Do you want to continue?`,
70
- initialValue: true,
71
- })
72
-
73
- if (isCancel(shouldContinue) || !shouldContinue) {
74
- cancel('Operation cancelled.')
75
- process.exit(0)
76
- }
77
- }
78
-
79
63
  // Mode is always file-router (TanStack Start)
80
64
  options.mode = 'file-router'
65
+ const template = cliOptions.template?.toLowerCase().trim()
66
+ const routerOnly =
67
+ !!cliOptions.routerOnly || (template ? template !== 'file-router' : false)
81
68
 
82
69
  // TypeScript is always enabled with file-router
83
70
  options.typescript = true
@@ -99,12 +86,20 @@ export async function promptForCreateOptions(
99
86
 
100
87
  // Deployment selection
101
88
  const deployment = showDeploymentOptions
102
- ? await selectDeployment(options.framework, cliOptions.deployment)
89
+ ? routerOnly
90
+ ? undefined
91
+ : await selectDeployment(options.framework, cliOptions.deployment)
103
92
  : undefined
104
93
 
105
94
  // Add-ons selection
106
95
  const addOns: Set<string> = new Set()
107
96
 
97
+ // Examples/demo pages are enabled by default
98
+ const includeExamples =
99
+ cliOptions.examples ?? (routerOnly ? false : await selectExamples())
100
+ ;(options as Required<Options> & { includeExamples?: boolean }).includeExamples =
101
+ includeExamples
102
+
108
103
  if (toolchain) {
109
104
  addOns.add(toolchain)
110
105
  }
@@ -112,18 +107,20 @@ export async function promptForCreateOptions(
112
107
  addOns.add(deployment)
113
108
  }
114
109
 
115
- for (const addOn of forcedAddOns) {
116
- addOns.add(addOn)
110
+ if (!routerOnly) {
111
+ for (const addOn of forcedAddOns) {
112
+ addOns.add(addOn)
113
+ }
117
114
  }
118
115
 
119
- if (Array.isArray(cliOptions.addOns)) {
116
+ if (!routerOnly && Array.isArray(cliOptions.addOns)) {
120
117
  for (const addOn of cliOptions.addOns) {
121
118
  if (addOn.toLowerCase() === 'start') {
122
119
  continue
123
120
  }
124
121
  addOns.add(addOn)
125
122
  }
126
- } else {
123
+ } else if (!routerOnly) {
127
124
  for (const addOn of await selectAddOns(
128
125
  options.framework,
129
126
  options.mode,
@@ -134,21 +131,26 @@ export async function promptForCreateOptions(
134
131
  addOns.add(addOn)
135
132
  }
136
133
 
137
- for (const addOn of await selectAddOns(
138
- options.framework,
139
- options.mode,
140
- 'example',
141
- 'Would you like an example?',
142
- forcedAddOns,
143
- false,
144
- )) {
145
- addOns.add(addOn)
134
+ if (includeExamples) {
135
+ for (const addOn of await selectAddOns(
136
+ options.framework,
137
+ options.mode,
138
+ 'example',
139
+ 'Would you like an example?',
140
+ forcedAddOns,
141
+ false,
142
+ )) {
143
+ addOns.add(addOn)
144
+ }
146
145
  }
147
146
  }
148
147
 
149
- options.chosenAddOns = Array.from(
148
+ const chosenAddOns = Array.from(
150
149
  await finalizeAddOns(options.framework, options.mode, Array.from(addOns)),
151
150
  )
151
+ options.chosenAddOns = includeExamples
152
+ ? chosenAddOns
153
+ : chosenAddOns.filter((addOn) => addOn.type !== 'example')
152
154
 
153
155
  // Tailwind is always enabled
154
156
  options.tailwind = true
@@ -168,7 +170,14 @@ export async function promptForCreateOptions(
168
170
  options.addOnOptions = { ...defaultOptions, ...userOptions }
169
171
  }
170
172
 
171
- options.git = cliOptions.git || (await selectGit())
173
+ // Prompt for env vars exposed by selected add-ons in interactive mode
174
+ const envVarValues = Array.isArray(cliOptions.addOns)
175
+ ? {}
176
+ : await promptForEnvVars(options.chosenAddOns)
177
+ ;(options as Required<Options> & { envVarValues?: Record<string, string> }).envVarValues =
178
+ envVarValues
179
+
180
+ options.git = cliOptions.git ?? (await selectGit())
172
181
  if (cliOptions.install === false) {
173
182
  options.install = false
174
183
  }
package/src/types.ts CHANGED
@@ -23,4 +23,5 @@ export interface CliOptions {
23
23
  routerOnly?: boolean
24
24
  template?: string
25
25
  tailwind?: boolean
26
+ examples?: boolean
26
27
  }
package/src/ui-prompts.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  isCancel,
5
5
  multiselect,
6
6
  note,
7
+ password,
7
8
  select,
8
9
  text,
9
10
  } from '@clack/prompts'
@@ -151,6 +152,18 @@ export async function selectGit(): Promise<boolean> {
151
152
  return git
152
153
  }
153
154
 
155
+ export async function selectExamples(): Promise<boolean> {
156
+ const includeExamples = await confirm({
157
+ message: 'Would you like to include demo/example pages?',
158
+ initialValue: true,
159
+ })
160
+ if (isCancel(includeExamples)) {
161
+ cancel('Operation cancelled.')
162
+ process.exit(0)
163
+ }
164
+ return includeExamples
165
+ }
166
+
154
167
  export async function selectToolchain(
155
168
  framework: Framework,
156
169
  toolchain?: string | false,
@@ -239,6 +252,69 @@ export async function promptForAddOnOptions(
239
252
  return addOnOptions
240
253
  }
241
254
 
255
+ export async function promptForEnvVars(
256
+ addOns: Array<AddOn>,
257
+ ): Promise<Record<string, string>> {
258
+ const envVars = new Map<
259
+ string,
260
+ {
261
+ name: string
262
+ description?: string
263
+ required?: boolean
264
+ default?: string
265
+ secret?: boolean
266
+ }
267
+ >()
268
+
269
+ for (const addOn of addOns as Array<any>) {
270
+ for (const envVar of addOn.envVars || []) {
271
+ if (!envVars.has(envVar.name)) {
272
+ envVars.set(envVar.name, envVar)
273
+ }
274
+ }
275
+ }
276
+
277
+ const result: Record<string, string> = {}
278
+
279
+ for (const envVar of envVars.values()) {
280
+ const label = envVar.description
281
+ ? `${envVar.name} (${envVar.description})`
282
+ : envVar.name
283
+
284
+ const value = envVar.secret
285
+ ? await password({
286
+ message: `Enter ${label}`,
287
+ validate: envVar.required
288
+ ? (v) =>
289
+ v && v.trim().length > 0
290
+ ? undefined
291
+ : `${envVar.name} is required`
292
+ : undefined,
293
+ })
294
+ : await text({
295
+ message: `Enter ${label}`,
296
+ defaultValue: envVar.default,
297
+ validate: envVar.required
298
+ ? (v) =>
299
+ v && v.trim().length > 0
300
+ ? undefined
301
+ : `${envVar.name} is required`
302
+ : undefined,
303
+ })
304
+
305
+ if (isCancel(value)) {
306
+ cancel('Operation cancelled.')
307
+ process.exit(0)
308
+ }
309
+
310
+ if (value && value.trim()) {
311
+ result[envVar.name] = value.trim()
312
+ }
313
+ }
314
+
315
+ return result
316
+ }
317
+
242
318
  export async function selectDeployment(
243
319
  framework: Framework,
244
320
  deployment?: string,
@@ -91,6 +91,23 @@ describe('normalizeOptions', () => {
91
91
  expect(solidOptions?.tailwind).toBe(true)
92
92
  })
93
93
 
94
+ it('defaults git initialization to enabled', async () => {
95
+ const options = await normalizeOptions({
96
+ projectName: 'test',
97
+ })
98
+
99
+ expect(options?.git).toBe(true)
100
+ })
101
+
102
+ it('respects explicit --no-git option', async () => {
103
+ const options = await normalizeOptions({
104
+ projectName: 'test',
105
+ git: false,
106
+ })
107
+
108
+ expect(options?.git).toBe(false)
109
+ })
110
+
94
111
  it('should handle a starter url', async () => {
95
112
  __testRegisterFramework({
96
113
  id: 'solid',
@@ -274,6 +291,73 @@ describe('normalizeOptions', () => {
274
291
  expect(options?.typescript).toBe(true)
275
292
  })
276
293
 
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('includes examples by default in non-router-only mode', async () => {
304
+ const options = await normalizeOptions({
305
+ projectName: 'test',
306
+ })
307
+
308
+ expect((options as any)?.includeExamples).toBe(true)
309
+ })
310
+
311
+ it('supports disabling examples from the CLI', async () => {
312
+ const options = await normalizeOptions({
313
+ projectName: 'test',
314
+ examples: false,
315
+ })
316
+
317
+ expect((options as any)?.includeExamples).toBe(false)
318
+ })
319
+
320
+ it('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => {
321
+ __testRegisterFramework({
322
+ id: 'react-cra',
323
+ name: 'react',
324
+ getAddOns: () => [
325
+ {
326
+ id: 'form',
327
+ name: 'Form',
328
+ modes: ['file-router'],
329
+ },
330
+ {
331
+ id: 'nitro',
332
+ name: 'nitro',
333
+ modes: ['file-router'],
334
+ type: 'deployment',
335
+ },
336
+ {
337
+ id: 'biome',
338
+ name: 'Biome',
339
+ modes: ['file-router'],
340
+ type: 'toolchain',
341
+ },
342
+ ],
343
+ })
344
+
345
+ const options = await normalizeOptions(
346
+ {
347
+ projectName: 'test',
348
+ framework: 'react-cra',
349
+ routerOnly: true,
350
+ addOns: ['form'],
351
+ deployment: 'nitro',
352
+ toolchain: 'biome',
353
+ },
354
+ ['form'],
355
+ { forcedDeployment: 'nitro' },
356
+ )
357
+
358
+ expect(options?.chosenAddOns.map((a) => a.id)).toEqual(['biome'])
359
+ })
360
+
277
361
  it('should handle the funky Windows edge case with CLI parsing', async () => {
278
362
  __testRegisterFramework({
279
363
  id: 'react-cra',