@tanstack/cli 0.59.6 → 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,26 @@
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
+
3
24
  ## 0.59.6
4
25
 
5
26
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { Command, InvalidArgumentError } from 'commander';
4
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';
@@ -300,7 +300,9 @@ export function cli({ name, appName, forcedAddOns = [], forcedDeployment, defaul
300
300
  .option('--router-only', 'Use router-only compatibility mode (file-based routing without TanStack Start)')
301
301
  .option('--template <type>', 'Deprecated: compatibility flag from create-tsrouter-app')
302
302
  .option('--tailwind', 'Deprecated: compatibility flag; Tailwind is always enabled')
303
- .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');
304
306
  if (deployments.size > 0) {
305
307
  cmd.option(`--deployment <${Array.from(deployments).join('|')}>`, `Explicitly tell the CLI to use this deployment adapter`, (value) => {
306
308
  if (!deployments.has(value)) {
@@ -473,6 +475,12 @@ Remove your node_modules directory and package lock file and re-install.`);
473
475
  .action(async () => {
474
476
  await compileAddOn(environment);
475
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
+ });
476
484
  // === STARTER SUBCOMMAND ===
477
485
  const starterCommand = program.command('starter');
478
486
  starterCommand
@@ -123,7 +123,11 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
123
123
  }
124
124
  return [];
125
125
  }
126
- 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');
127
131
  // Handle add-on configuration option
128
132
  let addOnOptionsFromCLI = {};
129
133
  if (cliOptions.addOnConfig) {
@@ -135,7 +139,7 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
135
139
  process.exit(1);
136
140
  }
137
141
  }
138
- return {
142
+ const normalized = {
139
143
  projectName: projectName,
140
144
  targetDir,
141
145
  framework,
@@ -154,6 +158,11 @@ export async function normalizeOptions(cliOptions, forcedAddOns, opts) {
154
158
  },
155
159
  starter: starter,
156
160
  };
161
+ normalized.includeExamples =
162
+ includeExamples;
163
+ normalized.envVarValues =
164
+ {};
165
+ return normalized;
157
166
  }
158
167
  export function validateDevWatchOptions(cliOptions) {
159
168
  if (!cliOptions.devWatch) {
package/dist/options.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { intro } from '@clack/prompts';
2
2
  import { finalizeAddOns, getFrameworkById, getPackageManager, populateAddOnOptionsDefaults, readConfigFile, } from '@tanstack/create';
3
- 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';
4
4
  import { getCurrentDirectoryName, sanitizePackageName, validateProjectName, } from './utils.js';
5
5
  export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], showDeploymentOptions = false, }) {
6
6
  const options = {};
@@ -48,6 +48,10 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
48
48
  : undefined;
49
49
  // Add-ons selection
50
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;
51
55
  if (toolchain) {
52
56
  addOns.add(toolchain);
53
57
  }
@@ -71,11 +75,16 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
71
75
  for (const addOn of await selectAddOns(options.framework, options.mode, 'add-on', 'What add-ons would you like for your project?', forcedAddOns)) {
72
76
  addOns.add(addOn);
73
77
  }
74
- for (const addOn of await selectAddOns(options.framework, options.mode, 'example', 'Would you like an example?', forcedAddOns, false)) {
75
- 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
+ }
76
82
  }
77
83
  }
78
- 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');
79
88
  // Tailwind is always enabled
80
89
  options.tailwind = true;
81
90
  // Prompt for add-on options in interactive mode
@@ -90,6 +99,12 @@ export async function promptForCreateOptions(cliOptions, { forcedAddOns = [], sh
90
99
  // Merge user options with defaults
91
100
  options.addOnOptions = { ...defaultOptions, ...userOptions };
92
101
  }
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;
93
108
  options.git = cliOptions.git ?? (await selectGit());
94
109
  if (cliOptions.install === false) {
95
110
  options.install = false;
@@ -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.6",
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.4",
42
- "@tanstack/create-ui": "0.59.6"
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
@@ -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,
@@ -466,6 +467,8 @@ export function cli({
466
467
  '--no-tailwind',
467
468
  'Deprecated: compatibility flag; Tailwind opt-out is ignored',
468
469
  )
470
+ .option('--examples', 'include demo/example pages')
471
+ .option('--no-examples', 'exclude demo/example pages')
469
472
 
470
473
  if (deployments.size > 0) {
471
474
  cmd.option<string>(
@@ -695,6 +698,14 @@ Remove your node_modules directory and package lock file and re-install.`,
695
698
  .action(async () => {
696
699
  await compileAddOn(environment)
697
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
+ })
698
709
 
699
710
  // === STARTER SUBCOMMAND ===
700
711
  const starterCommand = program.command('starter')
@@ -191,7 +191,11 @@ export async function normalizeOptions(
191
191
  return []
192
192
  }
193
193
 
194
- 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')
195
199
 
196
200
  // Handle add-on configuration option
197
201
  let addOnOptionsFromCLI = {}
@@ -204,7 +208,7 @@ export async function normalizeOptions(
204
208
  }
205
209
  }
206
210
 
207
- return {
211
+ const normalized = {
208
212
  projectName: projectName,
209
213
  targetDir,
210
214
  framework,
@@ -224,6 +228,13 @@ export async function normalizeOptions(
224
228
  },
225
229
  starter: starter,
226
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
227
238
  }
228
239
 
229
240
  export function validateDevWatchOptions(cliOptions: CliOptions): {
package/src/options.ts CHANGED
@@ -11,8 +11,10 @@ import {
11
11
  import {
12
12
  getProjectName,
13
13
  promptForAddOnOptions,
14
+ promptForEnvVars,
14
15
  selectAddOns,
15
16
  selectDeployment,
17
+ selectExamples,
16
18
  selectGit,
17
19
  selectPackageManager,
18
20
  selectToolchain,
@@ -92,6 +94,12 @@ export async function promptForCreateOptions(
92
94
  // Add-ons selection
93
95
  const addOns: Set<string> = new Set()
94
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
+
95
103
  if (toolchain) {
96
104
  addOns.add(toolchain)
97
105
  }
@@ -123,21 +131,26 @@ export async function promptForCreateOptions(
123
131
  addOns.add(addOn)
124
132
  }
125
133
 
126
- for (const addOn of await selectAddOns(
127
- options.framework,
128
- options.mode,
129
- 'example',
130
- 'Would you like an example?',
131
- forcedAddOns,
132
- false,
133
- )) {
134
- 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
+ }
135
145
  }
136
146
  }
137
147
 
138
- options.chosenAddOns = Array.from(
148
+ const chosenAddOns = Array.from(
139
149
  await finalizeAddOns(options.framework, options.mode, Array.from(addOns)),
140
150
  )
151
+ options.chosenAddOns = includeExamples
152
+ ? chosenAddOns
153
+ : chosenAddOns.filter((addOn) => addOn.type !== 'example')
141
154
 
142
155
  // Tailwind is always enabled
143
156
  options.tailwind = true
@@ -157,6 +170,13 @@ export async function promptForCreateOptions(
157
170
  options.addOnOptions = { ...defaultOptions, ...userOptions }
158
171
  }
159
172
 
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
+
160
180
  options.git = cliOptions.git ?? (await selectGit())
161
181
  if (cliOptions.install === false) {
162
182
  options.install = false
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,
@@ -300,6 +300,23 @@ describe('normalizeOptions', () => {
300
300
  expect(options?.mode).toBe('file-router')
301
301
  })
302
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
+
303
320
  it('should ignore add-ons and deployment in router-only mode but keep toolchain', async () => {
304
321
  __testRegisterFramework({
305
322
  id: 'react-cra',